From a49f472abfd2a73c2379d8d3220eb6be444b77f6 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Fri, 1 Nov 2024 20:45:02 +0100 Subject: [PATCH 01/28] feat: custom log format implementation --- cli/app.go | 15 +- cli/commands/flags.go | 85 ++-- cli/provider_cache.go | 12 +- options/options.go | 40 +- pkg/log/ff/config/config.go | 45 +++ pkg/log/ff/config/layout.go | 63 +++ pkg/log/ff/config/option.go | 255 ++++++++++++ pkg/log/ff/config/random_color.go | 69 ++++ pkg/log/ff/config/var.go | 241 +++++++++++ pkg/log/ff/config/var_filters.go | 102 +++++ pkg/log/ff/format.go | 79 ++++ pkg/log/ff/formatter.go | 375 ++++++++++++++++++ pkg/log/ff/options.go | 31 ++ pkg/log/fields.go | 2 +- pkg/log/{format => format.old}/color.go | 0 pkg/log/format.old/formatter.go | 285 +++++++++++++ .../{format => format.old}/json_formatter.go | 0 .../{format => format.old}/prefix_style.go | 0 pkg/log/format/format.go | 81 ++++ pkg/log/format/formatter.go | 273 ++----------- pkg/log/format/options/align.go | 53 +++ pkg/log/format/options/case.go | 48 +++ pkg/log/format/options/color.go | 201 ++++++++++ pkg/log/format/options/common.go | 76 ++++ pkg/log/format/options/escape.go | 44 ++ pkg/log/format/options/level_format.go | 38 ++ pkg/log/format/options/option.go | 62 +++ pkg/log/format/options/path_format.go | 134 +++++++ pkg/log/format/options/prefix.go | 23 ++ pkg/log/format/options/suffix.go | 23 ++ pkg/log/format/options/time_format.go | 113 ++++++ pkg/log/format/options/width.go | 41 ++ pkg/log/format/placeholders/common.go | 44 ++ pkg/log/format/placeholders/field.go | 42 ++ pkg/log/format/placeholders/level.go | 56 +++ pkg/log/format/placeholders/message.go | 29 ++ pkg/log/format/placeholders/placeholder.go | 164 ++++++++ pkg/log/format/placeholders/plaintext.go | 25 ++ pkg/log/format/placeholders/time.go | 31 ++ pkg/log/format/silent_formatter.go | 13 - pkg/log/helper.go | 31 ++ pkg/log/hooks/relative_path.go | 101 ----- pkg/log/level.go | 5 + pkg/log/options.go | 4 +- shell/run_shell_cmd.go | 4 +- shell/run_shell_cmd_output_test.go | 3 +- test/integration_test.go | 2 +- 47 files changed, 3046 insertions(+), 417 deletions(-) create mode 100644 pkg/log/ff/config/config.go create mode 100644 pkg/log/ff/config/layout.go create mode 100644 pkg/log/ff/config/option.go create mode 100644 pkg/log/ff/config/random_color.go create mode 100644 pkg/log/ff/config/var.go create mode 100644 pkg/log/ff/config/var_filters.go create mode 100644 pkg/log/ff/format.go create mode 100644 pkg/log/ff/formatter.go create mode 100644 pkg/log/ff/options.go rename pkg/log/{format => format.old}/color.go (100%) create mode 100644 pkg/log/format.old/formatter.go rename pkg/log/{format => format.old}/json_formatter.go (100%) rename pkg/log/{format => format.old}/prefix_style.go (100%) create mode 100644 pkg/log/format/format.go create mode 100644 pkg/log/format/options/align.go create mode 100644 pkg/log/format/options/case.go create mode 100644 pkg/log/format/options/color.go create mode 100644 pkg/log/format/options/common.go create mode 100644 pkg/log/format/options/escape.go create mode 100644 pkg/log/format/options/level_format.go create mode 100644 pkg/log/format/options/option.go create mode 100644 pkg/log/format/options/path_format.go create mode 100644 pkg/log/format/options/prefix.go create mode 100644 pkg/log/format/options/suffix.go create mode 100644 pkg/log/format/options/time_format.go create mode 100644 pkg/log/format/options/width.go create mode 100644 pkg/log/format/placeholders/common.go create mode 100644 pkg/log/format/placeholders/field.go create mode 100644 pkg/log/format/placeholders/level.go create mode 100644 pkg/log/format/placeholders/message.go create mode 100644 pkg/log/format/placeholders/placeholder.go create mode 100644 pkg/log/format/placeholders/plaintext.go create mode 100644 pkg/log/format/placeholders/time.go delete mode 100644 pkg/log/format/silent_formatter.go create mode 100644 pkg/log/helper.go delete mode 100644 pkg/log/hooks/relative_path.go diff --git a/cli/app.go b/cli/app.go index 5c5ef060ca..56b2b2d06a 100644 --- a/cli/app.go +++ b/cli/app.go @@ -11,9 +11,6 @@ import ( "github.com/gruntwork-io/terragrunt/engine" "github.com/gruntwork-io/terragrunt/internal/os/exec" "github.com/gruntwork-io/terragrunt/internal/os/signal" - "github.com/gruntwork-io/terragrunt/pkg/log" - "github.com/gruntwork-io/terragrunt/pkg/log/format" - "github.com/gruntwork-io/terragrunt/pkg/log/hooks" "github.com/gruntwork-io/terragrunt/telemetry" "github.com/gruntwork-io/terragrunt/terraform" "golang.org/x/sync/errgroup" @@ -47,6 +44,7 @@ import ( validateinputs "github.com/gruntwork-io/terragrunt/cli/commands/validate-inputs" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/cli" + "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" ) func init() { @@ -294,17 +292,12 @@ func initialSetup(cliCtx *cli.Context, opts *options.TerragruntOptions) error { return errors.New(err) } - opts.Logger = opts.Logger.WithField(format.PrefixKeyName, workingDir) + opts.Logger = opts.Logger.WithField(placeholders.WorkDirKeyName, workingDir) opts.RootWorkingDir = filepath.ToSlash(workingDir) - if !opts.LogShowAbsPaths { - hook, err := hooks.NewRelativePathHook(opts.RootWorkingDir) - if err != nil { - return err - } - - opts.Logger.SetOptions(log.WithHooks(hook)) + if err := opts.LogFormatter.CreateRelativePathsCache(opts.RootWorkingDir); err != nil { + return nil } // --- Download Dir diff --git a/cli/commands/flags.go b/cli/commands/flags.go index 6fc426415d..dc8f804289 100644 --- a/cli/commands/flags.go +++ b/cli/commands/flags.go @@ -10,6 +10,7 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/cli" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/pkg/log/format" + "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" "github.com/gruntwork-io/terragrunt/shell" "github.com/gruntwork-io/terragrunt/util" ) @@ -143,6 +144,12 @@ const ( TerragruntForwardTFStdoutFlagName = "terragrunt-forward-tf-stdout" TerragruntForwardTFStdoutEnvName = "TERRAGRUNT_FORWARD_TF_STDOUT" + TerragruntLogFormatFlagName = "terragrunt-log-format" + TerragruntLogFormatEnvName = "TERRAGRUNT_LOG_FORMAT" + + TerragruntLogPrettyFormatFlagName = "terragrunt-log-pretty-format" + TerragruntLogPrettyFormatEnvName = "TERRAGRUNT_LOG_PRETTY_FORMAT" + // Terragrunt Provider Cache related flags/envs TerragruntProviderCacheFlagName = "terragrunt-provider-cache" @@ -340,7 +347,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { if collections.ListContainsElement(removedLevels, val) { opts.ForwardTFStdout = true - opts.Logger.SetOptions(log.WithFormatter(&format.SilentFormatter{})) + opts.LogFormatter.SetFormat(nil) return nil } @@ -352,7 +359,6 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { opts.Logger.SetOptions(log.WithLevel(level)) opts.LogLevel = level return nil - }, }, &cli.BoolFlag{ @@ -362,30 +368,31 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Destination: &opts.DisableLog, Action: func(ctx *cli.Context, _ bool) error { opts.ForwardTFStdout = true - opts.Logger.SetOptions(log.WithFormatter(&format.SilentFormatter{})) - return nil - }, - }, - &cli.BoolFlag{ - Name: TerragruntDisableLogFormattingFlagName, - EnvVar: TerragruntDisableLogFormattingEnvName, - Destination: &opts.DisableLogFormatting, - Usage: "If specified, logs will be displayed in key/value format. By default, logs are formatted in a human readable format.", - Action: func(ctx *cli.Context, val bool) error { - opts.LogFormatter.DisableLogFormatting = val - return nil - }, - }, - &cli.BoolFlag{ - Name: TerragruntJSONLogFlagName, - EnvVar: TerragruntJSONLogEnvName, - Destination: &opts.JSONLogFormat, - Usage: "If specified, Terragrunt will output its logs in JSON format.", - Action: func(ctx *cli.Context, _ bool) error { - opts.Logger.SetOptions(log.WithFormatter(&format.JSONFormatter{})) + opts.LogFormatter.SetFormat(nil) return nil }, }, + // TODO: remove + // &cli.BoolFlag{ + // Name: TerragruntDisableLogFormattingFlagName, + // EnvVar: TerragruntDisableLogFormattingEnvName, + // Destination: &opts.DisableLogFormatting, + // Usage: "If specified, logs will be displayed in key/value format. By default, logs are formatted in a human readable format.", + // Action: func(ctx *cli.Context, val bool) error { + // //opts.LogFormatter.DisableLogFormatting = val + // return nil + // }, + // }, + // &cli.BoolFlag{ + // Name: TerragruntJSONLogFlagName, + // EnvVar: TerragruntJSONLogEnvName, + // Destination: &opts.JSONLogFormat, + // Usage: "If specified, Terragrunt will output its logs in JSON format.", + // Action: func(ctx *cli.Context, _ bool) error { + // //opts.Logger.SetOptions(log.WithFormatter(&format.JSONFormatter{})) + // return nil + // }, + // }, &cli.BoolFlag{ Name: TerragruntShowLogAbsPathsFlagName, EnvVar: TerragruntShowLogAbsPathsEnvName, @@ -397,8 +404,8 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { EnvVar: TerragruntNoColorEnvName, Destination: &opts.DisableLogColors, Usage: "If specified, Terragrunt output won't contain any color.", - Action: func(ctx *cli.Context, val bool) error { - opts.LogFormatter.DisableColors = val + Action: func(ctx *cli.Context, _ bool) error { + opts.LogFormatter.DisableColors() return nil }, }, @@ -426,6 +433,34 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Destination: &opts.ForwardTFStdout, Usage: "If specified, the output of OpenTofu/Terraform commands will be printed as is, without being integrated into the Terragrunt log.", }, + &cli.GenericFlag[string]{ + Name: TerragruntLogFormatFlagName, + EnvVar: TerragruntLogFormatEnvName, + Usage: "", // TODO: write usage + Action: func(ctx *cli.Context, val string) error { + format := format.ParseFormat(val) + if format == nil { + return errors.Errorf("flag --%s, invalid format %q", TerragruntLogFormatFlagName, val) + } + + opts.LogFormatter.SetFormat(format) + return nil + }, + }, + &cli.GenericFlag[string]{ + Name: TerragruntLogPrettyFormatFlagName, + EnvVar: TerragruntLogPrettyFormatEnvName, + Usage: "", // TODO: write usage + Action: func(ctx *cli.Context, val string) error { + phs, err := placeholders.Parse(val, placeholders.Registered) + if err != nil { + return errors.Errorf("flag --%s, %w", TerragruntLogPrettyFormatFlagName, err) + } + + opts.LogFormatter.SetFormat(phs) + return nil + }, + }, &cli.BoolFlag{ Name: TerragruntStrictIncludeFlagName, EnvVar: TerragruntStrictIncludeEnvName, diff --git a/cli/provider_cache.go b/cli/provider_cache.go index ac0662ecb7..5d74382b3f 100644 --- a/cli/provider_cache.go +++ b/cli/provider_cache.go @@ -159,8 +159,18 @@ func (cache *ProviderCache) TerraformCommandHook( // To prevent a loop ctx = shell.ContextWithTerraformCommandHook(ctx, nil) + cliConfigFilename := filepath.Join(opts.WorkingDir, localCLIFilename) + + if !filepath.IsAbs(cliConfigFilename) { + absPath, err := filepath.Abs(cliConfigFilename) + if err != nil { + return nil, errors.New(err) + } + + cliConfigFilename = absPath + } + var ( - cliConfigFilename = filepath.Join(opts.WorkingDir, localCLIFilename) env = providerCacheEnvironment(opts, cliConfigFilename) skipRunTargetCommand bool ) diff --git a/options/options.go b/options/options.go index 7fe5146deb..28b08d345d 100644 --- a/options/options.go +++ b/options/options.go @@ -13,6 +13,7 @@ import ( "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/pkg/log/format" + "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" "github.com/gruntwork-io/terragrunt/util" "github.com/hashicorp/go-version" ) @@ -128,7 +129,7 @@ type TerragruntOptions struct { // Disable Terragrunt colors DisableLogColors bool - // Output Terragrunt logs in JSON format + // Output Terragrunt logs in JSON formatter JSONLogFormat bool // Disable replacing full paths in logs with short relative paths @@ -143,10 +144,10 @@ type TerragruntOptions struct { // If true, logs will be disabled DisableLog bool - // If true, logs will be displayed in format key/value, by default logs are formatted in human-readable format. + // If true, logs will be displayed in formatter key/value, by default logs are formatted in human-readable formatter. DisableLogFormatting bool - // Wrap Terraform logs in JSON format + // Wrap Terraform logs in JSON formatter TerraformLogsToJSON bool // ValidateStrict mode for the validate-inputs command @@ -530,20 +531,23 @@ func (opts *TerragruntOptions) Clone(terragruntConfigPath string) (*TerragruntOp // during xxx-all commands (e.g., apply-all, plan-all). See https://github.com/gruntwork-io/terragrunt/issues/367 // for more info. return &TerragruntOptions{ - TerragruntConfigPath: terragruntConfigPath, - OriginalTerragruntConfigPath: opts.OriginalTerragruntConfigPath, - TerraformPath: opts.TerraformPath, - OriginalTerraformCommand: opts.OriginalTerraformCommand, - TerraformCommand: opts.TerraformCommand, - TerraformVersion: opts.TerraformVersion, - TerragruntVersion: opts.TerragruntVersion, - AutoInit: opts.AutoInit, - RunAllAutoApprove: opts.RunAllAutoApprove, - NonInteractive: opts.NonInteractive, - TerraformCliArgs: util.CloneStringList(opts.TerraformCliArgs), - WorkingDir: workingDir, - RootWorkingDir: opts.RootWorkingDir, - Logger: opts.Logger.WithField(format.PrefixKeyName, workingDir), + TerragruntConfigPath: terragruntConfigPath, + OriginalTerragruntConfigPath: opts.OriginalTerragruntConfigPath, + TerraformPath: opts.TerraformPath, + OriginalTerraformCommand: opts.OriginalTerraformCommand, + TerraformCommand: opts.TerraformCommand, + TerraformVersion: opts.TerraformVersion, + TerragruntVersion: opts.TerragruntVersion, + AutoInit: opts.AutoInit, + RunAllAutoApprove: opts.RunAllAutoApprove, + NonInteractive: opts.NonInteractive, + TerraformCliArgs: util.CloneStringList(opts.TerraformCliArgs), + WorkingDir: workingDir, + RootWorkingDir: opts.RootWorkingDir, + Logger: opts.Logger.WithFields(log.Fields{ + placeholders.WorkDirKeyName: workingDir, + placeholders.DownloadDirKeyName: opts.DownloadDir, + }), LogLevel: opts.LogLevel, LogFormatter: opts.LogFormatter, ValidateStrict: opts.ValidateStrict, @@ -623,7 +627,7 @@ func cloneEngineOptions(opts *EngineOptions) *EngineOptions { } } -// Check if argument is planfile TODO check file format +// Check if argument is planfile TODO check file formatter func checkIfPlanFile(arg string) bool { return util.IsFile(arg) && filepath.Ext(arg) == ".tfplan" } diff --git a/pkg/log/ff/config/config.go b/pkg/log/ff/config/config.go new file mode 100644 index 0000000000..07334c77b2 --- /dev/null +++ b/pkg/log/ff/config/config.go @@ -0,0 +1,45 @@ +package config + +type Configs []*Config + +func (cfg Configs) Find(name string) *Config { + for _, cfg := range cfg { + if cfg.name == name { + return cfg + } + } + + return nil +} + +func (cfg Configs) Names() []string { + var names []string + + for _, cfg := range cfg { + if cfg.name != "" { + names = append(names, cfg.name) + } + } + + return names +} + +type Config struct { + name string + opts Options +} + +func (cfg *Config) Options() Options { + return cfg.opts +} + +func (cfg *Config) Name() string { + return cfg.name +} + +func New(name string, opts ...*Option) *Config { + return &Config{ + name: name, + opts: opts, + } +} diff --git a/pkg/log/ff/config/layout.go b/pkg/log/ff/config/layout.go new file mode 100644 index 0000000000..cac374cd7f --- /dev/null +++ b/pkg/log/ff/config/layout.go @@ -0,0 +1,63 @@ +package config + +import ( + "fmt" + "strings" +) + +type Layout struct { + format string + vars Vars + doPost map[PostTask]PostFunc +} + +func NewLayout(format string, vars ...*Var) *Layout { + return &Layout{ + format: format, + vars: vars, + doPost: make(map[PostTask]PostFunc), + } +} + +func (layout *Layout) Value(opt *Option, entry *Entry) string { + var vals []any + + for _, variable := range layout.vars { + val := variable.Value(opt, entry) + vals = append(vals, val) + } + + val := fmt.Sprintf(layout.format, vals...) + + for _, fn := range layout.doPost { + val = fn(val) + } + + return val +} + +func ParseLayout(str string) (*Layout, error) { + var ( + format = str + vars []*Var + ) + + if parts := strings.Split(str, varSeparator); len(parts) > 1 { + format = parts[0] + varNames := parts[1:] + + for _, name := range varNames { + variable, err := ParseVar(name) + if err != nil { + return nil, err + } + vars = append(vars, variable) + } + + if format == "" { + format = strings.Repeat("%s", len(vars)) + } + } + + return &Layout{format: format, vars: vars}, nil +} diff --git a/pkg/log/ff/config/option.go b/pkg/log/ff/config/option.go new file mode 100644 index 0000000000..701b667658 --- /dev/null +++ b/pkg/log/ff/config/option.go @@ -0,0 +1,255 @@ +package config + +import ( + "sort" + "strings" + + "github.com/gruntwork-io/terragrunt/pkg/log" +) + +type Options []*Option + +func (opts Options) Names() []string { + strs := make([]string, len(opts)) + + for i, opt := range opts { + strs[i] = opt.name + } + + return strs +} + +func (opts Options) SortByValue() Options { + sort.Slice(opts, func(i, j int) bool { + return opts[i].value <= opts[j].value + }) + + return opts +} + +func (opts Options) FindByName(name string) Options { + var foundOpts Options + + for _, opt := range opts { + if opt.name == name || opt.name == "" || opt.value == name { + foundOpts = append(foundOpts, opt) + } + } + + return foundOpts +} + +func (opts Options) FindByLevels(levels ...log.Level) Options { + var foundOpts Options + + for _, opt := range opts { + for _, level := range levels { + if opt.levels.Contains(level) { + foundOpts = append(foundOpts, opt) + } + } + } + + return foundOpts +} + +func (opts Options) FindWithoutLevels() Options { + var foundOpts Options + + for _, opt := range opts { + if len(opt.levels) == 0 { + foundOpts = append(foundOpts, opt) + } + } + + return foundOpts +} + +func (opts Options) FilterByNamePrefixes(mustContain bool, prefixes ...string) Options { + var filteredOpts Options + + for _, opt := range opts { + for _, prefix := range prefixes { + if strings.HasPrefix(opt.name, prefix) == mustContain { + filteredOpts = append(filteredOpts, opt) + } + } + } + + return filteredOpts +} + +func (opts Options) MergeIntoOne() *Option { + var new *Option + + for _, opt := range opts { + if new == nil { + new = &Option{ + name: opt.name, + value: opt.value, + enable: opt.enable, + layout: opt.layout, + levels: opt.levels, + randomColor: opt.randomColor, + } + } else { + if opt.layout != nil { + new.layout = opt.layout + } + if opt.value != "" { + new.value = opt.value + } + + new.enable = opt.enable + new.levels = opt.levels + new.randomColor = opt.randomColor + } + } + + return new +} + +func (opts Options) MergeByName() Options { + var news Options + + for _, opt := range opts { + isNew := true + for i, new := range news { + if opt.name == new.name { + news[i] = opt + isNew = false + break + } + } + if isNew { + news = append(news, opt) + } + } + + return news +} + +func (opts Options) MergeIntoOneWithPriorityByLevels(levels ...log.Level) *Option { + return append(opts.FindWithoutLevels(), opts.FindByLevels(levels...)...).MergeIntoOne() +} + +type Option struct { + name string + value string + enable bool + layout *Layout + levels log.Levels + + randomColor *RandomColor +} + +func (opt *Option) Enable() bool { + return opt.enable +} + +func (opt *Option) Name() string { + return opt.name +} + +func (opt *Option) Levels() log.Levels { + return opt.levels +} + +func (opt *Option) Layout() *Layout { + return opt.layout +} + +func (opt *Option) Value(entry *Entry) string { + if opt.layout == nil { + return "" + } + + return opt.layout.Value(opt, entry) +} + +func NewOption(name string, enable bool, layout *Layout, levels ...log.Level) *Option { + if layout == nil { + layout = NewLayout("%s", NewVar(name)) + } + + var value string + + if parts := strings.SplitN(name, "=", 2); len(parts) > 1 { + name = parts[0] + value = parts[1] + } + + return &Option{ + name: name, + value: value, + enable: enable, + layout: layout, + levels: levels, + randomColor: NewRandomColor(), + } +} + +func ParseOption(str string) (*Option, error) { + var ( + name = str + enable = true + layout *Layout + levels log.Levels + value string + err error + ) + + parts := strings.SplitN(name, ":", 2) + name = parts[0] + if strings.HasPrefix(name, "no-") { + name = name[3:] + enable = false + } + + if parts := strings.Split(name, "@"); len(parts) > 1 { + name = parts[0] + + if levels, err = ParseLevels(parts[1:]); err != nil { + return nil, err + } + } + + if parts := strings.SplitN(name, "=", 2); len(parts) > 1 { + name = parts[0] + value = parts[1] + } + + if len(parts) > 1 { + if layout, err = ParseLayout(parts[1]); err != nil { + return nil, err + } + } + + return &Option{ + name: name, + value: value, + enable: enable, + layout: layout, + levels: levels, + randomColor: NewRandomColor(), + }, nil +} + +func ParseLevels(levelNames []string) (log.Levels, error) { + var levels log.Levels + + for _, levelName := range levelNames { + if levelName == "" { + continue + } + + level, err := log.ParseLevel(levelName) + if err != nil { + return nil, err + } + + levels = append(levels, level) + } + + return levels, nil +} diff --git a/pkg/log/ff/config/random_color.go b/pkg/log/ff/config/random_color.go new file mode 100644 index 0000000000..937022fe87 --- /dev/null +++ b/pkg/log/ff/config/random_color.go @@ -0,0 +1,69 @@ +package config + +import ( + "github.com/mgutz/ansi" + "github.com/puzpuzpuz/xsync/v3" +) + +var ( + // defaultRandomColorStyles contains ANSI color codes that are assigned sequentially to each unique text in a rotating order + // https://user-images.githubusercontent.com/995050/47952855-ecb12480-df75-11e8-89d4-ac26c50e80b9.png + // https://www.hackitu.de/termcolor256/ + defaultRandomColorStyles = ColorStyles{ + "66", "67", "95", "96", "102", "103", "108", "109", "139", "138", "144", "145", + } +) + +type ColorStyles []ColorStyle + +func (styles ColorStyles) ColorCodes() []string { + codes := make([]string, len(styles)) + + for i, style := range styles { + codes[i] = style.ColorCode() + } + + return codes +} + +type ColorStyle string + +func (style ColorStyle) ColorCode() string { + return ansi.ColorCode(string(style)) +} + +type RandomColor struct { + // cache stores unique text with their color code. + // We use [xsync.MapOf](https://github.com/puzpuzpuz/xsync?tab=readme-ov-file#map) instaed of standard `sync.Map` since it's faster and has generic types. + cache *xsync.MapOf[string, string] + + codes []string + + // nextStyleIndex is used to get the next style from the `codes` list for a newly discovered text. + nextStyleIndex int +} + +func NewRandomColor() *RandomColor { + return &RandomColor{ + cache: xsync.NewMapOf[string, string](), + codes: defaultRandomColorStyles.ColorCodes(), + } +} + +func (color *RandomColor) ColorCode(text string) string { + if colorCode, ok := color.cache.Load(text); ok { + return colorCode + } + + if color.nextStyleIndex >= len(color.codes) { + color.nextStyleIndex = 0 + } + + colorCode := color.codes[color.nextStyleIndex] + + color.cache.Store(text, colorCode) + + color.nextStyleIndex++ + + return colorCode +} diff --git a/pkg/log/ff/config/var.go b/pkg/log/ff/config/var.go new file mode 100644 index 0000000000..e229dd2c11 --- /dev/null +++ b/pkg/log/ff/config/var.go @@ -0,0 +1,241 @@ +package config + +// --terragrunt-log-pretty-format "%if(cond=level==\"info\",scope=first-value)%C(red)%level(short,upper)%C(reset)" + +import ( + "fmt" + "strings" + "time" + + "github.com/gruntwork-io/terragrunt/pkg/log" + "github.com/mgutz/ansi" +) + +const ( + VarLevel = "level" + VarLevelShort = "level-short" + VarLevelTiny = "level-tiny" + VarMessage = "message" + + VarHour24Zero = "H" + VarHour12Zero = "h" + VarHour12 = "g" + VarMinZero = "i" + VarSecZero = "s" + VarMilliSec = "v" + VarMicroSec = "u" + VarYearFull = "Y" + VarYear = "y" + VarMonthNumZero = "m" + VarMonthNum = "n" + VarMonthText = "M" + VarDayZero = "d" + VarDay = "j" + VarDayText = "D" + VarPMUpper = "A" + VarPMLower = "a" + VarTZText = "T" + VarTZNumWithColon = "P" + VarTZNum = "O" + + VarDateTime = "date-time" + VarDateOnly = "date-only" + VarTimeOnly = "time-only" + VarRFC3339 = "rfc3339" + VarRFC3339Nano = "rfc3339-nano" + VarSinceStartSec = "since-start-sec" + + VarColorRed = "color-red" + VarColorWhite = "color-white" + VarColorYellow = "color-yellow" + VarColorGreen = "color-green" + VarColorBlueH = "color-blue+h" + VarColorCyan = "color-cyan" + VarColorBlackH = "color-black+h" + VarColorReset = "color-reset" + VarColorRandom = "color-random" +) + +const ( + doPostTaskResetColor PostTask = iota + doPostTaskRandomColor +) + +const ( + randomColorMask = "$$random-color$$" +) + +const varSeparator = "@" + +var ( + resetColor = ansi.ColorCode("reset") + + colorsMap = map[string]string{ + VarColorRed: ansi.ColorCode("red"), + VarColorWhite: ansi.ColorCode("white"), + VarColorYellow: ansi.ColorCode("yellow"), + VarColorGreen: ansi.ColorCode("green"), + VarColorBlueH: ansi.ColorCode("blue+h"), + VarColorCyan: ansi.ColorCode("cyan"), + VarColorBlackH: ansi.ColorCode("black+h"), + VarColorReset: resetColor, + } + + timestampsMap = map[string]string{ + VarYearFull: "2006", + VarYear: "06", + VarMonthNumZero: "01", + VarMonthNum: "1", + VarMonthText: "Jan", + VarDay: "2", + VarDayZero: "02", + VarDayText: "Mon", + VarPMUpper: "PM", + VarPMLower: "pm", + VarHour24Zero: "15", + VarHour12Zero: "03", + VarHour12: "3", + VarMinZero: "04", + VarSecZero: "05", + VarMicroSec: ".000000", + VarMilliSec: ".000", + VarTZText: "MST", + VarTZNum: "-0700", + VarTZNumWithColon: "-07:00", + VarRFC3339: time.RFC3339, + VarRFC3339Nano: time.RFC3339Nano, + VarDateTime: time.DateTime, + VarDateOnly: time.DateOnly, + VarTimeOnly: time.TimeOnly, + } + + ArgsFunc = make(map[string]VariableFunc) +) + +func init() { + for name, fmt := range timestampsMap { + ArgsFunc[name] = func(opt *Option, entry *Entry) string { + return entry.curTime.Format(fmt) + } + } + + for name, colorCode := range colorsMap { + ArgsFunc[name] = func(opt *Option, entry *Entry) string { + if entry.disableColor { + return "" + } + + if name != VarColorReset { + opt.layout.doPost[doPostTaskResetColor] = func(val string) string { + return val + resetColor + } + } + + return colorCode + } + } + + ArgsFunc[VarColorRandom] = func(opt *Option, entry *Entry) string { + if entry.disableColor { + return "" + } + + opt.layout.doPost[doPostTaskRandomColor] = func(val string) string { + colorCode := opt.randomColor.ColorCode(val) + val = strings.ReplaceAll(val, randomColorMask, colorCode) + return val + resetColor + } + + return randomColorMask + } + + ArgsFunc[VarSinceStartSec] = func(opt *Option, entry *Entry) string { + return fmt.Sprintf("%04d", time.Since(entry.baseTime)/time.Second) + } + + ArgsFunc[VarLevel] = func(opt *Option, entry *Entry) string { + return entry.level.String() + } + ArgsFunc[VarLevelShort] = func(opt *Option, entry *Entry) string { + return entry.level.ShortName() + } + ArgsFunc[VarLevelTiny] = func(opt *Option, entry *Entry) string { + return entry.level.TinyName() + } + + ArgsFunc[VarMessage] = func(opt *Option, entry *Entry) string { + return entry.message + } +} + +type PostFunc func(val string) string + +type PostTask byte + +type Entry struct { + baseTime time.Time + curTime time.Time + level log.Level + message string + fields log.Fields + disableColor bool +} + +func NewEntry(baseTime, curTime time.Time, level log.Level, msg string, fields log.Fields, disableColor bool) *Entry { + return &Entry{ + baseTime: baseTime, + curTime: curTime, + level: level, + message: msg, + fields: fields, + disableColor: disableColor, + } +} + +type VariableFunc func(opt *Option, entry *Entry) string + +type Vars []*Var + +type Var struct { + fn VariableFunc + filters Filters +} + +func (variable *Var) Value(opt *Option, entry *Entry) string { + val := variable.fn(opt, entry) + val = variable.filters.Value(val) + return val +} + +func NewVar(name string, filters ...Filter) *Var { + if fn, ok := ArgsFunc[name]; ok { + return &Var{fn: fn, filters: filters} + } + + fn := func(opt *Option, entry *Entry) string { + if val, ok := entry.fields[name]; ok { + return fmt.Sprintf("%s", val) + } + return "" + } + return &Var{fn: fn, filters: filters} +} + +func ParseVar(str string) (*Var, error) { + var ( + name = str + filters Filters + err error + ) + + if parts := strings.Split(name, varFilterSeparator); len(parts) > 1 { + name = parts[0] + + filters, err = ParseFilters(parts[1:]) + if err != nil { + return nil, err + } + } + + return NewVar(name, filters...), nil +} diff --git a/pkg/log/ff/config/var_filters.go b/pkg/log/ff/config/var_filters.go new file mode 100644 index 0000000000..94c39d4d81 --- /dev/null +++ b/pkg/log/ff/config/var_filters.go @@ -0,0 +1,102 @@ +package config + +import ( + "strings" + + "github.com/gruntwork-io/go-commons/errors" +) + +const varFilterSeparator = "|" + +const ( + FilterRequired Filter = iota + FilterUpper + FilterLower + FilterTitle +) + +var ( + // AllFilters exposes all var filters + AllFilters = Filters{ + FilterUpper, + FilterLower, + FilterTitle, + } + + varFilterNames = map[Filter]string{ + FilterUpper: "upper", + FilterLower: "lower", + FilterTitle: "title", + } +) + +type Filters []Filter + +func (filters Filters) Value(val string) string { + for _, filter := range filters { + switch filter { + case FilterUpper: + val = strings.ToUpper(val) + case FilterLower: + val = strings.ToLower(val) + case FilterTitle: + val = strings.Title(val) + } + } + + return val +} + +func (filters Filters) String() string { + return strings.Join(filters.Names(), ", ") +} + +func (filters Filters) Names() []string { + strs := make([]string, len(filters)) + + for i, filter := range filters { + strs[i] = filter.String() + } + + return strs +} + +// ParseFilters takes a string and returns the var filter constants. +func ParseFilters(names []string) (Filters, error) { + var filters Filters + + for _, name := range names { + if name == "" { + continue + } + filter, err := ParseFilter(name) + if err != nil { + return nil, err + } + filters = append(filters, filter) + } + + return filters, nil +} + +type Filter byte + +// ParseFilter takes a string and returns the var filter constant. +func ParseFilter(str string) (Filter, error) { + for filter, name := range varFilterNames { + if strings.EqualFold(name, str) { + return filter, nil + } + } + + return Filter(0), errors.Errorf("invalid variable filter %q, supported variable filtres: %s", str, AllFilters) +} + +// String implements fmt.Stringer. +func (filter Filter) String() string { + if name, ok := varFilterNames[filter]; ok { + return name + } + + return "" +} diff --git a/pkg/log/ff/format.go b/pkg/log/ff/format.go new file mode 100644 index 0000000000..f3023631bf --- /dev/null +++ b/pkg/log/ff/format.go @@ -0,0 +1,79 @@ +package format + +import ( + "github.com/gruntwork-io/terragrunt/pkg/log" + "github.com/gruntwork-io/terragrunt/pkg/log/format/config" +) + +const ( + ColumnTime = "#time=#1" + ColumnLevel = "#level=#2" + ColumnPrefix = "#prefix=#3" + ColumnMessage = "#message=#4" +) + +var ( + DefaultConfig = config.New(defaultConfigName, + config.NewOption(OptionColor, true, nil), + config.NewOption(ColumnTime, true, + config.NewLayout("%s%s:%s:%s%s", + config.NewVar(config.VarColorBlackH), + config.NewVar(config.VarHour24Zero), + config.NewVar(config.VarMinZero), + config.NewVar(config.VarSecZero), + config.NewVar(config.VarMilliSec)), + ), + + config.NewOption(ColumnLevel, true, + config.NewLayout("%-6s", + config.NewVar(config.VarLevel, config.FilterUpper)), + ), + + config.NewOption(ColumnLevel, true, + config.NewLayout("%s%-6s", + config.NewVar(config.VarColorRed), + config.NewVar(config.VarLevel, config.FilterUpper)), + log.ErrorLevel, log.StderrLevel, + ), + + config.NewOption(ColumnLevel, true, + config.NewLayout("%s%-6s", + config.NewVar(config.VarColorYellow), + config.NewVar(config.VarLevel, config.FilterUpper)), + log.WarnLevel, + ), + + config.NewOption(ColumnLevel, true, + config.NewLayout("%s%-6s", + config.NewVar(config.VarColorGreen), + config.NewVar(config.VarLevel, config.FilterUpper)), + log.InfoLevel, + ), + + config.NewOption(ColumnLevel, true, + config.NewLayout("%s%-6s", + config.NewVar(config.VarColorBlueH), + config.NewVar(config.VarLevel, config.FilterUpper)), + log.DebugLevel, + ), + + config.NewOption(ColumnPrefix, true, + config.NewLayout("%s[%s]", + config.NewVar(config.VarColorRandom), + config.NewVar("rel-prefix"))), + + config.NewOption("prefix", false, nil), + config.NewOption("rel-prefix", false, nil), + config.NewOption("sub-prefix", false, nil), + config.NewOption(ColumnMessage, true, config.NewLayout("%s", config.NewVar(config.VarMessage))), + ) + + TinyConfig = config.New("tiny", + config.NewOption(OptionColor, true, nil), + config.NewOption(ColumnTime, true, config.NewLayout("%s", config.NewVar(config.VarSinceStartSec))), + config.NewOption(ColumnLevel, true, config.NewLayout("%s", config.NewVar(config.VarLevelShort, config.FilterUpper))), + config.NewOption(ColumnMessage, true, config.NewLayout("%s", config.NewVar("message"))), + ) + + Configs = config.Configs{DefaultConfig, TinyConfig} +) diff --git a/pkg/log/ff/formatter.go b/pkg/log/ff/formatter.go new file mode 100644 index 0000000000..67f4ae6082 --- /dev/null +++ b/pkg/log/ff/formatter.go @@ -0,0 +1,375 @@ +package format + +import ( + "bytes" + "encoding/json" + "fmt" + "regexp" + "strings" + "time" + + "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/pkg/log" + "github.com/gruntwork-io/terragrunt/pkg/log/format/config" + "github.com/gruntwork-io/terragrunt/util" + "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" +) + +const ( + optionSeparator = "," + + columnPrefix = "#" + columnPrefixLen = len(columnPrefix) + + defaultConfigName = "" + defaultQuoteCharacter = "\"" +) + +var ( + optionSeparatorReg = regexp.MustCompile(`.*?[^\\](` + optionSeparator + `|$)`) +) + +const ( + OptionColor = "color" + OptionKeyValue = "key-value" + OptionJSON = "json" + OptionIndent = "indent" +) + +type Formatter struct { + baseTime time.Time + + presetConfigs config.Configs + selectedConfigName string + + userOpts config.Options + + quoteEmptyFields bool + + quoteCharacter string +} + +func NewFormatter(opts ...Option) *Formatter { + formatter := &Formatter{ + presetConfigs: config.Configs{DefaultConfig}, + + baseTime: time.Now(), + selectedConfigName: defaultConfigName, + + quoteCharacter: defaultQuoteCharacter, + } + + formatter.SetOption(opts...) + + return formatter +} + +func (formatter *Formatter) SetOption(opts ...Option) { + for _, opt := range opts { + opt(formatter) + } +} + +// String implements fmt.Stringer +func (formatter *Formatter) String() string { + var strs []string + + if configName := formatter.selectedConfigName; configName != "" { + strs = append(strs, configName) + } + + // get all non-column options + opts := formatter.Options().FilterByNamePrefixes(false, columnPrefix) + + strs = append(strs, opts.Names()...) + strs = util.RemoveDuplicatesFromList(strs) + + return strings.Join(strs, ",") +} + +func (formatter *Formatter) Options() config.Options { + var opts config.Options + + if preset := formatter.presetConfigs.Find(formatter.selectedConfigName); preset != nil { + opts = preset.Options() + } + + return append(opts, formatter.userOpts...) +} + +// SetFormat parses options in the given `str` and sets them to the formatter. +func (formatter *Formatter) SetFormat(str string) error { + parts := optionSeparatorReg.FindAllString(str, -1) + for i, str := range parts { + if i < len(parts)-1 { + str = str[:len(str)-1] + } + str = strings.ReplaceAll(str, `\`+optionSeparator, optionSeparator) + + if selectedConfig := formatter.presetConfigs.Find(str); selectedConfig != nil { + formatter.selectedConfigName = selectedConfig.Name() + continue + } + + opt, err := config.ParseOption(str) + if err != nil { + return err + } + + formatter.userOpts = append(formatter.userOpts, opt) + } + + return nil +} + +func (formatter *Formatter) GetOption(name string, levels ...log.Level) *config.Option { + var opts config.Options + + if preset := formatter.presetConfigs.Find(formatter.selectedConfigName); preset != nil { + if opt := preset.Options().FindByName(name).MergeIntoOneWithPriorityByLevels(levels...); opt != nil { + opts = append(opts, opt) + } + } + + if opt := formatter.userOpts.FindByName(name).MergeIntoOneWithPriorityByLevels(levels...); opt != nil { + opts = append(opts, opt) + } + + return opts.MergeIntoOne() +} + +func (formatter *Formatter) getOptionsByNamePrefixAndLevel(prefix string, levels ...log.Level) config.Options { + optsNames := formatter.Options().FilterByNamePrefixes(true, columnPrefix).Names() + optsNames = util.RemoveDuplicatesFromList(optsNames) + + opts := make(config.Options, len(optsNames)) + + for i, optName := range optsNames { + opts[i] = formatter.GetOption(optName, levels...) + } + + return opts +} + +func (formatter *Formatter) optionValue(name string, level log.Level, entry *config.Entry) (string, bool) { + opt := formatter.GetOption(name, level) + if opt == nil { + return "", true + } + + return opt.Value(entry), opt.Enable() +} + +// Format implements logrus.Formatter +func (formatter *Formatter) Format(entry *logrus.Entry) ([]byte, error) { + buf := entry.Buffer + if buf == nil { + buf = new(bytes.Buffer) + } + + var ( + colsFields = make(log.Fields) + colsNames []string + colsValues []string + level = log.FromLogrusLevel(entry.Level) + fields = log.Fields(entry.Data) + msg = entry.Message + curTime = entry.Time + disableColor bool + jsonFormat bool + keyValueFormat bool + ) + + if opt := formatter.GetOption(OptionColor, level); opt != nil && !opt.Enable() { + disableColor = true + } + + if opt := formatter.GetOption(OptionJSON, level); opt != nil && opt.Enable() { + disableColor = true + jsonFormat = true + } + + if opt := formatter.GetOption(OptionKeyValue, level); opt != nil && opt.Enable() { + disableColor = true + keyValueFormat = true + } + + presetEntry := config.NewEntry(formatter.baseTime, curTime, level, msg, fields, disableColor) + + opts := formatter.getOptionsByNamePrefixAndLevel(columnPrefix, level).MergeByName().SortByValue() + + for _, opt := range opts { + if !opt.Enable() { + continue + } + if val := opt.Value(presetEntry); val != "" { + if disableColor { + val = log.RemoveAllASCISeq(val) + } + + colName := opt.Name()[columnPrefixLen:] + colsNames = append(colsNames, colName) + colsValues = append(colsValues, val) + colsFields[colName] = val + } + } + + for key, value := range fields { + if val, ok := formatter.optionValue(key, level, presetEntry); !ok { + delete(fields, key) + continue + } else if val != "" { + fields[key] = val + continue + } + + if val, ok := value.(string); ok && disableColor { + fields[key] = log.RemoveAllASCISeq(val) + } + } + + if len(colsValues) == 0 && len(fields) == 0 { + return nil, nil + } + + if keyValueFormat { + return formatter.keyValueFormat(buf, level, colsNames, colsFields, fields) + } + + if jsonFormat { + return formatter.jsonFormat(buf, level, fields, colsFields) + } + + return formatter.textFormat(buf, fields, colsValues) +} + +func (formatter *Formatter) textFormat(buf *bytes.Buffer, fields log.Fields, colsValues []string) ([]byte, error) { + if _, err := fmt.Fprint(buf, strings.Join(colsValues, " ")); err != nil { + return nil, errors.WithStackTrace(err) + } + + for _, key := range fields.Keys() { + value := fields[key] + if err := formatter.appendKeyValue(buf, key, value); err != nil { + return nil, err + } + } + + if err := buf.WriteByte('\n'); err != nil { + return nil, errors.WithStackTrace(err) + } + + return buf.Bytes(), nil +} + +func (formatter *Formatter) keyValueFormat(buf *bytes.Buffer, level log.Level, colsNames []string, colsFields, fields log.Fields) ([]byte, error) { + for _, key := range colsNames { + val, ok := colsFields[key] + if !ok { + continue + } + if err := formatter.appendKeyValue(buf, key, val); err != nil { + return nil, err + } + } + + for _, key := range fields.Keys() { + val := fields[key] + if err := formatter.appendKeyValue(buf, key, val); err != nil { + return nil, err + } + } + + if err := buf.WriteByte('\n'); err != nil { + return nil, errors.WithStackTrace(err) + } + + return buf.Bytes(), nil +} + +func (formatter *Formatter) jsonFormat(buf *bytes.Buffer, level log.Level, fields, colsFields log.Fields) ([]byte, error) { + encoder := json.NewEncoder(buf) + + if opt := formatter.GetOption(OptionIndent, level); opt != nil && opt.Enable() { + encoder.SetIndent("", " ") + } + + maps.Copy(fields, colsFields) + + if err := encoder.Encode(fields); err != nil { + return nil, errors.Errorf("failed to marshal fields to JSON, %w", err) + } + + return buf.Bytes(), nil +} + +func (formatter *Formatter) appendKeyValue(buf *bytes.Buffer, key string, value interface{}) error { + if err := formatter.appendKey(buf, key); err != nil { + return err + } + + if err := formatter.appendValue(buf, value); err != nil { + return err + } + + return nil +} + +func (format *Formatter) appendKey(buf *bytes.Buffer, key interface{}) error { + keyFmt := "%s=" + if buf.Len() > 0 { + keyFmt = " " + keyFmt + } + + if _, err := fmt.Fprintf(buf, keyFmt, key); err != nil { + return errors.WithStackTrace(err) + } + + return nil +} + +func (format *Formatter) appendValue(buf *bytes.Buffer, value interface{}) error { + var str string + + switch value := value.(type) { + case string: + str = value + case error: + str = value.Error() + default: + if _, err := fmt.Fprint(buf, value); err != nil { + return errors.WithStackTrace(err) + } + + return nil + } + + valueFmt := "%v" + if format.needsQuoting(str) { + valueFmt = format.quoteCharacter + valueFmt + format.quoteCharacter + } + + if _, err := fmt.Fprintf(buf, valueFmt, value); err != nil { + return errors.WithStackTrace(err) + } + + return nil +} + +func (format *Formatter) needsQuoting(text string) bool { + if format.quoteEmptyFields && len(text) == 0 { + return true + } + + for _, ch := range text { + if !((ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == '-' || ch == '.') { + return true + } + } + + return false +} diff --git a/pkg/log/ff/options.go b/pkg/log/ff/options.go new file mode 100644 index 0000000000..dec3ffd99f --- /dev/null +++ b/pkg/log/ff/options.go @@ -0,0 +1,31 @@ +package format + +import "github.com/gruntwork-io/terragrunt/pkg/log/format/config" + +type Option func(*Formatter) + +func WithPresetConfigs(cfgs ...*config.Config) Option { + return func(formatter *Formatter) { + formatter.presetConfigs = cfgs + } +} + +func WithSelectedConfig(name string) Option { + return func(formatter *Formatter) { + formatter.selectedConfigName = name + } +} + +// WithQuoteCharacter overrides the default quoting character " with something else. For example: ', or `. +func WithQuoteCharacter(quoteCharacter string) Option { + return func(formatter *Formatter) { + formatter.quoteCharacter = quoteCharacter + } +} + +// WithQuoteEmptyFields wraps empty fields in quotes if true. +func WithQuoteEmptyFields() Option { + return func(formatter *Formatter) { + formatter.quoteEmptyFields = true + } +} diff --git a/pkg/log/fields.go b/pkg/log/fields.go index 2c526731ce..0bca9c0749 100644 --- a/pkg/log/fields.go +++ b/pkg/log/fields.go @@ -5,7 +5,7 @@ import "sort" // Fields is the type used to pass arguments to `WithFields`. type Fields map[string]interface{} -func (fields Fields) Keys(removeKeys ...string) []string { +func (fields Fields) RemoveKeys(removeKeys ...string) []string { var keys []string for key := range fields { diff --git a/pkg/log/format/color.go b/pkg/log/format.old/color.go similarity index 100% rename from pkg/log/format/color.go rename to pkg/log/format.old/color.go diff --git a/pkg/log/format.old/formatter.go b/pkg/log/format.old/formatter.go new file mode 100644 index 0000000000..13cb66bb6e --- /dev/null +++ b/pkg/log/format.old/formatter.go @@ -0,0 +1,285 @@ +// Package format provides a logrus formatter that formats log entries in a structured way. +package format + +import ( + "bytes" + "fmt" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/gruntwork-io/terragrunt/internal/errors" + "github.com/gruntwork-io/terragrunt/pkg/log" + "github.com/sirupsen/logrus" + "golang.org/x/exp/maps" +) + +const ( + defaultTimestampForFormattedLayout = "15:04:05.000" + defaultTimestamp = time.RFC3339 + + WorkDirKeyName = "prefix" + TFPathKeyName = "tfBinary" +) + +// Formatter implements logrus.Formatter +var _ logrus.Formatter = new(Formatter) + +type PrefixStyle interface { + // ColorFunc creates a closure to avoid computation ANSI color code. + ColorFunc(prefixName string) ColorFunc +} + +type Formatter struct { + // Disable formatted layout + DisableLogFormatting bool + + // Force disabling colors. For a TTY colors are enabled by default. + DisableColors bool + + // Disable the conversion of the log levels to uppercase + DisableUppercase bool + + // Timestamp format to use for display when a full timestamp is printed. + TimestampFormat string + + // The fields are sorted by default for a consistent output. + DisableSorting bool + + // Wrap empty fields in quotes if true. + QuoteEmptyFields bool + + // Can be set to the override the default quoting character " with something else. For example: ', or `. + QuoteCharacter string + + // PrefixStyle is used to assign different styles (colors) to each prefix. + PrefixStyle PrefixStyle + + // Color scheme to use. + colorScheme compiledColorScheme +} + +// NewFormatter returns a new Formatter instance with default values. +func NewFormatter() *Formatter { + return &Formatter{ + colorScheme: defaultColorScheme.Compile(), + PrefixStyle: NewPrefixStyle(), + } +} + +func (formatter *Formatter) SetColorScheme(colorScheme *ColorScheme) { + maps.Copy(formatter.colorScheme, colorScheme.Compile()) +} + +// Format implements logrus.Formatter +func (formatter *Formatter) Format(entry *logrus.Entry) ([]byte, error) { + buf := entry.Buffer + if buf == nil { + buf = new(bytes.Buffer) + } + + if !formatter.DisableLogFormatting { + if err := formatter.printFormatted(buf, entry); err != nil { + return nil, err + } + } else { + if err := formatter.printKeyValue(buf, entry); err != nil { + return nil, err + } + } + + if err := buf.WriteByte('\n'); err != nil { + return nil, errors.New(err) + } + + return buf.Bytes(), nil +} + +func (formatter *Formatter) printKeyValue(buf *bytes.Buffer, entry *logrus.Entry) error { + timestampFormat := formatter.TimestampFormat + if timestampFormat == "" { + timestampFormat = defaultTimestamp + } + + if err := formatter.appendKeyValue(buf, "time", entry.Time.Format(timestampFormat), false); err != nil { + return err + } + + if err := formatter.appendKeyValue(buf, "level", log.FromLogrusLevel(entry.Level), true); err != nil { + return err + } + + if val, ok := entry.Data[WorkDirKeyName]; ok && val != nil { + if val, ok := val.(string); ok && val != "" { + if err := formatter.appendKeyValue(buf, "prefix", val, true); err != nil { + return err + } + } + } + + if val, ok := entry.Data[TFPathKeyName]; ok && val != nil { + if val, ok := val.(string); ok && val != "" { + if err := formatter.appendKeyValue(buf, "binary", filepath.Base(val), true); err != nil { + return err + } + } + } + + if entry.Message != "" { + if err := formatter.appendKeyValue(buf, "msg", entry.Message, true); err != nil { + return err + } + } + + keys := formatter.keys(entry.Data, WorkDirKeyName, TFPathKeyName) + for _, key := range keys { + if err := formatter.appendKeyValue(buf, key, entry.Data[key], true); err != nil { + return err + } + } + + return nil +} + +func (formatter *Formatter) printFormatted(buf *bytes.Buffer, entry *logrus.Entry) error { + level := fmt.Sprintf("%-6s ", log.FromLogrusLevel(entry.Level)) + if !formatter.DisableUppercase { + level = strings.ToUpper(level) + } + + var ( + prefix string + tfBinary string + timestamp string + ) + + if val, ok := entry.Data[WorkDirKeyName]; ok && val != nil && val != "." { + if val, ok := val.(string); ok && val != "" { + prefix = fmt.Sprintf("[%s] ", val) + } + } + + if val, ok := entry.Data[TFPathKeyName]; ok && val != nil { + if val, ok := val.(string); ok && val != "" { + tfBinary = val + ": " + } + } + + timestampFormat := formatter.TimestampFormat + if timestampFormat == "" { + timestampFormat = defaultTimestampForFormattedLayout + } + + timestamp = entry.Time.Format(timestampFormat) + " " + + if !formatter.DisableColors { + level = formatter.colorScheme.LevelColorFunc(log.FromLogrusLevel(entry.Level))(level) + prefix = formatter.PrefixStyle.ColorFunc(prefix)(prefix) + tfBinary = formatter.colorScheme.ColorFunc(TFBinaryStyle)(tfBinary) + timestamp = formatter.colorScheme.ColorFunc(TimestampStyle)(timestamp) + } + + if _, err := fmt.Fprintf(buf, "%s%s%s%s%s", timestamp, level, prefix, tfBinary, entry.Message); err != nil { + return errors.New(err) + } + + keys := formatter.keys(entry.Data, WorkDirKeyName, TFPathKeyName) + for _, key := range keys { + value := entry.Data[key] + if err := formatter.appendKeyValue(buf, key, value, true); err != nil { + return err + } + } + + return nil +} + +func (formatter *Formatter) appendKeyValue(buf *bytes.Buffer, key string, value interface{}, appendSpace bool) error { + keyFmt := "%s=" + if appendSpace { + keyFmt = " " + keyFmt + } + + if _, err := fmt.Fprintf(buf, keyFmt, key); err != nil { + return errors.New(err) + } + + if err := formatter.appendValue(buf, value); err != nil { + return err + } + + return nil +} + +func (formatter *Formatter) appendValue(buf *bytes.Buffer, value interface{}) error { + var str string + + switch value := value.(type) { + case string: + str = value + case error: + str = value.Error() + default: + if _, err := fmt.Fprint(buf, value); err != nil { + return errors.New(err) + } + + return nil + } + + valueFmt := "%v" + if formatter.needsQuoting(str) { + valueFmt = formatter.QuoteCharacter + valueFmt + formatter.QuoteCharacter + } + + if _, err := fmt.Fprintf(buf, valueFmt, value); err != nil { + return errors.New(err) + } + + return nil +} + +func (formatter *Formatter) keys(data logrus.Fields, removeKeys ...string) []string { + var ( + keys []string + ) + + for key := range data { + var skip bool + + for _, removeKey := range removeKeys { + if key == removeKey { + skip = true + break + } + } + + if !skip { + keys = append(keys, key) + } + } + + if !formatter.DisableSorting { + sort.Strings(keys) + } + + return keys +} + +func (formatter *Formatter) needsQuoting(text string) bool { + if formatter.QuoteEmptyFields && len(text) == 0 { + return true + } + + for _, ch := range text { + if !((ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == '-' || ch == '.') { + return true + } + } + + return false +} diff --git a/pkg/log/format/json_formatter.go b/pkg/log/format.old/json_formatter.go similarity index 100% rename from pkg/log/format/json_formatter.go rename to pkg/log/format.old/json_formatter.go diff --git a/pkg/log/format/prefix_style.go b/pkg/log/format.old/prefix_style.go similarity index 100% rename from pkg/log/format/prefix_style.go rename to pkg/log/format.old/prefix_style.go diff --git a/pkg/log/format/format.go b/pkg/log/format/format.go new file mode 100644 index 0000000000..f02f91554c --- /dev/null +++ b/pkg/log/format/format.go @@ -0,0 +1,81 @@ +package format + +import ( + "fmt" + + . "github.com/gruntwork-io/terragrunt/pkg/log/format/options" + . "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" +) + +const ( + PrettyFormat = "pretty" + JSONFormat = "json" +) + +var presets = map[string]Placeholders{ + PrettyFormat: Placeholders{ + Time( + TimeFormat(fmt.Sprintf("%s:%s:%s%s", Hour24Zero, MinZero, SecZero, MilliSec)), + Suffix(" "), + Color(BlackHColor), + ), + Level( + Width(6), + Case(UpperCase), + Suffix(" "), + Color(AutoColor), + ), + Field(WorkDirKeyName, + PathFormat(ShortPath), + Prefix("["), + Suffix("] "), + Color(RandomColor), + ), + Field(TFPathKeyName, + PathFormat(FilenamePath), + Suffix(": "), + Color(CyanColor), + ), + Message( + PathFormat(RelativePath), + ), + }, + JSONFormat: Placeholders{ + PlainText(`{"time":"`), + Time( + TimeFormat(fmt.Sprintf("%s:%s:%s%s", Hour24Zero, MinZero, SecZero, MilliSec)), + Escape(JSONEscape), + ), + PlainText(`", "level":"`), + Level( + Escape(JSONEscape), + ), + PlainText(`", "work-dir":"`), + Field(WorkDirKeyName, + PathFormat(ShortPath), + Escape(JSONEscape), + ), + PlainText(`", "tfpath":"`), + Field(TFPathKeyName, + PathFormat(FilenamePath), + Escape(JSONEscape), + ), + PlainText(`", "message":"`), + Message( + PathFormat(RelativePath), + Color(DisableColor), + Escape(JSONEscape), + ), + PlainText(`"}`), + }, +} + +func ParseFormat(str string) Placeholders { + for name, format := range presets { + if name == str { + return format + } + } + + return nil +} diff --git a/pkg/log/format/formatter.go b/pkg/log/format/formatter.go index 60765eb0a5..a9328ae728 100644 --- a/pkg/log/format/formatter.go +++ b/pkg/log/format/formatter.go @@ -1,285 +1,76 @@ -// Package format provides a logrus formatter that formats log entries in a structured way. package format import ( "bytes" - "fmt" - "path/filepath" - "sort" - "strings" - "time" "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" - "github.com/sirupsen/logrus" - "golang.org/x/exp/maps" -) - -const ( - defaultTimestampForFormattedLayout = "15:04:05.000" - defaultTimestamp = time.RFC3339 - - PrefixKeyName = "prefix" - TFBinaryKeyName = "tfBinary" + "github.com/gruntwork-io/terragrunt/pkg/log/format/options" + "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" ) // Formatter implements logrus.Formatter -var _ logrus.Formatter = new(Formatter) - -type PrefixStyle interface { - // ColorFunc creates a closure to avoid computation ANSI color code. - ColorFunc(prefixName string) ColorFunc -} +var _ log.Formatter = new(Formatter) type Formatter struct { - // Disable formatted layout - DisableLogFormatting bool - - // Force disabling colors. For a TTY colors are enabled by default. - DisableColors bool - - // Disable the conversion of the log levels to uppercase - DisableUppercase bool - - // Timestamp format to use for display when a full timestamp is printed. - TimestampFormat string - - // The fields are sorted by default for a consistent output. - DisableSorting bool - - // Wrap empty fields in quotes if true. - QuoteEmptyFields bool - - // Can be set to the override the default quoting character " with something else. For example: ', or `. - QuoteCharacter string - - // PrefixStyle is used to assign different styles (colors) to each prefix. - PrefixStyle PrefixStyle - - // Color scheme to use. - colorScheme compiledColorScheme + format placeholders.Placeholders + disableColors bool + relativePather *options.RelativePather } // NewFormatter returns a new Formatter instance with default values. func NewFormatter() *Formatter { return &Formatter{ - colorScheme: defaultColorScheme.Compile(), - PrefixStyle: NewPrefixStyle(), + format: presets[PrettyFormat], } } -func (formatter *Formatter) SetColorScheme(colorScheme *ColorScheme) { - maps.Copy(formatter.colorScheme, colorScheme.Compile()) -} +// Format implements logrus.Format +func (formatter *Formatter) Format(entry *log.Entry) ([]byte, error) { + if formatter.format == nil { + return nil, nil + } -// Format implements logrus.Formatter -func (formatter *Formatter) Format(entry *logrus.Entry) ([]byte, error) { buf := entry.Buffer if buf == nil { buf = new(bytes.Buffer) } - if !formatter.DisableLogFormatting { - if err := formatter.printFormatted(buf, entry); err != nil { - return nil, err - } - } else { - if err := formatter.printKeyValue(buf, entry); err != nil { - return nil, err - } - } - - if err := buf.WriteByte('\n'); err != nil { - return nil, errors.New(err) - } - - return buf.Bytes(), nil -} - -func (formatter *Formatter) printKeyValue(buf *bytes.Buffer, entry *logrus.Entry) error { - timestampFormat := formatter.TimestampFormat - if timestampFormat == "" { - timestampFormat = defaultTimestamp - } - - if err := formatter.appendKeyValue(buf, "time", entry.Time.Format(timestampFormat), false); err != nil { - return err - } - - if err := formatter.appendKeyValue(buf, "level", log.FromLogrusLevel(entry.Level), true); err != nil { - return err - } - - if val, ok := entry.Data[PrefixKeyName]; ok && val != nil { - if val, ok := val.(string); ok && val != "" { - if err := formatter.appendKeyValue(buf, "prefix", val, true); err != nil { - return err - } - } - } + str := formatter.format.Evaluate(&options.Data{ + Entry: entry, + DisableColors: formatter.disableColors, + RelativePather: formatter.relativePather, + }) - if val, ok := entry.Data[TFBinaryKeyName]; ok && val != nil { - if val, ok := val.(string); ok && val != "" { - if err := formatter.appendKeyValue(buf, "binary", filepath.Base(val), true); err != nil { - return err - } + if str != "" { + if _, err := buf.WriteString(str); err != nil { + return nil, errors.New(err) } - } - if entry.Message != "" { - if err := formatter.appendKeyValue(buf, "msg", entry.Message, true); err != nil { - return err + if err := buf.WriteByte('\n'); err != nil { + return nil, errors.New(err) } } - keys := formatter.keys(entry.Data, PrefixKeyName, TFBinaryKeyName) - for _, key := range keys { - if err := formatter.appendKeyValue(buf, key, entry.Data[key], true); err != nil { - return err - } - } - - return nil + return buf.Bytes(), nil } -func (formatter *Formatter) printFormatted(buf *bytes.Buffer, entry *logrus.Entry) error { - level := fmt.Sprintf("%-6s ", log.FromLogrusLevel(entry.Level)) - if !formatter.DisableUppercase { - level = strings.ToUpper(level) - } - - var ( - prefix string - tfBinary string - timestamp string - ) - - if val, ok := entry.Data[PrefixKeyName]; ok && val != nil && val != "." { - if val, ok := val.(string); ok && val != "" { - prefix = fmt.Sprintf("[%s] ", val) - } - } - - if val, ok := entry.Data[TFBinaryKeyName]; ok && val != nil { - if val, ok := val.(string); ok && val != "" { - tfBinary = val + ": " - } - } - - timestampFormat := formatter.TimestampFormat - if timestampFormat == "" { - timestampFormat = defaultTimestampForFormattedLayout - } - - timestamp = entry.Time.Format(timestampFormat) + " " - - if !formatter.DisableColors { - level = formatter.colorScheme.LevelColorFunc(log.FromLogrusLevel(entry.Level))(level) - prefix = formatter.PrefixStyle.ColorFunc(prefix)(prefix) - tfBinary = formatter.colorScheme.ColorFunc(TFBinaryStyle)(tfBinary) - timestamp = formatter.colorScheme.ColorFunc(TimestampStyle)(timestamp) - } - - if _, err := fmt.Fprintf(buf, "%s%s%s%s%s", timestamp, level, prefix, tfBinary, entry.Message); err != nil { - return errors.New(err) - } - - keys := formatter.keys(entry.Data, PrefixKeyName, TFBinaryKeyName) - for _, key := range keys { - value := entry.Data[key] - if err := formatter.appendKeyValue(buf, key, value, true); err != nil { - return err - } - } - - return nil +// DisableColors disables log color +func (formatter *Formatter) DisableColors() { + formatter.disableColors = true } -func (formatter *Formatter) appendKeyValue(buf *bytes.Buffer, key string, value interface{}, appendSpace bool) error { - keyFmt := "%s=" - if appendSpace { - keyFmt = " " + keyFmt - } - - if _, err := fmt.Fprintf(buf, keyFmt, key); err != nil { - return errors.New(err) - } - - if err := formatter.appendValue(buf, value); err != nil { +func (formatter *Formatter) CreateRelativePathsCache(baseDir string) error { + pather, err := options.NewRelativePather(baseDir) + if err != nil { return err } - return nil -} - -func (formatter *Formatter) appendValue(buf *bytes.Buffer, value interface{}) error { - var str string - - switch value := value.(type) { - case string: - str = value - case error: - str = value.Error() - default: - if _, err := fmt.Fprint(buf, value); err != nil { - return errors.New(err) - } - - return nil - } - - valueFmt := "%v" - if formatter.needsQuoting(str) { - valueFmt = formatter.QuoteCharacter + valueFmt + formatter.QuoteCharacter - } - - if _, err := fmt.Fprintf(buf, valueFmt, value); err != nil { - return errors.New(err) - } + formatter.relativePather = pather return nil } -func (formatter *Formatter) keys(data logrus.Fields, removeKeys ...string) []string { - var ( - keys []string - ) - - for key := range data { - var skip bool - - for _, removeKey := range removeKeys { - if key == removeKey { - skip = true - break - } - } - - if !skip { - keys = append(keys, key) - } - } - - if !formatter.DisableSorting { - sort.Strings(keys) - } - - return keys -} - -func (formatter *Formatter) needsQuoting(text string) bool { - if formatter.QuoteEmptyFields && len(text) == 0 { - return true - } - - for _, ch := range text { - if !((ch >= 'a' && ch <= 'z') || - (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch <= '9') || - ch == '-' || ch == '.') { - return true - } - } - - return false +func (formatter *Formatter) SetFormat(format placeholders.Placeholders) { + formatter.format = format } diff --git a/pkg/log/format/options/align.go b/pkg/log/format/options/align.go new file mode 100644 index 0000000000..7c0d4eeee4 --- /dev/null +++ b/pkg/log/format/options/align.go @@ -0,0 +1,53 @@ +package options + +import ( + "strings" +) + +const AlignOptionName = "align" + +const ( + NoneAlign AlignValue = iota + LeftAlign + CenterAlign + RightAlign +) + +var alignValues = CommonMapValues[AlignValue]{ + LeftAlign: "left", + CenterAlign: "center", + RightAlign: "right", +} + +type AlignValue byte + +type align struct { + *CommonOption[AlignValue] +} + +func (option *align) Evaluate(data *Data, str string) string { + leftSpaces := len(str) - len(strings.TrimLeft(str, " ")) + rightSpaces := len(str) - len(strings.TrimRight(str, " ")) + + switch option.value { + case LeftAlign: + return strings.TrimLeft(str, " ") + strings.Repeat(" ", leftSpaces) + case RightAlign: + return strings.Repeat(" ", rightSpaces) + strings.TrimRight(str, " ") + case CenterAlign: + spaces := leftSpaces + rightSpaces + + rightSpaces = (spaces - spaces%2) / 2 + leftSpaces = spaces - rightSpaces + + return strings.Repeat(" ", leftSpaces) + strings.TrimSpace(str) + strings.Repeat(" ", rightSpaces) + } + + return str +} + +func Align(value AlignValue) Option { + return &align{ + CommonOption: NewCommonOption[AlignValue](AlignOptionName, value, alignValues), + } +} diff --git a/pkg/log/format/options/case.go b/pkg/log/format/options/case.go new file mode 100644 index 0000000000..7c7cb42635 --- /dev/null +++ b/pkg/log/format/options/case.go @@ -0,0 +1,48 @@ +package options + +import ( + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +const CaseOptionName = "case" + +const ( + NoneCase CaseValue = iota + UpperCase + LowerCase + CapitalizeCase +) + +var textCaseValues = CommonMapValues[CaseValue]{ + UpperCase: "upper", + LowerCase: "lower", + CapitalizeCase: "capitalize", +} + +type CaseValue byte + +type textCase struct { + *CommonOption[CaseValue] +} + +func (option *textCase) Evaluate(data *Data, str string) string { + switch option.value { + case UpperCase: + return strings.ToUpper(str) + case LowerCase: + return strings.ToLower(str) + case CapitalizeCase: + return cases.Title(language.English, cases.Compact).String(str) + } + + return str +} + +func Case(value CaseValue) Option { + return &textCase{ + CommonOption: NewCommonOption[CaseValue](CaseOptionName, value, textCaseValues), + } +} diff --git a/pkg/log/format/options/color.go b/pkg/log/format/options/color.go new file mode 100644 index 0000000000..91c529b724 --- /dev/null +++ b/pkg/log/format/options/color.go @@ -0,0 +1,201 @@ +package options + +import ( + "github.com/gruntwork-io/terragrunt/pkg/log" + "github.com/mgutz/ansi" + "github.com/puzpuzpuz/xsync/v3" +) + +const ColorOptionName = "color" + +const ( + NoneColor ColorValue = iota + DisableColor + RedColor + WhiteColor + YellowColor + GreenColor + CyanColor + BlueHColor + BlackHColor + AutoColor + RandomColor + + Color66 + Color67 + Color95 + Color96 + Color102 + Color103 + Color108 + Color109 + Color138 + Color139 + Color144 + Color145 +) + +var colorValues = CommonMapValues[ColorValue]{ + RedColor: "red", + WhiteColor: "white", + YellowColor: "yellow", + GreenColor: "green", + CyanColor: "cyan", + BlueHColor: "light-blue", + BlackHColor: "light-black", + AutoColor: "auto", + RandomColor: "random", + DisableColor: "disable", +} + +var ( + colorScheme = ColorScheme{ + RedColor: "red", + WhiteColor: "white", + YellowColor: "yellow", + GreenColor: "green", + CyanColor: "cyan", + BlueHColor: "blue+h", + BlackHColor: "black+h", + + Color66: "66", + Color67: "67", + Color95: "95", + Color96: "96", + Color102: "102", + Color103: "103", + Color108: "108", + Color109: "109", + Color138: "138", + Color139: "139", + Color144: "144", + Color145: "145", + } +) + +type ColorScheme map[ColorValue]ColorStyle + +func (scheme ColorScheme) Compile() compiledColorScheme { + compiled := make(compiledColorScheme, len(scheme)) + + for name, val := range scheme { + compiled[name] = val.ColorFunc() + } + + return compiled +} + +type ColorStyle string + +func (val ColorStyle) ColorFunc() ColorFunc { + return ansi.ColorFunc(string(val)) +} + +type ColorFunc func(string) string + +type ColorValue byte + +type compiledColorScheme map[ColorValue]ColorFunc + +type ColorOption struct { + *CommonOption[ColorValue] + compiledColors compiledColorScheme + randomColor *randomColor +} + +func (color *ColorOption) Evaluate(data *Data, str string) string { + value := color.value + + if value == DisableColor || data.DisableColors { + return log.RemoveAllASCISeq(str) + } + + if value == AutoColor && data.AutoColorFn != nil { + value = data.AutoColorFn() + } + + if value == RandomColor && color.randomColor != nil { + value = color.randomColor.Value(str) + } + + if colorFn, ok := color.compiledColors[value]; ok { + str = colorFn(str) + } + + return str + +} + +func (ColorOption *ColorOption) SetValue(str string) error { + val, err := colorValues.Parse(str) + if err != nil { + return err + } + + ColorOption.value = val + + return nil +} + +func Color(val ColorValue) Option { + return &ColorOption{ + CommonOption: NewCommonOption[ColorValue](ColorOptionName, val, colorValues), + compiledColors: colorScheme.Compile(), + randomColor: newRandomColor(), + } +} + +var ( + // defaultAutoColorValues contains ANSI color codes that are assigned sequentially to each unique text in a rotating order + // https://user-images.githubusercontent.com/995050/47952855-ecb12480-df75-11e8-89d4-ac26c50e80b9.png + // https://www.hackitu.de/termcolor256/ + defaultAutoColorValues = []ColorValue{ + Color66, + Color67, + Color95, + Color96, + Color102, + Color103, + Color108, + Color109, + Color138, + Color139, + Color144, + Color145, + } +) + +type randomColor struct { + // cache stores unique text with their color code. + // We use [xsync.MapOf](https://github.com/puzpuzpuz/xsync?tab=readme-ov-file#map) instaed of standard `sync.Map` since it's faster and has generic types. + cache *xsync.MapOf[string, ColorValue] + values []ColorValue + + // nextStyleIndex is used to get the next style from the `codes` list for a newly discovered text. + nextStyleIndex int +} + +func newRandomColor() *randomColor { + return &randomColor{ + cache: xsync.NewMapOf[string, ColorValue](), + values: defaultAutoColorValues, + } +} + +func (color *randomColor) Value(text string) ColorValue { + if colorCode, ok := color.cache.Load(text); ok { + return colorCode + } + + if color.nextStyleIndex >= len(color.values) { + color.nextStyleIndex = 0 + } + + colorCode := color.values[color.nextStyleIndex] + + color.cache.Store(text, colorCode) + + color.nextStyleIndex++ + + return colorCode +} diff --git a/pkg/log/format/options/common.go b/pkg/log/format/options/common.go new file mode 100644 index 0000000000..cd6fd43b9f --- /dev/null +++ b/pkg/log/format/options/common.go @@ -0,0 +1,76 @@ +package options + +import ( + "fmt" + "strings" + + "github.com/gruntwork-io/terragrunt/internal/errors" + "golang.org/x/exp/maps" +) + +type CommonOption[T comparable] struct { + name string + value T + values OptionValues[T] +} + +func NewCommonOption[T comparable](name string, value T, values OptionValues[T]) *CommonOption[T] { + return &CommonOption[T]{ + name: name, + value: value, + values: values, + } +} + +func (option *CommonOption[T]) Name() string { + return option.name +} + +func (option *CommonOption[T]) Value() T { + return option.value +} + +func (option *CommonOption[T]) String() string { + return fmt.Sprintf("%v", option.value) +} + +func (option *CommonOption[T]) Evaluate(data *Data, str string) string { + return str +} + +func (option *CommonOption[T]) SetValue(str string) error { + val, err := option.values.Parse(str) + if err != nil { + return err + } + + option.value = val + + return nil +} + +type CommonMapValues[T comparable] map[T]string + +func (valNames CommonMapValues[T]) Parse(str string) (T, error) { + for val, name := range valNames { + if name == str { + return val, nil + } + } + + t := new(T) + + return *t, errors.Errorf("available values: %s", strings.Join(maps.Values(valNames), ",")) +} + +func (valNames CommonMapValues[T]) Filter(vals ...T) CommonMapValues[T] { + filtered := make(map[T]string, len(vals)) + + for _, val := range vals { + if name, ok := valNames[val]; ok { + filtered[val] = name + } + } + + return filtered +} diff --git a/pkg/log/format/options/escape.go b/pkg/log/format/options/escape.go new file mode 100644 index 0000000000..82b0e6594c --- /dev/null +++ b/pkg/log/format/options/escape.go @@ -0,0 +1,44 @@ +package options + +import ( + "encoding/json" + "fmt" +) + +const EscapeOptionName = "escape" + +const ( + NoneEscape EscapeValue = iota + JSONEscape +) + +var textEscapeValues = CommonMapValues[EscapeValue]{ + JSONEscape: "json", +} + +type EscapeValue byte + +type textEscape struct { + *CommonOption[EscapeValue] +} + +func (option *textEscape) Evaluate(data *Data, str string) string { + switch option.value { + case JSONEscape: + b, err := json.Marshal(str) + if err != nil { + fmt.Printf("Failed to marhsal %q, %v\n", str, err) + } + + // Trim the beginning and trailing " character. + return string(b[1 : len(b)-1]) + } + + return str +} + +func Escape(value EscapeValue) Option { + return &textEscape{ + CommonOption: NewCommonOption[EscapeValue](EscapeOptionName, value, textEscapeValues), + } +} diff --git a/pkg/log/format/options/level_format.go b/pkg/log/format/options/level_format.go new file mode 100644 index 0000000000..1c51b6faaf --- /dev/null +++ b/pkg/log/format/options/level_format.go @@ -0,0 +1,38 @@ +package options + +const LevelFormatOptionName = "format" + +const ( + LevelFormatTiny LevelFormatValue = iota + LevelFormatShort + LevelFormatFull +) + +var levelFormatValues = CommonMapValues[LevelFormatValue]{ + LevelFormatTiny: "tiny", + LevelFormatShort: "short", + LevelFormatFull: "full", +} + +type LevelFormatValue byte + +type levelFormat struct { + *CommonOption[LevelFormatValue] +} + +func (format *levelFormat) Evaluate(data *Data, str string) string { + switch format.Value() { + case LevelFormatTiny: + return data.Level.TinyName() + case LevelFormatShort: + return data.Level.ShortName() + } + + return data.Level.FullName() +} + +func LevelFormat(val LevelFormatValue) Option { + return &levelFormat{ + CommonOption: NewCommonOption[LevelFormatValue](LevelFormatOptionName, val, levelFormatValues), + } +} diff --git a/pkg/log/format/options/option.go b/pkg/log/format/options/option.go new file mode 100644 index 0000000000..3efc98a41d --- /dev/null +++ b/pkg/log/format/options/option.go @@ -0,0 +1,62 @@ +package options + +import ( + "reflect" + + "github.com/gruntwork-io/terragrunt/pkg/log" +) + +type Options []Option + +func (options Options) Get(name string) Option { + for _, option := range options { + if option.Name() == name { + return option + } + } + + return nil +} + +func (options Options) Merge(withOptions ...Option) Options { + for i := range options { + for t := range withOptions { + if reflect.TypeOf(options[i]) == reflect.TypeOf(withOptions[t]) { + options[i] = withOptions[t] + withOptions = append(withOptions[:t], withOptions[t+1:]...) + break + } + } + } + + return append(options, withOptions...) +} + +func (options Options) Evaluate(data *Data, str string) string { + for _, option := range options { + str = option.Evaluate(data, str) + + if str == "" { + return "" + } + } + + return str +} + +type OptionValues[Value any] interface { + Parse(str string) (Value, error) +} + +type Option interface { + Name() string + Evaluate(data *Data, str string) string + SetValue(str string) error +} + +type Data struct { + *log.Entry + DisableColors bool + RelativePather *RelativePather + AutoColorFn func() ColorValue +} diff --git a/pkg/log/format/options/path_format.go b/pkg/log/format/options/path_format.go new file mode 100644 index 0000000000..14c9287f95 --- /dev/null +++ b/pkg/log/format/options/path_format.go @@ -0,0 +1,134 @@ +package options + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/gruntwork-io/terragrunt/internal/errors" + "github.com/gruntwork-io/terragrunt/pkg/log" +) + +const PathFormatOptionName = "path" + +const ( + NonePath PathFormatValue = iota + RelativePath + ShortPath + FilenamePath + DirectoryPath +) + +var pathFormatValues = CommonMapValues[PathFormatValue]{ + RelativePath: "relative", + ShortPath: "short", + FilenamePath: "filename", + DirectoryPath: "dir", +} + +type PathFormatValue byte + +type pathFormat struct { + *CommonOption[PathFormatValue] +} + +func (option *pathFormat) Evaluate(data *Data, str string) string { + switch option.value { + case RelativePath: + if data.RelativePather == nil { + break + } + + return data.RelativePather.ReplaceAbsPaths(str) + case ShortPath: + if data.RelativePather == nil { + break + } + + str = data.RelativePather.ReplaceAbsPaths(str) + + if str == log.CurDir { + return "" + } + + if strings.HasPrefix(str, log.CurDirWithSeparator) { + return str[len(log.CurDirWithSeparator):] + } + + return str + case FilenamePath: + return filepath.Base(str) + case DirectoryPath: + return filepath.Dir(str) + } + + return str +} + +func PathFormat(val PathFormatValue, allowed ...PathFormatValue) Option { + values := pathFormatValues + if len(allowed) > 0 { + values = values.Filter(allowed...) + } + + return &pathFormat{ + CommonOption: NewCommonOption[PathFormatValue](PathFormatOptionName, val, values), + } +} + +// RelativePather replaces absolute paths with relative ones, +// For better performance, during instance creation, we creating a cache of relative paths for each subdirectory of baseDir. +// +// Example of cache: +// /path/to/dir ./ +// /path/to ../ +// /path ../.. +type RelativePather struct { + relPaths []string + absPathsReg []*regexp.Regexp +} + +// NewRelativePather returns a new RelativePather instance. +// It returns an error if the cache of relative paths could not be created for the given `baseDir`. +func NewRelativePather(baseDir string) (*RelativePather, error) { + baseDir = filepath.Clean(baseDir) + + pathSeparator := string(os.PathSeparator) + dirs := strings.Split(baseDir, pathSeparator) + absPath := dirs[0] + dirs = dirs[1:] + + relPaths := make([]string, len(dirs)) + absPathsReg := make([]*regexp.Regexp, len(dirs)) + reversIndex := len(dirs) + + for _, dir := range dirs { + absPath = filepath.Join(absPath, pathSeparator, dir) + + relPath, err := filepath.Rel(baseDir, absPath) + if err != nil { + return nil, errors.New(err) + } + + reversIndex-- + relPaths[reversIndex] = relPath + + regStr := fmt.Sprintf(`(^|[^%[1]s\w])%[2]s([%[1]s"'\s]|$)`, regexp.QuoteMeta(pathSeparator), regexp.QuoteMeta(absPath)) + absPathsReg[reversIndex] = regexp.MustCompile(regStr) + } + + return &RelativePather{ + absPathsReg: absPathsReg, + relPaths: relPaths, + }, nil +} + +func (hook *RelativePather) ReplaceAbsPaths(str string) string { + for i, absPath := range hook.absPathsReg { + str = absPath.ReplaceAllString(str, "$1"+hook.relPaths[i]+"$2") + } + + return str +} diff --git a/pkg/log/format/options/prefix.go b/pkg/log/format/options/prefix.go new file mode 100644 index 0000000000..c22f4d9f13 --- /dev/null +++ b/pkg/log/format/options/prefix.go @@ -0,0 +1,23 @@ +package options + +const PrefixOptionName = "prefix" + +type prefix struct { + *CommonOption[string] +} + +func (option *prefix) Evaluate(data *Data, str string) string { + return option.value + str +} + +func (option *prefix) SetValue(str string) error { + option.value = str + + return nil +} + +func Prefix(value string) Option { + return &prefix{ + CommonOption: NewCommonOption[string](PrefixOptionName, value, nil), + } +} diff --git a/pkg/log/format/options/suffix.go b/pkg/log/format/options/suffix.go new file mode 100644 index 0000000000..359c9122ab --- /dev/null +++ b/pkg/log/format/options/suffix.go @@ -0,0 +1,23 @@ +package options + +const SuffixOptionName = "suffix" + +type suffix struct { + *CommonOption[string] +} + +func (option *suffix) Evaluate(data *Data, str string) string { + return str + option.value +} + +func (option *suffix) SetValue(str string) error { + option.value = str + + return nil +} + +func Suffix(value string) Option { + return &suffix{ + CommonOption: NewCommonOption[string](SuffixOptionName, value, nil), + } +} diff --git a/pkg/log/format/options/time_format.go b/pkg/log/format/options/time_format.go new file mode 100644 index 0000000000..393f65ad2d --- /dev/null +++ b/pkg/log/format/options/time_format.go @@ -0,0 +1,113 @@ +package options + +import ( + "sort" + "strings" + "time" + + "golang.org/x/exp/maps" +) + +const TimeFormatOptionName = "format" + +const ( + DateTime = "date-time" + DateOnly = "date-only" + TimeOnly = "time-only" + RFC3339 = "rfc3339" + RFC3339Nano = "rfc3339-nano" + + Hour24Zero = "H" + Hour12Zero = "h" + Hour12 = "g" + MinZero = "i" + SecZero = "s" + MilliSec = "v" + MicroSec = "u" + YearFull = "Y" + Year = "y" + MonthNumZero = "m" + MonthNum = "n" + MonthText = "M" + DayZero = "d" + Day = "j" + DayText = "D" + PMUpper = "A" + PMLower = "a" + TZText = "T" + TZNumWithColon = "P" + TZNum = "O" +) + +var ( + timeFormatValueMap = TimeFormatValueMap{ + YearFull: "2006", + Year: "06", + MonthNumZero: "01", + MonthNum: "1", + MonthText: "Jan", + Day: "2", + DayZero: "02", + DayText: "Mon", + PMUpper: "PM", + PMLower: "pm", + Hour24Zero: "15", + Hour12Zero: "03", + Hour12: "3", + MinZero: "04", + SecZero: "05", + MicroSec: ".000000", + MilliSec: ".000", + TZText: "MST", + TZNum: "-0700", + TZNumWithColon: "-07:00", + RFC3339: time.RFC3339, + RFC3339Nano: time.RFC3339Nano, + DateTime: time.DateTime, + DateOnly: time.DateOnly, + TimeOnly: time.TimeOnly, + } +) + +type TimeFormatValueMap map[string]string + +func (valMap TimeFormatValueMap) SortedKeys() []string { + keys := maps.Keys(valMap) + + sort.Slice(keys, func(i, j int) bool { + return timeFormatValueMap[keys[i]] < timeFormatValueMap[keys[j]] + }) + + return keys +} + +func (valMap TimeFormatValueMap) Value(str string) string { + for _, key := range valMap.SortedKeys() { + str = strings.ReplaceAll(str, key, timeFormatValueMap[key]) + } + + return str +} + +type TimeFormatValue string + +type timeFormat struct { + *CommonOption[string] + sortedValueMapKeys []string +} + +func (option *timeFormat) SetValue(str string) error { + option.value = timeFormatValueMap.Value(str) + + return nil +} + +func (option *timeFormat) Evaluate(data *Data, str string) string { + return data.Time.Format(option.Value()) +} + +func TimeFormat(str string) Option { + return &timeFormat{ + CommonOption: NewCommonOption[string](TimeFormatOptionName, timeFormatValueMap.Value(str), nil), + } +} diff --git a/pkg/log/format/options/width.go b/pkg/log/format/options/width.go new file mode 100644 index 0000000000..66455ab885 --- /dev/null +++ b/pkg/log/format/options/width.go @@ -0,0 +1,41 @@ +package options + +import ( + "strconv" + "strings" + + "github.com/gruntwork-io/terragrunt/internal/errors" + "github.com/gruntwork-io/terragrunt/pkg/log" +) + +const WidthOptionName = "width" + +type WidthValue int + +func (val WidthValue) Parse(str string) (WidthValue, error) { + if val, err := strconv.Atoi(str); err == nil { + return WidthValue(val), nil + } + + return val, errors.Errorf("incorrect option value: %s", str) +} + +type width struct { + *CommonOption[WidthValue] +} + +func (option *width) Evaluate(data *Data, str string) string { + rightSpaces := int(option.value) + + if rightSpaces -= len(log.RemoveAllASCISeq(str)); rightSpaces < 1 { + return str + } + + return str + strings.Repeat(" ", rightSpaces) +} + +func Width(value WidthValue) Option { + return &width{ + CommonOption: NewCommonOption[WidthValue](WidthOptionName, value, value), + } +} diff --git a/pkg/log/format/placeholders/common.go b/pkg/log/format/placeholders/common.go new file mode 100644 index 0000000000..8f4edb11fa --- /dev/null +++ b/pkg/log/format/placeholders/common.go @@ -0,0 +1,44 @@ +package placeholders + +import ( + "github.com/gruntwork-io/terragrunt/pkg/log/format/options" +) + +func WithCommonOptions(opts ...options.Option) options.Options { + return options.Options(append(opts, + options.Case(options.NoneCase), + options.Width(0), + options.Align(options.NoneAlign), + options.Prefix(""), + options.Suffix(""), + options.Color(options.NoneColor), + )) +} + +type CommonPlaceholder struct { + name string + opts options.Options +} + +func NewCommonPlaceholder(name string, opts ...options.Option) *CommonPlaceholder { + return &CommonPlaceholder{ + name: name, + opts: opts, + } +} + +func (common *CommonPlaceholder) Name() string { + return common.name +} + +func (common *CommonPlaceholder) GetOption(str string) options.Option { + return common.opts.Get(str) +} + +func (common *CommonPlaceholder) SetValue(str string) error { + return nil +} + +func (common *CommonPlaceholder) Evaluate(data *options.Data) string { + return common.opts.Evaluate(data, "") +} diff --git a/pkg/log/format/placeholders/field.go b/pkg/log/format/placeholders/field.go new file mode 100644 index 0000000000..2060d83403 --- /dev/null +++ b/pkg/log/format/placeholders/field.go @@ -0,0 +1,42 @@ +package placeholders + +import ( + "github.com/gruntwork-io/terragrunt/pkg/log/format/options" +) + +const ( + WorkDirKeyName = "workdir" + DownloadDirKeyName = "downloaddir" + TFPathKeyName = "tfpath" +) + +type fieldPlaceholder struct { + *CommonPlaceholder +} + +func (field *fieldPlaceholder) Evaluate(data *options.Data) string { + if val, ok := data.Fields[field.Name()]; ok { + if val, ok := val.(string); ok { + return field.opts.Evaluate(data, val) + } + } + + return "" +} + +func Field(fieldName string, opts ...options.Option) Placeholder { + opts = WithCommonOptions( + options.PathFormat(options.NonePath), + ).Merge(opts...) + + return &fieldPlaceholder{ + CommonPlaceholder: NewCommonPlaceholder(fieldName, opts...), + } +} + +func init() { + Registered.Add( + Field(WorkDirKeyName, options.PathFormat(options.NonePath, options.RelativePath, options.ShortPath)), + Field(TFPathKeyName, options.PathFormat(options.NonePath, options.FilenamePath, options.DirectoryPath)), + ) +} diff --git a/pkg/log/format/placeholders/level.go b/pkg/log/format/placeholders/level.go new file mode 100644 index 0000000000..1d95c7e892 --- /dev/null +++ b/pkg/log/format/placeholders/level.go @@ -0,0 +1,56 @@ +package placeholders + +import ( + "github.com/gruntwork-io/terragrunt/pkg/log" + "github.com/gruntwork-io/terragrunt/pkg/log/format/options" +) + +const LevelPlaceholderName = "level" + +var levlAutoColorFunc = func(level log.Level) options.ColorValue { + switch level { + case log.TraceLevel: + return options.WhiteColor + case log.DebugLevel: + return options.BlueHColor + case log.InfoLevel: + return options.GreenColor + case log.WarnLevel: + return options.YellowColor + case log.ErrorLevel: + return options.RedColor + case log.StdoutLevel: + return options.WhiteColor + case log.StderrLevel: + return options.RedColor + default: + return options.NoneColor + } +} + +type level struct { + *CommonPlaceholder +} + +func (level *level) Evaluate(data *options.Data) string { + newData := *data + newData.AutoColorFn = func() options.ColorValue { + return levlAutoColorFunc(data.Level) + } + + return level.opts.Evaluate(&newData, data.Level.String()) +} + +func Level(opts ...options.Option) Placeholder { + opts = WithCommonOptions( + options.LevelFormat(options.LevelFormatFull), + ).Merge(opts...) + + return &level{ + CommonPlaceholder: NewCommonPlaceholder(LevelPlaceholderName, opts...), + } +} + +func init() { + Registered.Add(Level()) +} diff --git a/pkg/log/format/placeholders/message.go b/pkg/log/format/placeholders/message.go new file mode 100644 index 0000000000..7596ec2c4b --- /dev/null +++ b/pkg/log/format/placeholders/message.go @@ -0,0 +1,29 @@ +package placeholders + +import ( + "github.com/gruntwork-io/terragrunt/pkg/log/format/options" +) + +const MessagePlaceholderName = "message" + +type message struct { + *CommonPlaceholder +} + +func (msg *message) Evaluate(data *options.Data) string { + return msg.opts.Evaluate(data, data.Message) +} + +func Message(opts ...options.Option) Placeholder { + opts = WithCommonOptions( + options.PathFormat(options.NonePath, options.RelativePath), + ).Merge(opts...) + + return &message{ + CommonPlaceholder: NewCommonPlaceholder(MessagePlaceholderName, opts...), + } +} + +func init() { + Registered.Add(Message()) +} diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go new file mode 100644 index 0000000000..682c378767 --- /dev/null +++ b/pkg/log/format/placeholders/placeholder.go @@ -0,0 +1,164 @@ +package placeholders + +import ( + "strings" + + "github.com/gruntwork-io/terragrunt/internal/errors" + "github.com/gruntwork-io/terragrunt/pkg/log/format/options" +) + +const placeholderSign = '%' + +var Registered Placeholders + +type Placeholders []Placeholder + +func (phs Placeholders) Get(name string) Placeholder { + for _, ph := range phs { + if ph.Name() == name { + return ph + } + } + + return nil +} + +func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, error) { + var ( + next int + quoted byte + placeholder Placeholder + option options.Option + ) + + for i := 0; i < len(str); i++ { + ch := str[i] + + if ch == '"' || ch == '\'' { + if quoted == 0 { + quoted = ch + } else if i > 0 && str[i-1] != '\\' { + quoted = 0 + } + } + + if quoted != 0 { + continue + } + + if placeholder == nil { + if !isPlaceholderName(ch) { + return nil, 0, errors.Errorf("invalid placeholder name %q", str[next:i]) + } + + name := str[next : i+1] + + if placeholder = registered.Get(name); placeholder != nil { + next = i + 2 + } + continue + } + + if next-1 == i && ch != '(' { + return placeholder, i - 1, nil + } + + if ch == '=' || ch == ',' || ch == ')' { + val := str[next:i] + val = strings.Trim(val, "'") + val = strings.Trim(val, "\"") + + if str[next-1] == '=' { + if option == nil { + return nil, 0, errors.Errorf("empty option name for placeholder %q", placeholder.Name()) + } + if err := option.SetValue(val); err != nil { + return nil, 0, errors.Errorf("invalid value %q for option %q, placeholder %q: %w", val, option.Name(), placeholder.Name(), err) + } + } else if val != "" { + if option = placeholder.GetOption(val); option == nil { + return nil, 0, errors.Errorf("invalid option name %q for placeholder %q", val, placeholder.Name()) + } + } + + next = i + 1 + } + + if ch == ')' { + return placeholder, i, nil + } + } + + if placeholder == nil { + return nil, 0, errors.Errorf("invalid placeholder name %q", str) + } + + if next < len(str) { + return nil, 0, errors.Errorf("invalid option %q for placeholder %q", str[next:], placeholder.Name()) + } + + return placeholder, len(str) - 1, nil +} + +func Parse(str string, registered Placeholders) (Placeholders, error) { + var ( + placeholders Placeholders + next int + ) + + for i := 0; i < len(str); i++ { + ch := str[i] + + if ch == placeholderSign { + if i+1 >= len(str) { + return nil, errors.Errorf("empty placeholder name") + } + + if str[i+1] == placeholderSign { + str = str[:i] + str[i+1:] + continue + } + + if next != i { + placeholder := PlainText(str[next:i]) + placeholders = append(placeholders, placeholder) + } + + placeholder, num, err := parsePlaceholder(str[i+1:], registered) + if err != nil { + return nil, err + } + + placeholders = append(placeholders, placeholder) + i += num + 1 + next = i + 1 + } + } + + return placeholders, nil +} + +func (phs *Placeholders) Add(new ...Placeholder) { + *phs = append(*phs, new...) +} + +func (phs Placeholders) Evaluate(data *options.Data) string { + var str string + + for _, ph := range phs { + str += ph.Evaluate(data) + } + + return str +} + +type Placeholder interface { + Name() string + GetOption(name string) options.Option + Evaluate(data *options.Data) string +} + +func isPlaceholderName(c byte) bool { + // Check if the byte value falls within the range of alphanumeric characters + return c == '-' || c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') +} diff --git a/pkg/log/format/placeholders/plaintext.go b/pkg/log/format/placeholders/plaintext.go new file mode 100644 index 0000000000..1884c64b13 --- /dev/null +++ b/pkg/log/format/placeholders/plaintext.go @@ -0,0 +1,25 @@ +package placeholders + +import ( + "github.com/gruntwork-io/terragrunt/pkg/log/format/options" +) + +const PlainTextPlaceholderName = "" + +type plainText struct { + *CommonPlaceholder + value string +} + +func (plainText *plainText) Evaluate(data *options.Data) string { + return plainText.opts.Evaluate(data, plainText.value) +} + +func PlainText(value string, opts ...options.Option) Placeholder { + opts = WithCommonOptions().Merge(opts...) + + return &plainText{ + CommonPlaceholder: NewCommonPlaceholder(PlainTextPlaceholderName, opts...), + value: value, + } +} diff --git a/pkg/log/format/placeholders/time.go b/pkg/log/format/placeholders/time.go new file mode 100644 index 0000000000..41d70c06bc --- /dev/null +++ b/pkg/log/format/placeholders/time.go @@ -0,0 +1,31 @@ +package placeholders + +import ( + "fmt" + + "github.com/gruntwork-io/terragrunt/pkg/log/format/options" +) + +const TimePlaceholderName = "time" + +type timePlaceholder struct { + *CommonPlaceholder +} + +func (t *timePlaceholder) Evaluate(data *options.Data) string { + return t.opts.Evaluate(data, data.Time.String()) +} + +func Time(opts ...options.Option) Placeholder { + opts = WithCommonOptions( + options.TimeFormat(fmt.Sprintf("%s:%s:%s%s", options.Hour24Zero, options.MinZero, options.SecZero, options.MilliSec)), + ).Merge(opts...) + + return &timePlaceholder{ + CommonPlaceholder: NewCommonPlaceholder(TimePlaceholderName, opts...), + } +} + +func init() { + Registered.Add(Time()) +} diff --git a/pkg/log/format/silent_formatter.go b/pkg/log/format/silent_formatter.go deleted file mode 100644 index ce5c8c56e9..0000000000 --- a/pkg/log/format/silent_formatter.go +++ /dev/null @@ -1,13 +0,0 @@ -package format - -import ( - "github.com/sirupsen/logrus" -) - -// SilentFormatter disables logging by not outputting anything. -type SilentFormatter struct{} - -// Format implements logrus.Formatter interface. -func (f *SilentFormatter) Format(entry *logrus.Entry) ([]byte, error) { - return nil, nil -} diff --git a/pkg/log/helper.go b/pkg/log/helper.go new file mode 100644 index 0000000000..bdd43312af --- /dev/null +++ b/pkg/log/helper.go @@ -0,0 +1,31 @@ +package log + +import ( + "github.com/sirupsen/logrus" +) + +// Formatter is used to implement a custom Formatter. +type Formatter interface { + Format(*Entry) ([]byte, error) +} + +// Entry is the final logging entry. +type Entry struct { + *logrus.Entry + Level Level + Fields Fields +} + +type logruFormatter struct { + Formatter +} + +func (f *logruFormatter) Format(parent *logrus.Entry) ([]byte, error) { + entry := &Entry{ + Entry: parent, + Level: FromLogrusLevel(parent.Level), + Fields: Fields(parent.Data), + } + + return f.Formatter.Format(entry) +} diff --git a/pkg/log/hooks/relative_path.go b/pkg/log/hooks/relative_path.go deleted file mode 100644 index 00985a2a78..0000000000 --- a/pkg/log/hooks/relative_path.go +++ /dev/null @@ -1,101 +0,0 @@ -// Package hooks provides hooks for the Terragrunt logger. -package hooks - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/gruntwork-io/terragrunt/internal/errors" - "github.com/gruntwork-io/terragrunt/pkg/log" - "github.com/gruntwork-io/terragrunt/pkg/log/format" - "github.com/sirupsen/logrus" -) - -// RelativePathHook represents a hook for logrus logger. -// The purpose is to replace all absolute paths found in the main message or data fields with paths relative to `baseDir`. -// For better performance, during instance creation, we creating a cache of relative paths for each subdirectory of baseDir. -// -// Example of cache: -// /path/to/dir ./ -// /path/to ../ -// /path ../.. -type RelativePathHook struct { - relPaths []string - absPathsReg []*regexp.Regexp - triggerLevels []logrus.Level -} - -// NewRelativePathHook returns a new RelativePathHook instance. -// It returns an error if the cache of relative paths could not be created for the given `baseDir`. -func NewRelativePathHook(baseDir string) (*RelativePathHook, error) { - baseDir = filepath.Clean(baseDir) - - pathSeparator := string(os.PathSeparator) - dirs := strings.Split(baseDir, pathSeparator) - absPath := dirs[0] - dirs = dirs[1:] - - relPaths := make([]string, len(dirs)) - absPathsReg := make([]*regexp.Regexp, len(dirs)) - reversIndex := len(dirs) - - for _, dir := range dirs { - absPath = filepath.Join(absPath, pathSeparator, dir) - - relPath, err := filepath.Rel(baseDir, absPath) - if err != nil { - return nil, errors.New(err) - } - - reversIndex-- - relPaths[reversIndex] = relPath - - regStr := fmt.Sprintf(`(^|[^%[1]s\w])%[2]s([%[1]s"'\s]|$)`, regexp.QuoteMeta(pathSeparator), regexp.QuoteMeta(absPath)) - absPathsReg[reversIndex] = regexp.MustCompile(regStr) - } - - return &RelativePathHook{ - absPathsReg: absPathsReg, - relPaths: relPaths, - triggerLevels: log.AllLevels.ToLogrusLevels(), - }, nil -} - -// Levels implements logrus.Hook.Levels() -func (hook *RelativePathHook) Levels() []logrus.Level { - return hook.triggerLevels -} - -// Fire implements logrus.Hook.Fire() -func (hook *RelativePathHook) Fire(entry *logrus.Entry) error { - entry.Message = hook.replaceAbsPathsWithRel(entry.Message) - - for key, field := range entry.Data { - if val, ok := field.(string); ok { - newVal := hook.replaceAbsPathsWithRel(val) - - if newVal == val { - continue - } - - if key == format.PrefixKeyName && strings.HasPrefix(newVal, log.CurDirWithSeparator) { - newVal = newVal[len(log.CurDirWithSeparator):] - } - - entry.Data[key] = newVal - } - } - - return nil -} - -func (hook *RelativePathHook) replaceAbsPathsWithRel(text string) string { - for i, absPath := range hook.absPathsReg { - text = absPath.ReplaceAllString(text, "$1"+hook.relPaths[i]+"$2") - } - - return text -} diff --git a/pkg/log/level.go b/pkg/log/level.go index 68d741f73e..ffcb924b95 100644 --- a/pkg/log/level.go +++ b/pkg/log/level.go @@ -95,6 +95,11 @@ func ParseLevel(str string) (Level, error) { // String implements fmt.Stringer. func (level Level) String() string { + return level.FullName() +} + +// Name returns the full level name. +func (level Level) FullName() string { if name, ok := levelNames[level]; ok { return name } diff --git a/pkg/log/options.go b/pkg/log/options.go index 796ca242c2..109a7c7d12 100644 --- a/pkg/log/options.go +++ b/pkg/log/options.go @@ -24,9 +24,9 @@ func WithOutput(output io.Writer) Option { } // WithFormatter sets the logger formatter. -func WithFormatter(formatter logrus.Formatter) Option { +func WithFormatter(formatter Formatter) Option { return func(logger *logger) { - logger.Logger.SetFormatter(formatter) + logger.Logger.SetFormatter(&logruFormatter{Formatter: formatter}) } } diff --git a/shell/run_shell_cmd.go b/shell/run_shell_cmd.go index 06e6b68c55..731541d2d9 100644 --- a/shell/run_shell_cmd.go +++ b/shell/run_shell_cmd.go @@ -16,7 +16,7 @@ import ( "github.com/gruntwork-io/terragrunt/internal/os/exec" "github.com/gruntwork-io/terragrunt/pkg/cli" "github.com/gruntwork-io/terragrunt/pkg/log" - "github.com/gruntwork-io/terragrunt/pkg/log/format" + "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" "github.com/gruntwork-io/terragrunt/pkg/log/writer" "github.com/gruntwork-io/terragrunt/terraform" @@ -125,7 +125,7 @@ func RunShellCommandWithOutput( outWriter = logger.WithOptions(log.WithOutput(errWriter)).Writer() errWriter = logger.WithOptions(log.WithOutput(errWriter)).WriterLevel(log.ErrorLevel) } else if command == opts.TerraformPath && !opts.TerraformLogsToJSON && !opts.ForwardTFStdout && !shouldForceForwardTFStdout(args) { - logger := opts.Logger.WithField(format.TFBinaryKeyName, filepath.Base(opts.TerraformPath)) + logger := opts.Logger.WithField(placeholders.TFPathKeyName, filepath.Base(opts.TerraformPath)) outWriter = writer.New( writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))), diff --git a/shell/run_shell_cmd_output_test.go b/shell/run_shell_cmd_output_test.go index b06ad2bb1a..b5b41c003c 100644 --- a/shell/run_shell_cmd_output_test.go +++ b/shell/run_shell_cmd_output_test.go @@ -14,6 +14,7 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/pkg/log/format" + "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" "github.com/gruntwork-io/terragrunt/shell" "github.com/gruntwork-io/terragrunt/util" @@ -73,7 +74,7 @@ func TestCommandOutputPrefix(t *testing.T) { testCommandOutput(t, func(terragruntOptions *options.TerragruntOptions) { terragruntOptions.TerraformPath = terraformPath terragruntOptions.Logger.SetOptions(log.WithFormatter(logFormatter)) - terragruntOptions.Logger = terragruntOptions.Logger.WithField(format.PrefixKeyName, prefix) + terragruntOptions.Logger = terragruntOptions.Logger.WithField(placeholders.WorkDirKeyName, prefix) }, assertOutputs(t, prefixedOutput, Stdout, diff --git a/test/integration_test.go b/test/integration_test.go index b7257010f9..688026eb7f 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -74,7 +74,7 @@ const ( testFixtureInitOnce = "fixtures/init-once" testFixtureInputs = "fixtures/inputs" testFixtureInputsFromDependency = "fixtures/inputs-from-dependency" - testFixtureLogFormatter = "fixtures/log/formatter" + testFixtureLogFormatter = "fixtures/log/format" testFixtureLogRelPaths = "fixtures/log/rel-paths" testFixtureMissingDependence = "fixtures/missing-dependencies/main" testFixtureModulePathError = "fixtures/module-path-in-error" From bc4fdf729d6caaaee0171b1a891bfc2a38710643 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Fri, 1 Nov 2024 20:47:36 +0100 Subject: [PATCH 02/28] chore: remove old code --- pkg/log/ff/config/config.go | 45 ---- pkg/log/ff/config/layout.go | 63 ----- pkg/log/ff/config/option.go | 255 ------------------ pkg/log/ff/config/random_color.go | 69 ----- pkg/log/ff/config/var.go | 241 ----------------- pkg/log/ff/config/var_filters.go | 102 -------- pkg/log/ff/format.go | 79 ------ pkg/log/ff/formatter.go | 375 --------------------------- pkg/log/ff/options.go | 31 --- pkg/log/format.old/color.go | 86 ------ pkg/log/format.old/formatter.go | 285 -------------------- pkg/log/format.old/json_formatter.go | 154 ----------- pkg/log/format.old/prefix_style.go | 53 ---- 13 files changed, 1838 deletions(-) delete mode 100644 pkg/log/ff/config/config.go delete mode 100644 pkg/log/ff/config/layout.go delete mode 100644 pkg/log/ff/config/option.go delete mode 100644 pkg/log/ff/config/random_color.go delete mode 100644 pkg/log/ff/config/var.go delete mode 100644 pkg/log/ff/config/var_filters.go delete mode 100644 pkg/log/ff/format.go delete mode 100644 pkg/log/ff/formatter.go delete mode 100644 pkg/log/ff/options.go delete mode 100644 pkg/log/format.old/color.go delete mode 100644 pkg/log/format.old/formatter.go delete mode 100644 pkg/log/format.old/json_formatter.go delete mode 100644 pkg/log/format.old/prefix_style.go diff --git a/pkg/log/ff/config/config.go b/pkg/log/ff/config/config.go deleted file mode 100644 index 07334c77b2..0000000000 --- a/pkg/log/ff/config/config.go +++ /dev/null @@ -1,45 +0,0 @@ -package config - -type Configs []*Config - -func (cfg Configs) Find(name string) *Config { - for _, cfg := range cfg { - if cfg.name == name { - return cfg - } - } - - return nil -} - -func (cfg Configs) Names() []string { - var names []string - - for _, cfg := range cfg { - if cfg.name != "" { - names = append(names, cfg.name) - } - } - - return names -} - -type Config struct { - name string - opts Options -} - -func (cfg *Config) Options() Options { - return cfg.opts -} - -func (cfg *Config) Name() string { - return cfg.name -} - -func New(name string, opts ...*Option) *Config { - return &Config{ - name: name, - opts: opts, - } -} diff --git a/pkg/log/ff/config/layout.go b/pkg/log/ff/config/layout.go deleted file mode 100644 index cac374cd7f..0000000000 --- a/pkg/log/ff/config/layout.go +++ /dev/null @@ -1,63 +0,0 @@ -package config - -import ( - "fmt" - "strings" -) - -type Layout struct { - format string - vars Vars - doPost map[PostTask]PostFunc -} - -func NewLayout(format string, vars ...*Var) *Layout { - return &Layout{ - format: format, - vars: vars, - doPost: make(map[PostTask]PostFunc), - } -} - -func (layout *Layout) Value(opt *Option, entry *Entry) string { - var vals []any - - for _, variable := range layout.vars { - val := variable.Value(opt, entry) - vals = append(vals, val) - } - - val := fmt.Sprintf(layout.format, vals...) - - for _, fn := range layout.doPost { - val = fn(val) - } - - return val -} - -func ParseLayout(str string) (*Layout, error) { - var ( - format = str - vars []*Var - ) - - if parts := strings.Split(str, varSeparator); len(parts) > 1 { - format = parts[0] - varNames := parts[1:] - - for _, name := range varNames { - variable, err := ParseVar(name) - if err != nil { - return nil, err - } - vars = append(vars, variable) - } - - if format == "" { - format = strings.Repeat("%s", len(vars)) - } - } - - return &Layout{format: format, vars: vars}, nil -} diff --git a/pkg/log/ff/config/option.go b/pkg/log/ff/config/option.go deleted file mode 100644 index 701b667658..0000000000 --- a/pkg/log/ff/config/option.go +++ /dev/null @@ -1,255 +0,0 @@ -package config - -import ( - "sort" - "strings" - - "github.com/gruntwork-io/terragrunt/pkg/log" -) - -type Options []*Option - -func (opts Options) Names() []string { - strs := make([]string, len(opts)) - - for i, opt := range opts { - strs[i] = opt.name - } - - return strs -} - -func (opts Options) SortByValue() Options { - sort.Slice(opts, func(i, j int) bool { - return opts[i].value <= opts[j].value - }) - - return opts -} - -func (opts Options) FindByName(name string) Options { - var foundOpts Options - - for _, opt := range opts { - if opt.name == name || opt.name == "" || opt.value == name { - foundOpts = append(foundOpts, opt) - } - } - - return foundOpts -} - -func (opts Options) FindByLevels(levels ...log.Level) Options { - var foundOpts Options - - for _, opt := range opts { - for _, level := range levels { - if opt.levels.Contains(level) { - foundOpts = append(foundOpts, opt) - } - } - } - - return foundOpts -} - -func (opts Options) FindWithoutLevels() Options { - var foundOpts Options - - for _, opt := range opts { - if len(opt.levels) == 0 { - foundOpts = append(foundOpts, opt) - } - } - - return foundOpts -} - -func (opts Options) FilterByNamePrefixes(mustContain bool, prefixes ...string) Options { - var filteredOpts Options - - for _, opt := range opts { - for _, prefix := range prefixes { - if strings.HasPrefix(opt.name, prefix) == mustContain { - filteredOpts = append(filteredOpts, opt) - } - } - } - - return filteredOpts -} - -func (opts Options) MergeIntoOne() *Option { - var new *Option - - for _, opt := range opts { - if new == nil { - new = &Option{ - name: opt.name, - value: opt.value, - enable: opt.enable, - layout: opt.layout, - levels: opt.levels, - randomColor: opt.randomColor, - } - } else { - if opt.layout != nil { - new.layout = opt.layout - } - if opt.value != "" { - new.value = opt.value - } - - new.enable = opt.enable - new.levels = opt.levels - new.randomColor = opt.randomColor - } - } - - return new -} - -func (opts Options) MergeByName() Options { - var news Options - - for _, opt := range opts { - isNew := true - for i, new := range news { - if opt.name == new.name { - news[i] = opt - isNew = false - break - } - } - if isNew { - news = append(news, opt) - } - } - - return news -} - -func (opts Options) MergeIntoOneWithPriorityByLevels(levels ...log.Level) *Option { - return append(opts.FindWithoutLevels(), opts.FindByLevels(levels...)...).MergeIntoOne() -} - -type Option struct { - name string - value string - enable bool - layout *Layout - levels log.Levels - - randomColor *RandomColor -} - -func (opt *Option) Enable() bool { - return opt.enable -} - -func (opt *Option) Name() string { - return opt.name -} - -func (opt *Option) Levels() log.Levels { - return opt.levels -} - -func (opt *Option) Layout() *Layout { - return opt.layout -} - -func (opt *Option) Value(entry *Entry) string { - if opt.layout == nil { - return "" - } - - return opt.layout.Value(opt, entry) -} - -func NewOption(name string, enable bool, layout *Layout, levels ...log.Level) *Option { - if layout == nil { - layout = NewLayout("%s", NewVar(name)) - } - - var value string - - if parts := strings.SplitN(name, "=", 2); len(parts) > 1 { - name = parts[0] - value = parts[1] - } - - return &Option{ - name: name, - value: value, - enable: enable, - layout: layout, - levels: levels, - randomColor: NewRandomColor(), - } -} - -func ParseOption(str string) (*Option, error) { - var ( - name = str - enable = true - layout *Layout - levels log.Levels - value string - err error - ) - - parts := strings.SplitN(name, ":", 2) - name = parts[0] - if strings.HasPrefix(name, "no-") { - name = name[3:] - enable = false - } - - if parts := strings.Split(name, "@"); len(parts) > 1 { - name = parts[0] - - if levels, err = ParseLevels(parts[1:]); err != nil { - return nil, err - } - } - - if parts := strings.SplitN(name, "=", 2); len(parts) > 1 { - name = parts[0] - value = parts[1] - } - - if len(parts) > 1 { - if layout, err = ParseLayout(parts[1]); err != nil { - return nil, err - } - } - - return &Option{ - name: name, - value: value, - enable: enable, - layout: layout, - levels: levels, - randomColor: NewRandomColor(), - }, nil -} - -func ParseLevels(levelNames []string) (log.Levels, error) { - var levels log.Levels - - for _, levelName := range levelNames { - if levelName == "" { - continue - } - - level, err := log.ParseLevel(levelName) - if err != nil { - return nil, err - } - - levels = append(levels, level) - } - - return levels, nil -} diff --git a/pkg/log/ff/config/random_color.go b/pkg/log/ff/config/random_color.go deleted file mode 100644 index 937022fe87..0000000000 --- a/pkg/log/ff/config/random_color.go +++ /dev/null @@ -1,69 +0,0 @@ -package config - -import ( - "github.com/mgutz/ansi" - "github.com/puzpuzpuz/xsync/v3" -) - -var ( - // defaultRandomColorStyles contains ANSI color codes that are assigned sequentially to each unique text in a rotating order - // https://user-images.githubusercontent.com/995050/47952855-ecb12480-df75-11e8-89d4-ac26c50e80b9.png - // https://www.hackitu.de/termcolor256/ - defaultRandomColorStyles = ColorStyles{ - "66", "67", "95", "96", "102", "103", "108", "109", "139", "138", "144", "145", - } -) - -type ColorStyles []ColorStyle - -func (styles ColorStyles) ColorCodes() []string { - codes := make([]string, len(styles)) - - for i, style := range styles { - codes[i] = style.ColorCode() - } - - return codes -} - -type ColorStyle string - -func (style ColorStyle) ColorCode() string { - return ansi.ColorCode(string(style)) -} - -type RandomColor struct { - // cache stores unique text with their color code. - // We use [xsync.MapOf](https://github.com/puzpuzpuz/xsync?tab=readme-ov-file#map) instaed of standard `sync.Map` since it's faster and has generic types. - cache *xsync.MapOf[string, string] - - codes []string - - // nextStyleIndex is used to get the next style from the `codes` list for a newly discovered text. - nextStyleIndex int -} - -func NewRandomColor() *RandomColor { - return &RandomColor{ - cache: xsync.NewMapOf[string, string](), - codes: defaultRandomColorStyles.ColorCodes(), - } -} - -func (color *RandomColor) ColorCode(text string) string { - if colorCode, ok := color.cache.Load(text); ok { - return colorCode - } - - if color.nextStyleIndex >= len(color.codes) { - color.nextStyleIndex = 0 - } - - colorCode := color.codes[color.nextStyleIndex] - - color.cache.Store(text, colorCode) - - color.nextStyleIndex++ - - return colorCode -} diff --git a/pkg/log/ff/config/var.go b/pkg/log/ff/config/var.go deleted file mode 100644 index e229dd2c11..0000000000 --- a/pkg/log/ff/config/var.go +++ /dev/null @@ -1,241 +0,0 @@ -package config - -// --terragrunt-log-pretty-format "%if(cond=level==\"info\",scope=first-value)%C(red)%level(short,upper)%C(reset)" - -import ( - "fmt" - "strings" - "time" - - "github.com/gruntwork-io/terragrunt/pkg/log" - "github.com/mgutz/ansi" -) - -const ( - VarLevel = "level" - VarLevelShort = "level-short" - VarLevelTiny = "level-tiny" - VarMessage = "message" - - VarHour24Zero = "H" - VarHour12Zero = "h" - VarHour12 = "g" - VarMinZero = "i" - VarSecZero = "s" - VarMilliSec = "v" - VarMicroSec = "u" - VarYearFull = "Y" - VarYear = "y" - VarMonthNumZero = "m" - VarMonthNum = "n" - VarMonthText = "M" - VarDayZero = "d" - VarDay = "j" - VarDayText = "D" - VarPMUpper = "A" - VarPMLower = "a" - VarTZText = "T" - VarTZNumWithColon = "P" - VarTZNum = "O" - - VarDateTime = "date-time" - VarDateOnly = "date-only" - VarTimeOnly = "time-only" - VarRFC3339 = "rfc3339" - VarRFC3339Nano = "rfc3339-nano" - VarSinceStartSec = "since-start-sec" - - VarColorRed = "color-red" - VarColorWhite = "color-white" - VarColorYellow = "color-yellow" - VarColorGreen = "color-green" - VarColorBlueH = "color-blue+h" - VarColorCyan = "color-cyan" - VarColorBlackH = "color-black+h" - VarColorReset = "color-reset" - VarColorRandom = "color-random" -) - -const ( - doPostTaskResetColor PostTask = iota - doPostTaskRandomColor -) - -const ( - randomColorMask = "$$random-color$$" -) - -const varSeparator = "@" - -var ( - resetColor = ansi.ColorCode("reset") - - colorsMap = map[string]string{ - VarColorRed: ansi.ColorCode("red"), - VarColorWhite: ansi.ColorCode("white"), - VarColorYellow: ansi.ColorCode("yellow"), - VarColorGreen: ansi.ColorCode("green"), - VarColorBlueH: ansi.ColorCode("blue+h"), - VarColorCyan: ansi.ColorCode("cyan"), - VarColorBlackH: ansi.ColorCode("black+h"), - VarColorReset: resetColor, - } - - timestampsMap = map[string]string{ - VarYearFull: "2006", - VarYear: "06", - VarMonthNumZero: "01", - VarMonthNum: "1", - VarMonthText: "Jan", - VarDay: "2", - VarDayZero: "02", - VarDayText: "Mon", - VarPMUpper: "PM", - VarPMLower: "pm", - VarHour24Zero: "15", - VarHour12Zero: "03", - VarHour12: "3", - VarMinZero: "04", - VarSecZero: "05", - VarMicroSec: ".000000", - VarMilliSec: ".000", - VarTZText: "MST", - VarTZNum: "-0700", - VarTZNumWithColon: "-07:00", - VarRFC3339: time.RFC3339, - VarRFC3339Nano: time.RFC3339Nano, - VarDateTime: time.DateTime, - VarDateOnly: time.DateOnly, - VarTimeOnly: time.TimeOnly, - } - - ArgsFunc = make(map[string]VariableFunc) -) - -func init() { - for name, fmt := range timestampsMap { - ArgsFunc[name] = func(opt *Option, entry *Entry) string { - return entry.curTime.Format(fmt) - } - } - - for name, colorCode := range colorsMap { - ArgsFunc[name] = func(opt *Option, entry *Entry) string { - if entry.disableColor { - return "" - } - - if name != VarColorReset { - opt.layout.doPost[doPostTaskResetColor] = func(val string) string { - return val + resetColor - } - } - - return colorCode - } - } - - ArgsFunc[VarColorRandom] = func(opt *Option, entry *Entry) string { - if entry.disableColor { - return "" - } - - opt.layout.doPost[doPostTaskRandomColor] = func(val string) string { - colorCode := opt.randomColor.ColorCode(val) - val = strings.ReplaceAll(val, randomColorMask, colorCode) - return val + resetColor - } - - return randomColorMask - } - - ArgsFunc[VarSinceStartSec] = func(opt *Option, entry *Entry) string { - return fmt.Sprintf("%04d", time.Since(entry.baseTime)/time.Second) - } - - ArgsFunc[VarLevel] = func(opt *Option, entry *Entry) string { - return entry.level.String() - } - ArgsFunc[VarLevelShort] = func(opt *Option, entry *Entry) string { - return entry.level.ShortName() - } - ArgsFunc[VarLevelTiny] = func(opt *Option, entry *Entry) string { - return entry.level.TinyName() - } - - ArgsFunc[VarMessage] = func(opt *Option, entry *Entry) string { - return entry.message - } -} - -type PostFunc func(val string) string - -type PostTask byte - -type Entry struct { - baseTime time.Time - curTime time.Time - level log.Level - message string - fields log.Fields - disableColor bool -} - -func NewEntry(baseTime, curTime time.Time, level log.Level, msg string, fields log.Fields, disableColor bool) *Entry { - return &Entry{ - baseTime: baseTime, - curTime: curTime, - level: level, - message: msg, - fields: fields, - disableColor: disableColor, - } -} - -type VariableFunc func(opt *Option, entry *Entry) string - -type Vars []*Var - -type Var struct { - fn VariableFunc - filters Filters -} - -func (variable *Var) Value(opt *Option, entry *Entry) string { - val := variable.fn(opt, entry) - val = variable.filters.Value(val) - return val -} - -func NewVar(name string, filters ...Filter) *Var { - if fn, ok := ArgsFunc[name]; ok { - return &Var{fn: fn, filters: filters} - } - - fn := func(opt *Option, entry *Entry) string { - if val, ok := entry.fields[name]; ok { - return fmt.Sprintf("%s", val) - } - return "" - } - return &Var{fn: fn, filters: filters} -} - -func ParseVar(str string) (*Var, error) { - var ( - name = str - filters Filters - err error - ) - - if parts := strings.Split(name, varFilterSeparator); len(parts) > 1 { - name = parts[0] - - filters, err = ParseFilters(parts[1:]) - if err != nil { - return nil, err - } - } - - return NewVar(name, filters...), nil -} diff --git a/pkg/log/ff/config/var_filters.go b/pkg/log/ff/config/var_filters.go deleted file mode 100644 index 94c39d4d81..0000000000 --- a/pkg/log/ff/config/var_filters.go +++ /dev/null @@ -1,102 +0,0 @@ -package config - -import ( - "strings" - - "github.com/gruntwork-io/go-commons/errors" -) - -const varFilterSeparator = "|" - -const ( - FilterRequired Filter = iota - FilterUpper - FilterLower - FilterTitle -) - -var ( - // AllFilters exposes all var filters - AllFilters = Filters{ - FilterUpper, - FilterLower, - FilterTitle, - } - - varFilterNames = map[Filter]string{ - FilterUpper: "upper", - FilterLower: "lower", - FilterTitle: "title", - } -) - -type Filters []Filter - -func (filters Filters) Value(val string) string { - for _, filter := range filters { - switch filter { - case FilterUpper: - val = strings.ToUpper(val) - case FilterLower: - val = strings.ToLower(val) - case FilterTitle: - val = strings.Title(val) - } - } - - return val -} - -func (filters Filters) String() string { - return strings.Join(filters.Names(), ", ") -} - -func (filters Filters) Names() []string { - strs := make([]string, len(filters)) - - for i, filter := range filters { - strs[i] = filter.String() - } - - return strs -} - -// ParseFilters takes a string and returns the var filter constants. -func ParseFilters(names []string) (Filters, error) { - var filters Filters - - for _, name := range names { - if name == "" { - continue - } - filter, err := ParseFilter(name) - if err != nil { - return nil, err - } - filters = append(filters, filter) - } - - return filters, nil -} - -type Filter byte - -// ParseFilter takes a string and returns the var filter constant. -func ParseFilter(str string) (Filter, error) { - for filter, name := range varFilterNames { - if strings.EqualFold(name, str) { - return filter, nil - } - } - - return Filter(0), errors.Errorf("invalid variable filter %q, supported variable filtres: %s", str, AllFilters) -} - -// String implements fmt.Stringer. -func (filter Filter) String() string { - if name, ok := varFilterNames[filter]; ok { - return name - } - - return "" -} diff --git a/pkg/log/ff/format.go b/pkg/log/ff/format.go deleted file mode 100644 index f3023631bf..0000000000 --- a/pkg/log/ff/format.go +++ /dev/null @@ -1,79 +0,0 @@ -package format - -import ( - "github.com/gruntwork-io/terragrunt/pkg/log" - "github.com/gruntwork-io/terragrunt/pkg/log/format/config" -) - -const ( - ColumnTime = "#time=#1" - ColumnLevel = "#level=#2" - ColumnPrefix = "#prefix=#3" - ColumnMessage = "#message=#4" -) - -var ( - DefaultConfig = config.New(defaultConfigName, - config.NewOption(OptionColor, true, nil), - config.NewOption(ColumnTime, true, - config.NewLayout("%s%s:%s:%s%s", - config.NewVar(config.VarColorBlackH), - config.NewVar(config.VarHour24Zero), - config.NewVar(config.VarMinZero), - config.NewVar(config.VarSecZero), - config.NewVar(config.VarMilliSec)), - ), - - config.NewOption(ColumnLevel, true, - config.NewLayout("%-6s", - config.NewVar(config.VarLevel, config.FilterUpper)), - ), - - config.NewOption(ColumnLevel, true, - config.NewLayout("%s%-6s", - config.NewVar(config.VarColorRed), - config.NewVar(config.VarLevel, config.FilterUpper)), - log.ErrorLevel, log.StderrLevel, - ), - - config.NewOption(ColumnLevel, true, - config.NewLayout("%s%-6s", - config.NewVar(config.VarColorYellow), - config.NewVar(config.VarLevel, config.FilterUpper)), - log.WarnLevel, - ), - - config.NewOption(ColumnLevel, true, - config.NewLayout("%s%-6s", - config.NewVar(config.VarColorGreen), - config.NewVar(config.VarLevel, config.FilterUpper)), - log.InfoLevel, - ), - - config.NewOption(ColumnLevel, true, - config.NewLayout("%s%-6s", - config.NewVar(config.VarColorBlueH), - config.NewVar(config.VarLevel, config.FilterUpper)), - log.DebugLevel, - ), - - config.NewOption(ColumnPrefix, true, - config.NewLayout("%s[%s]", - config.NewVar(config.VarColorRandom), - config.NewVar("rel-prefix"))), - - config.NewOption("prefix", false, nil), - config.NewOption("rel-prefix", false, nil), - config.NewOption("sub-prefix", false, nil), - config.NewOption(ColumnMessage, true, config.NewLayout("%s", config.NewVar(config.VarMessage))), - ) - - TinyConfig = config.New("tiny", - config.NewOption(OptionColor, true, nil), - config.NewOption(ColumnTime, true, config.NewLayout("%s", config.NewVar(config.VarSinceStartSec))), - config.NewOption(ColumnLevel, true, config.NewLayout("%s", config.NewVar(config.VarLevelShort, config.FilterUpper))), - config.NewOption(ColumnMessage, true, config.NewLayout("%s", config.NewVar("message"))), - ) - - Configs = config.Configs{DefaultConfig, TinyConfig} -) diff --git a/pkg/log/ff/formatter.go b/pkg/log/ff/formatter.go deleted file mode 100644 index 67f4ae6082..0000000000 --- a/pkg/log/ff/formatter.go +++ /dev/null @@ -1,375 +0,0 @@ -package format - -import ( - "bytes" - "encoding/json" - "fmt" - "regexp" - "strings" - "time" - - "github.com/gruntwork-io/go-commons/errors" - "github.com/gruntwork-io/terragrunt/pkg/log" - "github.com/gruntwork-io/terragrunt/pkg/log/format/config" - "github.com/gruntwork-io/terragrunt/util" - "github.com/sirupsen/logrus" - "golang.org/x/exp/maps" -) - -const ( - optionSeparator = "," - - columnPrefix = "#" - columnPrefixLen = len(columnPrefix) - - defaultConfigName = "" - defaultQuoteCharacter = "\"" -) - -var ( - optionSeparatorReg = regexp.MustCompile(`.*?[^\\](` + optionSeparator + `|$)`) -) - -const ( - OptionColor = "color" - OptionKeyValue = "key-value" - OptionJSON = "json" - OptionIndent = "indent" -) - -type Formatter struct { - baseTime time.Time - - presetConfigs config.Configs - selectedConfigName string - - userOpts config.Options - - quoteEmptyFields bool - - quoteCharacter string -} - -func NewFormatter(opts ...Option) *Formatter { - formatter := &Formatter{ - presetConfigs: config.Configs{DefaultConfig}, - - baseTime: time.Now(), - selectedConfigName: defaultConfigName, - - quoteCharacter: defaultQuoteCharacter, - } - - formatter.SetOption(opts...) - - return formatter -} - -func (formatter *Formatter) SetOption(opts ...Option) { - for _, opt := range opts { - opt(formatter) - } -} - -// String implements fmt.Stringer -func (formatter *Formatter) String() string { - var strs []string - - if configName := formatter.selectedConfigName; configName != "" { - strs = append(strs, configName) - } - - // get all non-column options - opts := formatter.Options().FilterByNamePrefixes(false, columnPrefix) - - strs = append(strs, opts.Names()...) - strs = util.RemoveDuplicatesFromList(strs) - - return strings.Join(strs, ",") -} - -func (formatter *Formatter) Options() config.Options { - var opts config.Options - - if preset := formatter.presetConfigs.Find(formatter.selectedConfigName); preset != nil { - opts = preset.Options() - } - - return append(opts, formatter.userOpts...) -} - -// SetFormat parses options in the given `str` and sets them to the formatter. -func (formatter *Formatter) SetFormat(str string) error { - parts := optionSeparatorReg.FindAllString(str, -1) - for i, str := range parts { - if i < len(parts)-1 { - str = str[:len(str)-1] - } - str = strings.ReplaceAll(str, `\`+optionSeparator, optionSeparator) - - if selectedConfig := formatter.presetConfigs.Find(str); selectedConfig != nil { - formatter.selectedConfigName = selectedConfig.Name() - continue - } - - opt, err := config.ParseOption(str) - if err != nil { - return err - } - - formatter.userOpts = append(formatter.userOpts, opt) - } - - return nil -} - -func (formatter *Formatter) GetOption(name string, levels ...log.Level) *config.Option { - var opts config.Options - - if preset := formatter.presetConfigs.Find(formatter.selectedConfigName); preset != nil { - if opt := preset.Options().FindByName(name).MergeIntoOneWithPriorityByLevels(levels...); opt != nil { - opts = append(opts, opt) - } - } - - if opt := formatter.userOpts.FindByName(name).MergeIntoOneWithPriorityByLevels(levels...); opt != nil { - opts = append(opts, opt) - } - - return opts.MergeIntoOne() -} - -func (formatter *Formatter) getOptionsByNamePrefixAndLevel(prefix string, levels ...log.Level) config.Options { - optsNames := formatter.Options().FilterByNamePrefixes(true, columnPrefix).Names() - optsNames = util.RemoveDuplicatesFromList(optsNames) - - opts := make(config.Options, len(optsNames)) - - for i, optName := range optsNames { - opts[i] = formatter.GetOption(optName, levels...) - } - - return opts -} - -func (formatter *Formatter) optionValue(name string, level log.Level, entry *config.Entry) (string, bool) { - opt := formatter.GetOption(name, level) - if opt == nil { - return "", true - } - - return opt.Value(entry), opt.Enable() -} - -// Format implements logrus.Formatter -func (formatter *Formatter) Format(entry *logrus.Entry) ([]byte, error) { - buf := entry.Buffer - if buf == nil { - buf = new(bytes.Buffer) - } - - var ( - colsFields = make(log.Fields) - colsNames []string - colsValues []string - level = log.FromLogrusLevel(entry.Level) - fields = log.Fields(entry.Data) - msg = entry.Message - curTime = entry.Time - disableColor bool - jsonFormat bool - keyValueFormat bool - ) - - if opt := formatter.GetOption(OptionColor, level); opt != nil && !opt.Enable() { - disableColor = true - } - - if opt := formatter.GetOption(OptionJSON, level); opt != nil && opt.Enable() { - disableColor = true - jsonFormat = true - } - - if opt := formatter.GetOption(OptionKeyValue, level); opt != nil && opt.Enable() { - disableColor = true - keyValueFormat = true - } - - presetEntry := config.NewEntry(formatter.baseTime, curTime, level, msg, fields, disableColor) - - opts := formatter.getOptionsByNamePrefixAndLevel(columnPrefix, level).MergeByName().SortByValue() - - for _, opt := range opts { - if !opt.Enable() { - continue - } - if val := opt.Value(presetEntry); val != "" { - if disableColor { - val = log.RemoveAllASCISeq(val) - } - - colName := opt.Name()[columnPrefixLen:] - colsNames = append(colsNames, colName) - colsValues = append(colsValues, val) - colsFields[colName] = val - } - } - - for key, value := range fields { - if val, ok := formatter.optionValue(key, level, presetEntry); !ok { - delete(fields, key) - continue - } else if val != "" { - fields[key] = val - continue - } - - if val, ok := value.(string); ok && disableColor { - fields[key] = log.RemoveAllASCISeq(val) - } - } - - if len(colsValues) == 0 && len(fields) == 0 { - return nil, nil - } - - if keyValueFormat { - return formatter.keyValueFormat(buf, level, colsNames, colsFields, fields) - } - - if jsonFormat { - return formatter.jsonFormat(buf, level, fields, colsFields) - } - - return formatter.textFormat(buf, fields, colsValues) -} - -func (formatter *Formatter) textFormat(buf *bytes.Buffer, fields log.Fields, colsValues []string) ([]byte, error) { - if _, err := fmt.Fprint(buf, strings.Join(colsValues, " ")); err != nil { - return nil, errors.WithStackTrace(err) - } - - for _, key := range fields.Keys() { - value := fields[key] - if err := formatter.appendKeyValue(buf, key, value); err != nil { - return nil, err - } - } - - if err := buf.WriteByte('\n'); err != nil { - return nil, errors.WithStackTrace(err) - } - - return buf.Bytes(), nil -} - -func (formatter *Formatter) keyValueFormat(buf *bytes.Buffer, level log.Level, colsNames []string, colsFields, fields log.Fields) ([]byte, error) { - for _, key := range colsNames { - val, ok := colsFields[key] - if !ok { - continue - } - if err := formatter.appendKeyValue(buf, key, val); err != nil { - return nil, err - } - } - - for _, key := range fields.Keys() { - val := fields[key] - if err := formatter.appendKeyValue(buf, key, val); err != nil { - return nil, err - } - } - - if err := buf.WriteByte('\n'); err != nil { - return nil, errors.WithStackTrace(err) - } - - return buf.Bytes(), nil -} - -func (formatter *Formatter) jsonFormat(buf *bytes.Buffer, level log.Level, fields, colsFields log.Fields) ([]byte, error) { - encoder := json.NewEncoder(buf) - - if opt := formatter.GetOption(OptionIndent, level); opt != nil && opt.Enable() { - encoder.SetIndent("", " ") - } - - maps.Copy(fields, colsFields) - - if err := encoder.Encode(fields); err != nil { - return nil, errors.Errorf("failed to marshal fields to JSON, %w", err) - } - - return buf.Bytes(), nil -} - -func (formatter *Formatter) appendKeyValue(buf *bytes.Buffer, key string, value interface{}) error { - if err := formatter.appendKey(buf, key); err != nil { - return err - } - - if err := formatter.appendValue(buf, value); err != nil { - return err - } - - return nil -} - -func (format *Formatter) appendKey(buf *bytes.Buffer, key interface{}) error { - keyFmt := "%s=" - if buf.Len() > 0 { - keyFmt = " " + keyFmt - } - - if _, err := fmt.Fprintf(buf, keyFmt, key); err != nil { - return errors.WithStackTrace(err) - } - - return nil -} - -func (format *Formatter) appendValue(buf *bytes.Buffer, value interface{}) error { - var str string - - switch value := value.(type) { - case string: - str = value - case error: - str = value.Error() - default: - if _, err := fmt.Fprint(buf, value); err != nil { - return errors.WithStackTrace(err) - } - - return nil - } - - valueFmt := "%v" - if format.needsQuoting(str) { - valueFmt = format.quoteCharacter + valueFmt + format.quoteCharacter - } - - if _, err := fmt.Fprintf(buf, valueFmt, value); err != nil { - return errors.WithStackTrace(err) - } - - return nil -} - -func (format *Formatter) needsQuoting(text string) bool { - if format.quoteEmptyFields && len(text) == 0 { - return true - } - - for _, ch := range text { - if !((ch >= 'a' && ch <= 'z') || - (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch <= '9') || - ch == '-' || ch == '.') { - return true - } - } - - return false -} diff --git a/pkg/log/ff/options.go b/pkg/log/ff/options.go deleted file mode 100644 index dec3ffd99f..0000000000 --- a/pkg/log/ff/options.go +++ /dev/null @@ -1,31 +0,0 @@ -package format - -import "github.com/gruntwork-io/terragrunt/pkg/log/format/config" - -type Option func(*Formatter) - -func WithPresetConfigs(cfgs ...*config.Config) Option { - return func(formatter *Formatter) { - formatter.presetConfigs = cfgs - } -} - -func WithSelectedConfig(name string) Option { - return func(formatter *Formatter) { - formatter.selectedConfigName = name - } -} - -// WithQuoteCharacter overrides the default quoting character " with something else. For example: ', or `. -func WithQuoteCharacter(quoteCharacter string) Option { - return func(formatter *Formatter) { - formatter.quoteCharacter = quoteCharacter - } -} - -// WithQuoteEmptyFields wraps empty fields in quotes if true. -func WithQuoteEmptyFields() Option { - return func(formatter *Formatter) { - formatter.quoteEmptyFields = true - } -} diff --git a/pkg/log/format.old/color.go b/pkg/log/format.old/color.go deleted file mode 100644 index 226176f8ea..0000000000 --- a/pkg/log/format.old/color.go +++ /dev/null @@ -1,86 +0,0 @@ -package format - -import ( - "github.com/gruntwork-io/terragrunt/pkg/log" - "github.com/mgutz/ansi" -) - -var ( - defaultColorScheme = &ColorScheme{ - InfoLevelStyle: "green", - WarnLevelStyle: "yellow", - ErrorLevelStyle: "red", - FatalLevelStyle: "red", - PanicLevelStyle: "red", - DebugLevelStyle: "blue+h", - TraceLevelStyle: "white", - TFBinaryStyle: "cyan", - TimestampStyle: "black+h", - } -) - -const ( - None ColorStyleName = iota - InfoLevelStyle - WarnLevelStyle - ErrorLevelStyle - FatalLevelStyle - PanicLevelStyle - DebugLevelStyle - TraceLevelStyle - TFBinaryStyle - TimestampStyle -) - -type ColorStyleName byte - -type ColorFunc func(string) string - -type ColorStyle string - -func (style ColorStyle) ColorFunc() ColorFunc { - return ansi.ColorFunc(string(style)) -} - -type ColorScheme map[ColorStyleName]ColorStyle - -func (scheme ColorScheme) Compile() compiledColorScheme { - compiled := make(compiledColorScheme, len(scheme)) - - for name, style := range scheme { - compiled[name] = style.ColorFunc() - } - - return compiled -} - -type compiledColorScheme map[ColorStyleName]ColorFunc - -func (scheme compiledColorScheme) LevelColorFunc(level log.Level) ColorFunc { - switch level { - case log.TraceLevel: - return scheme.ColorFunc(TraceLevelStyle) - case log.DebugLevel: - return scheme.ColorFunc(DebugLevelStyle) - case log.InfoLevel: - return scheme.ColorFunc(InfoLevelStyle) - case log.WarnLevel: - return scheme.ColorFunc(WarnLevelStyle) - case log.ErrorLevel: - return scheme.ColorFunc(ErrorLevelStyle) - case log.StdoutLevel: - return scheme.ColorFunc(TraceLevelStyle) - case log.StderrLevel: - return scheme.ColorFunc(ErrorLevelStyle) - default: - return scheme.ColorFunc(None) - } -} - -func (scheme compiledColorScheme) ColorFunc(name ColorStyleName) ColorFunc { - if colorFunc, ok := scheme[name]; ok { - return colorFunc - } - - return func(s string) string { return s } -} diff --git a/pkg/log/format.old/formatter.go b/pkg/log/format.old/formatter.go deleted file mode 100644 index 13cb66bb6e..0000000000 --- a/pkg/log/format.old/formatter.go +++ /dev/null @@ -1,285 +0,0 @@ -// Package format provides a logrus formatter that formats log entries in a structured way. -package format - -import ( - "bytes" - "fmt" - "path/filepath" - "sort" - "strings" - "time" - - "github.com/gruntwork-io/terragrunt/internal/errors" - "github.com/gruntwork-io/terragrunt/pkg/log" - "github.com/sirupsen/logrus" - "golang.org/x/exp/maps" -) - -const ( - defaultTimestampForFormattedLayout = "15:04:05.000" - defaultTimestamp = time.RFC3339 - - WorkDirKeyName = "prefix" - TFPathKeyName = "tfBinary" -) - -// Formatter implements logrus.Formatter -var _ logrus.Formatter = new(Formatter) - -type PrefixStyle interface { - // ColorFunc creates a closure to avoid computation ANSI color code. - ColorFunc(prefixName string) ColorFunc -} - -type Formatter struct { - // Disable formatted layout - DisableLogFormatting bool - - // Force disabling colors. For a TTY colors are enabled by default. - DisableColors bool - - // Disable the conversion of the log levels to uppercase - DisableUppercase bool - - // Timestamp format to use for display when a full timestamp is printed. - TimestampFormat string - - // The fields are sorted by default for a consistent output. - DisableSorting bool - - // Wrap empty fields in quotes if true. - QuoteEmptyFields bool - - // Can be set to the override the default quoting character " with something else. For example: ', or `. - QuoteCharacter string - - // PrefixStyle is used to assign different styles (colors) to each prefix. - PrefixStyle PrefixStyle - - // Color scheme to use. - colorScheme compiledColorScheme -} - -// NewFormatter returns a new Formatter instance with default values. -func NewFormatter() *Formatter { - return &Formatter{ - colorScheme: defaultColorScheme.Compile(), - PrefixStyle: NewPrefixStyle(), - } -} - -func (formatter *Formatter) SetColorScheme(colorScheme *ColorScheme) { - maps.Copy(formatter.colorScheme, colorScheme.Compile()) -} - -// Format implements logrus.Formatter -func (formatter *Formatter) Format(entry *logrus.Entry) ([]byte, error) { - buf := entry.Buffer - if buf == nil { - buf = new(bytes.Buffer) - } - - if !formatter.DisableLogFormatting { - if err := formatter.printFormatted(buf, entry); err != nil { - return nil, err - } - } else { - if err := formatter.printKeyValue(buf, entry); err != nil { - return nil, err - } - } - - if err := buf.WriteByte('\n'); err != nil { - return nil, errors.New(err) - } - - return buf.Bytes(), nil -} - -func (formatter *Formatter) printKeyValue(buf *bytes.Buffer, entry *logrus.Entry) error { - timestampFormat := formatter.TimestampFormat - if timestampFormat == "" { - timestampFormat = defaultTimestamp - } - - if err := formatter.appendKeyValue(buf, "time", entry.Time.Format(timestampFormat), false); err != nil { - return err - } - - if err := formatter.appendKeyValue(buf, "level", log.FromLogrusLevel(entry.Level), true); err != nil { - return err - } - - if val, ok := entry.Data[WorkDirKeyName]; ok && val != nil { - if val, ok := val.(string); ok && val != "" { - if err := formatter.appendKeyValue(buf, "prefix", val, true); err != nil { - return err - } - } - } - - if val, ok := entry.Data[TFPathKeyName]; ok && val != nil { - if val, ok := val.(string); ok && val != "" { - if err := formatter.appendKeyValue(buf, "binary", filepath.Base(val), true); err != nil { - return err - } - } - } - - if entry.Message != "" { - if err := formatter.appendKeyValue(buf, "msg", entry.Message, true); err != nil { - return err - } - } - - keys := formatter.keys(entry.Data, WorkDirKeyName, TFPathKeyName) - for _, key := range keys { - if err := formatter.appendKeyValue(buf, key, entry.Data[key], true); err != nil { - return err - } - } - - return nil -} - -func (formatter *Formatter) printFormatted(buf *bytes.Buffer, entry *logrus.Entry) error { - level := fmt.Sprintf("%-6s ", log.FromLogrusLevel(entry.Level)) - if !formatter.DisableUppercase { - level = strings.ToUpper(level) - } - - var ( - prefix string - tfBinary string - timestamp string - ) - - if val, ok := entry.Data[WorkDirKeyName]; ok && val != nil && val != "." { - if val, ok := val.(string); ok && val != "" { - prefix = fmt.Sprintf("[%s] ", val) - } - } - - if val, ok := entry.Data[TFPathKeyName]; ok && val != nil { - if val, ok := val.(string); ok && val != "" { - tfBinary = val + ": " - } - } - - timestampFormat := formatter.TimestampFormat - if timestampFormat == "" { - timestampFormat = defaultTimestampForFormattedLayout - } - - timestamp = entry.Time.Format(timestampFormat) + " " - - if !formatter.DisableColors { - level = formatter.colorScheme.LevelColorFunc(log.FromLogrusLevel(entry.Level))(level) - prefix = formatter.PrefixStyle.ColorFunc(prefix)(prefix) - tfBinary = formatter.colorScheme.ColorFunc(TFBinaryStyle)(tfBinary) - timestamp = formatter.colorScheme.ColorFunc(TimestampStyle)(timestamp) - } - - if _, err := fmt.Fprintf(buf, "%s%s%s%s%s", timestamp, level, prefix, tfBinary, entry.Message); err != nil { - return errors.New(err) - } - - keys := formatter.keys(entry.Data, WorkDirKeyName, TFPathKeyName) - for _, key := range keys { - value := entry.Data[key] - if err := formatter.appendKeyValue(buf, key, value, true); err != nil { - return err - } - } - - return nil -} - -func (formatter *Formatter) appendKeyValue(buf *bytes.Buffer, key string, value interface{}, appendSpace bool) error { - keyFmt := "%s=" - if appendSpace { - keyFmt = " " + keyFmt - } - - if _, err := fmt.Fprintf(buf, keyFmt, key); err != nil { - return errors.New(err) - } - - if err := formatter.appendValue(buf, value); err != nil { - return err - } - - return nil -} - -func (formatter *Formatter) appendValue(buf *bytes.Buffer, value interface{}) error { - var str string - - switch value := value.(type) { - case string: - str = value - case error: - str = value.Error() - default: - if _, err := fmt.Fprint(buf, value); err != nil { - return errors.New(err) - } - - return nil - } - - valueFmt := "%v" - if formatter.needsQuoting(str) { - valueFmt = formatter.QuoteCharacter + valueFmt + formatter.QuoteCharacter - } - - if _, err := fmt.Fprintf(buf, valueFmt, value); err != nil { - return errors.New(err) - } - - return nil -} - -func (formatter *Formatter) keys(data logrus.Fields, removeKeys ...string) []string { - var ( - keys []string - ) - - for key := range data { - var skip bool - - for _, removeKey := range removeKeys { - if key == removeKey { - skip = true - break - } - } - - if !skip { - keys = append(keys, key) - } - } - - if !formatter.DisableSorting { - sort.Strings(keys) - } - - return keys -} - -func (formatter *Formatter) needsQuoting(text string) bool { - if formatter.QuoteEmptyFields && len(text) == 0 { - return true - } - - for _, ch := range text { - if !((ch >= 'a' && ch <= 'z') || - (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch <= '9') || - ch == '-' || ch == '.') { - return true - } - } - - return false -} diff --git a/pkg/log/format.old/json_formatter.go b/pkg/log/format.old/json_formatter.go deleted file mode 100644 index c278227785..0000000000 --- a/pkg/log/format.old/json_formatter.go +++ /dev/null @@ -1,154 +0,0 @@ -package format - -import ( - "bytes" - "encoding/json" - "fmt" - "time" - - "github.com/gruntwork-io/terragrunt/pkg/log" - "github.com/sirupsen/logrus" -) - -// Default key names for the default fields. -const ( - reservedFourFields = 4 - defaultTimestampFormat = time.RFC3339 - FieldKeyMsg = "msg" - FieldKeyLevel = "level" - FieldKeyTime = "time" - FieldKeyLogrusError = "logrus_error" - FieldKeyFunc = "func" - FieldKeyFile = "file" -) - -type fieldKey string - -// FieldMap allows customization of the key names for default fields. -type FieldMap map[fieldKey]string - -func (f FieldMap) resolve(key fieldKey) string { - if k, ok := f[key]; ok { - return k - } - - return string(key) -} - -// JSONFormatter formats logs into parsable json. -type JSONFormatter struct { - // TimestampFormat sets the format used for marshaling timestamps. - TimestampFormat string - - // DisableTimestamp allows disabling automatic timestamps in output. - DisableTimestamp bool - - // DisableHTMLEscape allows disabling html escaping in output. - DisableHTMLEscape bool - - // DataKey allows users to put all the log entry parameters into a nested dictionary at a given key. - DataKey string - - // FieldMap allows users to customize the names of keys for default fields. - FieldMap FieldMap - - // PrettyPrint will indent all json logs. - PrettyPrint bool -} - -// Format implements logrus.Formatter interface. -func (f *JSONFormatter) Format(entry *logrus.Entry) ([]byte, error) { - data := make(logrus.Fields, len(entry.Data)+reservedFourFields) - - for k, v := range entry.Data { - switch v := v.(type) { - case error: - // Otherwise errors are ignored by `encoding/json` - // https://github.com/sirupsen/logrus/issues/137 - data[k] = v.Error() - default: - data[k] = v - } - } - - if f.DataKey != "" { - newData := make(logrus.Fields, reservedFourFields) - newData[f.DataKey] = data - data = newData - } - - prefixFieldClashes(data, f.FieldMap, entry.HasCaller()) - - timestampFormat := f.TimestampFormat - if timestampFormat == "" { - timestampFormat = defaultTimestampFormat - } - - if !f.DisableTimestamp { - data[f.FieldMap.resolve(FieldKeyTime)] = entry.Time.Format(timestampFormat) - } - - data[f.FieldMap.resolve(FieldKeyMsg)] = entry.Message - data[f.FieldMap.resolve(FieldKeyLevel)] = log.FromLogrusLevel(entry.Level).String() - - var b *bytes.Buffer - if entry.Buffer != nil { - b = entry.Buffer - } else { - b = &bytes.Buffer{} - } - - encoder := json.NewEncoder(b) - encoder.SetEscapeHTML(!f.DisableHTMLEscape) - - if f.PrettyPrint { - encoder.SetIndent("", " ") - } - - if err := encoder.Encode(data); err != nil { - return nil, fmt.Errorf("failed to marshal fields to JSON, %w", err) - } - - return b.Bytes(), nil -} - -// This is to not silently overwrite `time`, `msg`, `func` and `level` fields when dumping it. -func prefixFieldClashes(data logrus.Fields, fieldMap FieldMap, reportCaller bool) { - timeKey := fieldMap.resolve(FieldKeyTime) - if t, ok := data[timeKey]; ok { - data["fields."+timeKey] = t - delete(data, timeKey) - } - - msgKey := fieldMap.resolve(FieldKeyMsg) - if m, ok := data[msgKey]; ok { - data["fields."+msgKey] = m - delete(data, msgKey) - } - - levelKey := fieldMap.resolve(FieldKeyLevel) - if l, ok := data[levelKey]; ok { - data["fields."+levelKey] = l - delete(data, levelKey) - } - - logrusErrKey := fieldMap.resolve(FieldKeyLogrusError) - if l, ok := data[logrusErrKey]; ok { - data["fields."+logrusErrKey] = l - delete(data, logrusErrKey) - } - - // If reportCaller is not set, 'func' will not conflict. - if reportCaller { - funcKey := fieldMap.resolve(FieldKeyFunc) - if l, ok := data[funcKey]; ok { - data["fields."+funcKey] = l - } - - fileKey := fieldMap.resolve(FieldKeyFile) - - if l, ok := data[fileKey]; ok { - data["fields."+fileKey] = l - } - } -} diff --git a/pkg/log/format.old/prefix_style.go b/pkg/log/format.old/prefix_style.go deleted file mode 100644 index 44b00b0b84..0000000000 --- a/pkg/log/format.old/prefix_style.go +++ /dev/null @@ -1,53 +0,0 @@ -package format - -import ( - "github.com/puzpuzpuz/xsync/v3" -) - -var ( - // defaultPrefixStyles contains ANSI color codes that are assigned sequentially to each unique prefix in a rotating order - // https://user-images.githubusercontent.com/995050/47952855-ecb12480-df75-11e8-89d4-ac26c50e80b9.png - // https://www.hackitu.de/termcolor256/ - defaultPrefixStyles = []ColorStyle{ - "66", "67", "95", "96", "102", "103", "108", "109", "139", "138", "144", "145", - } - - // prefixStyle implements PrefixStyle - _ PrefixStyle = new(prefixStyle) -) - -type prefixStyle struct { - // cache stores prefixes with their color schemes. - // We use [xsync.MapOf](https://github.com/puzpuzpuz/xsync?tab=readme-ov-file#map) instead of standard `sync.Map` since it's faster and has generic types. - cache *xsync.MapOf[string, ColorFunc] - - availableStyles []ColorStyle - - // nextStyleIndex is used to get the next style from the `defaultPrefixStyles` list for a newly discovered prefix. - nextStyleIndex int -} - -func NewPrefixStyle() *prefixStyle { - return &prefixStyle{ - cache: xsync.NewMapOf[string, ColorFunc](), - availableStyles: defaultPrefixStyles, - } -} - -func (prefix *prefixStyle) ColorFunc(prefixName string) ColorFunc { - if colorFunc, ok := prefix.cache.Load(prefixName); ok { - return colorFunc - } - - if prefix.nextStyleIndex >= len(prefix.availableStyles) { - prefix.nextStyleIndex = 0 - } - - colorFunc := prefix.availableStyles[prefix.nextStyleIndex].ColorFunc() - - prefix.cache.Store(prefixName, colorFunc) - - prefix.nextStyleIndex++ - - return colorFunc -} From f74f1d9f7e211593b2b3feb642b627cad23a8a01 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Mon, 4 Nov 2024 23:47:50 +0100 Subject: [PATCH 03/28] chore: add presets --- cli/app.go | 2 +- options/options.go | 2 +- pkg/log/format/format.go | 76 ++++++++++++++++++++----- pkg/log/format/formatter.go | 9 ++- pkg/log/format/options/align.go | 14 ++--- pkg/log/format/options/option.go | 1 + pkg/log/format/options/path_format.go | 20 +++++-- pkg/log/format/options/width.go | 13 +++-- pkg/log/format/placeholders/field.go | 2 +- pkg/log/format/placeholders/interval.go | 32 +++++++++++ 10 files changed, 133 insertions(+), 38 deletions(-) create mode 100644 pkg/log/format/placeholders/interval.go diff --git a/cli/app.go b/cli/app.go index 56b2b2d06a..9cbca456e2 100644 --- a/cli/app.go +++ b/cli/app.go @@ -296,7 +296,7 @@ func initialSetup(cliCtx *cli.Context, opts *options.TerragruntOptions) error { opts.RootWorkingDir = filepath.ToSlash(workingDir) - if err := opts.LogFormatter.CreateRelativePathsCache(opts.RootWorkingDir); err != nil { + if err := opts.LogFormatter.SetBaseDir(opts.RootWorkingDir); err != nil { return nil } diff --git a/options/options.go b/options/options.go index 28b08d345d..1f81aec516 100644 --- a/options/options.go +++ b/options/options.go @@ -405,7 +405,7 @@ func NewTerragruntOptions() *TerragruntOptions { } func NewTerragruntOptionsWithWriters(stdout, stderr io.Writer) *TerragruntOptions { - var logFormatter = format.NewFormatter() + var logFormatter = format.NewFormatter(format.PrettyFormat) return &TerragruntOptions{ TerraformPath: DefaultWrappedPath, diff --git a/pkg/log/format/format.go b/pkg/log/format/format.go index f02f91554c..9303a9c15e 100644 --- a/pkg/log/format/format.go +++ b/pkg/log/format/format.go @@ -7,26 +7,39 @@ import ( . "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" ) -const ( - PrettyFormat = "pretty" - JSONFormat = "json" -) +var ( + BareFormat = Placeholders{ + Level( + Width(4), + Case(UpperCase), + ), + Interval( + Prefix("["), + Suffix("]"), + ), + PlainText(" "), + Message(), + Field(WorkDirKeyName, + PathFormat(ModulePath), + Prefix("\t prefix=["), + Suffix("] "), + ), + } -var presets = map[string]Placeholders{ - PrettyFormat: Placeholders{ + PrettyFormat = Placeholders{ Time( TimeFormat(fmt.Sprintf("%s:%s:%s%s", Hour24Zero, MinZero, SecZero, MilliSec)), - Suffix(" "), Color(BlackHColor), ), + PlainText(" "), Level( Width(6), Case(UpperCase), - Suffix(" "), Color(AutoColor), ), + PlainText(" "), Field(WorkDirKeyName, - PathFormat(ShortPath), + PathFormat(RelativeModulePath), Prefix("["), Suffix("] "), Color(RandomColor), @@ -39,11 +52,12 @@ var presets = map[string]Placeholders{ Message( PathFormat(RelativePath), ), - }, - JSONFormat: Placeholders{ + } + + JSONFormat = Placeholders{ PlainText(`{"time":"`), Time( - TimeFormat(fmt.Sprintf("%s:%s:%s%s", Hour24Zero, MinZero, SecZero, MilliSec)), + TimeFormat(RFC3339), Escape(JSONEscape), ), PlainText(`", "level":"`), @@ -52,7 +66,7 @@ var presets = map[string]Placeholders{ ), PlainText(`", "work-dir":"`), Field(WorkDirKeyName, - PathFormat(ShortPath), + PathFormat(ModulePath), Escape(JSONEscape), ), PlainText(`", "tfpath":"`), @@ -67,7 +81,41 @@ var presets = map[string]Placeholders{ Escape(JSONEscape), ), PlainText(`"}`), - }, + } + + KeyValueFormat = Placeholders{ + Time( + Prefix("time="), + TimeFormat(RFC3339), + ), + Level( + Prefix(" level="), + Escape(JSONEscape), + ), + Field(WorkDirKeyName, + Prefix(" work-dir="), + PathFormat(RelativeModulePath), + Escape(JSONEscape), + ), + Field(TFPathKeyName, + Prefix(" tfpath="), + PathFormat(FilenamePath), + Escape(JSONEscape), + ), + Message( + Prefix(" message="), + PathFormat(RelativePath), + Color(DisableColor), + Escape(JSONEscape), + ), + } +) + +var presets = map[string]Placeholders{ + "bare": BareFormat, + "pretty": PrettyFormat, + "json": JSONFormat, + "key-value": KeyValueFormat, } func ParseFormat(str string) Placeholders { diff --git a/pkg/log/format/formatter.go b/pkg/log/format/formatter.go index a9328ae728..8a7bc47408 100644 --- a/pkg/log/format/formatter.go +++ b/pkg/log/format/formatter.go @@ -13,15 +13,16 @@ import ( var _ log.Formatter = new(Formatter) type Formatter struct { + baseDir string format placeholders.Placeholders disableColors bool relativePather *options.RelativePather } // NewFormatter returns a new Formatter instance with default values. -func NewFormatter() *Formatter { +func NewFormatter(format placeholders.Placeholders) *Formatter { return &Formatter{ - format: presets[PrettyFormat], + format: format, } } @@ -38,6 +39,7 @@ func (formatter *Formatter) Format(entry *log.Entry) ([]byte, error) { str := formatter.format.Evaluate(&options.Data{ Entry: entry, + BaseDir: formatter.baseDir, DisableColors: formatter.disableColors, RelativePather: formatter.relativePather, }) @@ -60,13 +62,14 @@ func (formatter *Formatter) DisableColors() { formatter.disableColors = true } -func (formatter *Formatter) CreateRelativePathsCache(baseDir string) error { +func (formatter *Formatter) SetBaseDir(baseDir string) error { pather, err := options.NewRelativePather(baseDir) if err != nil { return err } formatter.relativePather = pather + formatter.baseDir = baseDir return nil } diff --git a/pkg/log/format/options/align.go b/pkg/log/format/options/align.go index 7c0d4eeee4..7b06d1a8d4 100644 --- a/pkg/log/format/options/align.go +++ b/pkg/log/format/options/align.go @@ -26,19 +26,17 @@ type align struct { } func (option *align) Evaluate(data *Data, str string) string { - leftSpaces := len(str) - len(strings.TrimLeft(str, " ")) - rightSpaces := len(str) - len(strings.TrimRight(str, " ")) + withoutSpaces := strings.TrimSpace(str) + spaces := len(str) - len(withoutSpaces) switch option.value { case LeftAlign: - return strings.TrimLeft(str, " ") + strings.Repeat(" ", leftSpaces) + return withoutSpaces + strings.Repeat(" ", spaces) case RightAlign: - return strings.Repeat(" ", rightSpaces) + strings.TrimRight(str, " ") + return strings.Repeat(" ", spaces) + withoutSpaces case CenterAlign: - spaces := leftSpaces + rightSpaces - - rightSpaces = (spaces - spaces%2) / 2 - leftSpaces = spaces - rightSpaces + rightSpaces := (spaces - spaces%2) / 2 + leftSpaces := spaces - rightSpaces return strings.Repeat(" ", leftSpaces) + strings.TrimSpace(str) + strings.Repeat(" ", rightSpaces) } diff --git a/pkg/log/format/options/option.go b/pkg/log/format/options/option.go index 3efc98a41d..b9e05fd66f 100644 --- a/pkg/log/format/options/option.go +++ b/pkg/log/format/options/option.go @@ -56,6 +56,7 @@ type Option interface { type Data struct { *log.Entry + BaseDir string DisableColors bool RelativePather *RelativePather AutoColorFn func() ColorValue diff --git a/pkg/log/format/options/path_format.go b/pkg/log/format/options/path_format.go index 14c9287f95..c8368ca0a2 100644 --- a/pkg/log/format/options/path_format.go +++ b/pkg/log/format/options/path_format.go @@ -16,16 +16,18 @@ const PathFormatOptionName = "path" const ( NonePath PathFormatValue = iota RelativePath - ShortPath + RelativeModulePath + ModulePath FilenamePath DirectoryPath ) var pathFormatValues = CommonMapValues[PathFormatValue]{ - RelativePath: "relative", - ShortPath: "short", - FilenamePath: "filename", - DirectoryPath: "dir", + RelativePath: "relative", + RelativeModulePath: "relative-module", + ModulePath: "module", + FilenamePath: "filename", + DirectoryPath: "dir", } type PathFormatValue byte @@ -42,7 +44,7 @@ func (option *pathFormat) Evaluate(data *Data, str string) string { } return data.RelativePather.ReplaceAbsPaths(str) - case ShortPath: + case RelativeModulePath: if data.RelativePather == nil { break } @@ -57,6 +59,12 @@ func (option *pathFormat) Evaluate(data *Data, str string) string { return str[len(log.CurDirWithSeparator):] } + return str + case ModulePath: + if str == data.BaseDir { + return "" + } + return str case FilenamePath: return filepath.Base(str) diff --git a/pkg/log/format/options/width.go b/pkg/log/format/options/width.go index 66455ab885..80087cb5ea 100644 --- a/pkg/log/format/options/width.go +++ b/pkg/log/format/options/width.go @@ -25,13 +25,18 @@ type width struct { } func (option *width) Evaluate(data *Data, str string) string { - rightSpaces := int(option.value) - - if rightSpaces -= len(log.RemoveAllASCISeq(str)); rightSpaces < 1 { + width := int(option.value) + if width == 0 { return str } - return str + strings.Repeat(" ", rightSpaces) + strLen := len(log.RemoveAllASCISeq(str)) + + if width < strLen { + return str[:width] + } + + return str + strings.Repeat(" ", width-strLen) } func Width(value WidthValue) Option { diff --git a/pkg/log/format/placeholders/field.go b/pkg/log/format/placeholders/field.go index 2060d83403..837d9bdf89 100644 --- a/pkg/log/format/placeholders/field.go +++ b/pkg/log/format/placeholders/field.go @@ -36,7 +36,7 @@ func Field(fieldName string, opts ...options.Option) Placeholder { func init() { Registered.Add( - Field(WorkDirKeyName, options.PathFormat(options.NonePath, options.RelativePath, options.ShortPath)), + Field(WorkDirKeyName, options.PathFormat(options.NonePath, options.RelativePath, options.RelativeModulePath, options.ModulePath)), Field(TFPathKeyName, options.PathFormat(options.NonePath, options.FilenamePath, options.DirectoryPath)), ) } diff --git a/pkg/log/format/placeholders/interval.go b/pkg/log/format/placeholders/interval.go new file mode 100644 index 0000000000..2308f30158 --- /dev/null +++ b/pkg/log/format/placeholders/interval.go @@ -0,0 +1,32 @@ +package placeholders + +import ( + "fmt" + "time" + + "github.com/gruntwork-io/terragrunt/pkg/log/format/options" +) + +const IntervalPlaceholderName = "interval" + +type intervalPlaceholder struct { + baseTime time.Time + *CommonPlaceholder +} + +func (t *intervalPlaceholder) Evaluate(data *options.Data) string { + return t.opts.Evaluate(data, fmt.Sprintf("%04d", time.Since(t.baseTime)/time.Second)) +} + +func Interval(opts ...options.Option) Placeholder { + opts = WithCommonOptions().Merge(opts...) + + return &intervalPlaceholder{ + baseTime: time.Now(), + CommonPlaceholder: NewCommonPlaceholder(IntervalPlaceholderName, opts...), + } +} + +func init() { + Registered.Add(Interval()) +} From 479066691daf0f20a8590528a56462418acaf580 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Tue, 5 Nov 2024 19:02:04 +0100 Subject: [PATCH 04/28] chore: fix lint --- cli/app.go | 2 +- config/config_test.go | 5 ++--- configstack/log_test.go | 5 ++--- pkg/log/format/format.go | 9 +++++---- pkg/log/format/options/align.go | 4 +++- pkg/log/format/options/case.go | 1 + pkg/log/format/options/color.go | 5 ++--- pkg/log/format/options/escape.go | 19 +++++++++---------- pkg/log/format/options/level_format.go | 1 + pkg/log/format/options/option.go | 2 ++ pkg/log/format/options/path_format.go | 1 + pkg/log/format/options/time_format.go | 1 - pkg/log/format/placeholders/placeholder.go | 5 ++++- pkg/log/level.go | 2 +- shell/run_shell_cmd_output_test.go | 3 +-- test/integration_common_test.go | 5 ++--- 16 files changed, 37 insertions(+), 33 deletions(-) diff --git a/cli/app.go b/cli/app.go index 9cbca456e2..095a0b06c3 100644 --- a/cli/app.go +++ b/cli/app.go @@ -297,7 +297,7 @@ func initialSetup(cliCtx *cli.Context, opts *options.TerragruntOptions) error { opts.RootWorkingDir = filepath.ToSlash(workingDir) if err := opts.LogFormatter.SetBaseDir(opts.RootWorkingDir); err != nil { - return nil + return err } // --- Download Dir diff --git a/config/config_test.go b/config/config_test.go index 0e01718ed8..996de2a2e9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -18,9 +18,8 @@ import ( ) func createLogger() log.Logger { - formatter := format.NewFormatter() - formatter.DisableColors = true - formatter.DisableLogFormatting = true + formatter := format.NewFormatter(format.KeyValueFormat) + formatter.DisableColors() return log.New(log.WithLevel(log.DebugLevel), log.WithFormatter(formatter)) } diff --git a/configstack/log_test.go b/configstack/log_test.go index a6a045c51f..2702b694d5 100644 --- a/configstack/log_test.go +++ b/configstack/log_test.go @@ -22,9 +22,8 @@ func TestLogReductionHook(t *testing.T) { stdout := bytes.Buffer{} - formatter := format.NewFormatter() - formatter.DisableColors = true - formatter.DisableLogFormatting = true + formatter := format.NewFormatter(format.KeyValueFormat) + formatter.DisableColors() var testLogger = log.New( log.WithOutput(&stdout), diff --git a/pkg/log/format/format.go b/pkg/log/format/format.go index 9303a9c15e..53288e06ec 100644 --- a/pkg/log/format/format.go +++ b/pkg/log/format/format.go @@ -1,16 +1,17 @@ +// Package format implements a custom format logs package format import ( "fmt" - . "github.com/gruntwork-io/terragrunt/pkg/log/format/options" - . "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" + . "github.com/gruntwork-io/terragrunt/pkg/log/format/options" //nolint:stylecheck + . "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" //nolint:stylecheck ) var ( BareFormat = Placeholders{ Level( - Width(4), + Width(4), //nolint:mnd Case(UpperCase), ), Interval( @@ -33,7 +34,7 @@ var ( ), PlainText(" "), Level( - Width(6), + Width(6), //nolint:mnd Case(UpperCase), Color(AutoColor), ), diff --git a/pkg/log/format/options/align.go b/pkg/log/format/options/align.go index 7b06d1a8d4..8889434760 100644 --- a/pkg/log/format/options/align.go +++ b/pkg/log/format/options/align.go @@ -35,10 +35,12 @@ func (option *align) Evaluate(data *Data, str string) string { case RightAlign: return strings.Repeat(" ", spaces) + withoutSpaces case CenterAlign: - rightSpaces := (spaces - spaces%2) / 2 + twoSides := 2 + rightSpaces := (spaces - spaces%2) / twoSides leftSpaces := spaces - rightSpaces return strings.Repeat(" ", leftSpaces) + strings.TrimSpace(str) + strings.Repeat(" ", rightSpaces) + case NoneAlign: } return str diff --git a/pkg/log/format/options/case.go b/pkg/log/format/options/case.go index 7c7cb42635..b054675361 100644 --- a/pkg/log/format/options/case.go +++ b/pkg/log/format/options/case.go @@ -36,6 +36,7 @@ func (option *textCase) Evaluate(data *Data, str string) string { return strings.ToLower(str) case CapitalizeCase: return cases.Title(language.English, cases.Compact).String(str) + case NoneCase: } return str diff --git a/pkg/log/format/options/color.go b/pkg/log/format/options/color.go index 91c529b724..4962ae2d29 100644 --- a/pkg/log/format/options/color.go +++ b/pkg/log/format/options/color.go @@ -123,16 +123,15 @@ func (color *ColorOption) Evaluate(data *Data, str string) string { } return str - } -func (ColorOption *ColorOption) SetValue(str string) error { +func (color *ColorOption) SetValue(str string) error { val, err := colorValues.Parse(str) if err != nil { return err } - ColorOption.value = val + color.value = val return nil } diff --git a/pkg/log/format/options/escape.go b/pkg/log/format/options/escape.go index 82b0e6594c..d78d0b15f6 100644 --- a/pkg/log/format/options/escape.go +++ b/pkg/log/format/options/escape.go @@ -23,18 +23,17 @@ type textEscape struct { } func (option *textEscape) Evaluate(data *Data, str string) string { - switch option.value { - case JSONEscape: - b, err := json.Marshal(str) - if err != nil { - fmt.Printf("Failed to marhsal %q, %v\n", str, err) - } - - // Trim the beginning and trailing " character. - return string(b[1 : len(b)-1]) + if option.value != JSONEscape { + return str } - return str + b, err := json.Marshal(str) + if err != nil { + fmt.Printf("Failed to marhsal %q, %v\n", str, err) + } + + // Trim the beginning and trailing " character. + return string(b[1 : len(b)-1]) } func Escape(value EscapeValue) Option { diff --git a/pkg/log/format/options/level_format.go b/pkg/log/format/options/level_format.go index 1c51b6faaf..c42aecb761 100644 --- a/pkg/log/format/options/level_format.go +++ b/pkg/log/format/options/level_format.go @@ -26,6 +26,7 @@ func (format *levelFormat) Evaluate(data *Data, str string) string { return data.Level.TinyName() case LevelFormatShort: return data.Level.ShortName() + case LevelFormatFull: } return data.Level.FullName() diff --git a/pkg/log/format/options/option.go b/pkg/log/format/options/option.go index b9e05fd66f..21b21c94ab 100644 --- a/pkg/log/format/options/option.go +++ b/pkg/log/format/options/option.go @@ -1,3 +1,4 @@ +// Package options implements placeholders options. package options import ( @@ -24,6 +25,7 @@ func (options Options) Merge(withOptions ...Option) Options { if reflect.TypeOf(options[i]) == reflect.TypeOf(withOptions[t]) { options[i] = withOptions[t] withOptions = append(withOptions[:t], withOptions[t+1:]...) + break } } diff --git a/pkg/log/format/options/path_format.go b/pkg/log/format/options/path_format.go index c8368ca0a2..7d7e35505e 100644 --- a/pkg/log/format/options/path_format.go +++ b/pkg/log/format/options/path_format.go @@ -70,6 +70,7 @@ func (option *pathFormat) Evaluate(data *Data, str string) string { return filepath.Base(str) case DirectoryPath: return filepath.Dir(str) + case NonePath: } return str diff --git a/pkg/log/format/options/time_format.go b/pkg/log/format/options/time_format.go index 393f65ad2d..7e72e795e8 100644 --- a/pkg/log/format/options/time_format.go +++ b/pkg/log/format/options/time_format.go @@ -93,7 +93,6 @@ type TimeFormatValue string type timeFormat struct { *CommonOption[string] - sortedValueMapKeys []string } func (option *timeFormat) SetValue(str string) error { diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go index 682c378767..1d83b2cd64 100644 --- a/pkg/log/format/placeholders/placeholder.go +++ b/pkg/log/format/placeholders/placeholder.go @@ -1,3 +1,4 @@ +// Package placeholders implements fillers from which to format logs. package placeholders import ( @@ -54,8 +55,9 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er name := str[next : i+1] if placeholder = registered.Get(name); placeholder != nil { - next = i + 2 + next = i + 2 //nolint:mnd } + continue } @@ -72,6 +74,7 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er if option == nil { return nil, 0, errors.Errorf("empty option name for placeholder %q", placeholder.Name()) } + if err := option.SetValue(val); err != nil { return nil, 0, errors.Errorf("invalid value %q for option %q, placeholder %q: %w", val, option.Name(), placeholder.Name(), err) } diff --git a/pkg/log/level.go b/pkg/log/level.go index ffcb924b95..a72681eb99 100644 --- a/pkg/log/level.go +++ b/pkg/log/level.go @@ -98,7 +98,7 @@ func (level Level) String() string { return level.FullName() } -// Name returns the full level name. +// FullName returns the full level name. func (level Level) FullName() string { if name, ok := levelNames[level]; ok { return name diff --git a/shell/run_shell_cmd_output_test.go b/shell/run_shell_cmd_output_test.go index b5b41c003c..0234fea45a 100644 --- a/shell/run_shell_cmd_output_test.go +++ b/shell/run_shell_cmd_output_test.go @@ -68,8 +68,7 @@ func TestCommandOutputPrefix(t *testing.T) { prefixedOutput = append(prefixedOutput, fmt.Sprintf("prefix=%s binary=%s msg=%s", prefix, filepath.Base(terraformPath), line)) } - logFormatter := format.NewFormatter() - logFormatter.DisableLogFormatting = true + logFormatter := format.NewFormatter(format.KeyValueFormat) testCommandOutput(t, func(terragruntOptions *options.TerragruntOptions) { terragruntOptions.TerraformPath = terraformPath diff --git a/test/integration_common_test.go b/test/integration_common_test.go index f16c086535..d18329fc80 100644 --- a/test/integration_common_test.go +++ b/test/integration_common_test.go @@ -58,9 +58,8 @@ func getPathsRelativeTo(t *testing.T, basePath string, paths []string) []string } func createLogger() log.Logger { - formatter := format.NewFormatter() - formatter.DisableColors = true - formatter.DisableLogFormatting = true + formatter := format.NewFormatter(format.KeyValueFormat) + formatter.DisableColors() return log.New(log.WithLevel(log.DebugLevel), log.WithFormatter(formatter)) } From 61300f79bf777d00aacedff1f7e7ad7269dfbedd Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Tue, 5 Nov 2024 21:24:13 +0100 Subject: [PATCH 05/28] chore: fix tests --- cli/commands/flags.go | 41 ++++++++++++++++----------------- test/integration_serial_test.go | 2 +- test/integration_test.go | 14 +++++------ 3 files changed, 28 insertions(+), 29 deletions(-) diff --git a/cli/commands/flags.go b/cli/commands/flags.go index dc8f804289..b50af91aa1 100644 --- a/cli/commands/flags.go +++ b/cli/commands/flags.go @@ -372,27 +372,26 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { return nil }, }, - // TODO: remove - // &cli.BoolFlag{ - // Name: TerragruntDisableLogFormattingFlagName, - // EnvVar: TerragruntDisableLogFormattingEnvName, - // Destination: &opts.DisableLogFormatting, - // Usage: "If specified, logs will be displayed in key/value format. By default, logs are formatted in a human readable format.", - // Action: func(ctx *cli.Context, val bool) error { - // //opts.LogFormatter.DisableLogFormatting = val - // return nil - // }, - // }, - // &cli.BoolFlag{ - // Name: TerragruntJSONLogFlagName, - // EnvVar: TerragruntJSONLogEnvName, - // Destination: &opts.JSONLogFormat, - // Usage: "If specified, Terragrunt will output its logs in JSON format.", - // Action: func(ctx *cli.Context, _ bool) error { - // //opts.Logger.SetOptions(log.WithFormatter(&format.JSONFormatter{})) - // return nil - // }, - // }, + &cli.BoolFlag{ + Name: TerragruntDisableLogFormattingFlagName, + EnvVar: TerragruntDisableLogFormattingEnvName, + Destination: &opts.DisableLogFormatting, + Usage: "If specified, logs will be displayed in key/value format. By default, logs are formatted in a human readable format.", + Action: func(ctx *cli.Context, val bool) error { + opts.LogFormatter = format.NewFormatter(format.KeyValueFormat) + return nil + }, + }, + &cli.BoolFlag{ + Name: TerragruntJSONLogFlagName, + EnvVar: TerragruntJSONLogEnvName, + Destination: &opts.JSONLogFormat, + Usage: "If specified, Terragrunt will output its logs in JSON format.", + Action: func(ctx *cli.Context, _ bool) error { + opts.LogFormatter = format.NewFormatter(format.JSONFormat) + return nil + }, + }, &cli.BoolFlag{ Name: TerragruntShowLogAbsPathsFlagName, EnvVar: TerragruntShowLogAbsPathsEnvName, diff --git a/test/integration_serial_test.go b/test/integration_serial_test.go index 40256c1b73..5991b0675c 100644 --- a/test/integration_serial_test.go +++ b/test/integration_serial_test.go @@ -697,7 +697,7 @@ func TestParseTFLog(t *testing.T) { tmpEnvPath := copyEnvironment(t, testFixtureLogFormatter) rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter) - _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-disable-log-formatting=false -no-color --terragrunt-no-color --terragrunt-working-dir "+rootPath) + _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-log-format=pretty -no-color --terragrunt-no-color --terragrunt-working-dir "+rootPath) require.NoError(t, err) for _, prefixName := range []string{"app", "dep"} { diff --git a/test/integration_test.go b/test/integration_test.go index 688026eb7f..3b6a69e67a 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -123,7 +123,7 @@ func TestDisableLogging(t *testing.T) { tmpEnvPath := copyEnvironment(t, testFixtureLogFormatter) rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter) - stdout, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-log-disable --terragrunt-non-interactive --terragrunt-disable-log-formatting=false -no-color --terragrunt-no-color --terragrunt-working-dir "+rootPath) + stdout, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-log-disable --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-working-dir "+rootPath) require.NoError(t, err) assert.Contains(t, stdout, "Initializing provider plugins...") @@ -137,7 +137,7 @@ func TestLogWithAbsPath(t *testing.T) { tmpEnvPath := copyEnvironment(t, testFixtureLogFormatter) rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter) - _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-log-show-abs-paths --terragrunt-non-interactive --terragrunt-disable-log-formatting=false -no-color --terragrunt-no-color --terragrunt-working-dir "+rootPath) + _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-log-show-abs-paths --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-working-dir "+rootPath) require.NoError(t, err) for _, prefixName := range []string{"app", "dep"} { @@ -178,7 +178,7 @@ func TestLogWithRelPath(t *testing.T) { t.Run(fmt.Sprintf("testCase-%d", i), func(t *testing.T) { t.Parallel() - stdout, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-disable-log-formatting=false -no-color --terragrunt-no-color --terragrunt-working-dir "+workingDir) + stdout, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-working-dir "+workingDir) require.NoError(t, err) testCase.assertFn(t, stdout, stderr) @@ -193,7 +193,7 @@ func TestLogFormatterPrettyOutput(t *testing.T) { tmpEnvPath := copyEnvironment(t, testFixtureLogFormatter) rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter) - stdout, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-disable-log-formatting=false -no-color --terragrunt-no-color --terragrunt-working-dir "+rootPath) + stdout, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-working-dir "+rootPath) require.NoError(t, err) for _, prefixName := range []string{"app", "dep"} { @@ -212,7 +212,7 @@ func TestLogFormatterKeyValueOutput(t *testing.T) { tmpEnvPath := copyEnvironment(t, testFixtureLogFormatter) rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter) - _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init -no-color --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-disable-log-formatting --terragrunt-working-dir "+rootPath) + _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init -no-color --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-working-dir "+rootPath) require.NoError(t, err) for _, prefixName := range []string{"app", "dep"} { @@ -2695,8 +2695,8 @@ func runTerragruntCommand(t *testing.T, command string, writer io.Writer, errwri args := strings.Split(command, " ") - if !strings.Contains(command, "-terragrunt-disable-log-formatting") { - args = append(args, "--terragrunt-disable-log-formatting") + if !strings.Contains(command, "-terragrunt-log-format") { + args = append(args, "--terragrunt-log-format=key-value") } t.Log(args) From c58d235eee1a7df8310692b2df5d41891bfcf13e Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Tue, 5 Nov 2024 22:03:09 +0100 Subject: [PATCH 06/28] chore: fix tests --- pkg/log/format/format.go | 12 ++++-------- pkg/log/format/placeholders/field.go | 2 +- pkg/log/format/placeholders/message.go | 2 +- shell/run_shell_cmd_output_test.go | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/pkg/log/format/format.go b/pkg/log/format/format.go index 53288e06ec..25eabddb93 100644 --- a/pkg/log/format/format.go +++ b/pkg/log/format/format.go @@ -65,7 +65,7 @@ var ( Level( Escape(JSONEscape), ), - PlainText(`", "work-dir":"`), + PlainText(`", "prefix":"`), Field(WorkDirKeyName, PathFormat(ModulePath), Escape(JSONEscape), @@ -75,7 +75,7 @@ var ( PathFormat(FilenamePath), Escape(JSONEscape), ), - PlainText(`", "message":"`), + PlainText(`", "msg":"`), Message( PathFormat(RelativePath), Color(DisableColor), @@ -91,23 +91,19 @@ var ( ), Level( Prefix(" level="), - Escape(JSONEscape), ), Field(WorkDirKeyName, - Prefix(" work-dir="), + Prefix(" prefix="), PathFormat(RelativeModulePath), - Escape(JSONEscape), ), Field(TFPathKeyName, Prefix(" tfpath="), PathFormat(FilenamePath), - Escape(JSONEscape), ), Message( - Prefix(" message="), + Prefix(" msg="), PathFormat(RelativePath), Color(DisableColor), - Escape(JSONEscape), ), } ) diff --git a/pkg/log/format/placeholders/field.go b/pkg/log/format/placeholders/field.go index 837d9bdf89..b754823258 100644 --- a/pkg/log/format/placeholders/field.go +++ b/pkg/log/format/placeholders/field.go @@ -5,7 +5,7 @@ import ( ) const ( - WorkDirKeyName = "workdir" + WorkDirKeyName = "prefix" DownloadDirKeyName = "downloaddir" TFPathKeyName = "tfpath" ) diff --git a/pkg/log/format/placeholders/message.go b/pkg/log/format/placeholders/message.go index 7596ec2c4b..d8931ff2a8 100644 --- a/pkg/log/format/placeholders/message.go +++ b/pkg/log/format/placeholders/message.go @@ -4,7 +4,7 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log/format/options" ) -const MessagePlaceholderName = "message" +const MessagePlaceholderName = "msg" type message struct { *CommonPlaceholder diff --git a/shell/run_shell_cmd_output_test.go b/shell/run_shell_cmd_output_test.go index 0234fea45a..0a940b30dc 100644 --- a/shell/run_shell_cmd_output_test.go +++ b/shell/run_shell_cmd_output_test.go @@ -65,7 +65,7 @@ func TestCommandOutputPrefix(t *testing.T) { terraformPath := "testdata/test_outputs.sh" prefixedOutput := []string{} for _, line := range FullOutput { - prefixedOutput = append(prefixedOutput, fmt.Sprintf("prefix=%s binary=%s msg=%s", prefix, filepath.Base(terraformPath), line)) + prefixedOutput = append(prefixedOutput, fmt.Sprintf("prefix=%s tfpath=%s msg=%s", prefix, filepath.Base(terraformPath), line)) } logFormatter := format.NewFormatter(format.KeyValueFormat) From 948a625825d1e2e148a9a3a112676dedafe07050 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Thu, 7 Nov 2024 13:55:18 +0100 Subject: [PATCH 07/28] chore: fix tests --- cli/app.go | 4 ++++ cli/commands/flags.go | 20 ++++++++++++----- pkg/log/format/format.go | 26 +++++++++++++++------- pkg/log/format/formatter.go | 18 +++++++++------ pkg/log/format/options/align.go | 6 ++--- pkg/log/format/options/case.go | 6 ++--- pkg/log/format/options/color.go | 26 +++++++++++----------- pkg/log/format/options/common.go | 2 +- pkg/log/format/options/escape.go | 6 ++--- pkg/log/format/options/level_format.go | 6 ++--- pkg/log/format/options/option.go | 2 +- pkg/log/format/options/path_format.go | 6 ++--- pkg/log/format/options/prefix.go | 8 +++---- pkg/log/format/options/suffix.go | 8 +++---- pkg/log/format/options/time_format.go | 8 +++---- pkg/log/format/options/width.go | 16 ++++++------- pkg/log/format/placeholders/placeholder.go | 2 +- test/integration_test.go | 11 +++++---- 18 files changed, 103 insertions(+), 78 deletions(-) diff --git a/cli/app.go b/cli/app.go index 095a0b06c3..11cf909306 100644 --- a/cli/app.go +++ b/cli/app.go @@ -300,6 +300,10 @@ func initialSetup(cliCtx *cli.Context, opts *options.TerragruntOptions) error { return err } + if opts.LogShowAbsPaths { + opts.LogFormatter.DisableRelativePaths() + } + // --- Download Dir if opts.DownloadDir == "" { opts.DownloadDir = util.JoinPath(opts.WorkingDir, util.TerragruntCacheDir) diff --git a/cli/commands/flags.go b/cli/commands/flags.go index b50af91aa1..1a8715730b 100644 --- a/cli/commands/flags.go +++ b/cli/commands/flags.go @@ -378,7 +378,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Destination: &opts.DisableLogFormatting, Usage: "If specified, logs will be displayed in key/value format. By default, logs are formatted in a human readable format.", Action: func(ctx *cli.Context, val bool) error { - opts.LogFormatter = format.NewFormatter(format.KeyValueFormat) + opts.LogFormatter.SetFormat(format.KeyValueFormat) return nil }, }, @@ -388,7 +388,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Destination: &opts.JSONLogFormat, Usage: "If specified, Terragrunt will output its logs in JSON format.", Action: func(ctx *cli.Context, _ bool) error { - opts.LogFormatter = format.NewFormatter(format.JSONFormat) + opts.LogFormatter.SetFormat(format.JSONFormat) return nil }, }, @@ -437,12 +437,20 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { EnvVar: TerragruntLogFormatEnvName, Usage: "", // TODO: write usage Action: func(ctx *cli.Context, val string) error { - format := format.ParseFormat(val) - if format == nil { - return errors.Errorf("flag --%s, invalid format %q", TerragruntLogFormatFlagName, val) + phs, err := format.ParseFormat(val) + if err != nil { + return errors.Errorf("flag --%s, invalid format %q, %v", TerragruntLogFormatFlagName, val, err) + } + + if opts.DisableLog || opts.DisableLogFormatting || opts.JSONLogFormat { + return nil } - opts.LogFormatter.SetFormat(format) + if val == format.BareFormatName { + opts.ForwardTFStdout = true + } + + opts.LogFormatter.SetFormat(phs) return nil }, }, diff --git a/pkg/log/format/format.go b/pkg/log/format/format.go index 25eabddb93..2634fd43ea 100644 --- a/pkg/log/format/format.go +++ b/pkg/log/format/format.go @@ -3,9 +3,19 @@ package format import ( "fmt" + "strings" + "github.com/gruntwork-io/terragrunt/internal/errors" . "github.com/gruntwork-io/terragrunt/pkg/log/format/options" //nolint:stylecheck . "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" //nolint:stylecheck + "golang.org/x/exp/maps" +) + +const ( + BareFormatName = "bare" + PrettyFormatName = "pretty" + JSONFormatName = "json" + KeyValueFormatName = "key-value" ) var ( @@ -43,7 +53,7 @@ var ( PathFormat(RelativeModulePath), Prefix("["), Suffix("] "), - Color(RandomColor), + Color(GradientColor), ), Field(TFPathKeyName, PathFormat(FilenamePath), @@ -109,18 +119,18 @@ var ( ) var presets = map[string]Placeholders{ - "bare": BareFormat, - "pretty": PrettyFormat, - "json": JSONFormat, - "key-value": KeyValueFormat, + BareFormatName: BareFormat, + PrettyFormatName: PrettyFormat, + JSONFormatName: JSONFormat, + KeyValueFormatName: KeyValueFormat, } -func ParseFormat(str string) Placeholders { +func ParseFormat(str string) (Placeholders, error) { for name, format := range presets { if name == str { - return format + return format, nil } } - return nil + return nil, errors.Errorf("available values: %s", strings.Join(maps.Keys(presets), ",")) } diff --git a/pkg/log/format/formatter.go b/pkg/log/format/formatter.go index 8a7bc47408..9dcccf50d8 100644 --- a/pkg/log/format/formatter.go +++ b/pkg/log/format/formatter.go @@ -14,21 +14,21 @@ var _ log.Formatter = new(Formatter) type Formatter struct { baseDir string - format placeholders.Placeholders + placeholders placeholders.Placeholders disableColors bool relativePather *options.RelativePather } // NewFormatter returns a new Formatter instance with default values. -func NewFormatter(format placeholders.Placeholders) *Formatter { +func NewFormatter(phs placeholders.Placeholders) *Formatter { return &Formatter{ - format: format, + placeholders: phs, } } // Format implements logrus.Format func (formatter *Formatter) Format(entry *log.Entry) ([]byte, error) { - if formatter.format == nil { + if formatter.placeholders == nil { return nil, nil } @@ -37,7 +37,7 @@ func (formatter *Formatter) Format(entry *log.Entry) ([]byte, error) { buf = new(bytes.Buffer) } - str := formatter.format.Evaluate(&options.Data{ + str := formatter.placeholders.Evaluate(&options.Data{ Entry: entry, BaseDir: formatter.baseDir, DisableColors: formatter.disableColors, @@ -74,6 +74,10 @@ func (formatter *Formatter) SetBaseDir(baseDir string) error { return nil } -func (formatter *Formatter) SetFormat(format placeholders.Placeholders) { - formatter.format = format +func (formatter *Formatter) DisableRelativePaths() { + formatter.relativePather = nil +} + +func (formatter *Formatter) SetFormat(phs placeholders.Placeholders) { + formatter.placeholders = phs } diff --git a/pkg/log/format/options/align.go b/pkg/log/format/options/align.go index 8889434760..ea8dbe9e07 100644 --- a/pkg/log/format/options/align.go +++ b/pkg/log/format/options/align.go @@ -21,11 +21,11 @@ var alignValues = CommonMapValues[AlignValue]{ type AlignValue byte -type align struct { +type AlignOption struct { *CommonOption[AlignValue] } -func (option *align) Evaluate(data *Data, str string) string { +func (option *AlignOption) Evaluate(data *Data, str string) string { withoutSpaces := strings.TrimSpace(str) spaces := len(str) - len(withoutSpaces) @@ -47,7 +47,7 @@ func (option *align) Evaluate(data *Data, str string) string { } func Align(value AlignValue) Option { - return &align{ + return &AlignOption{ CommonOption: NewCommonOption[AlignValue](AlignOptionName, value, alignValues), } } diff --git a/pkg/log/format/options/case.go b/pkg/log/format/options/case.go index b054675361..99a81941d8 100644 --- a/pkg/log/format/options/case.go +++ b/pkg/log/format/options/case.go @@ -24,11 +24,11 @@ var textCaseValues = CommonMapValues[CaseValue]{ type CaseValue byte -type textCase struct { +type CaseOption struct { *CommonOption[CaseValue] } -func (option *textCase) Evaluate(data *Data, str string) string { +func (option *CaseOption) Evaluate(data *Data, str string) string { switch option.value { case UpperCase: return strings.ToUpper(str) @@ -43,7 +43,7 @@ func (option *textCase) Evaluate(data *Data, str string) string { } func Case(value CaseValue) Option { - return &textCase{ + return &CaseOption{ CommonOption: NewCommonOption[CaseValue](CaseOptionName, value, textCaseValues), } } diff --git a/pkg/log/format/options/color.go b/pkg/log/format/options/color.go index 4962ae2d29..91d30c9d7b 100644 --- a/pkg/log/format/options/color.go +++ b/pkg/log/format/options/color.go @@ -19,7 +19,7 @@ const ( BlueHColor BlackHColor AutoColor - RandomColor + GradientColor Color66 Color67 @@ -36,16 +36,16 @@ const ( ) var colorValues = CommonMapValues[ColorValue]{ - RedColor: "red", - WhiteColor: "white", - YellowColor: "yellow", - GreenColor: "green", - CyanColor: "cyan", - BlueHColor: "light-blue", - BlackHColor: "light-black", - AutoColor: "auto", - RandomColor: "random", - DisableColor: "disable", + RedColor: "red", + WhiteColor: "white", + YellowColor: "yellow", + GreenColor: "green", + CyanColor: "cyan", + BlueHColor: "light-blue", + BlackHColor: "light-black", + AutoColor: "auto", + GradientColor: "gradient", + DisableColor: "disable", } var ( @@ -114,7 +114,7 @@ func (color *ColorOption) Evaluate(data *Data, str string) string { value = data.AutoColorFn() } - if value == RandomColor && color.randomColor != nil { + if value == GradientColor && color.randomColor != nil { value = color.randomColor.Value(str) } @@ -125,7 +125,7 @@ func (color *ColorOption) Evaluate(data *Data, str string) string { return str } -func (color *ColorOption) SetValue(str string) error { +func (color *ColorOption) ParseValue(str string) error { val, err := colorValues.Parse(str) if err != nil { return err diff --git a/pkg/log/format/options/common.go b/pkg/log/format/options/common.go index cd6fd43b9f..0955ff5989 100644 --- a/pkg/log/format/options/common.go +++ b/pkg/log/format/options/common.go @@ -38,7 +38,7 @@ func (option *CommonOption[T]) Evaluate(data *Data, str string) string { return str } -func (option *CommonOption[T]) SetValue(str string) error { +func (option *CommonOption[T]) ParseValue(str string) error { val, err := option.values.Parse(str) if err != nil { return err diff --git a/pkg/log/format/options/escape.go b/pkg/log/format/options/escape.go index d78d0b15f6..d7154fd07d 100644 --- a/pkg/log/format/options/escape.go +++ b/pkg/log/format/options/escape.go @@ -18,11 +18,11 @@ var textEscapeValues = CommonMapValues[EscapeValue]{ type EscapeValue byte -type textEscape struct { +type EscapeOption struct { *CommonOption[EscapeValue] } -func (option *textEscape) Evaluate(data *Data, str string) string { +func (option *EscapeOption) Evaluate(data *Data, str string) string { if option.value != JSONEscape { return str } @@ -37,7 +37,7 @@ func (option *textEscape) Evaluate(data *Data, str string) string { } func Escape(value EscapeValue) Option { - return &textEscape{ + return &EscapeOption{ CommonOption: NewCommonOption[EscapeValue](EscapeOptionName, value, textEscapeValues), } } diff --git a/pkg/log/format/options/level_format.go b/pkg/log/format/options/level_format.go index c42aecb761..6ad6b81833 100644 --- a/pkg/log/format/options/level_format.go +++ b/pkg/log/format/options/level_format.go @@ -16,11 +16,11 @@ var levelFormatValues = CommonMapValues[LevelFormatValue]{ type LevelFormatValue byte -type levelFormat struct { +type LevelFormatOption struct { *CommonOption[LevelFormatValue] } -func (format *levelFormat) Evaluate(data *Data, str string) string { +func (format *LevelFormatOption) Evaluate(data *Data, str string) string { switch format.Value() { case LevelFormatTiny: return data.Level.TinyName() @@ -33,7 +33,7 @@ func (format *levelFormat) Evaluate(data *Data, str string) string { } func LevelFormat(val LevelFormatValue) Option { - return &levelFormat{ + return &LevelFormatOption{ CommonOption: NewCommonOption[LevelFormatValue](LevelFormatOptionName, val, levelFormatValues), } } diff --git a/pkg/log/format/options/option.go b/pkg/log/format/options/option.go index 21b21c94ab..cec3cc75d7 100644 --- a/pkg/log/format/options/option.go +++ b/pkg/log/format/options/option.go @@ -53,7 +53,7 @@ type OptionValues[Value any] interface { type Option interface { Name() string Evaluate(data *Data, str string) string - SetValue(str string) error + ParseValue(str string) error } type Data struct { diff --git a/pkg/log/format/options/path_format.go b/pkg/log/format/options/path_format.go index 7d7e35505e..2ecc42ed2e 100644 --- a/pkg/log/format/options/path_format.go +++ b/pkg/log/format/options/path_format.go @@ -32,11 +32,11 @@ var pathFormatValues = CommonMapValues[PathFormatValue]{ type PathFormatValue byte -type pathFormat struct { +type PathFormatOption struct { *CommonOption[PathFormatValue] } -func (option *pathFormat) Evaluate(data *Data, str string) string { +func (option *PathFormatOption) Evaluate(data *Data, str string) string { switch option.value { case RelativePath: if data.RelativePather == nil { @@ -82,7 +82,7 @@ func PathFormat(val PathFormatValue, allowed ...PathFormatValue) Option { values = values.Filter(allowed...) } - return &pathFormat{ + return &PathFormatOption{ CommonOption: NewCommonOption[PathFormatValue](PathFormatOptionName, val, values), } } diff --git a/pkg/log/format/options/prefix.go b/pkg/log/format/options/prefix.go index c22f4d9f13..a3b0a5e091 100644 --- a/pkg/log/format/options/prefix.go +++ b/pkg/log/format/options/prefix.go @@ -2,22 +2,22 @@ package options const PrefixOptionName = "prefix" -type prefix struct { +type PrefixOption struct { *CommonOption[string] } -func (option *prefix) Evaluate(data *Data, str string) string { +func (option *PrefixOption) Evaluate(data *Data, str string) string { return option.value + str } -func (option *prefix) SetValue(str string) error { +func (option *PrefixOption) ParseValue(str string) error { option.value = str return nil } func Prefix(value string) Option { - return &prefix{ + return &PrefixOption{ CommonOption: NewCommonOption[string](PrefixOptionName, value, nil), } } diff --git a/pkg/log/format/options/suffix.go b/pkg/log/format/options/suffix.go index 359c9122ab..81bfbf85f7 100644 --- a/pkg/log/format/options/suffix.go +++ b/pkg/log/format/options/suffix.go @@ -2,22 +2,22 @@ package options const SuffixOptionName = "suffix" -type suffix struct { +type SuffixOption struct { *CommonOption[string] } -func (option *suffix) Evaluate(data *Data, str string) string { +func (option *SuffixOption) Evaluate(data *Data, str string) string { return str + option.value } -func (option *suffix) SetValue(str string) error { +func (option *SuffixOption) ParseValue(str string) error { option.value = str return nil } func Suffix(value string) Option { - return &suffix{ + return &SuffixOption{ CommonOption: NewCommonOption[string](SuffixOptionName, value, nil), } } diff --git a/pkg/log/format/options/time_format.go b/pkg/log/format/options/time_format.go index 7e72e795e8..4c923c0f10 100644 --- a/pkg/log/format/options/time_format.go +++ b/pkg/log/format/options/time_format.go @@ -91,22 +91,22 @@ func (valMap TimeFormatValueMap) Value(str string) string { type TimeFormatValue string -type timeFormat struct { +type TimeFormatOption struct { *CommonOption[string] } -func (option *timeFormat) SetValue(str string) error { +func (option *TimeFormatOption) ParseValue(str string) error { option.value = timeFormatValueMap.Value(str) return nil } -func (option *timeFormat) Evaluate(data *Data, str string) string { +func (option *TimeFormatOption) Evaluate(data *Data, str string) string { return data.Time.Format(option.Value()) } func TimeFormat(str string) Option { - return &timeFormat{ + return &TimeFormatOption{ CommonOption: NewCommonOption[string](TimeFormatOptionName, timeFormatValueMap.Value(str), nil), } } diff --git a/pkg/log/format/options/width.go b/pkg/log/format/options/width.go index 80087cb5ea..268c60958a 100644 --- a/pkg/log/format/options/width.go +++ b/pkg/log/format/options/width.go @@ -20,27 +20,27 @@ func (val WidthValue) Parse(str string) (WidthValue, error) { return val, errors.Errorf("incorrect option value: %s", str) } -type width struct { +type WidthOption struct { *CommonOption[WidthValue] } -func (option *width) Evaluate(data *Data, str string) string { - width := int(option.value) - if width == 0 { +func (option *WidthOption) Evaluate(data *Data, str string) string { + WidthOption := int(option.value) + if WidthOption == 0 { return str } strLen := len(log.RemoveAllASCISeq(str)) - if width < strLen { - return str[:width] + if WidthOption < strLen { + return str[:WidthOption] } - return str + strings.Repeat(" ", width-strLen) + return str + strings.Repeat(" ", WidthOption-strLen) } func Width(value WidthValue) Option { - return &width{ + return &WidthOption{ CommonOption: NewCommonOption[WidthValue](WidthOptionName, value, value), } } diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go index 1d83b2cd64..44d722c2f3 100644 --- a/pkg/log/format/placeholders/placeholder.go +++ b/pkg/log/format/placeholders/placeholder.go @@ -75,7 +75,7 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er return nil, 0, errors.Errorf("empty option name for placeholder %q", placeholder.Name()) } - if err := option.SetValue(val); err != nil { + if err := option.ParseValue(val); err != nil { return nil, 0, errors.Errorf("invalid value %q for option %q, placeholder %q: %w", val, option.Name(), placeholder.Name(), err) } } else if val != "" { diff --git a/test/integration_test.go b/test/integration_test.go index 3b6a69e67a..cf74ed7a0b 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -74,7 +74,7 @@ const ( testFixtureInitOnce = "fixtures/init-once" testFixtureInputs = "fixtures/inputs" testFixtureInputsFromDependency = "fixtures/inputs-from-dependency" - testFixtureLogFormatter = "fixtures/log/format" + testFixtureLogFormatter = "fixtures/log/formatter" testFixtureLogRelPaths = "fixtures/log/rel-paths" testFixtureMissingDependence = "fixtures/missing-dependencies/main" testFixtureModulePathError = "fixtures/module-path-in-error" @@ -137,7 +137,7 @@ func TestLogWithAbsPath(t *testing.T) { tmpEnvPath := copyEnvironment(t, testFixtureLogFormatter) rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter) - _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-log-show-abs-paths --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-working-dir "+rootPath) + _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-log-show-abs-paths --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-log-format=pretty --terragrunt-working-dir "+rootPath) require.NoError(t, err) for _, prefixName := range []string{"app", "dep"} { @@ -178,7 +178,7 @@ func TestLogWithRelPath(t *testing.T) { t.Run(fmt.Sprintf("testCase-%d", i), func(t *testing.T) { t.Parallel() - stdout, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-working-dir "+workingDir) + stdout, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-log-format=pretty --terragrunt-working-dir "+workingDir) require.NoError(t, err) testCase.assertFn(t, stdout, stderr) @@ -193,7 +193,7 @@ func TestLogFormatterPrettyOutput(t *testing.T) { tmpEnvPath := copyEnvironment(t, testFixtureLogFormatter) rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter) - stdout, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-working-dir "+rootPath) + stdout, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init --terragrunt-log-level debug --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-log-format=pretty --terragrunt-working-dir "+rootPath) require.NoError(t, err) for _, prefixName := range []string{"app", "dep"} { @@ -216,10 +216,9 @@ func TestLogFormatterKeyValueOutput(t *testing.T) { require.NoError(t, err) for _, prefixName := range []string{"app", "dep"} { - assert.Contains(t, stderr, "level=stdout prefix="+prefixName+" binary="+wrappedBinary()+" msg=Initializing provider plugins...\n") + assert.Contains(t, stderr, "level=stdout prefix="+prefixName+" tfpath="+wrappedBinary()+" msg=Initializing provider plugins...\n") assert.Contains(t, stderr, "level=debug prefix="+prefixName+" msg=Reading Terragrunt config file at ./"+prefixName+"/terragrunt.hcl\n") } - assert.Contains(t, stderr, "level=debug prefix=. msg=Terragrunt Version:") } func TestLogRawModuleOutput(t *testing.T) { From 4eda5384e37700ad5d7d01d391239256beb231bb Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Thu, 7 Nov 2024 19:16:00 +0100 Subject: [PATCH 08/28] chore: code improvements --- cli/commands/flags.go | 18 ++++++------ configstack/options.go | 2 ++ pkg/log/format/formatter.go | 5 +++- pkg/log/format/options/align.go | 10 +++---- pkg/log/format/options/case.go | 10 +++---- pkg/log/format/options/color.go | 8 +++--- pkg/log/format/options/common.go | 4 +-- pkg/log/format/options/escape.go | 11 ++++---- pkg/log/format/options/level_format.go | 8 +++--- pkg/log/format/options/option.go | 15 +++++----- pkg/log/format/options/path_format.go | 20 ++++++------- pkg/log/format/options/prefix.go | 4 +-- pkg/log/format/options/suffix.go | 4 +-- pkg/log/format/options/time_format.go | 4 +-- pkg/log/format/options/width.go | 8 +++--- pkg/log/format/placeholders/common.go | 2 +- pkg/log/format/placeholders/field.go | 11 ++------ pkg/log/format/placeholders/interval.go | 6 +--- pkg/log/format/placeholders/level.go | 6 +--- pkg/log/format/placeholders/message.go | 6 +--- pkg/log/format/placeholders/placeholder.go | 33 ++++++++++++++-------- pkg/log/format/placeholders/plaintext.go | 2 +- pkg/log/format/placeholders/time.go | 6 +--- pkg/log/helper.go | 2 +- 24 files changed, 101 insertions(+), 104 deletions(-) diff --git a/cli/commands/flags.go b/cli/commands/flags.go index 1a8715730b..799dd0fda9 100644 --- a/cli/commands/flags.go +++ b/cli/commands/flags.go @@ -337,7 +337,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { EnvVar: TerragruntLogLevelEnvName, DefaultText: opts.LogLevel.String(), Usage: fmt.Sprintf("Sets the logging level for Terragrunt. Supported levels: %s", log.AllLevels), - Action: func(ctx *cli.Context, val string) error { + Action: func(_ *cli.Context, val string) error { // Before the release of v0.67.0, these levels actually disabled logs, since we do not use these levels for logging. // For backward compatibility we simulate the same behavior. removedLevels := []string{ @@ -366,7 +366,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { EnvVar: TerragruntLogDisableEnvName, Usage: "Disable logging", Destination: &opts.DisableLog, - Action: func(ctx *cli.Context, _ bool) error { + Action: func(_ *cli.Context, _ bool) error { opts.ForwardTFStdout = true opts.LogFormatter.SetFormat(nil) return nil @@ -377,7 +377,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { EnvVar: TerragruntDisableLogFormattingEnvName, Destination: &opts.DisableLogFormatting, Usage: "If specified, logs will be displayed in key/value format. By default, logs are formatted in a human readable format.", - Action: func(ctx *cli.Context, val bool) error { + Action: func(_ *cli.Context, _ bool) error { opts.LogFormatter.SetFormat(format.KeyValueFormat) return nil }, @@ -387,7 +387,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { EnvVar: TerragruntJSONLogEnvName, Destination: &opts.JSONLogFormat, Usage: "If specified, Terragrunt will output its logs in JSON format.", - Action: func(ctx *cli.Context, _ bool) error { + Action: func(_ *cli.Context, _ bool) error { opts.LogFormatter.SetFormat(format.JSONFormat) return nil }, @@ -403,7 +403,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { EnvVar: TerragruntNoColorEnvName, Destination: &opts.DisableLogColors, Usage: "If specified, Terragrunt output won't contain any color.", - Action: func(ctx *cli.Context, _ bool) error { + Action: func(_ *cli.Context, _ bool) error { opts.LogFormatter.DisableColors() return nil }, @@ -436,7 +436,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Name: TerragruntLogFormatFlagName, EnvVar: TerragruntLogFormatEnvName, Usage: "", // TODO: write usage - Action: func(ctx *cli.Context, val string) error { + Action: func(_ *cli.Context, val string) error { phs, err := format.ParseFormat(val) if err != nil { return errors.Errorf("flag --%s, invalid format %q, %v", TerragruntLogFormatFlagName, val, err) @@ -451,6 +451,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { } opts.LogFormatter.SetFormat(phs) + return nil }, }, @@ -458,13 +459,14 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Name: TerragruntLogPrettyFormatFlagName, EnvVar: TerragruntLogPrettyFormatEnvName, Usage: "", // TODO: write usage - Action: func(ctx *cli.Context, val string) error { - phs, err := placeholders.Parse(val, placeholders.Registered) + Action: func(_ *cli.Context, val string) error { + phs, err := placeholders.Parse(val) if err != nil { return errors.Errorf("flag --%s, %w", TerragruntLogPrettyFormatFlagName, err) } opts.LogFormatter.SetFormat(phs) + return nil }, }, diff --git a/configstack/options.go b/configstack/options.go index 659bae56ad..fdd9cd61f1 100644 --- a/configstack/options.go +++ b/configstack/options.go @@ -10,6 +10,7 @@ type Option func(Stack) Stack func WithChildTerragruntConfig(config *config.TerragruntConfig) Option { return func(stack Stack) Stack { stack.childTerragruntConfig = config + return stack } } @@ -17,6 +18,7 @@ func WithChildTerragruntConfig(config *config.TerragruntConfig) Option { func WithParseOptions(parserOptions []hclparse.Option) Option { return func(stack Stack) Stack { stack.parserOptions = parserOptions + return stack } } diff --git a/pkg/log/format/formatter.go b/pkg/log/format/formatter.go index 9dcccf50d8..4073dd16bf 100644 --- a/pkg/log/format/formatter.go +++ b/pkg/log/format/formatter.go @@ -37,12 +37,15 @@ func (formatter *Formatter) Format(entry *log.Entry) ([]byte, error) { buf = new(bytes.Buffer) } - str := formatter.placeholders.Evaluate(&options.Data{ + str, err := formatter.placeholders.Evaluate(&options.Data{ Entry: entry, BaseDir: formatter.baseDir, DisableColors: formatter.disableColors, RelativePather: formatter.relativePather, }) + if err != nil { + return nil, err + } if str != "" { if _, err := buf.WriteString(str); err != nil { diff --git a/pkg/log/format/options/align.go b/pkg/log/format/options/align.go index ea8dbe9e07..e680e742f9 100644 --- a/pkg/log/format/options/align.go +++ b/pkg/log/format/options/align.go @@ -25,25 +25,25 @@ type AlignOption struct { *CommonOption[AlignValue] } -func (option *AlignOption) Evaluate(data *Data, str string) string { +func (option *AlignOption) Evaluate(data *Data, str string) (string, error) { withoutSpaces := strings.TrimSpace(str) spaces := len(str) - len(withoutSpaces) switch option.value { case LeftAlign: - return withoutSpaces + strings.Repeat(" ", spaces) + return withoutSpaces + strings.Repeat(" ", spaces), nil case RightAlign: - return strings.Repeat(" ", spaces) + withoutSpaces + return strings.Repeat(" ", spaces) + withoutSpaces, nil case CenterAlign: twoSides := 2 rightSpaces := (spaces - spaces%2) / twoSides leftSpaces := spaces - rightSpaces - return strings.Repeat(" ", leftSpaces) + strings.TrimSpace(str) + strings.Repeat(" ", rightSpaces) + return strings.Repeat(" ", leftSpaces) + strings.TrimSpace(str) + strings.Repeat(" ", rightSpaces), nil case NoneAlign: } - return str + return str, nil } func Align(value AlignValue) Option { diff --git a/pkg/log/format/options/case.go b/pkg/log/format/options/case.go index 99a81941d8..642ad97309 100644 --- a/pkg/log/format/options/case.go +++ b/pkg/log/format/options/case.go @@ -28,18 +28,18 @@ type CaseOption struct { *CommonOption[CaseValue] } -func (option *CaseOption) Evaluate(data *Data, str string) string { +func (option *CaseOption) Evaluate(data *Data, str string) (string, error) { switch option.value { case UpperCase: - return strings.ToUpper(str) + return strings.ToUpper(str), nil case LowerCase: - return strings.ToLower(str) + return strings.ToLower(str), nil case CapitalizeCase: - return cases.Title(language.English, cases.Compact).String(str) + return cases.Title(language.English, cases.Compact).String(str), nil case NoneCase: } - return str + return str, nil } func Case(value CaseValue) Option { diff --git a/pkg/log/format/options/color.go b/pkg/log/format/options/color.go index 91d30c9d7b..8fda0b54e0 100644 --- a/pkg/log/format/options/color.go +++ b/pkg/log/format/options/color.go @@ -103,11 +103,11 @@ type ColorOption struct { randomColor *randomColor } -func (color *ColorOption) Evaluate(data *Data, str string) string { +func (color *ColorOption) Evaluate(data *Data, str string) (string, error) { value := color.value if value == DisableColor || data.DisableColors { - return log.RemoveAllASCISeq(str) + return log.RemoveAllASCISeq(str), nil } if value == AutoColor && data.AutoColorFn != nil { @@ -122,7 +122,7 @@ func (color *ColorOption) Evaluate(data *Data, str string) string { str = colorFn(str) } - return str + return str, nil } func (color *ColorOption) ParseValue(str string) error { @@ -166,7 +166,7 @@ var ( type randomColor struct { // cache stores unique text with their color code. - // We use [xsync.MapOf](https://github.com/puzpuzpuz/xsync?tab=readme-ov-file#map) instaed of standard `sync.Map` since it's faster and has generic types. + // We use [xsync.MapOf](https://github.com/puzpuzpuz/xsync?tab=readme-ov-file#map) instead of standard `sync.Map` since it's faster and has generic types. cache *xsync.MapOf[string, ColorValue] values []ColorValue diff --git a/pkg/log/format/options/common.go b/pkg/log/format/options/common.go index 0955ff5989..ab81bb1da2 100644 --- a/pkg/log/format/options/common.go +++ b/pkg/log/format/options/common.go @@ -34,8 +34,8 @@ func (option *CommonOption[T]) String() string { return fmt.Sprintf("%v", option.value) } -func (option *CommonOption[T]) Evaluate(data *Data, str string) string { - return str +func (option *CommonOption[T]) Evaluate(data *Data, str string) (string, error) { + return str, nil } func (option *CommonOption[T]) ParseValue(str string) error { diff --git a/pkg/log/format/options/escape.go b/pkg/log/format/options/escape.go index d7154fd07d..8b7641edd6 100644 --- a/pkg/log/format/options/escape.go +++ b/pkg/log/format/options/escape.go @@ -2,7 +2,8 @@ package options import ( "encoding/json" - "fmt" + + "github.com/gruntwork-io/terragrunt/internal/errors" ) const EscapeOptionName = "escape" @@ -22,18 +23,18 @@ type EscapeOption struct { *CommonOption[EscapeValue] } -func (option *EscapeOption) Evaluate(data *Data, str string) string { +func (option *EscapeOption) Evaluate(data *Data, str string) (string, error) { if option.value != JSONEscape { - return str + return str, nil } b, err := json.Marshal(str) if err != nil { - fmt.Printf("Failed to marhsal %q, %v\n", str, err) + return "", errors.New(err) } // Trim the beginning and trailing " character. - return string(b[1 : len(b)-1]) + return string(b[1 : len(b)-1]), nil } func Escape(value EscapeValue) Option { diff --git a/pkg/log/format/options/level_format.go b/pkg/log/format/options/level_format.go index 6ad6b81833..4aa33b3762 100644 --- a/pkg/log/format/options/level_format.go +++ b/pkg/log/format/options/level_format.go @@ -20,16 +20,16 @@ type LevelFormatOption struct { *CommonOption[LevelFormatValue] } -func (format *LevelFormatOption) Evaluate(data *Data, str string) string { +func (format *LevelFormatOption) Evaluate(data *Data, str string) (string, error) { switch format.Value() { case LevelFormatTiny: - return data.Level.TinyName() + return data.Level.TinyName(), nil case LevelFormatShort: - return data.Level.ShortName() + return data.Level.ShortName(), nil case LevelFormatFull: } - return data.Level.FullName() + return data.Level.FullName(), nil } func LevelFormat(val LevelFormatValue) Option { diff --git a/pkg/log/format/options/option.go b/pkg/log/format/options/option.go index cec3cc75d7..d8398be72b 100644 --- a/pkg/log/format/options/option.go +++ b/pkg/log/format/options/option.go @@ -34,16 +34,17 @@ func (options Options) Merge(withOptions ...Option) Options { return append(options, withOptions...) } -func (options Options) Evaluate(data *Data, str string) string { - for _, option := range options { - str = option.Evaluate(data, str) +func (options Options) Evaluate(data *Data, str string) (string, error) { + var err error - if str == "" { - return "" + for _, option := range options { + str, err = option.Evaluate(data, str) + if str == "" || err != nil { + return "", err } } - return str + return str, nil } type OptionValues[Value any] interface { @@ -52,7 +53,7 @@ type OptionValues[Value any] interface { type Option interface { Name() string - Evaluate(data *Data, str string) string + Evaluate(data *Data, str string) (string, error) ParseValue(str string) error } diff --git a/pkg/log/format/options/path_format.go b/pkg/log/format/options/path_format.go index 2ecc42ed2e..4ca200bdff 100644 --- a/pkg/log/format/options/path_format.go +++ b/pkg/log/format/options/path_format.go @@ -36,14 +36,14 @@ type PathFormatOption struct { *CommonOption[PathFormatValue] } -func (option *PathFormatOption) Evaluate(data *Data, str string) string { +func (option *PathFormatOption) Evaluate(data *Data, str string) (string, error) { switch option.value { case RelativePath: if data.RelativePather == nil { break } - return data.RelativePather.ReplaceAbsPaths(str) + return data.RelativePather.ReplaceAbsPaths(str), nil case RelativeModulePath: if data.RelativePather == nil { break @@ -52,28 +52,28 @@ func (option *PathFormatOption) Evaluate(data *Data, str string) string { str = data.RelativePather.ReplaceAbsPaths(str) if str == log.CurDir { - return "" + return "", nil } if strings.HasPrefix(str, log.CurDirWithSeparator) { - return str[len(log.CurDirWithSeparator):] + return str[len(log.CurDirWithSeparator):], nil } - return str + return str, nil case ModulePath: if str == data.BaseDir { - return "" + return "", nil } - return str + return str, nil case FilenamePath: - return filepath.Base(str) + return filepath.Base(str), nil case DirectoryPath: - return filepath.Dir(str) + return filepath.Dir(str), nil case NonePath: } - return str + return str, nil } func PathFormat(val PathFormatValue, allowed ...PathFormatValue) Option { diff --git a/pkg/log/format/options/prefix.go b/pkg/log/format/options/prefix.go index a3b0a5e091..42e2620cc8 100644 --- a/pkg/log/format/options/prefix.go +++ b/pkg/log/format/options/prefix.go @@ -6,8 +6,8 @@ type PrefixOption struct { *CommonOption[string] } -func (option *PrefixOption) Evaluate(data *Data, str string) string { - return option.value + str +func (option *PrefixOption) Evaluate(data *Data, str string) (string, error) { + return option.value + str, nil } func (option *PrefixOption) ParseValue(str string) error { diff --git a/pkg/log/format/options/suffix.go b/pkg/log/format/options/suffix.go index 81bfbf85f7..4265b6b13f 100644 --- a/pkg/log/format/options/suffix.go +++ b/pkg/log/format/options/suffix.go @@ -6,8 +6,8 @@ type SuffixOption struct { *CommonOption[string] } -func (option *SuffixOption) Evaluate(data *Data, str string) string { - return str + option.value +func (option *SuffixOption) Evaluate(data *Data, str string) (string, error) { + return str + option.value, nil } func (option *SuffixOption) ParseValue(str string) error { diff --git a/pkg/log/format/options/time_format.go b/pkg/log/format/options/time_format.go index 4c923c0f10..78610ab6d7 100644 --- a/pkg/log/format/options/time_format.go +++ b/pkg/log/format/options/time_format.go @@ -101,8 +101,8 @@ func (option *TimeFormatOption) ParseValue(str string) error { return nil } -func (option *TimeFormatOption) Evaluate(data *Data, str string) string { - return data.Time.Format(option.Value()) +func (option *TimeFormatOption) Evaluate(data *Data, str string) (string, error) { + return data.Time.Format(option.Value()), nil } func TimeFormat(str string) Option { diff --git a/pkg/log/format/options/width.go b/pkg/log/format/options/width.go index 268c60958a..99c5232f0b 100644 --- a/pkg/log/format/options/width.go +++ b/pkg/log/format/options/width.go @@ -24,19 +24,19 @@ type WidthOption struct { *CommonOption[WidthValue] } -func (option *WidthOption) Evaluate(data *Data, str string) string { +func (option *WidthOption) Evaluate(data *Data, str string) (string, error) { WidthOption := int(option.value) if WidthOption == 0 { - return str + return str, nil } strLen := len(log.RemoveAllASCISeq(str)) if WidthOption < strLen { - return str[:WidthOption] + return str[:WidthOption], nil } - return str + strings.Repeat(" ", WidthOption-strLen) + return str + strings.Repeat(" ", WidthOption-strLen), nil } func Width(value WidthValue) Option { diff --git a/pkg/log/format/placeholders/common.go b/pkg/log/format/placeholders/common.go index 8f4edb11fa..d1627488b7 100644 --- a/pkg/log/format/placeholders/common.go +++ b/pkg/log/format/placeholders/common.go @@ -39,6 +39,6 @@ func (common *CommonPlaceholder) SetValue(str string) error { return nil } -func (common *CommonPlaceholder) Evaluate(data *options.Data) string { +func (common *CommonPlaceholder) Evaluate(data *options.Data) (string, error) { return common.opts.Evaluate(data, "") } diff --git a/pkg/log/format/placeholders/field.go b/pkg/log/format/placeholders/field.go index b754823258..e74b144e0f 100644 --- a/pkg/log/format/placeholders/field.go +++ b/pkg/log/format/placeholders/field.go @@ -14,14 +14,14 @@ type fieldPlaceholder struct { *CommonPlaceholder } -func (field *fieldPlaceholder) Evaluate(data *options.Data) string { +func (field *fieldPlaceholder) Evaluate(data *options.Data) (string, error) { if val, ok := data.Fields[field.Name()]; ok { if val, ok := val.(string); ok { return field.opts.Evaluate(data, val) } } - return "" + return "", nil } func Field(fieldName string, opts ...options.Option) Placeholder { @@ -33,10 +33,3 @@ func Field(fieldName string, opts ...options.Option) Placeholder { CommonPlaceholder: NewCommonPlaceholder(fieldName, opts...), } } - -func init() { - Registered.Add( - Field(WorkDirKeyName, options.PathFormat(options.NonePath, options.RelativePath, options.RelativeModulePath, options.ModulePath)), - Field(TFPathKeyName, options.PathFormat(options.NonePath, options.FilenamePath, options.DirectoryPath)), - ) -} diff --git a/pkg/log/format/placeholders/interval.go b/pkg/log/format/placeholders/interval.go index 2308f30158..31374a706f 100644 --- a/pkg/log/format/placeholders/interval.go +++ b/pkg/log/format/placeholders/interval.go @@ -14,7 +14,7 @@ type intervalPlaceholder struct { *CommonPlaceholder } -func (t *intervalPlaceholder) Evaluate(data *options.Data) string { +func (t *intervalPlaceholder) Evaluate(data *options.Data) (string, error) { return t.opts.Evaluate(data, fmt.Sprintf("%04d", time.Since(t.baseTime)/time.Second)) } @@ -26,7 +26,3 @@ func Interval(opts ...options.Option) Placeholder { CommonPlaceholder: NewCommonPlaceholder(IntervalPlaceholderName, opts...), } } - -func init() { - Registered.Add(Interval()) -} diff --git a/pkg/log/format/placeholders/level.go b/pkg/log/format/placeholders/level.go index 1d95c7e892..faf01d2fa2 100644 --- a/pkg/log/format/placeholders/level.go +++ b/pkg/log/format/placeholders/level.go @@ -32,7 +32,7 @@ type level struct { *CommonPlaceholder } -func (level *level) Evaluate(data *options.Data) string { +func (level *level) Evaluate(data *options.Data) (string, error) { newData := *data newData.AutoColorFn = func() options.ColorValue { return levlAutoColorFunc(data.Level) @@ -50,7 +50,3 @@ func Level(opts ...options.Option) Placeholder { CommonPlaceholder: NewCommonPlaceholder(LevelPlaceholderName, opts...), } } - -func init() { - Registered.Add(Level()) -} diff --git a/pkg/log/format/placeholders/message.go b/pkg/log/format/placeholders/message.go index d8931ff2a8..67181bf071 100644 --- a/pkg/log/format/placeholders/message.go +++ b/pkg/log/format/placeholders/message.go @@ -10,7 +10,7 @@ type message struct { *CommonPlaceholder } -func (msg *message) Evaluate(data *options.Data) string { +func (msg *message) Evaluate(data *options.Data) (string, error) { return msg.opts.Evaluate(data, data.Message) } @@ -23,7 +23,3 @@ func Message(opts ...options.Option) Placeholder { CommonPlaceholder: NewCommonPlaceholder(MessagePlaceholderName, opts...), } } - -func init() { - Registered.Add(Message()) -} diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go index 44d722c2f3..b376948794 100644 --- a/pkg/log/format/placeholders/placeholder.go +++ b/pkg/log/format/placeholders/placeholder.go @@ -10,8 +10,6 @@ import ( const placeholderSign = '%' -var Registered Placeholders - type Placeholders []Placeholder func (phs Placeholders) Get(name string) Placeholder { @@ -24,6 +22,17 @@ func (phs Placeholders) Get(name string) Placeholder { return nil } +func newPlaceholders() Placeholders { + return Placeholders{ + Interval(), + Time(), + Level(), + Message(), + Field(WorkDirKeyName, options.PathFormat(options.NonePath, options.RelativePath, options.RelativeModulePath, options.ModulePath)), + Field(TFPathKeyName, options.PathFormat(options.NonePath, options.FilenamePath, options.DirectoryPath)), + } +} + func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, error) { var ( next int @@ -103,8 +112,9 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er return placeholder, len(str) - 1, nil } -func Parse(str string, registered Placeholders) (Placeholders, error) { +func Parse(str string) (Placeholders, error) { var ( + registered = newPlaceholders() placeholders Placeholders next int ) @@ -141,24 +151,25 @@ func Parse(str string, registered Placeholders) (Placeholders, error) { return placeholders, nil } -func (phs *Placeholders) Add(new ...Placeholder) { - *phs = append(*phs, new...) -} - -func (phs Placeholders) Evaluate(data *options.Data) string { +func (phs Placeholders) Evaluate(data *options.Data) (string, error) { var str string for _, ph := range phs { - str += ph.Evaluate(data) + s, err := ph.Evaluate(data) + if err != nil { + return "", err + } + + str += s } - return str + return str, nil } type Placeholder interface { Name() string GetOption(name string) options.Option - Evaluate(data *options.Data) string + Evaluate(data *options.Data) (string, error) } func isPlaceholderName(c byte) bool { diff --git a/pkg/log/format/placeholders/plaintext.go b/pkg/log/format/placeholders/plaintext.go index 1884c64b13..e166d5dda5 100644 --- a/pkg/log/format/placeholders/plaintext.go +++ b/pkg/log/format/placeholders/plaintext.go @@ -11,7 +11,7 @@ type plainText struct { value string } -func (plainText *plainText) Evaluate(data *options.Data) string { +func (plainText *plainText) Evaluate(data *options.Data) (string, error) { return plainText.opts.Evaluate(data, plainText.value) } diff --git a/pkg/log/format/placeholders/time.go b/pkg/log/format/placeholders/time.go index 41d70c06bc..549bd9b5ee 100644 --- a/pkg/log/format/placeholders/time.go +++ b/pkg/log/format/placeholders/time.go @@ -12,7 +12,7 @@ type timePlaceholder struct { *CommonPlaceholder } -func (t *timePlaceholder) Evaluate(data *options.Data) string { +func (t *timePlaceholder) Evaluate(data *options.Data) (string, error) { return t.opts.Evaluate(data, data.Time.String()) } @@ -25,7 +25,3 @@ func Time(opts ...options.Option) Placeholder { CommonPlaceholder: NewCommonPlaceholder(TimePlaceholderName, opts...), } } - -func init() { - Registered.Add(Time()) -} diff --git a/pkg/log/helper.go b/pkg/log/helper.go index bdd43312af..030ae93da5 100644 --- a/pkg/log/helper.go +++ b/pkg/log/helper.go @@ -6,7 +6,7 @@ import ( // Formatter is used to implement a custom Formatter. type Formatter interface { - Format(*Entry) ([]byte, error) + Format(entry *Entry) ([]byte, error) } // Entry is the final logging entry. From 2f5647aa772af6efaccbe917d339456128751c20 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Thu, 7 Nov 2024 19:47:38 +0100 Subject: [PATCH 09/28] chore: code improvements --- cli/commands/flags.go | 4 +- config/config_test.go | 2 +- configstack/log_test.go | 2 +- options/options.go | 2 +- pkg/cli/flag.go | 2 +- pkg/log/fields.go | 25 ---------- pkg/log/format/format.go | 38 +++++++++------ pkg/log/format/formatter.go | 8 +-- pkg/log/format/options/align.go | 4 +- pkg/log/format/options/case.go | 4 +- pkg/log/format/options/color.go | 46 ++++++++--------- pkg/log/format/options/common.go | 2 +- pkg/log/format/options/escape.go | 4 +- pkg/log/format/options/level_format.go | 4 +- pkg/log/format/options/path_format.go | 2 +- pkg/log/format/options/prefix.go | 2 +- pkg/log/format/options/suffix.go | 2 +- pkg/log/format/options/time_format.go | 4 +- pkg/log/format/options/width.go | 2 +- pkg/log/format/placeholders/placeholder.go | 57 +++++++++++----------- shell/run_shell_cmd_output_test.go | 2 +- test/integration_common_test.go | 2 +- 22 files changed, 102 insertions(+), 118 deletions(-) diff --git a/cli/commands/flags.go b/cli/commands/flags.go index 799dd0fda9..99f6c8bd10 100644 --- a/cli/commands/flags.go +++ b/cli/commands/flags.go @@ -378,7 +378,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Destination: &opts.DisableLogFormatting, Usage: "If specified, logs will be displayed in key/value format. By default, logs are formatted in a human readable format.", Action: func(_ *cli.Context, _ bool) error { - opts.LogFormatter.SetFormat(format.KeyValueFormat) + opts.LogFormatter.SetFormat(format.NewKeyValueFormat()) return nil }, }, @@ -388,7 +388,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Destination: &opts.JSONLogFormat, Usage: "If specified, Terragrunt will output its logs in JSON format.", Action: func(_ *cli.Context, _ bool) error { - opts.LogFormatter.SetFormat(format.JSONFormat) + opts.LogFormatter.SetFormat(format.NewJSONFormat()) return nil }, }, diff --git a/config/config_test.go b/config/config_test.go index 996de2a2e9..1383d9b1c8 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -18,7 +18,7 @@ import ( ) func createLogger() log.Logger { - formatter := format.NewFormatter(format.KeyValueFormat) + formatter := format.NewFormatter(format.NewKeyValueFormat()) formatter.DisableColors() return log.New(log.WithLevel(log.DebugLevel), log.WithFormatter(formatter)) diff --git a/configstack/log_test.go b/configstack/log_test.go index 2702b694d5..f8399cde5c 100644 --- a/configstack/log_test.go +++ b/configstack/log_test.go @@ -22,7 +22,7 @@ func TestLogReductionHook(t *testing.T) { stdout := bytes.Buffer{} - formatter := format.NewFormatter(format.KeyValueFormat) + formatter := format.NewFormatter(format.NewKeyValueFormat()) formatter.DisableColors() var testLogger = log.New( diff --git a/options/options.go b/options/options.go index 1f81aec516..e6e7728529 100644 --- a/options/options.go +++ b/options/options.go @@ -405,7 +405,7 @@ func NewTerragruntOptions() *TerragruntOptions { } func NewTerragruntOptionsWithWriters(stdout, stderr io.Writer) *TerragruntOptions { - var logFormatter = format.NewFormatter(format.PrettyFormat) + var logFormatter = format.NewFormatter(format.NewPrettyFormat()) return &TerragruntOptions{ TerraformPath: DefaultWrappedPath, diff --git a/pkg/cli/flag.go b/pkg/cli/flag.go index 3e318b715e..96405a853a 100644 --- a/pkg/cli/flag.go +++ b/pkg/cli/flag.go @@ -16,7 +16,7 @@ var ( // ActionableFlag is an interface that wraps Flag interface and RunAction operation. type ActionableFlag interface { Flag - RunAction(*Context) error + RunAction(ctx *Context) error } type FlagType[T any] interface { diff --git a/pkg/log/fields.go b/pkg/log/fields.go index 0bca9c0749..f965a6f66e 100644 --- a/pkg/log/fields.go +++ b/pkg/log/fields.go @@ -1,29 +1,4 @@ package log -import "sort" - // Fields is the type used to pass arguments to `WithFields`. type Fields map[string]interface{} - -func (fields Fields) RemoveKeys(removeKeys ...string) []string { - var keys []string - - for key := range fields { - var skip bool - - for _, removeKey := range removeKeys { - if key == removeKey { - skip = true - break - } - } - - if !skip { - keys = append(keys, key) - } - } - - sort.Strings(keys) - - return keys -} diff --git a/pkg/log/format/format.go b/pkg/log/format/format.go index 2634fd43ea..a7be3a1217 100644 --- a/pkg/log/format/format.go +++ b/pkg/log/format/format.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/gruntwork-io/terragrunt/internal/errors" - . "github.com/gruntwork-io/terragrunt/pkg/log/format/options" //nolint:stylecheck - . "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" //nolint:stylecheck + . "github.com/gruntwork-io/terragrunt/pkg/log/format/options" //nolint:stylecheck,revive + . "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" //nolint:stylecheck,revive "golang.org/x/exp/maps" ) @@ -18,8 +18,8 @@ const ( KeyValueFormatName = "key-value" ) -var ( - BareFormat = Placeholders{ +func NewBareFormat() Placeholders { + return Placeholders{ Level( Width(4), //nolint:mnd Case(UpperCase), @@ -36,8 +36,10 @@ var ( Suffix("] "), ), } +} - PrettyFormat = Placeholders{ +func NewPrettyFormat() Placeholders { + return Placeholders{ Time( TimeFormat(fmt.Sprintf("%s:%s:%s%s", Hour24Zero, MinZero, SecZero, MilliSec)), Color(BlackHColor), @@ -64,8 +66,10 @@ var ( PathFormat(RelativePath), ), } +} - JSONFormat = Placeholders{ +func NewJSONFormat() Placeholders { + return Placeholders{ PlainText(`{"time":"`), Time( TimeFormat(RFC3339), @@ -93,8 +97,10 @@ var ( ), PlainText(`"}`), } +} - KeyValueFormat = Placeholders{ +func NewKeyValueFormat() Placeholders { + return Placeholders{ Time( Prefix("time="), TimeFormat(RFC3339), @@ -116,19 +122,19 @@ var ( Color(DisableColor), ), } -) - -var presets = map[string]Placeholders{ - BareFormatName: BareFormat, - PrettyFormatName: PrettyFormat, - JSONFormatName: JSONFormat, - KeyValueFormatName: KeyValueFormat, } func ParseFormat(str string) (Placeholders, error) { - for name, format := range presets { + var presets = map[string]func() Placeholders{ + BareFormatName: NewBareFormat, + PrettyFormatName: NewPrettyFormat, + JSONFormatName: NewJSONFormat, + KeyValueFormatName: NewKeyValueFormat, + } + + for name, formatFn := range presets { if name == str { - return format, nil + return formatFn(), nil } } diff --git a/pkg/log/format/formatter.go b/pkg/log/format/formatter.go index 4073dd16bf..b50f473311 100644 --- a/pkg/log/format/formatter.go +++ b/pkg/log/format/formatter.go @@ -9,7 +9,7 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders" ) -// Formatter implements logrus.Formatter +// Formatter implements logrus.Formatter. var _ log.Formatter = new(Formatter) type Formatter struct { @@ -26,7 +26,7 @@ func NewFormatter(phs placeholders.Placeholders) *Formatter { } } -// Format implements logrus.Format +// Format implements logrus.Format. func (formatter *Formatter) Format(entry *log.Entry) ([]byte, error) { if formatter.placeholders == nil { return nil, nil @@ -60,7 +60,7 @@ func (formatter *Formatter) Format(entry *log.Entry) ([]byte, error) { return buf.Bytes(), nil } -// DisableColors disables log color +// DisableColors disables log color. func (formatter *Formatter) DisableColors() { formatter.disableColors = true } @@ -77,10 +77,12 @@ func (formatter *Formatter) SetBaseDir(baseDir string) error { return nil } +// DisableColors disables the conversion of absolute paths to relative ones. func (formatter *Formatter) DisableRelativePaths() { formatter.relativePather = nil } +// SetFormat sets log format. func (formatter *Formatter) SetFormat(phs placeholders.Placeholders) { formatter.placeholders = phs } diff --git a/pkg/log/format/options/align.go b/pkg/log/format/options/align.go index e680e742f9..a78954d95a 100644 --- a/pkg/log/format/options/align.go +++ b/pkg/log/format/options/align.go @@ -13,7 +13,7 @@ const ( RightAlign ) -var alignValues = CommonMapValues[AlignValue]{ +var alignValues = CommonMapValues[AlignValue]{ //nolint:gochecknoglobals LeftAlign: "left", CenterAlign: "center", RightAlign: "right", @@ -25,7 +25,7 @@ type AlignOption struct { *CommonOption[AlignValue] } -func (option *AlignOption) Evaluate(data *Data, str string) (string, error) { +func (option *AlignOption) Evaluate(_ *Data, str string) (string, error) { withoutSpaces := strings.TrimSpace(str) spaces := len(str) - len(withoutSpaces) diff --git a/pkg/log/format/options/case.go b/pkg/log/format/options/case.go index 642ad97309..26f8a40cfd 100644 --- a/pkg/log/format/options/case.go +++ b/pkg/log/format/options/case.go @@ -16,7 +16,7 @@ const ( CapitalizeCase ) -var textCaseValues = CommonMapValues[CaseValue]{ +var textCaseValues = CommonMapValues[CaseValue]{ //nolint:gochecknoglobals UpperCase: "upper", LowerCase: "lower", CapitalizeCase: "capitalize", @@ -28,7 +28,7 @@ type CaseOption struct { *CommonOption[CaseValue] } -func (option *CaseOption) Evaluate(data *Data, str string) (string, error) { +func (option *CaseOption) Evaluate(_ *Data, str string) (string, error) { switch option.value { case UpperCase: return strings.ToUpper(str), nil diff --git a/pkg/log/format/options/color.go b/pkg/log/format/options/color.go index 8fda0b54e0..09690d09ac 100644 --- a/pkg/log/format/options/color.go +++ b/pkg/log/format/options/color.go @@ -35,21 +35,21 @@ const ( Color145 ) -var colorValues = CommonMapValues[ColorValue]{ - RedColor: "red", - WhiteColor: "white", - YellowColor: "yellow", - GreenColor: "green", - CyanColor: "cyan", - BlueHColor: "light-blue", - BlackHColor: "light-black", - AutoColor: "auto", - GradientColor: "gradient", - DisableColor: "disable", -} - var ( - colorScheme = ColorScheme{ + colorValues = CommonMapValues[ColorValue]{ //nolint:gochecknoglobals + RedColor: "red", + WhiteColor: "white", + YellowColor: "yellow", + GreenColor: "green", + CyanColor: "cyan", + BlueHColor: "light-blue", + BlackHColor: "light-black", + AutoColor: "auto", + GradientColor: "gradient", + DisableColor: "disable", + } + + colorScheme = ColorScheme{ //nolint:gochecknoglobals RedColor: "red", WhiteColor: "white", YellowColor: "yellow", @@ -100,7 +100,7 @@ type compiledColorScheme map[ColorValue]ColorFunc type ColorOption struct { *CommonOption[ColorValue] compiledColors compiledColorScheme - randomColor *randomColor + gradientColor *gradientColor } func (color *ColorOption) Evaluate(data *Data, str string) (string, error) { @@ -114,8 +114,8 @@ func (color *ColorOption) Evaluate(data *Data, str string) (string, error) { value = data.AutoColorFn() } - if value == GradientColor && color.randomColor != nil { - value = color.randomColor.Value(str) + if value == GradientColor && color.gradientColor != nil { + value = color.gradientColor.Value(str) } if colorFn, ok := color.compiledColors[value]; ok { @@ -140,7 +140,7 @@ func Color(val ColorValue) Option { return &ColorOption{ CommonOption: NewCommonOption[ColorValue](ColorOptionName, val, colorValues), compiledColors: colorScheme.Compile(), - randomColor: newRandomColor(), + gradientColor: newGradientColor(), } } @@ -148,7 +148,7 @@ var ( // defaultAutoColorValues contains ANSI color codes that are assigned sequentially to each unique text in a rotating order // https://user-images.githubusercontent.com/995050/47952855-ecb12480-df75-11e8-89d4-ac26c50e80b9.png // https://www.hackitu.de/termcolor256/ - defaultAutoColorValues = []ColorValue{ + defaultAutoColorValues = []ColorValue{ //nolint:gochecknoglobals Color66, Color67, Color95, @@ -164,7 +164,7 @@ var ( } ) -type randomColor struct { +type gradientColor struct { // cache stores unique text with their color code. // We use [xsync.MapOf](https://github.com/puzpuzpuz/xsync?tab=readme-ov-file#map) instead of standard `sync.Map` since it's faster and has generic types. cache *xsync.MapOf[string, ColorValue] @@ -174,14 +174,14 @@ type randomColor struct { nextStyleIndex int } -func newRandomColor() *randomColor { - return &randomColor{ +func newGradientColor() *gradientColor { + return &gradientColor{ cache: xsync.NewMapOf[string, ColorValue](), values: defaultAutoColorValues, } } -func (color *randomColor) Value(text string) ColorValue { +func (color *gradientColor) Value(text string) ColorValue { if colorCode, ok := color.cache.Load(text); ok { return colorCode } diff --git a/pkg/log/format/options/common.go b/pkg/log/format/options/common.go index ab81bb1da2..4ae9272daa 100644 --- a/pkg/log/format/options/common.go +++ b/pkg/log/format/options/common.go @@ -34,7 +34,7 @@ func (option *CommonOption[T]) String() string { return fmt.Sprintf("%v", option.value) } -func (option *CommonOption[T]) Evaluate(data *Data, str string) (string, error) { +func (option *CommonOption[T]) Evaluate(_ *Data, str string) (string, error) { return str, nil } diff --git a/pkg/log/format/options/escape.go b/pkg/log/format/options/escape.go index 8b7641edd6..95e27fd280 100644 --- a/pkg/log/format/options/escape.go +++ b/pkg/log/format/options/escape.go @@ -13,7 +13,7 @@ const ( JSONEscape ) -var textEscapeValues = CommonMapValues[EscapeValue]{ +var textEscapeValues = CommonMapValues[EscapeValue]{ //nolint:gochecknoglobals JSONEscape: "json", } @@ -23,7 +23,7 @@ type EscapeOption struct { *CommonOption[EscapeValue] } -func (option *EscapeOption) Evaluate(data *Data, str string) (string, error) { +func (option *EscapeOption) Evaluate(_ *Data, str string) (string, error) { if option.value != JSONEscape { return str, nil } diff --git a/pkg/log/format/options/level_format.go b/pkg/log/format/options/level_format.go index 4aa33b3762..05db6a9f25 100644 --- a/pkg/log/format/options/level_format.go +++ b/pkg/log/format/options/level_format.go @@ -8,7 +8,7 @@ const ( LevelFormatFull ) -var levelFormatValues = CommonMapValues[LevelFormatValue]{ +var levelFormatValues = CommonMapValues[LevelFormatValue]{ //nolint:gochecknoglobals LevelFormatTiny: "tiny", LevelFormatShort: "short", LevelFormatFull: "full", @@ -20,7 +20,7 @@ type LevelFormatOption struct { *CommonOption[LevelFormatValue] } -func (format *LevelFormatOption) Evaluate(data *Data, str string) (string, error) { +func (format *LevelFormatOption) Evaluate(data *Data, _ string) (string, error) { switch format.Value() { case LevelFormatTiny: return data.Level.TinyName(), nil diff --git a/pkg/log/format/options/path_format.go b/pkg/log/format/options/path_format.go index 4ca200bdff..0612dc8596 100644 --- a/pkg/log/format/options/path_format.go +++ b/pkg/log/format/options/path_format.go @@ -22,7 +22,7 @@ const ( DirectoryPath ) -var pathFormatValues = CommonMapValues[PathFormatValue]{ +var pathFormatValues = CommonMapValues[PathFormatValue]{ //nolint:gochecknoglobals RelativePath: "relative", RelativeModulePath: "relative-module", ModulePath: "module", diff --git a/pkg/log/format/options/prefix.go b/pkg/log/format/options/prefix.go index 42e2620cc8..5049370569 100644 --- a/pkg/log/format/options/prefix.go +++ b/pkg/log/format/options/prefix.go @@ -6,7 +6,7 @@ type PrefixOption struct { *CommonOption[string] } -func (option *PrefixOption) Evaluate(data *Data, str string) (string, error) { +func (option *PrefixOption) Evaluate(_ *Data, str string) (string, error) { return option.value + str, nil } diff --git a/pkg/log/format/options/suffix.go b/pkg/log/format/options/suffix.go index 4265b6b13f..9541ccd968 100644 --- a/pkg/log/format/options/suffix.go +++ b/pkg/log/format/options/suffix.go @@ -6,7 +6,7 @@ type SuffixOption struct { *CommonOption[string] } -func (option *SuffixOption) Evaluate(data *Data, str string) (string, error) { +func (option *SuffixOption) Evaluate(_ *Data, str string) (string, error) { return str + option.value, nil } diff --git a/pkg/log/format/options/time_format.go b/pkg/log/format/options/time_format.go index 78610ab6d7..c2f7e89880 100644 --- a/pkg/log/format/options/time_format.go +++ b/pkg/log/format/options/time_format.go @@ -40,7 +40,7 @@ const ( ) var ( - timeFormatValueMap = TimeFormatValueMap{ + timeFormatValueMap = TimeFormatValueMap{ //nolint:gochecknoglobals YearFull: "2006", Year: "06", MonthNumZero: "01", @@ -101,7 +101,7 @@ func (option *TimeFormatOption) ParseValue(str string) error { return nil } -func (option *TimeFormatOption) Evaluate(data *Data, str string) (string, error) { +func (option *TimeFormatOption) Evaluate(data *Data, _ string) (string, error) { return data.Time.Format(option.Value()), nil } diff --git a/pkg/log/format/options/width.go b/pkg/log/format/options/width.go index 99c5232f0b..b777e1146c 100644 --- a/pkg/log/format/options/width.go +++ b/pkg/log/format/options/width.go @@ -24,7 +24,7 @@ type WidthOption struct { *CommonOption[WidthValue] } -func (option *WidthOption) Evaluate(data *Data, str string) (string, error) { +func (option *WidthOption) Evaluate(_ *Data, str string) (string, error) { WidthOption := int(option.value) if WidthOption == 0 { return str, nil diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go index b376948794..75c66e1276 100644 --- a/pkg/log/format/placeholders/placeholder.go +++ b/pkg/log/format/placeholders/placeholder.go @@ -41,13 +41,13 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er option options.Option ) - for i := 0; i < len(str); i++ { - ch := str[i] + for index := range len(str) { + char := str[index] - if ch == '"' || ch == '\'' { + if char == '"' || char == '\'' { if quoted == 0 { - quoted = ch - } else if i > 0 && str[i-1] != '\\' { + quoted = char + } else if index > 0 && str[index-1] != '\\' { quoted = 0 } } @@ -57,25 +57,25 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er } if placeholder == nil { - if !isPlaceholderName(ch) { - return nil, 0, errors.Errorf("invalid placeholder name %q", str[next:i]) + if !isPlaceholderCharacter(char) { + return nil, 0, errors.Errorf("invalid placeholder name %q", str[next:index]) } - name := str[next : i+1] + name := str[next : index+1] if placeholder = registered.Get(name); placeholder != nil { - next = i + 2 //nolint:mnd + next = index + 2 //nolint:mnd } continue } - if next-1 == i && ch != '(' { - return placeholder, i - 1, nil + if next-1 == index && char != '(' { + return placeholder, index - 1, nil } - if ch == '=' || ch == ',' || ch == ')' { - val := str[next:i] + if char == '=' || char == ',' || char == ')' { + val := str[next:index] val = strings.Trim(val, "'") val = strings.Trim(val, "\"") @@ -93,11 +93,11 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er } } - next = i + 1 + next = index + 1 } - if ch == ')' { - return placeholder, i, nil + if char == ')' { + return placeholder, index, nil } } @@ -119,32 +119,33 @@ func Parse(str string) (Placeholders, error) { next int ) - for i := 0; i < len(str); i++ { - ch := str[i] + for index := 0; index < len(str); index++ { + char := str[index] - if ch == placeholderSign { - if i+1 >= len(str) { + if char == placeholderSign { + if index+1 >= len(str) { return nil, errors.Errorf("empty placeholder name") } - if str[i+1] == placeholderSign { - str = str[:i] + str[i+1:] + if str[index+1] == placeholderSign { + str = str[:index] + str[index+1:] + continue } - if next != i { - placeholder := PlainText(str[next:i]) + if next != index { + placeholder := PlainText(str[next:index]) placeholders = append(placeholders, placeholder) } - placeholder, num, err := parsePlaceholder(str[i+1:], registered) + placeholder, num, err := parsePlaceholder(str[index+1:], registered) if err != nil { return nil, err } placeholders = append(placeholders, placeholder) - i += num + 1 - next = i + 1 + index += num + 1 + next = index + 1 } } @@ -172,7 +173,7 @@ type Placeholder interface { Evaluate(data *options.Data) (string, error) } -func isPlaceholderName(c byte) bool { +func isPlaceholderCharacter(c byte) bool { // Check if the byte value falls within the range of alphanumeric characters return c == '-' || c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') } diff --git a/shell/run_shell_cmd_output_test.go b/shell/run_shell_cmd_output_test.go index 0a940b30dc..37a249a129 100644 --- a/shell/run_shell_cmd_output_test.go +++ b/shell/run_shell_cmd_output_test.go @@ -68,7 +68,7 @@ func TestCommandOutputPrefix(t *testing.T) { prefixedOutput = append(prefixedOutput, fmt.Sprintf("prefix=%s tfpath=%s msg=%s", prefix, filepath.Base(terraformPath), line)) } - logFormatter := format.NewFormatter(format.KeyValueFormat) + logFormatter := format.NewFormatter(format.NewKeyValueFormat()) testCommandOutput(t, func(terragruntOptions *options.TerragruntOptions) { terragruntOptions.TerraformPath = terraformPath diff --git a/test/integration_common_test.go b/test/integration_common_test.go index d18329fc80..3587765c47 100644 --- a/test/integration_common_test.go +++ b/test/integration_common_test.go @@ -58,7 +58,7 @@ func getPathsRelativeTo(t *testing.T, basePath string, paths []string) []string } func createLogger() log.Logger { - formatter := format.NewFormatter(format.KeyValueFormat) + formatter := format.NewFormatter(format.NewKeyValueFormat()) formatter.DisableColors() return log.New(log.WithLevel(log.DebugLevel), log.WithFormatter(formatter)) From c3f42ff87bf875521232f33505b9858694c540c1 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Fri, 8 Nov 2024 00:53:52 +0100 Subject: [PATCH 10/28] chore: rename flag --- cli/commands/flags.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/commands/flags.go b/cli/commands/flags.go index b60635a7d2..8ebf8755f3 100644 --- a/cli/commands/flags.go +++ b/cli/commands/flags.go @@ -151,8 +151,8 @@ const ( TerragruntLogFormatFlagName = "terragrunt-log-format" TerragruntLogFormatEnvName = "TERRAGRUNT_LOG_FORMAT" - TerragruntLogPrettyFormatFlagName = "terragrunt-log-pretty-format" - TerragruntLogPrettyFormatEnvName = "TERRAGRUNT_LOG_PRETTY_FORMAT" + TerragruntLogCustomFormatFlagName = "terragrunt-log-custom-format" + TerragruntLogCustomFormatEnvName = "TERRAGRUNT_LOG_CUSTOM_FORMAT" // Strict Mode related flags/envs TerragruntStrictModeFlagName = "strict-mode" @@ -467,13 +467,13 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { }, }, &cli.GenericFlag[string]{ - Name: TerragruntLogPrettyFormatFlagName, - EnvVar: TerragruntLogPrettyFormatEnvName, + Name: TerragruntLogCustomFormatFlagName, + EnvVar: TerragruntLogCustomFormatEnvName, Usage: "", // TODO: write usage Action: func(_ *cli.Context, val string) error { phs, err := placeholders.Parse(val) if err != nil { - return errors.Errorf("flag --%s, %w", TerragruntLogPrettyFormatFlagName, err) + return errors.Errorf("flag --%s, %w", TerragruntLogCustomFormatFlagName, err) } opts.LogFormatter.SetFormat(phs) From 885e1bd6f1b87e5ba4379a023c9b3421c4817fec Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Fri, 8 Nov 2024 01:12:07 +0100 Subject: [PATCH 11/28] chore: deprecated flags --- cli/commands/flags.go | 30 ++---------------------------- cli/deprecated_flags.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/cli/commands/flags.go b/cli/commands/flags.go index 8ebf8755f3..06996a09c3 100644 --- a/cli/commands/flags.go +++ b/cli/commands/flags.go @@ -17,12 +17,6 @@ import ( ) const ( - TerragruntDisableLogFormattingFlagName = "terragrunt-disable-log-formatting" - TerragruntDisableLogFormattingEnvName = "TERRAGRUNT_DISABLE_LOG_FORMATTING" - - TerragruntJSONLogFlagName = "terragrunt-json-log" - TerragruntJSONLogEnvName = "TERRAGRUNT_JSON_LOG" - TerragruntConfigFlagName = "terragrunt-config" TerragruntConfigEnvName = "TERRAGRUNT_CONFIG" @@ -383,26 +377,6 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { return nil }, }, - &cli.BoolFlag{ - Name: TerragruntDisableLogFormattingFlagName, - EnvVar: TerragruntDisableLogFormattingEnvName, - Destination: &opts.DisableLogFormatting, - Usage: "If specified, logs will be displayed in key/value format. By default, logs are formatted in a human readable format.", - Action: func(_ *cli.Context, _ bool) error { - opts.LogFormatter.SetFormat(format.NewKeyValueFormat()) - return nil - }, - }, - &cli.BoolFlag{ - Name: TerragruntJSONLogFlagName, - EnvVar: TerragruntJSONLogEnvName, - Destination: &opts.JSONLogFormat, - Usage: "If specified, Terragrunt will output its logs in JSON format.", - Action: func(_ *cli.Context, _ bool) error { - opts.LogFormatter.SetFormat(format.NewJSONFormat()) - return nil - }, - }, &cli.BoolFlag{ Name: TerragruntShowLogAbsPathsFlagName, EnvVar: TerragruntShowLogAbsPathsEnvName, @@ -446,7 +420,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { &cli.GenericFlag[string]{ Name: TerragruntLogFormatFlagName, EnvVar: TerragruntLogFormatEnvName, - Usage: "", // TODO: write usage + Usage: "Set the log format", Action: func(_ *cli.Context, val string) error { phs, err := format.ParseFormat(val) if err != nil { @@ -469,7 +443,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { &cli.GenericFlag[string]{ Name: TerragruntLogCustomFormatFlagName, EnvVar: TerragruntLogCustomFormatEnvName, - Usage: "", // TODO: write usage + Usage: "Set the custom log formatting", Action: func(_ *cli.Context, val string) error { phs, err := placeholders.Parse(val) if err != nil { diff --git a/cli/deprecated_flags.go b/cli/deprecated_flags.go index 144f7c3c65..1e3ece5971 100644 --- a/cli/deprecated_flags.go +++ b/cli/deprecated_flags.go @@ -5,12 +5,19 @@ import ( "github.com/gruntwork-io/terragrunt/cli/commands" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/cli" + "github.com/gruntwork-io/terragrunt/pkg/log/format" ) // The following flags are DEPRECATED const ( TerragruntIncludeModulePrefixFlagName = "terragrunt-include-module-prefix" TerragruntIncludeModulePrefixEnvName = "TERRAGRUNT_INCLUDE_MODULE_PREFIX" + + TerragruntDisableLogFormattingFlagName = "terragrunt-disable-log-formatting" + TerragruntDisableLogFormattingEnvName = "TERRAGRUNT_DISABLE_LOG_FORMATTING" + + TerragruntJSONLogFlagName = "terragrunt-json-log" + TerragruntJSONLogEnvName = "TERRAGRUNT_JSON_LOG" ) // NewDeprecatedFlags creates and returns deprecated flags. @@ -26,6 +33,28 @@ func NewDeprecatedFlags(opts *options.TerragruntOptions) cli.Flags { return nil }, }, + &cli.BoolFlag{ + Name: TerragruntDisableLogFormattingFlagName, + EnvVar: TerragruntDisableLogFormattingEnvName, + Destination: &opts.DisableLogFormatting, + Usage: "If specified, logs will be displayed in key/value format. By default, logs are formatted in a human readable format.", + Action: func(_ *cli.Context, _ bool) error { + opts.LogFormatter.SetFormat(format.NewKeyValueFormat()) + opts.Logger.Warnf("The \"--%s\" flag is deprecated. Use \"--%s %s\" instead.", TerragruntDisableLogFormattingFlagName, commands.TerragruntLogFormatFlagName, format.KeyValueFormatName) + return nil + }, + }, + &cli.BoolFlag{ + Name: TerragruntJSONLogFlagName, + EnvVar: TerragruntJSONLogEnvName, + Destination: &opts.JSONLogFormat, + Usage: "If specified, Terragrunt will output its logs in JSON format.", + Action: func(_ *cli.Context, _ bool) error { + opts.LogFormatter.SetFormat(format.NewJSONFormat()) + opts.Logger.Warnf("The \"--%s\" flag is deprecated. Use \"--%s %s\" instead.", TerragruntJSONLogFlagName, commands.TerragruntLogFormatFlagName, format.JSONFormatName) + return nil + }, + }, } return flags From ba7dd43c3c5f7ed19a81d860f8c3f4175f294018 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Fri, 8 Nov 2024 22:23:22 +0100 Subject: [PATCH 12/28] chore: add tests, strict control --- cli/deprecated_flags.go | 23 +++- internal/strict/strict.go | 13 +- pkg/log/format/placeholders/placeholder.go | 6 +- test/integration_common_test.go | 34 +++++ test/integration_test.go | 137 ++++++++++++++++----- 5 files changed, 173 insertions(+), 40 deletions(-) diff --git a/cli/deprecated_flags.go b/cli/deprecated_flags.go index 1e3ece5971..8f2030683c 100644 --- a/cli/deprecated_flags.go +++ b/cli/deprecated_flags.go @@ -3,6 +3,7 @@ package cli import ( "github.com/gruntwork-io/terragrunt/cli/commands" + "github.com/gruntwork-io/terragrunt/internal/strict" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/cli" "github.com/gruntwork-io/terragrunt/pkg/log/format" @@ -40,7 +41,16 @@ func NewDeprecatedFlags(opts *options.TerragruntOptions) cli.Flags { Usage: "If specified, logs will be displayed in key/value format. By default, logs are formatted in a human readable format.", Action: func(_ *cli.Context, _ bool) error { opts.LogFormatter.SetFormat(format.NewKeyValueFormat()) - opts.Logger.Warnf("The \"--%s\" flag is deprecated. Use \"--%s %s\" instead.", TerragruntDisableLogFormattingFlagName, commands.TerragruntLogFormatFlagName, format.KeyValueFormatName) + + if control, ok := strict.GetStrictControl(strict.DisableLogFormatting); ok { + warn, err := control.Evaluate(opts) + if err != nil { + return err + } + + opts.Logger.Warnf(warn) + } + return nil }, }, @@ -51,7 +61,16 @@ func NewDeprecatedFlags(opts *options.TerragruntOptions) cli.Flags { Usage: "If specified, Terragrunt will output its logs in JSON format.", Action: func(_ *cli.Context, _ bool) error { opts.LogFormatter.SetFormat(format.NewJSONFormat()) - opts.Logger.Warnf("The \"--%s\" flag is deprecated. Use \"--%s %s\" instead.", TerragruntJSONLogFlagName, commands.TerragruntLogFormatFlagName, format.JSONFormatName) + + if control, ok := strict.GetStrictControl(strict.JSONLog); ok { + warn, err := control.Evaluate(opts) + if err != nil { + return err + } + + opts.Logger.Warnf(warn) + } + return nil }, }, diff --git a/internal/strict/strict.go b/internal/strict/strict.go index 3121589d6c..893dd71f04 100644 --- a/internal/strict/strict.go +++ b/internal/strict/strict.go @@ -41,9 +41,12 @@ const ( OutputAll = "output-all" // ValidateAll is the control that prevents the deprecated `validate-all` command from being used. ValidateAll = "validate-all" - // SkipDependenciesInputs is the control that prevents reading dependencies inputs and get performance boost. SkipDependenciesInputs = "skip-dependencies-inputs" + // DisableLogFormattingName is the control that prevents the deprecated `--terragrunt-disable-log-formatting` flag from being used. + DisableLogFormatting = "terragrunt-disable-log-formatting" + // JSONLog is the control that prevents the deprecated `--terragrunt-json-log` flag from being used. + JSONLog = "terragrunt-json-log" ) // GetStrictControl returns the strict control with the given name. @@ -111,6 +114,14 @@ var StrictControls = Controls{ Error: errors.Errorf("The `%s` option is deprecated. Reading inputs from dependencies has been deprecated and will be removed in a future version of Terragrunt. To continue using inputs from dependencies, forward them as outputs.", SkipDependenciesInputs), Warning: fmt.Sprintf("The `%s` option is deprecated and will be removed in a future version of Terragrunt. Reading inputs from dependencies has been deprecated. To continue using inputs from dependencies, forward them as outputs.", SkipDependenciesInputs), }, + DisableLogFormatting: { + Error: errors.Errorf("The `--%s` flag is no longer supported. Use `--terragrunt-log-format=key-value` instead.", DisableLogFormatting), + Warning: fmt.Sprintf("The `--%s` flag is deprecated and will be removed in a future version. Use `--terragrunt-log-format=key-value` instead.", DisableLogFormatting), + }, + JSONLog: { + Error: errors.Errorf("The `--%s` flag is no longer supported. Use `--terragrunt-log-format=json` instead.", JSONLog), + Warning: fmt.Sprintf("The `--%s` flag is deprecated and will be removed in a future version. Use `--terragrunt-log-format=json` instead.", JSONLog), + }, } // Names returns the names of all strict controls. diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go index 75c66e1276..5cd1b72efa 100644 --- a/pkg/log/format/placeholders/placeholder.go +++ b/pkg/log/format/placeholders/placeholder.go @@ -47,7 +47,7 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er if char == '"' || char == '\'' { if quoted == 0 { quoted = char - } else if index > 0 && str[index-1] != '\\' { + } else if quoted == char && index > 0 && str[index-1] != '\\' { quoted = 0 } } @@ -57,7 +57,7 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er } if placeholder == nil { - if !isPlaceholderCharacter(char) { + if !isPlaceholderNameCharacter(char) { return nil, 0, errors.Errorf("invalid placeholder name %q", str[next:index]) } @@ -173,7 +173,7 @@ type Placeholder interface { Evaluate(data *options.Data) (string, error) } -func isPlaceholderCharacter(c byte) bool { +func isPlaceholderNameCharacter(c byte) bool { // Check if the byte value falls within the range of alphanumeric characters return c == '-' || c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') } diff --git a/test/integration_common_test.go b/test/integration_common_test.go index 3587765c47..7ff59bad13 100644 --- a/test/integration_common_test.go +++ b/test/integration_common_test.go @@ -21,6 +21,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "testing" "time" @@ -35,6 +36,39 @@ import ( "github.com/NYTimes/gziphandler" ) +func splitCommand(command string) []string { + var ( + next int + quoted byte + args []string + ) + + for index := range len(command) { + char := command[index] + + if char == '"' || char == '\'' { + if quoted == 0 { + quoted = char + } else if quoted == char && index > 0 && command[index-1] != '\\' { + quoted = 0 + } + } + + if quoted != 0 || char != ' ' { + continue + } + + arg := strings.TrimSpace(command[next:index]) + next = index + 1 + + if arg != "" { + args = append(args, arg) + } + } + + return append(args, command[next:]) +} + func getPathRelativeTo(t *testing.T, path string, basePath string) string { t.Helper() diff --git a/test/integration_test.go b/test/integration_test.go index 8bcaad2d39..ffa5fe9ae8 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -118,6 +118,63 @@ const ( tofuBinary = "tofu" ) +func TestLogCustomFormatOutput(t *testing.T) { + t.Parallel() + + var ( + absPathReg = `(?:/[^/]+)*/` + regexp.QuoteMeta(testFixtureLogFormatter) + ) + + testCases := []struct { + logCustomFormat string + expectedOutputRegs []*regexp.Regexp + }{ + { + logCustomFormat: "%time %level %prefix %msg", + expectedOutputRegs: []*regexp.Regexp{ + regexp.MustCompile(`\d{2}:\d{2}:\d{2}\.\d{3} debug ` + absPathReg + regexp.QuoteMeta(" Terragrunt Version: 0.0.0")), + regexp.MustCompile(`\d{2}:\d{2}:\d{2}\.\d{3} debug ` + absPathReg + `/dep Module ` + absPathReg + `/dep must wait for 0 dependencies to finish`), + regexp.MustCompile(`\d{2}:\d{2}:\d{2}\.\d{3} debug ` + absPathReg + `/app Module ` + absPathReg + `/app must wait for 1 dependencies to finish`), + }, + }, + { + logCustomFormat: "%interval %level(case=upper) %prefix(path=relative-module,prefix='[',suffix='] ')%msg(path=relative)", + expectedOutputRegs: []*regexp.Regexp{ + regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG Terragrunt Version: 0.0.0")), + regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG [dep] Module ./dep must wait for 0 dependencies to finish")), + regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG [app] Module ./app must wait for 1 dependencies to finish")), + }, + }, + { + logCustomFormat: "%interval %level(case=upper,width=6) %prefix(path=relative-module,suffix=' ')%tfpath(suffix=': ')%msg(path=relative)", + expectedOutputRegs: []*regexp.Regexp{ + regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG Terragrunt Version: 0.0.0")), + regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" STDOUT dep "+wrappedBinary()+": Initializing the backend...")), + regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" STDOUT app "+wrappedBinary()+": Initializing the backend...")), + }, + }, + } + + for i, testCase := range testCases { + testCase := testCase + + t.Run(fmt.Sprintf("testCase-%d", i), func(t *testing.T) { + t.Parallel() + + cleanupTerraformFolder(t, testFixtureLogFormatter) + tmpEnvPath := copyEnvironment(t, testFixtureLogFormatter) + rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter) + + _, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all init --terragrunt-log-level debug --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-log-custom-format=%q --terragrunt-working-dir %s", testCase.logCustomFormat, rootPath)) + require.NoError(t, err) + + for _, reg := range testCase.expectedOutputRegs { + assert.Regexp(t, reg, stderr) + } + }) + } +} + func TestBufferModuleOutput(t *testing.T) { t.Parallel() @@ -212,7 +269,7 @@ func TestLogWithRelPath(t *testing.T) { } } -func TestLogFormatterPrettyOutput(t *testing.T) { +func TestLogFormatPrettyOutput(t *testing.T) { t.Parallel() cleanupTerraformFolder(t, testFixtureLogFormatter) @@ -231,19 +288,25 @@ func TestLogFormatterPrettyOutput(t *testing.T) { assert.Contains(t, stderr, "DEBUG Terragrunt Version:") } -func TestLogFormatterKeyValueOutput(t *testing.T) { +func TestLogFormatKeyValueOutput(t *testing.T) { t.Parallel() - cleanupTerraformFolder(t, testFixtureLogFormatter) - tmpEnvPath := copyEnvironment(t, testFixtureLogFormatter) - rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter) + for _, flag := range []string{"--terragrunt-log-format=key-value", "--terragrunt-disable-log-formatting"} { + t.Run("testCase-flag-"+flag, func(t *testing.T) { + t.Parallel() - _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init -no-color --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-working-dir "+rootPath) - require.NoError(t, err) + cleanupTerraformFolder(t, testFixtureLogFormatter) + tmpEnvPath := copyEnvironment(t, testFixtureLogFormatter) + rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter) - for _, prefixName := range []string{"app", "dep"} { - assert.Contains(t, stderr, "level=stdout prefix="+prefixName+" tfpath="+wrappedBinary()+" msg=Initializing provider plugins...\n") - assert.Contains(t, stderr, "level=debug prefix="+prefixName+" msg=Reading Terragrunt config file at ./"+prefixName+"/terragrunt.hcl\n") + _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init -no-color --terragrunt-log-level debug --terragrunt-non-interactive "+flag+" --terragrunt-working-dir "+rootPath) + require.NoError(t, err) + + for _, prefixName := range []string{"app", "dep"} { + assert.Contains(t, stderr, "level=stdout prefix="+prefixName+" tfpath="+wrappedBinary()+" msg=Initializing provider plugins...\n") + assert.Contains(t, stderr, "level=debug prefix="+prefixName+" msg=Reading Terragrunt config file at ./"+prefixName+"/terragrunt.hcl\n") + } + }) } } @@ -2738,9 +2801,9 @@ func removeFolder(t *testing.T, path string) { func runTerragruntCommand(t *testing.T, command string, writer io.Writer, errwriter io.Writer) error { t.Helper() - args := strings.Split(command, " ") + args := splitCommand(command) - if !strings.Contains(command, "-terragrunt-log-format") { + if !strings.Contains(command, "-terragrunt-log-format") && !strings.Contains(command, "-terragrunt-log-custom-format") { args = append(args, "--terragrunt-log-format=key-value") } @@ -3826,39 +3889,45 @@ func createTmpTerragruntConfigWithParentAndChild(t *testing.T, parentPath string return childTerragruntDestPath } -func TestTerragruntOutputJson(t *testing.T) { +func TestLogFormatJSONOutput(t *testing.T) { t.Parallel() - tmpEnvPath := copyEnvironment(t, testFixtureNotExistingSource) - cleanupTerraformFolder(t, tmpEnvPath) - testPath := util.JoinPath(tmpEnvPath, testFixtureNotExistingSource) + for _, flag := range []string{"--terragrunt-log-format=json", "--terragrunt-json-log"} { + t.Run("testCase-flag-"+flag, func(t *testing.T) { + t.Parallel() - _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt apply --terragrunt-json-log --terragrunt-non-interactive --terragrunt-working-dir "+testPath) - require.Error(t, err) + tmpEnvPath := copyEnvironment(t, testFixtureNotExistingSource) + cleanupTerraformFolder(t, tmpEnvPath) + testPath := util.JoinPath(tmpEnvPath, testFixtureNotExistingSource) - // for windows OS - output := bytes.ReplaceAll([]byte(stderr), []byte("\r\n"), []byte("\n")) + _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt apply "+flag+" --terragrunt-non-interactive --terragrunt-working-dir "+testPath) + require.Error(t, err) - multipeJSONs := bytes.Split(output, []byte("\n")) + // for windows OS + output := bytes.ReplaceAll([]byte(stderr), []byte("\r\n"), []byte("\n")) - var msgs = make([]string, 0, len(multipeJSONs)) + multipeJSONs := bytes.Split(output, []byte("\n")) - for _, jsonBytes := range multipeJSONs { - if len(jsonBytes) == 0 { - continue - } + var msgs = make([]string, 0, len(multipeJSONs)) - var output map[string]interface{} + for _, jsonBytes := range multipeJSONs { + if len(jsonBytes) == 0 { + continue + } - err = json.Unmarshal(jsonBytes, &output) - require.NoError(t, err) + var output map[string]interface{} - msg, ok := output["msg"].(string) - assert.True(t, ok) - msgs = append(msgs, msg) - } + err = json.Unmarshal(jsonBytes, &output) + require.NoError(t, err) - assert.Contains(t, strings.Join(msgs, ""), "Downloading Terraform configurations from git::https://github.com/gruntwork-io/terragrunt.git?ref=v0.9.9") + msg, ok := output["msg"].(string) + assert.True(t, ok) + msgs = append(msgs, msg) + } + + assert.Contains(t, strings.Join(msgs, ""), "Downloading Terraform configurations from git::https://github.com/gruntwork-io/terragrunt.git?ref=v0.9.9") + }) + } } func TestTerragruntTerraformOutputJson(t *testing.T) { From 4a876e67e1f3b4d5cff8d6eadef43e881c6f4e65 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Fri, 8 Nov 2024 22:39:41 +0100 Subject: [PATCH 13/28] chore: fix test --- test/integration_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/integration_test.go b/test/integration_test.go index ffa5fe9ae8..5294e77803 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -132,7 +132,7 @@ func TestLogCustomFormatOutput(t *testing.T) { { logCustomFormat: "%time %level %prefix %msg", expectedOutputRegs: []*regexp.Regexp{ - regexp.MustCompile(`\d{2}:\d{2}:\d{2}\.\d{3} debug ` + absPathReg + regexp.QuoteMeta(" Terragrunt Version: 0.0.0")), + regexp.MustCompile(`\d{2}:\d{2}:\d{2}\.\d{3} debug ` + absPathReg + regexp.QuoteMeta(" Terragrunt Version:")), regexp.MustCompile(`\d{2}:\d{2}:\d{2}\.\d{3} debug ` + absPathReg + `/dep Module ` + absPathReg + `/dep must wait for 0 dependencies to finish`), regexp.MustCompile(`\d{2}:\d{2}:\d{2}\.\d{3} debug ` + absPathReg + `/app Module ` + absPathReg + `/app must wait for 1 dependencies to finish`), }, @@ -140,7 +140,7 @@ func TestLogCustomFormatOutput(t *testing.T) { { logCustomFormat: "%interval %level(case=upper) %prefix(path=relative-module,prefix='[',suffix='] ')%msg(path=relative)", expectedOutputRegs: []*regexp.Regexp{ - regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG Terragrunt Version: 0.0.0")), + regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG Terragrunt Version:")), regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG [dep] Module ./dep must wait for 0 dependencies to finish")), regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG [app] Module ./app must wait for 1 dependencies to finish")), }, @@ -148,7 +148,7 @@ func TestLogCustomFormatOutput(t *testing.T) { { logCustomFormat: "%interval %level(case=upper,width=6) %prefix(path=relative-module,suffix=' ')%tfpath(suffix=': ')%msg(path=relative)", expectedOutputRegs: []*regexp.Regexp{ - regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG Terragrunt Version: 0.0.0")), + regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG Terragrunt Version:")), regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" STDOUT dep "+wrappedBinary()+": Initializing the backend...")), regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" STDOUT app "+wrappedBinary()+": Initializing the backend...")), }, From ea0425f82d0076d301c9ed84ff6634991ad2a2b0 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Mon, 11 Nov 2024 13:29:52 +0100 Subject: [PATCH 14/28] chore: fix exit code message --- cli/commands/flags.go | 2 +- pkg/cli/app.go | 5 ++- pkg/cli/errors.go | 13 +++++++- pkg/log/format/options/align.go | 2 +- pkg/log/format/options/case.go | 2 +- pkg/log/format/options/color.go | 2 +- pkg/log/format/options/content.go | 23 +++++++++++++ pkg/log/format/options/escape.go | 2 +- pkg/log/format/options/level_format.go | 2 +- pkg/log/format/options/option.go | 38 ++++++++++++++-------- pkg/log/format/options/path_format.go | 2 +- pkg/log/format/options/prefix.go | 2 +- pkg/log/format/options/suffix.go | 2 +- pkg/log/format/options/time_format.go | 2 +- pkg/log/format/options/width.go | 2 +- pkg/log/format/placeholders/common.go | 18 +++++++--- pkg/log/format/placeholders/interval.go | 3 ++ pkg/log/format/placeholders/level.go | 3 ++ pkg/log/format/placeholders/message.go | 2 ++ pkg/log/format/placeholders/placeholder.go | 30 ++++++++++++++--- pkg/log/format/placeholders/plaintext.go | 14 ++++---- pkg/log/format/placeholders/time.go | 3 ++ util/shell.go | 16 ++++++--- 23 files changed, 141 insertions(+), 49 deletions(-) create mode 100644 pkg/log/format/options/content.go diff --git a/cli/commands/flags.go b/cli/commands/flags.go index 06996a09c3..ca4e8de9ec 100644 --- a/cli/commands/flags.go +++ b/cli/commands/flags.go @@ -447,7 +447,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Action: func(_ *cli.Context, val string) error { phs, err := placeholders.Parse(val) if err != nil { - return errors.Errorf("flag --%s, %w", TerragruntLogCustomFormatFlagName, err) + return cli.NewExitError(errors.Errorf("flag --%s, %w", TerragruntLogCustomFormatFlagName, err), 1) } opts.LogFormatter.SetFormat(phs) diff --git a/pkg/cli/app.go b/pkg/cli/app.go index 88174f5bb8..f626a58179 100644 --- a/pkg/cli/app.go +++ b/pkg/cli/app.go @@ -75,8 +75,11 @@ type App struct { // NewApp returns app new App instance. func NewApp() *App { + cliApp := cli.NewApp() + cliApp.ExitErrHandler = func(_ *cli.Context, _ error) {} + return &App{ - App: cli.NewApp(), + App: cliApp, OsExiter: os.Exit, Autocomplete: true, } diff --git a/pkg/cli/errors.go b/pkg/cli/errors.go index 13499cccfe..7181ea603a 100644 --- a/pkg/cli/errors.go +++ b/pkg/cli/errors.go @@ -31,6 +31,10 @@ type exitError struct { err error } +func (ee *exitError) Unwrap() error { + return ee.err +} + func (ee *exitError) Error() string { if ee.err == nil { return "" @@ -43,8 +47,15 @@ func (ee *exitError) ExitCode() int { return ee.exitCode } +// ExitCoder is the interface checked by `App` and `Command` for a custom exit code +type ExitCoder interface { + error + ExitCode() int + Unwrap() error +} + // NewExitError calls Exit to create a new ExitCoder. -func NewExitError(message interface{}, exitCode int) cli.ExitCoder { +func NewExitError(message interface{}, exitCode int) ExitCoder { var err error if message != nil { diff --git a/pkg/log/format/options/align.go b/pkg/log/format/options/align.go index a78954d95a..058a1443ed 100644 --- a/pkg/log/format/options/align.go +++ b/pkg/log/format/options/align.go @@ -48,6 +48,6 @@ func (option *AlignOption) Evaluate(_ *Data, str string) (string, error) { func Align(value AlignValue) Option { return &AlignOption{ - CommonOption: NewCommonOption[AlignValue](AlignOptionName, value, alignValues), + CommonOption: NewCommonOption(AlignOptionName, value, alignValues), } } diff --git a/pkg/log/format/options/case.go b/pkg/log/format/options/case.go index 26f8a40cfd..4202fe11f2 100644 --- a/pkg/log/format/options/case.go +++ b/pkg/log/format/options/case.go @@ -44,6 +44,6 @@ func (option *CaseOption) Evaluate(_ *Data, str string) (string, error) { func Case(value CaseValue) Option { return &CaseOption{ - CommonOption: NewCommonOption[CaseValue](CaseOptionName, value, textCaseValues), + CommonOption: NewCommonOption(CaseOptionName, value, textCaseValues), } } diff --git a/pkg/log/format/options/color.go b/pkg/log/format/options/color.go index 09690d09ac..67c7dfdf03 100644 --- a/pkg/log/format/options/color.go +++ b/pkg/log/format/options/color.go @@ -138,7 +138,7 @@ func (color *ColorOption) ParseValue(str string) error { func Color(val ColorValue) Option { return &ColorOption{ - CommonOption: NewCommonOption[ColorValue](ColorOptionName, val, colorValues), + CommonOption: NewCommonOption(ColorOptionName, val, colorValues), compiledColors: colorScheme.Compile(), gradientColor: newGradientColor(), } diff --git a/pkg/log/format/options/content.go b/pkg/log/format/options/content.go new file mode 100644 index 0000000000..3e6e576aa7 --- /dev/null +++ b/pkg/log/format/options/content.go @@ -0,0 +1,23 @@ +package options + +const ContentOptionName = "content" + +type ContentValue string + +func (val ContentValue) Parse(str string) (ContentValue, error) { + return ContentValue(str), nil +} + +type ContentOption struct { + *CommonOption[ContentValue] +} + +func (option *ContentOption) Evaluate(_ *Data, str string) (string, error) { + return string(option.value), nil +} + +func Content(value ContentValue) Option { + return &ContentOption{ + CommonOption: NewCommonOption(ContentOptionName, value, value), + } +} diff --git a/pkg/log/format/options/escape.go b/pkg/log/format/options/escape.go index 95e27fd280..1d0aff5743 100644 --- a/pkg/log/format/options/escape.go +++ b/pkg/log/format/options/escape.go @@ -39,6 +39,6 @@ func (option *EscapeOption) Evaluate(_ *Data, str string) (string, error) { func Escape(value EscapeValue) Option { return &EscapeOption{ - CommonOption: NewCommonOption[EscapeValue](EscapeOptionName, value, textEscapeValues), + CommonOption: NewCommonOption(EscapeOptionName, value, textEscapeValues), } } diff --git a/pkg/log/format/options/level_format.go b/pkg/log/format/options/level_format.go index 05db6a9f25..d731a52988 100644 --- a/pkg/log/format/options/level_format.go +++ b/pkg/log/format/options/level_format.go @@ -34,6 +34,6 @@ func (format *LevelFormatOption) Evaluate(data *Data, _ string) (string, error) func LevelFormat(val LevelFormatValue) Option { return &LevelFormatOption{ - CommonOption: NewCommonOption[LevelFormatValue](LevelFormatOptionName, val, levelFormatValues), + CommonOption: NewCommonOption(LevelFormatOptionName, val, levelFormatValues), } } diff --git a/pkg/log/format/options/option.go b/pkg/log/format/options/option.go index d8398be72b..a61bd72479 100644 --- a/pkg/log/format/options/option.go +++ b/pkg/log/format/options/option.go @@ -9,36 +9,46 @@ import ( type Options []Option -func (options Options) Get(name string) Option { - for _, option := range options { - if option.Name() == name { - return option +func (opts Options) Get(name string) Option { + for _, opt := range opts { + if opt.Name() == name { + return opt } } return nil } -func (options Options) Merge(withOptions ...Option) Options { - for i := range options { - for t := range withOptions { - if reflect.TypeOf(options[i]) == reflect.TypeOf(withOptions[t]) { - options[i] = withOptions[t] - withOptions = append(withOptions[:t], withOptions[t+1:]...) +func (opts Options) Names() []string { + var names = make([]string, len(opts)) + + for i, opt := range opts { + names[i] = opt.Name() + } + + return names +} + +func (opts Options) Merge(withOpts ...Option) Options { + for i := range opts { + for t := range withOpts { + if reflect.TypeOf(opts[i]) == reflect.TypeOf(withOpts[t]) { + opts[i] = withOpts[t] + withOpts = append(withOpts[:t], withOpts[t+1:]...) break } } } - return append(options, withOptions...) + return append(opts, withOpts...) } -func (options Options) Evaluate(data *Data, str string) (string, error) { +func (opts Options) Evaluate(data *Data, str string) (string, error) { var err error - for _, option := range options { - str, err = option.Evaluate(data, str) + for _, opt := range opts { + str, err = opt.Evaluate(data, str) if str == "" || err != nil { return "", err } diff --git a/pkg/log/format/options/path_format.go b/pkg/log/format/options/path_format.go index 0612dc8596..5bb611412e 100644 --- a/pkg/log/format/options/path_format.go +++ b/pkg/log/format/options/path_format.go @@ -83,7 +83,7 @@ func PathFormat(val PathFormatValue, allowed ...PathFormatValue) Option { } return &PathFormatOption{ - CommonOption: NewCommonOption[PathFormatValue](PathFormatOptionName, val, values), + CommonOption: NewCommonOption(PathFormatOptionName, val, values), } } diff --git a/pkg/log/format/options/prefix.go b/pkg/log/format/options/prefix.go index 5049370569..49aad73f1b 100644 --- a/pkg/log/format/options/prefix.go +++ b/pkg/log/format/options/prefix.go @@ -18,6 +18,6 @@ func (option *PrefixOption) ParseValue(str string) error { func Prefix(value string) Option { return &PrefixOption{ - CommonOption: NewCommonOption[string](PrefixOptionName, value, nil), + CommonOption: NewCommonOption(PrefixOptionName, value, nil), } } diff --git a/pkg/log/format/options/suffix.go b/pkg/log/format/options/suffix.go index 9541ccd968..d029d26cbb 100644 --- a/pkg/log/format/options/suffix.go +++ b/pkg/log/format/options/suffix.go @@ -18,6 +18,6 @@ func (option *SuffixOption) ParseValue(str string) error { func Suffix(value string) Option { return &SuffixOption{ - CommonOption: NewCommonOption[string](SuffixOptionName, value, nil), + CommonOption: NewCommonOption(SuffixOptionName, value, nil), } } diff --git a/pkg/log/format/options/time_format.go b/pkg/log/format/options/time_format.go index c2f7e89880..ef3f712c86 100644 --- a/pkg/log/format/options/time_format.go +++ b/pkg/log/format/options/time_format.go @@ -107,6 +107,6 @@ func (option *TimeFormatOption) Evaluate(data *Data, _ string) (string, error) { func TimeFormat(str string) Option { return &TimeFormatOption{ - CommonOption: NewCommonOption[string](TimeFormatOptionName, timeFormatValueMap.Value(str), nil), + CommonOption: NewCommonOption(TimeFormatOptionName, timeFormatValueMap.Value(str), nil), } } diff --git a/pkg/log/format/options/width.go b/pkg/log/format/options/width.go index b777e1146c..f823eb80de 100644 --- a/pkg/log/format/options/width.go +++ b/pkg/log/format/options/width.go @@ -41,6 +41,6 @@ func (option *WidthOption) Evaluate(_ *Data, str string) (string, error) { func Width(value WidthValue) Option { return &WidthOption{ - CommonOption: NewCommonOption[WidthValue](WidthOptionName, value, value), + CommonOption: NewCommonOption(WidthOptionName, value, value), } } diff --git a/pkg/log/format/placeholders/common.go b/pkg/log/format/placeholders/common.go index d1627488b7..561dc2cfa5 100644 --- a/pkg/log/format/placeholders/common.go +++ b/pkg/log/format/placeholders/common.go @@ -1,9 +1,13 @@ package placeholders import ( + "strings" + + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log/format/options" ) +// WithCommonOptions is a set of common options that are used in all placeholders. func WithCommonOptions(opts ...options.Option) options.Options { return options.Options(append(opts, options.Case(options.NoneCase), @@ -20,6 +24,7 @@ type CommonPlaceholder struct { opts options.Options } +// NewCommonPlaceholder creates a new Common placeholder. func NewCommonPlaceholder(name string, opts ...options.Option) *CommonPlaceholder { return &CommonPlaceholder{ name: name, @@ -27,18 +32,21 @@ func NewCommonPlaceholder(name string, opts ...options.Option) *CommonPlaceholde } } +// Name implements `Placeholder` interface. func (common *CommonPlaceholder) Name() string { return common.name } -func (common *CommonPlaceholder) GetOption(str string) options.Option { - return common.opts.Get(str) -} +// GetOption implements `Placeholder` interface. +func (common *CommonPlaceholder) GetOption(str string) (options.Option, error) { + if opt := common.opts.Get(str); opt != nil { + return opt, nil + } -func (common *CommonPlaceholder) SetValue(str string) error { - return nil + return nil, errors.Errorf("available values: %s", strings.Join(common.opts.Names(), ",")) } +// Evaluate implements `Placeholder` interface. func (common *CommonPlaceholder) Evaluate(data *options.Data) (string, error) { return common.opts.Evaluate(data, "") } diff --git a/pkg/log/format/placeholders/interval.go b/pkg/log/format/placeholders/interval.go index 31374a706f..be84a3f9a0 100644 --- a/pkg/log/format/placeholders/interval.go +++ b/pkg/log/format/placeholders/interval.go @@ -7,6 +7,7 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log/format/options" ) +// IntervalPlaceholderName is the placeholder name. const IntervalPlaceholderName = "interval" type intervalPlaceholder struct { @@ -14,10 +15,12 @@ type intervalPlaceholder struct { *CommonPlaceholder } +// Evaluate implements `Placeholder` interface. func (t *intervalPlaceholder) Evaluate(data *options.Data) (string, error) { return t.opts.Evaluate(data, fmt.Sprintf("%04d", time.Since(t.baseTime)/time.Second)) } +// Interval creates a placeholder that displays seconds that have passed since app started. func Interval(opts ...options.Option) Placeholder { opts = WithCommonOptions().Merge(opts...) diff --git a/pkg/log/format/placeholders/level.go b/pkg/log/format/placeholders/level.go index faf01d2fa2..e921cbf0c4 100644 --- a/pkg/log/format/placeholders/level.go +++ b/pkg/log/format/placeholders/level.go @@ -5,6 +5,7 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log/format/options" ) +// LevelPlaceholderName is the placeholder name. const LevelPlaceholderName = "level" var levlAutoColorFunc = func(level log.Level) options.ColorValue { @@ -32,6 +33,7 @@ type level struct { *CommonPlaceholder } +// Evaluate implements `Placeholder` interface. func (level *level) Evaluate(data *options.Data) (string, error) { newData := *data newData.AutoColorFn = func() options.ColorValue { @@ -41,6 +43,7 @@ func (level *level) Evaluate(data *options.Data) (string, error) { return level.opts.Evaluate(&newData, data.Level.String()) } +// Level creates a placeholder that displays log level name. func Level(opts ...options.Option) Placeholder { opts = WithCommonOptions( options.LevelFormat(options.LevelFormatFull), diff --git a/pkg/log/format/placeholders/message.go b/pkg/log/format/placeholders/message.go index 67181bf071..0bd447c6cf 100644 --- a/pkg/log/format/placeholders/message.go +++ b/pkg/log/format/placeholders/message.go @@ -4,12 +4,14 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log/format/options" ) +// MessagePlaceholderName is the placeholder name. const MessagePlaceholderName = "msg" type message struct { *CommonPlaceholder } +// Evaluate implements `Placeholder` interface. func (msg *message) Evaluate(data *options.Data) (string, error) { return msg.opts.Evaluate(data, data.Message) } diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go index 5cd1b72efa..02e4fb8064 100644 --- a/pkg/log/format/placeholders/placeholder.go +++ b/pkg/log/format/placeholders/placeholder.go @@ -22,6 +22,16 @@ func (phs Placeholders) Get(name string) Placeholder { return nil } +func (phs Placeholders) Names() []string { + var names = make([]string, len(phs)) + + for i, ph := range phs { + names[i] = ph.Name() + } + + return names +} + func newPlaceholders() Placeholders { return Placeholders{ Interval(), @@ -56,9 +66,15 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er continue } + if index == 0 && char == '(' { + placeholder = PlainText("") + next = index + 1 + } + if placeholder == nil { if !isPlaceholderNameCharacter(char) { - return nil, 0, errors.Errorf("invalid placeholder name %q", str[next:index]) + str = str[next:index] + break } name := str[next : index+1] @@ -76,6 +92,7 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er if char == '=' || char == ',' || char == ')' { val := str[next:index] + val = strings.TrimSpace(val) val = strings.Trim(val, "'") val = strings.Trim(val, "\"") @@ -88,9 +105,12 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er return nil, 0, errors.Errorf("invalid value %q for option %q, placeholder %q: %w", val, option.Name(), placeholder.Name(), err) } } else if val != "" { - if option = placeholder.GetOption(val); option == nil { - return nil, 0, errors.Errorf("invalid option name %q for placeholder %q", val, placeholder.Name()) + opt, err := placeholder.GetOption(val) + if err != nil { + return nil, 0, errors.Errorf("invalid option name %q for placeholder %q: %w", val, placeholder.Name(), err) } + + option = opt } next = index + 1 @@ -102,7 +122,7 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er } if placeholder == nil { - return nil, 0, errors.Errorf("invalid placeholder name %q", str) + return nil, 0, errors.Errorf("invalid placeholder name %q, available values: %s", str, strings.Join(registered.Names(), ",")) } if next < len(str) { @@ -169,7 +189,7 @@ func (phs Placeholders) Evaluate(data *options.Data) (string, error) { type Placeholder interface { Name() string - GetOption(name string) options.Option + GetOption(name string) (options.Option, error) Evaluate(data *options.Data) (string, error) } diff --git a/pkg/log/format/placeholders/plaintext.go b/pkg/log/format/placeholders/plaintext.go index e166d5dda5..ce98723dd4 100644 --- a/pkg/log/format/placeholders/plaintext.go +++ b/pkg/log/format/placeholders/plaintext.go @@ -4,22 +4,22 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log/format/options" ) +// PlainTextPlaceholderName is the placeholder name. const PlainTextPlaceholderName = "" type plainText struct { *CommonPlaceholder - value string -} - -func (plainText *plainText) Evaluate(data *options.Data) (string, error) { - return plainText.opts.Evaluate(data, plainText.value) } +// PlainText creates a placeholder that displays plaintext. +// Although plaintext can be used as is without placeholder, this allows you to format the contant, +// for example set a color: `%(contant='just text',color=green)`. func PlainText(value string, opts ...options.Option) Placeholder { - opts = WithCommonOptions().Merge(opts...) + opts = WithCommonOptions( + options.Content(options.ContentValue(value)), + ).Merge(opts...) return &plainText{ CommonPlaceholder: NewCommonPlaceholder(PlainTextPlaceholderName, opts...), - value: value, } } diff --git a/pkg/log/format/placeholders/time.go b/pkg/log/format/placeholders/time.go index 549bd9b5ee..8d51dd3e9c 100644 --- a/pkg/log/format/placeholders/time.go +++ b/pkg/log/format/placeholders/time.go @@ -6,16 +6,19 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log/format/options" ) +// TimePlaceholderName is the placeholder name. Example `%time()`. const TimePlaceholderName = "time" type timePlaceholder struct { *CommonPlaceholder } +// Evaluate implements `Placeholder` interface. func (t *timePlaceholder) Evaluate(data *options.Data) (string, error) { return t.opts.Evaluate(data, data.Time.String()) } +// Time creates a placeholder that displays time. func Time(opts ...options.Option) Placeholder { opts = WithCommonOptions( options.TimeFormat(fmt.Sprintf("%s:%s:%s%s", options.Hour24Zero, options.MinZero, options.SecZero, options.MilliSec)), diff --git a/util/shell.go b/util/shell.go index 62b1dd6759..db14c0fef0 100644 --- a/util/shell.go +++ b/util/shell.go @@ -9,6 +9,7 @@ import ( "os/exec" "github.com/gruntwork-io/terragrunt/internal/errors" + "github.com/gruntwork-io/terragrunt/pkg/cli" ) // IsCommandExecutable - returns true if a command can be executed without errors. @@ -36,16 +37,21 @@ type CmdOutput struct { } // GetExitCode returns the exit code of a command. If the error does not -// implement iErrorCode or is not an exec.ExitError +// implement errorCode or is not an exec.ExitError // or *errors.MultiError type, the error is returned. func GetExitCode(err error) (int, error) { - // Interface to determine if we can retrieve an exit status from an error - type iErrorCode interface { + var exitStatus interface { ExitStatus() (int, error) } - if exiterr, ok := errors.Unwrap(err).(iErrorCode); ok { - return exiterr.ExitStatus() + if errors.As(err, &exitStatus) { + return exitStatus.ExitStatus() + } + + var exitCoder cli.ExitCoder + + if errors.As(err, &exitCoder) { + return exitCoder.ExitCode(), nil } var exiterr *exec.ExitError From 46465f95e811357298eefe62499fcc8f97090b22 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Mon, 11 Nov 2024 18:02:11 +0100 Subject: [PATCH 15/28] chore: code improvements --- pkg/log/format/formatter.go | 2 +- pkg/log/format/options/align.go | 10 +- pkg/log/format/options/case.go | 10 +- pkg/log/format/options/color.go | 21 +--- pkg/log/format/options/common.go | 105 +++++++++++----- pkg/log/format/options/content.go | 16 +-- pkg/log/format/options/escape.go | 12 +- pkg/log/format/options/level_format.go | 10 +- pkg/log/format/options/option.go | 48 ++++---- pkg/log/format/options/path_format.go | 14 +-- pkg/log/format/options/prefix.go | 14 +-- pkg/log/format/options/suffix.go | 14 +-- pkg/log/format/options/time_format.go | 52 ++++---- pkg/log/format/options/width.go | 30 ++--- pkg/log/format/placeholders/common.go | 6 +- pkg/log/format/placeholders/field.go | 6 +- pkg/log/format/placeholders/interval.go | 6 +- pkg/log/format/placeholders/level.go | 6 +- pkg/log/format/placeholders/message.go | 7 +- pkg/log/format/placeholders/placeholder.go | 133 +++++++++++---------- pkg/log/format/placeholders/plaintext.go | 6 +- pkg/log/format/placeholders/time.go | 8 +- test/integration_test.go | 12 +- 23 files changed, 292 insertions(+), 256 deletions(-) diff --git a/pkg/log/format/formatter.go b/pkg/log/format/formatter.go index 5b017567a5..09a0691e60 100644 --- a/pkg/log/format/formatter.go +++ b/pkg/log/format/formatter.go @@ -37,7 +37,7 @@ func (formatter *Formatter) Format(entry *log.Entry) ([]byte, error) { buf = new(bytes.Buffer) } - str, err := formatter.placeholders.Evaluate(&options.Data{ + str, err := formatter.placeholders.Format(&options.Data{ Entry: entry, BaseDir: formatter.baseDir, DisableColors: formatter.disableColors, diff --git a/pkg/log/format/options/align.go b/pkg/log/format/options/align.go index 058a1443ed..afc2e35f1b 100644 --- a/pkg/log/format/options/align.go +++ b/pkg/log/format/options/align.go @@ -13,11 +13,11 @@ const ( RightAlign ) -var alignValues = CommonMapValues[AlignValue]{ //nolint:gochecknoglobals +var alignList = NewMapValue(map[AlignValue]string{ //nolint:gochecknoglobals LeftAlign: "left", CenterAlign: "center", RightAlign: "right", -} +}) type AlignValue byte @@ -25,11 +25,11 @@ type AlignOption struct { *CommonOption[AlignValue] } -func (option *AlignOption) Evaluate(_ *Data, str string) (string, error) { +func (option *AlignOption) Format(_ *Data, str string) (string, error) { withoutSpaces := strings.TrimSpace(str) spaces := len(str) - len(withoutSpaces) - switch option.value { + switch option.value.Get() { case LeftAlign: return withoutSpaces + strings.Repeat(" ", spaces), nil case RightAlign: @@ -48,6 +48,6 @@ func (option *AlignOption) Evaluate(_ *Data, str string) (string, error) { func Align(value AlignValue) Option { return &AlignOption{ - CommonOption: NewCommonOption(AlignOptionName, value, alignValues), + CommonOption: NewCommonOption(AlignOptionName, alignList.Set(value)), } } diff --git a/pkg/log/format/options/case.go b/pkg/log/format/options/case.go index 4202fe11f2..864b3ea8e2 100644 --- a/pkg/log/format/options/case.go +++ b/pkg/log/format/options/case.go @@ -16,11 +16,11 @@ const ( CapitalizeCase ) -var textCaseValues = CommonMapValues[CaseValue]{ //nolint:gochecknoglobals +var caseList = NewMapValue(map[CaseValue]string{ //nolint:gochecknoglobals UpperCase: "upper", LowerCase: "lower", CapitalizeCase: "capitalize", -} +}) type CaseValue byte @@ -28,8 +28,8 @@ type CaseOption struct { *CommonOption[CaseValue] } -func (option *CaseOption) Evaluate(_ *Data, str string) (string, error) { - switch option.value { +func (option *CaseOption) Format(_ *Data, str string) (string, error) { + switch option.value.Get() { case UpperCase: return strings.ToUpper(str), nil case LowerCase: @@ -44,6 +44,6 @@ func (option *CaseOption) Evaluate(_ *Data, str string) (string, error) { func Case(value CaseValue) Option { return &CaseOption{ - CommonOption: NewCommonOption(CaseOptionName, value, textCaseValues), + CommonOption: NewCommonOption(CaseOptionName, caseList.Set(value)), } } diff --git a/pkg/log/format/options/color.go b/pkg/log/format/options/color.go index 67c7dfdf03..ef84dfbb4d 100644 --- a/pkg/log/format/options/color.go +++ b/pkg/log/format/options/color.go @@ -36,7 +36,7 @@ const ( ) var ( - colorValues = CommonMapValues[ColorValue]{ //nolint:gochecknoglobals + colorList = NewMapValue(map[ColorValue]string{ //nolint:gochecknoglobals RedColor: "red", WhiteColor: "white", YellowColor: "yellow", @@ -47,7 +47,7 @@ var ( AutoColor: "auto", GradientColor: "gradient", DisableColor: "disable", - } + }) colorScheme = ColorScheme{ //nolint:gochecknoglobals RedColor: "red", @@ -103,8 +103,8 @@ type ColorOption struct { gradientColor *gradientColor } -func (color *ColorOption) Evaluate(data *Data, str string) (string, error) { - value := color.value +func (color *ColorOption) Format(data *Data, str string) (string, error) { + value := color.value.Get() if value == DisableColor || data.DisableColors { return log.RemoveAllASCISeq(str), nil @@ -125,20 +125,9 @@ func (color *ColorOption) Evaluate(data *Data, str string) (string, error) { return str, nil } -func (color *ColorOption) ParseValue(str string) error { - val, err := colorValues.Parse(str) - if err != nil { - return err - } - - color.value = val - - return nil -} - func Color(val ColorValue) Option { return &ColorOption{ - CommonOption: NewCommonOption(ColorOptionName, val, colorValues), + CommonOption: NewCommonOption(ColorOptionName, colorList.Set(val)), compiledColors: colorScheme.Compile(), gradientColor: newGradientColor(), } diff --git a/pkg/log/format/options/common.go b/pkg/log/format/options/common.go index 4ae9272daa..37890f9f56 100644 --- a/pkg/log/format/options/common.go +++ b/pkg/log/format/options/common.go @@ -2,6 +2,7 @@ package options import ( "fmt" + "strconv" "strings" "github.com/gruntwork-io/terragrunt/internal/errors" @@ -9,16 +10,14 @@ import ( ) type CommonOption[T comparable] struct { - name string - value T - values OptionValues[T] + name string + value OptionValue[T] } -func NewCommonOption[T comparable](name string, value T, values OptionValues[T]) *CommonOption[T] { +func NewCommonOption[T comparable](name string, value OptionValue[T]) *CommonOption[T] { return &CommonOption[T]{ - name: name, - value: value, - values: values, + name: name, + value: value, } } @@ -26,51 +25,99 @@ func (option *CommonOption[T]) Name() string { return option.name } -func (option *CommonOption[T]) Value() T { - return option.value -} - func (option *CommonOption[T]) String() string { - return fmt.Sprintf("%v", option.value) + return fmt.Sprintf("%v", option.value.Get()) } -func (option *CommonOption[T]) Evaluate(_ *Data, str string) (string, error) { +func (option *CommonOption[T]) Format(_ *Data, str string) (string, error) { return str, nil } func (option *CommonOption[T]) ParseValue(str string) error { - val, err := option.values.Parse(str) + return option.value.Parse(str) +} + +type StringValue string + +func NewStringValue(val string) *StringValue { + v := StringValue(val) + return &v +} + +func (val *StringValue) Parse(str string) error { + *val = StringValue(str) + + return nil +} + +func (val *StringValue) Get() string { + return string(*val) +} + +type IntValue int + +func NewIntValue(val int) *IntValue { + v := IntValue(val) + return &v +} + +func (val *IntValue) Parse(str string) error { + v, err := strconv.Atoi(str) if err != nil { - return err + return errors.Errorf("incorrect option value: %s", str) } - option.value = val + *val = IntValue(v) return nil } -type CommonMapValues[T comparable] map[T]string +func (val *IntValue) Get() int { + return int(*val) +} + +type MapValue[T comparable] struct { + list map[T]string + value T +} + +func NewMapValue[T comparable](list map[T]string) MapValue[T] { + return MapValue[T]{ + list: list, + } +} + +func (val *MapValue[T]) Get() T { + return val.value +} + +func (val MapValue[T]) Set(v T) *MapValue[T] { + val.value = v -func (valNames CommonMapValues[T]) Parse(str string) (T, error) { - for val, name := range valNames { + return &val +} + +func (val *MapValue[T]) Parse(str string) error { + for v, name := range val.list { if name == str { - return val, nil + val.value = v + return nil } } - t := new(T) - - return *t, errors.Errorf("available values: %s", strings.Join(maps.Values(valNames), ",")) + return errors.Errorf("available values: %s", strings.Join(maps.Values(val.list), ",")) } -func (valNames CommonMapValues[T]) Filter(vals ...T) CommonMapValues[T] { - filtered := make(map[T]string, len(vals)) +func (val *MapValue[T]) Filter(vals ...T) MapValue[T] { + newVal := MapValue[T]{ + list: make(map[T]string, len(vals)), + } - for _, val := range vals { - if name, ok := valNames[val]; ok { - filtered[val] = name + for _, v := range vals { + if name, ok := val.list[v]; ok { + newVal.list[v] = name } } - return filtered + return newVal } diff --git a/pkg/log/format/options/content.go b/pkg/log/format/options/content.go index 3e6e576aa7..9b0c708aa0 100644 --- a/pkg/log/format/options/content.go +++ b/pkg/log/format/options/content.go @@ -2,22 +2,16 @@ package options const ContentOptionName = "content" -type ContentValue string - -func (val ContentValue) Parse(str string) (ContentValue, error) { - return ContentValue(str), nil -} - type ContentOption struct { - *CommonOption[ContentValue] + *CommonOption[string] } -func (option *ContentOption) Evaluate(_ *Data, str string) (string, error) { - return string(option.value), nil +func (option *ContentOption) Format(_ *Data, str string) (string, error) { + return option.value.Get(), nil } -func Content(value ContentValue) Option { +func Content(val string) Option { return &ContentOption{ - CommonOption: NewCommonOption(ContentOptionName, value, value), + CommonOption: NewCommonOption(ContentOptionName, NewStringValue(val)), } } diff --git a/pkg/log/format/options/escape.go b/pkg/log/format/options/escape.go index 1d0aff5743..5bb7317b60 100644 --- a/pkg/log/format/options/escape.go +++ b/pkg/log/format/options/escape.go @@ -13,9 +13,9 @@ const ( JSONEscape ) -var textEscapeValues = CommonMapValues[EscapeValue]{ //nolint:gochecknoglobals +var escapeList = NewMapValue(map[EscapeValue]string{ //nolint:gochecknoglobals JSONEscape: "json", -} +}) type EscapeValue byte @@ -23,8 +23,8 @@ type EscapeOption struct { *CommonOption[EscapeValue] } -func (option *EscapeOption) Evaluate(_ *Data, str string) (string, error) { - if option.value != JSONEscape { +func (option *EscapeOption) Format(_ *Data, str string) (string, error) { + if option.value.Get() != JSONEscape { return str, nil } @@ -37,8 +37,8 @@ func (option *EscapeOption) Evaluate(_ *Data, str string) (string, error) { return string(b[1 : len(b)-1]), nil } -func Escape(value EscapeValue) Option { +func Escape(val EscapeValue) Option { return &EscapeOption{ - CommonOption: NewCommonOption(EscapeOptionName, value, textEscapeValues), + CommonOption: NewCommonOption(EscapeOptionName, escapeList.Set(val)), } } diff --git a/pkg/log/format/options/level_format.go b/pkg/log/format/options/level_format.go index d731a52988..fbb9b4c6ce 100644 --- a/pkg/log/format/options/level_format.go +++ b/pkg/log/format/options/level_format.go @@ -8,11 +8,11 @@ const ( LevelFormatFull ) -var levelFormatValues = CommonMapValues[LevelFormatValue]{ //nolint:gochecknoglobals +var levelFormatList = NewMapValue(map[LevelFormatValue]string{ //nolint:gochecknoglobals LevelFormatTiny: "tiny", LevelFormatShort: "short", LevelFormatFull: "full", -} +}) type LevelFormatValue byte @@ -20,8 +20,8 @@ type LevelFormatOption struct { *CommonOption[LevelFormatValue] } -func (format *LevelFormatOption) Evaluate(data *Data, _ string) (string, error) { - switch format.Value() { +func (format *LevelFormatOption) Format(data *Data, _ string) (string, error) { + switch format.value.Get() { case LevelFormatTiny: return data.Level.TinyName(), nil case LevelFormatShort: @@ -34,6 +34,6 @@ func (format *LevelFormatOption) Evaluate(data *Data, _ string) (string, error) func LevelFormat(val LevelFormatValue) Option { return &LevelFormatOption{ - CommonOption: NewCommonOption(LevelFormatOptionName, val, levelFormatValues), + CommonOption: NewCommonOption(LevelFormatOptionName, levelFormatList.Set(val)), } } diff --git a/pkg/log/format/options/option.go b/pkg/log/format/options/option.go index a61bd72479..4eebd29b78 100644 --- a/pkg/log/format/options/option.go +++ b/pkg/log/format/options/option.go @@ -1,4 +1,4 @@ -// Package options implements placeholders options. +// Package options represents a set of placeholders options. package options import ( @@ -7,6 +7,30 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log" ) +// OptionValue contains the value of the option. +type OptionValue[T any] interface { + Parse(str string) error + Get() T +} + +// Option represents a value modifier of placeholders. +type Option interface { + // Name returns the name of the option. + Name() string + // Format formats the given string. + Format(data *Data, str string) (string, error) + // ParseValue parses and sets the value of the option. + ParseValue(str string) error +} + +type Data struct { + *log.Entry + BaseDir string + DisableColors bool + RelativePather *RelativePather + AutoColorFn func() ColorValue +} + type Options []Option func (opts Options) Get(name string) Option { @@ -44,11 +68,11 @@ func (opts Options) Merge(withOpts ...Option) Options { return append(opts, withOpts...) } -func (opts Options) Evaluate(data *Data, str string) (string, error) { +func (opts Options) Format(data *Data, str string) (string, error) { var err error for _, opt := range opts { - str, err = opt.Evaluate(data, str) + str, err = opt.Format(data, str) if str == "" || err != nil { return "", err } @@ -56,21 +80,3 @@ func (opts Options) Evaluate(data *Data, str string) (string, error) { return str, nil } - -type OptionValues[Value any] interface { - Parse(str string) (Value, error) -} - -type Option interface { - Name() string - Evaluate(data *Data, str string) (string, error) - ParseValue(str string) error -} - -type Data struct { - *log.Entry - BaseDir string - DisableColors bool - RelativePather *RelativePather - AutoColorFn func() ColorValue -} diff --git a/pkg/log/format/options/path_format.go b/pkg/log/format/options/path_format.go index 5bb611412e..37fc9bfb88 100644 --- a/pkg/log/format/options/path_format.go +++ b/pkg/log/format/options/path_format.go @@ -22,13 +22,13 @@ const ( DirectoryPath ) -var pathFormatValues = CommonMapValues[PathFormatValue]{ //nolint:gochecknoglobals +var pathFormatList = NewMapValue(map[PathFormatValue]string{ //nolint:gochecknoglobals RelativePath: "relative", RelativeModulePath: "relative-module", ModulePath: "module", FilenamePath: "filename", DirectoryPath: "dir", -} +}) type PathFormatValue byte @@ -36,8 +36,8 @@ type PathFormatOption struct { *CommonOption[PathFormatValue] } -func (option *PathFormatOption) Evaluate(data *Data, str string) (string, error) { - switch option.value { +func (option *PathFormatOption) Format(data *Data, str string) (string, error) { + switch option.value.Get() { case RelativePath: if data.RelativePather == nil { break @@ -77,13 +77,13 @@ func (option *PathFormatOption) Evaluate(data *Data, str string) (string, error) } func PathFormat(val PathFormatValue, allowed ...PathFormatValue) Option { - values := pathFormatValues + list := pathFormatList if len(allowed) > 0 { - values = values.Filter(allowed...) + list = list.Filter(allowed...) } return &PathFormatOption{ - CommonOption: NewCommonOption(PathFormatOptionName, val, values), + CommonOption: NewCommonOption(PathFormatOptionName, list.Set(val)), } } diff --git a/pkg/log/format/options/prefix.go b/pkg/log/format/options/prefix.go index 49aad73f1b..b5954fb61f 100644 --- a/pkg/log/format/options/prefix.go +++ b/pkg/log/format/options/prefix.go @@ -6,18 +6,12 @@ type PrefixOption struct { *CommonOption[string] } -func (option *PrefixOption) Evaluate(_ *Data, str string) (string, error) { - return option.value + str, nil +func (option *PrefixOption) Format(_ *Data, str string) (string, error) { + return option.value.Get() + str, nil } -func (option *PrefixOption) ParseValue(str string) error { - option.value = str - - return nil -} - -func Prefix(value string) Option { +func Prefix(val string) Option { return &PrefixOption{ - CommonOption: NewCommonOption(PrefixOptionName, value, nil), + CommonOption: NewCommonOption(PrefixOptionName, NewStringValue(val)), } } diff --git a/pkg/log/format/options/suffix.go b/pkg/log/format/options/suffix.go index d029d26cbb..713d90c74a 100644 --- a/pkg/log/format/options/suffix.go +++ b/pkg/log/format/options/suffix.go @@ -6,18 +6,12 @@ type SuffixOption struct { *CommonOption[string] } -func (option *SuffixOption) Evaluate(_ *Data, str string) (string, error) { - return str + option.value, nil +func (option *SuffixOption) Format(_ *Data, str string) (string, error) { + return str + option.value.Get(), nil } -func (option *SuffixOption) ParseValue(str string) error { - option.value = str - - return nil -} - -func Suffix(value string) Option { +func Suffix(val string) Option { return &SuffixOption{ - CommonOption: NewCommonOption(SuffixOptionName, value, nil), + CommonOption: NewCommonOption(SuffixOptionName, NewStringValue(val)), } } diff --git a/pkg/log/format/options/time_format.go b/pkg/log/format/options/time_format.go index ef3f712c86..ecfc87ea43 100644 --- a/pkg/log/format/options/time_format.go +++ b/pkg/log/format/options/time_format.go @@ -40,7 +40,7 @@ const ( ) var ( - timeFormatValueMap = TimeFormatValueMap{ //nolint:gochecknoglobals + timeFormatList = NewTimeFormatValue(map[string]string{ //nolint:gochecknoglobals YearFull: "2006", Year: "06", MonthNumZero: "01", @@ -66,47 +66,59 @@ var ( DateTime: time.DateTime, DateOnly: time.DateOnly, TimeOnly: time.TimeOnly, - } + }) ) -type TimeFormatValueMap map[string]string +type TimeFormatValue struct { + MapValue[string] +} + +func NewTimeFormatValue(list map[string]string) *TimeFormatValue { + return &TimeFormatValue{ + MapValue: NewMapValue(list), + } +} -func (valMap TimeFormatValueMap) SortedKeys() []string { - keys := maps.Keys(valMap) +func (val TimeFormatValue) SortedKeys() []string { + keys := maps.Keys(val.list) sort.Slice(keys, func(i, j int) bool { - return timeFormatValueMap[keys[i]] < timeFormatValueMap[keys[j]] + return val.list[keys[i]] < val.list[keys[j]] }) return keys } -func (valMap TimeFormatValueMap) Value(str string) string { - for _, key := range valMap.SortedKeys() { - str = strings.ReplaceAll(str, key, timeFormatValueMap[key]) - } +func (val TimeFormatValue) Set(v string) *TimeFormatValue { + val.value = timeFormatList.Value(v) - return str + return &val } -type TimeFormatValue string +func (val TimeFormatValue) Value(str string) string { + for _, key := range val.SortedKeys() { + str = strings.ReplaceAll(str, key, val.list[key]) + } -type TimeFormatOption struct { - *CommonOption[string] + return str } -func (option *TimeFormatOption) ParseValue(str string) error { - option.value = timeFormatValueMap.Value(str) +func (val TimeFormatValue) Parse(str string) error { + val.value = timeFormatList.Value(str) return nil } -func (option *TimeFormatOption) Evaluate(data *Data, _ string) (string, error) { - return data.Time.Format(option.Value()), nil +type TimeFormatOption struct { + *CommonOption[string] +} + +func (option *TimeFormatOption) Format(data *Data, _ string) (string, error) { + return data.Time.Format(option.value.Get()), nil } -func TimeFormat(str string) Option { +func TimeFormat(val string) Option { return &TimeFormatOption{ - CommonOption: NewCommonOption(TimeFormatOptionName, timeFormatValueMap.Value(str), nil), + CommonOption: NewCommonOption(TimeFormatOptionName, timeFormatList.Set(val)), } } diff --git a/pkg/log/format/options/width.go b/pkg/log/format/options/width.go index f823eb80de..69410bb407 100644 --- a/pkg/log/format/options/width.go +++ b/pkg/log/format/options/width.go @@ -1,46 +1,34 @@ package options import ( - "strconv" "strings" - "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" ) const WidthOptionName = "width" -type WidthValue int - -func (val WidthValue) Parse(str string) (WidthValue, error) { - if val, err := strconv.Atoi(str); err == nil { - return WidthValue(val), nil - } - - return val, errors.Errorf("incorrect option value: %s", str) -} - type WidthOption struct { - *CommonOption[WidthValue] + *CommonOption[int] } -func (option *WidthOption) Evaluate(_ *Data, str string) (string, error) { - WidthOption := int(option.value) - if WidthOption == 0 { +func (option *WidthOption) Format(_ *Data, str string) (string, error) { + width := option.value.Get() + if width == 0 { return str, nil } strLen := len(log.RemoveAllASCISeq(str)) - if WidthOption < strLen { - return str[:WidthOption], nil + if width < strLen { + return str[:width], nil } - return str + strings.Repeat(" ", WidthOption-strLen), nil + return str + strings.Repeat(" ", width-strLen), nil } -func Width(value WidthValue) Option { +func Width(val int) Option { return &WidthOption{ - CommonOption: NewCommonOption(WidthOptionName, value, value), + CommonOption: NewCommonOption(WidthOptionName, NewIntValue(val)), } } diff --git a/pkg/log/format/placeholders/common.go b/pkg/log/format/placeholders/common.go index 561dc2cfa5..4e41cc348b 100644 --- a/pkg/log/format/placeholders/common.go +++ b/pkg/log/format/placeholders/common.go @@ -46,7 +46,7 @@ func (common *CommonPlaceholder) GetOption(str string) (options.Option, error) { return nil, errors.Errorf("available values: %s", strings.Join(common.opts.Names(), ",")) } -// Evaluate implements `Placeholder` interface. -func (common *CommonPlaceholder) Evaluate(data *options.Data) (string, error) { - return common.opts.Evaluate(data, "") +// Format implements `Placeholder` interface. +func (common *CommonPlaceholder) Format(data *options.Data) (string, error) { + return common.opts.Format(data, "") } diff --git a/pkg/log/format/placeholders/field.go b/pkg/log/format/placeholders/field.go index e74b144e0f..6989b7a44c 100644 --- a/pkg/log/format/placeholders/field.go +++ b/pkg/log/format/placeholders/field.go @@ -14,16 +14,18 @@ type fieldPlaceholder struct { *CommonPlaceholder } -func (field *fieldPlaceholder) Evaluate(data *options.Data) (string, error) { +// Format implements `Placeholder` interface. +func (field *fieldPlaceholder) Format(data *options.Data) (string, error) { if val, ok := data.Fields[field.Name()]; ok { if val, ok := val.(string); ok { - return field.opts.Evaluate(data, val) + return field.opts.Format(data, val) } } return "", nil } +// Field creates a placeholder that displays log field value. func Field(fieldName string, opts ...options.Option) Placeholder { opts = WithCommonOptions( options.PathFormat(options.NonePath), diff --git a/pkg/log/format/placeholders/interval.go b/pkg/log/format/placeholders/interval.go index be84a3f9a0..ea141fafda 100644 --- a/pkg/log/format/placeholders/interval.go +++ b/pkg/log/format/placeholders/interval.go @@ -15,9 +15,9 @@ type intervalPlaceholder struct { *CommonPlaceholder } -// Evaluate implements `Placeholder` interface. -func (t *intervalPlaceholder) Evaluate(data *options.Data) (string, error) { - return t.opts.Evaluate(data, fmt.Sprintf("%04d", time.Since(t.baseTime)/time.Second)) +// Format implements `Placeholder` interface. +func (t *intervalPlaceholder) Format(data *options.Data) (string, error) { + return t.opts.Format(data, fmt.Sprintf("%04d", time.Since(t.baseTime)/time.Second)) } // Interval creates a placeholder that displays seconds that have passed since app started. diff --git a/pkg/log/format/placeholders/level.go b/pkg/log/format/placeholders/level.go index e921cbf0c4..bd998618d7 100644 --- a/pkg/log/format/placeholders/level.go +++ b/pkg/log/format/placeholders/level.go @@ -33,14 +33,14 @@ type level struct { *CommonPlaceholder } -// Evaluate implements `Placeholder` interface. -func (level *level) Evaluate(data *options.Data) (string, error) { +// Format implements `Placeholder` interface. +func (level *level) Format(data *options.Data) (string, error) { newData := *data newData.AutoColorFn = func() options.ColorValue { return levlAutoColorFunc(data.Level) } - return level.opts.Evaluate(&newData, data.Level.String()) + return level.opts.Format(&newData, data.Level.String()) } // Level creates a placeholder that displays log level name. diff --git a/pkg/log/format/placeholders/message.go b/pkg/log/format/placeholders/message.go index 0bd447c6cf..2eb1b41158 100644 --- a/pkg/log/format/placeholders/message.go +++ b/pkg/log/format/placeholders/message.go @@ -11,11 +11,12 @@ type message struct { *CommonPlaceholder } -// Evaluate implements `Placeholder` interface. -func (msg *message) Evaluate(data *options.Data) (string, error) { - return msg.opts.Evaluate(data, data.Message) +// Format implements `Placeholder` interface. +func (msg *message) Format(data *options.Data) (string, error) { + return msg.opts.Format(data, data.Message) } +// Message creates a placeholder that displays log message. func Message(opts ...options.Option) Placeholder { opts = WithCommonOptions( options.PathFormat(options.NonePath, options.RelativePath), diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go index 02e4fb8064..e48f766ada 100644 --- a/pkg/log/format/placeholders/placeholder.go +++ b/pkg/log/format/placeholders/placeholder.go @@ -1,4 +1,4 @@ -// Package placeholders implements fillers from which to format logs. +// Package placeholders represents a set of placeholders for formatting various log values. package placeholders import ( @@ -10,8 +10,20 @@ import ( const placeholderSign = '%' +// The placeholder is part of the log message, used to format different log values. +type Placeholder interface { + // Name retruns a placeholder name. + Name() string + // GetOption returns the option with the given option name. + GetOption(name string) (options.Option, error) + // Format returns the formatted value. + Format(data *options.Data) (string, error) +} + +// Placeholders are a set of Placeholders. type Placeholders []Placeholder +// Get returns the placeholder by its name. func (phs Placeholders) Get(name string) Placeholder { for _, ph := range phs { if ph.Name() == name { @@ -22,6 +34,7 @@ func (phs Placeholders) Get(name string) Placeholder { return nil } +// Names returns the names of the placeholders. func (phs Placeholders) Names() []string { var names = make([]string, len(phs)) @@ -32,6 +45,63 @@ func (phs Placeholders) Names() []string { return names } +// Format returns a formatted string that is the concatenation of the formatted placeholder values. +func (phs Placeholders) Format(data *options.Data) (string, error) { + var str string + + for _, ph := range phs { + s, err := ph.Format(data) + if err != nil { + return "", err + } + + str += s + } + + return str, nil +} + +// Parse parses the given string and returns a set of placeholders that are then used to format log data. +func Parse(str string) (Placeholders, error) { + var ( + registered = newPlaceholders() + placeholders Placeholders + next int + ) + + for index := 0; index < len(str); index++ { + char := str[index] + + if char == placeholderSign { + if index+1 >= len(str) { + return nil, errors.Errorf("empty placeholder name") + } + + if str[index+1] == placeholderSign { + str = str[:index] + str[index+1:] + + continue + } + + if next != index { + placeholder := PlainText(str[next:index]) + placeholders = append(placeholders, placeholder) + } + + placeholder, num, err := parsePlaceholder(str[index+1:], registered) + if err != nil { + return nil, err + } + + placeholders = append(placeholders, placeholder) + index += num + 1 + next = index + 1 + } + } + + return placeholders, nil +} + func newPlaceholders() Placeholders { return Placeholders{ Interval(), @@ -132,67 +202,6 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er return placeholder, len(str) - 1, nil } -func Parse(str string) (Placeholders, error) { - var ( - registered = newPlaceholders() - placeholders Placeholders - next int - ) - - for index := 0; index < len(str); index++ { - char := str[index] - - if char == placeholderSign { - if index+1 >= len(str) { - return nil, errors.Errorf("empty placeholder name") - } - - if str[index+1] == placeholderSign { - str = str[:index] + str[index+1:] - - continue - } - - if next != index { - placeholder := PlainText(str[next:index]) - placeholders = append(placeholders, placeholder) - } - - placeholder, num, err := parsePlaceholder(str[index+1:], registered) - if err != nil { - return nil, err - } - - placeholders = append(placeholders, placeholder) - index += num + 1 - next = index + 1 - } - } - - return placeholders, nil -} - -func (phs Placeholders) Evaluate(data *options.Data) (string, error) { - var str string - - for _, ph := range phs { - s, err := ph.Evaluate(data) - if err != nil { - return "", err - } - - str += s - } - - return str, nil -} - -type Placeholder interface { - Name() string - GetOption(name string) (options.Option, error) - Evaluate(data *options.Data) (string, error) -} - func isPlaceholderNameCharacter(c byte) bool { // Check if the byte value falls within the range of alphanumeric characters return c == '-' || c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') diff --git a/pkg/log/format/placeholders/plaintext.go b/pkg/log/format/placeholders/plaintext.go index ce98723dd4..2787bbb479 100644 --- a/pkg/log/format/placeholders/plaintext.go +++ b/pkg/log/format/placeholders/plaintext.go @@ -12,11 +12,11 @@ type plainText struct { } // PlainText creates a placeholder that displays plaintext. -// Although plaintext can be used as is without placeholder, this allows you to format the contant, -// for example set a color: `%(contant='just text',color=green)`. +// Although plaintext can be used as is without placeholder, this allows you to format the content, +// for example set a color: `%(content='just text',color=green)`. func PlainText(value string, opts ...options.Option) Placeholder { opts = WithCommonOptions( - options.Content(options.ContentValue(value)), + options.Content(value), ).Merge(opts...) return &plainText{ diff --git a/pkg/log/format/placeholders/time.go b/pkg/log/format/placeholders/time.go index 8d51dd3e9c..e113fb3eda 100644 --- a/pkg/log/format/placeholders/time.go +++ b/pkg/log/format/placeholders/time.go @@ -13,12 +13,12 @@ type timePlaceholder struct { *CommonPlaceholder } -// Evaluate implements `Placeholder` interface. -func (t *timePlaceholder) Evaluate(data *options.Data) (string, error) { - return t.opts.Evaluate(data, data.Time.String()) +// Format implements `Placeholder` interface. +func (t *timePlaceholder) Format(data *options.Data) (string, error) { + return t.opts.Format(data, data.Time.String()) } -// Time creates a placeholder that displays time. +// Time creates a placeholder that displays log time. func Time(opts ...options.Option) Placeholder { opts = WithCommonOptions( options.TimeFormat(fmt.Sprintf("%s:%s:%s%s", options.Hour24Zero, options.MinZero, options.SecZero, options.MilliSec)), diff --git a/test/integration_test.go b/test/integration_test.go index 5294e77803..bfae94da23 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -146,11 +146,11 @@ func TestLogCustomFormatOutput(t *testing.T) { }, }, { - logCustomFormat: "%interval %level(case=upper,width=6) %prefix(path=relative-module,suffix=' ')%tfpath(suffix=': ')%msg(path=relative)", + logCustomFormat: "%interval%(content=' plain-text ')%level(case=upper,width=6) %prefix(path=relative-module,suffix=' ')%tfpath(suffix=': ')%msg(path=relative)", expectedOutputRegs: []*regexp.Regexp{ - regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG Terragrunt Version:")), - regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" STDOUT dep "+wrappedBinary()+": Initializing the backend...")), - regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" STDOUT app "+wrappedBinary()+": Initializing the backend...")), + regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" plain-text DEBUG Terragrunt Version:")), + regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" plain-text STDOUT dep "+wrappedBinary()+": Initializing the backend...")), + regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" plain-text STDOUT app "+wrappedBinary()+": Initializing the backend...")), }, }, } @@ -1847,7 +1847,7 @@ func TestDependencyOutputWithHooks(t *testing.T) { assert.True(t, util.FileExists(depPathFileOut)) assert.False(t, util.FileExists(mainPathFileOut)) - // Now delete file and run just main again. It should NOT create file.out. + // Now delete file and run plain main again. It should NOT create file.out. require.NoError(t, os.Remove(depPathFileOut)) runTerragrunt(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir "+mainPath) assert.False(t, util.FileExists(depPathFileOut)) @@ -3860,7 +3860,7 @@ func TestTerragruntRunAllPlanAndShow(t *testing.T) { stdout, _, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all show --terragrunt-non-interactive --terragrunt-log-level debug --terragrunt-forward-tf-stdout --terragrunt-working-dir %s --terragrunt-out-dir %s -no-color", testPath, tmpDir)) require.NoError(t, err) - // Verify that output contains the plan and not just the actual state output + // Verify that output contains the plan and not plain the actual state output assert.Contains(t, stdout, "No changes. Your infrastructure matches the configuration.") } From ec794ad322903ec1b5160428b8b20372ac59c31a Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Mon, 11 Nov 2024 18:11:51 +0100 Subject: [PATCH 16/28] chore: fix lint --- pkg/log/format/placeholders/placeholder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go index e48f766ada..a9ba6eb91f 100644 --- a/pkg/log/format/placeholders/placeholder.go +++ b/pkg/log/format/placeholders/placeholder.go @@ -10,7 +10,7 @@ import ( const placeholderSign = '%' -// The placeholder is part of the log message, used to format different log values. +// Placeholder is part of the log message, used to format different log values. type Placeholder interface { // Name retruns a placeholder name. Name() string From 3ea61feee0ec190ecb4f2e1f63707949a2c17234 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Mon, 11 Nov 2024 18:12:22 +0100 Subject: [PATCH 17/28] chore: fix grammar --- pkg/log/format/placeholders/placeholder.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go index a9ba6eb91f..deddec7014 100644 --- a/pkg/log/format/placeholders/placeholder.go +++ b/pkg/log/format/placeholders/placeholder.go @@ -12,7 +12,7 @@ const placeholderSign = '%' // Placeholder is part of the log message, used to format different log values. type Placeholder interface { - // Name retruns a placeholder name. + // Name returns a placeholder name. Name() string // GetOption returns the option with the given option name. GetOption(name string) (options.Option, error) From cb915bde051a8abdd54c79496b4cc002da20fa45 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Mon, 11 Nov 2024 22:02:44 +0100 Subject: [PATCH 18/28] chore: code comment --- pkg/log/format/options/align.go | 3 +++ pkg/log/format/options/case.go | 3 +++ pkg/log/format/options/color.go | 3 +++ pkg/log/format/options/common.go | 13 +++++++++---- pkg/log/format/options/content.go | 3 +++ pkg/log/format/options/escape.go | 3 +++ pkg/log/format/options/level_format.go | 3 +++ pkg/log/format/options/option.go | 8 ++++++++ pkg/log/format/options/path_format.go | 3 +++ pkg/log/format/options/prefix.go | 3 +++ pkg/log/format/options/suffix.go | 3 +++ pkg/log/format/options/time_format.go | 2 ++ pkg/log/format/options/width.go | 3 +++ 13 files changed, 49 insertions(+), 4 deletions(-) diff --git a/pkg/log/format/options/align.go b/pkg/log/format/options/align.go index afc2e35f1b..b555229618 100644 --- a/pkg/log/format/options/align.go +++ b/pkg/log/format/options/align.go @@ -4,6 +4,7 @@ import ( "strings" ) +// AlignOptionName is the option name. const AlignOptionName = "align" const ( @@ -25,6 +26,7 @@ type AlignOption struct { *CommonOption[AlignValue] } +// Format implements `Option` interface. func (option *AlignOption) Format(_ *Data, str string) (string, error) { withoutSpaces := strings.TrimSpace(str) spaces := len(str) - len(withoutSpaces) @@ -46,6 +48,7 @@ func (option *AlignOption) Format(_ *Data, str string) (string, error) { return str, nil } +// Align creates the option to align text relative to the edges. func Align(value AlignValue) Option { return &AlignOption{ CommonOption: NewCommonOption(AlignOptionName, alignList.Set(value)), diff --git a/pkg/log/format/options/case.go b/pkg/log/format/options/case.go index 864b3ea8e2..c2ee92c0fa 100644 --- a/pkg/log/format/options/case.go +++ b/pkg/log/format/options/case.go @@ -7,6 +7,7 @@ import ( "golang.org/x/text/language" ) +// CaseOptionName is the option name. const CaseOptionName = "case" const ( @@ -28,6 +29,7 @@ type CaseOption struct { *CommonOption[CaseValue] } +// Format implements `Option` interface. func (option *CaseOption) Format(_ *Data, str string) (string, error) { switch option.value.Get() { case UpperCase: @@ -42,6 +44,7 @@ func (option *CaseOption) Format(_ *Data, str string) (string, error) { return str, nil } +// Case creates the option to change the case of text. func Case(value CaseValue) Option { return &CaseOption{ CommonOption: NewCommonOption(CaseOptionName, caseList.Set(value)), diff --git a/pkg/log/format/options/color.go b/pkg/log/format/options/color.go index ef84dfbb4d..15cd83a05a 100644 --- a/pkg/log/format/options/color.go +++ b/pkg/log/format/options/color.go @@ -6,6 +6,7 @@ import ( "github.com/puzpuzpuz/xsync/v3" ) +// ColorOptionName is the option name. const ColorOptionName = "color" const ( @@ -103,6 +104,7 @@ type ColorOption struct { gradientColor *gradientColor } +// Format implements `Option` interface. func (color *ColorOption) Format(data *Data, str string) (string, error) { value := color.value.Get() @@ -125,6 +127,7 @@ func (color *ColorOption) Format(data *Data, str string) (string, error) { return str, nil } +// Color creates the option to change the color of text. func Color(val ColorValue) Option { return &ColorOption{ CommonOption: NewCommonOption(ColorOptionName, colorList.Set(val)), diff --git a/pkg/log/format/options/common.go b/pkg/log/format/options/common.go index 37890f9f56..f9dbb8deae 100644 --- a/pkg/log/format/options/common.go +++ b/pkg/log/format/options/common.go @@ -14,6 +14,7 @@ type CommonOption[T comparable] struct { value OptionValue[T] } +// NewCommonOption creates a new Common option. func NewCommonOption[T comparable](name string, value OptionValue[T]) *CommonOption[T] { return &CommonOption[T]{ name: name, @@ -21,18 +22,22 @@ func NewCommonOption[T comparable](name string, value OptionValue[T]) *CommonOpt } } -func (option *CommonOption[T]) Name() string { - return option.name -} - +// String implements `fmt.Stringer` interface. func (option *CommonOption[T]) String() string { return fmt.Sprintf("%v", option.value.Get()) } +// Name implements `Option` interface. +func (option *CommonOption[T]) Name() string { + return option.name +} + +// Format implements `Option` interface. func (option *CommonOption[T]) Format(_ *Data, str string) (string, error) { return str, nil } +// ParseValue implements `Option` interface. func (option *CommonOption[T]) ParseValue(str string) error { return option.value.Parse(str) } diff --git a/pkg/log/format/options/content.go b/pkg/log/format/options/content.go index 9b0c708aa0..c1ad7fc93c 100644 --- a/pkg/log/format/options/content.go +++ b/pkg/log/format/options/content.go @@ -1,15 +1,18 @@ package options +// ContentOptionName is the option name. const ContentOptionName = "content" type ContentOption struct { *CommonOption[string] } +// Format implements `Option` interface. func (option *ContentOption) Format(_ *Data, str string) (string, error) { return option.value.Get(), nil } +// Content creates the option that sets the content. func Content(val string) Option { return &ContentOption{ CommonOption: NewCommonOption(ContentOptionName, NewStringValue(val)), diff --git a/pkg/log/format/options/escape.go b/pkg/log/format/options/escape.go index 5bb7317b60..b27d090297 100644 --- a/pkg/log/format/options/escape.go +++ b/pkg/log/format/options/escape.go @@ -6,6 +6,7 @@ import ( "github.com/gruntwork-io/terragrunt/internal/errors" ) +// EscapeOptionName is the option name. const EscapeOptionName = "escape" const ( @@ -23,6 +24,7 @@ type EscapeOption struct { *CommonOption[EscapeValue] } +// Format implements `Option` interface. func (option *EscapeOption) Format(_ *Data, str string) (string, error) { if option.value.Get() != JSONEscape { return str, nil @@ -37,6 +39,7 @@ func (option *EscapeOption) Format(_ *Data, str string) (string, error) { return string(b[1 : len(b)-1]), nil } +// Escape creates the option to escape text. func Escape(val EscapeValue) Option { return &EscapeOption{ CommonOption: NewCommonOption(EscapeOptionName, escapeList.Set(val)), diff --git a/pkg/log/format/options/level_format.go b/pkg/log/format/options/level_format.go index fbb9b4c6ce..918f300dca 100644 --- a/pkg/log/format/options/level_format.go +++ b/pkg/log/format/options/level_format.go @@ -1,5 +1,6 @@ package options +// LevelFormatOptionName is the option name. const LevelFormatOptionName = "format" const ( @@ -20,6 +21,7 @@ type LevelFormatOption struct { *CommonOption[LevelFormatValue] } +// Format implements `Option` interface. func (format *LevelFormatOption) Format(data *Data, _ string) (string, error) { switch format.value.Get() { case LevelFormatTiny: @@ -32,6 +34,7 @@ func (format *LevelFormatOption) Format(data *Data, _ string) (string, error) { return data.Level.FullName(), nil } +// LevelFormat creates the option to format level name. func LevelFormat(val LevelFormatValue) Option { return &LevelFormatOption{ CommonOption: NewCommonOption(LevelFormatOptionName, levelFormatList.Set(val)), diff --git a/pkg/log/format/options/option.go b/pkg/log/format/options/option.go index 4eebd29b78..c9628d54c4 100644 --- a/pkg/log/format/options/option.go +++ b/pkg/log/format/options/option.go @@ -9,7 +9,9 @@ import ( // OptionValue contains the value of the option. type OptionValue[T any] interface { + // Parse parses and sets the value of the option. Parse(str string) error + // Get returns the value of the option. Get() T } @@ -23,6 +25,7 @@ type Option interface { ParseValue(str string) error } +// Data is a log entry data. type Data struct { *log.Entry BaseDir string @@ -31,8 +34,10 @@ type Data struct { AutoColorFn func() ColorValue } +// Options is a set of Options. type Options []Option +// Get returns the option with the given name. func (opts Options) Get(name string) Option { for _, opt := range opts { if opt.Name() == name { @@ -43,6 +48,7 @@ func (opts Options) Get(name string) Option { return nil } +// Names returns names of the options. func (opts Options) Names() []string { var names = make([]string, len(opts)) @@ -53,6 +59,7 @@ func (opts Options) Names() []string { return names } +// Merge replaces options with the same name and adds new ones to the end. func (opts Options) Merge(withOpts ...Option) Options { for i := range opts { for t := range withOpts { @@ -68,6 +75,7 @@ func (opts Options) Merge(withOpts ...Option) Options { return append(opts, withOpts...) } +// Format returns the formatted value. func (opts Options) Format(data *Data, str string) (string, error) { var err error diff --git a/pkg/log/format/options/path_format.go b/pkg/log/format/options/path_format.go index 37fc9bfb88..499269beac 100644 --- a/pkg/log/format/options/path_format.go +++ b/pkg/log/format/options/path_format.go @@ -11,6 +11,7 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log" ) +// PathFormatOptionName is the option name. const PathFormatOptionName = "path" const ( @@ -36,6 +37,7 @@ type PathFormatOption struct { *CommonOption[PathFormatValue] } +// Format implements `Option` interface. func (option *PathFormatOption) Format(data *Data, str string) (string, error) { switch option.value.Get() { case RelativePath: @@ -76,6 +78,7 @@ func (option *PathFormatOption) Format(data *Data, str string) (string, error) { return str, nil } +// PathFormat creates the option to format the paths. func PathFormat(val PathFormatValue, allowed ...PathFormatValue) Option { list := pathFormatList if len(allowed) > 0 { diff --git a/pkg/log/format/options/prefix.go b/pkg/log/format/options/prefix.go index b5954fb61f..9f19a1d525 100644 --- a/pkg/log/format/options/prefix.go +++ b/pkg/log/format/options/prefix.go @@ -1,15 +1,18 @@ package options +// PrefixOptionName is the option name. const PrefixOptionName = "prefix" type PrefixOption struct { *CommonOption[string] } +// Format implements `Option` interface. func (option *PrefixOption) Format(_ *Data, str string) (string, error) { return option.value.Get() + str, nil } +// Prefix creates the option to add a prefix to the text. func Prefix(val string) Option { return &PrefixOption{ CommonOption: NewCommonOption(PrefixOptionName, NewStringValue(val)), diff --git a/pkg/log/format/options/suffix.go b/pkg/log/format/options/suffix.go index 713d90c74a..25194bedf3 100644 --- a/pkg/log/format/options/suffix.go +++ b/pkg/log/format/options/suffix.go @@ -1,15 +1,18 @@ package options +// SuffixOptionName is the option name. const SuffixOptionName = "suffix" type SuffixOption struct { *CommonOption[string] } +// Format implements `Option` interface. func (option *SuffixOption) Format(_ *Data, str string) (string, error) { return str + option.value.Get(), nil } +// Suffix creates the option to add a suffix to the text. func Suffix(val string) Option { return &SuffixOption{ CommonOption: NewCommonOption(SuffixOptionName, NewStringValue(val)), diff --git a/pkg/log/format/options/time_format.go b/pkg/log/format/options/time_format.go index ecfc87ea43..5404635779 100644 --- a/pkg/log/format/options/time_format.go +++ b/pkg/log/format/options/time_format.go @@ -8,6 +8,7 @@ import ( "golang.org/x/exp/maps" ) +// TimeFormatOptionName is the option name. const TimeFormatOptionName = "format" const ( @@ -113,6 +114,7 @@ type TimeFormatOption struct { *CommonOption[string] } +// Format implements `Option` interface. func (option *TimeFormatOption) Format(data *Data, _ string) (string, error) { return data.Time.Format(option.value.Get()), nil } diff --git a/pkg/log/format/options/width.go b/pkg/log/format/options/width.go index 69410bb407..199ff0ac9d 100644 --- a/pkg/log/format/options/width.go +++ b/pkg/log/format/options/width.go @@ -6,12 +6,14 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log" ) +// WidthOptionName is the option name. const WidthOptionName = "width" type WidthOption struct { *CommonOption[int] } +// Format implements `Option` interface. func (option *WidthOption) Format(_ *Data, str string) (string, error) { width := option.value.Get() if width == 0 { @@ -27,6 +29,7 @@ func (option *WidthOption) Format(_ *Data, str string) (string, error) { return str + strings.Repeat(" ", width-strLen), nil } +// Width creates the option to set the column width. func Width(val int) Option { return &WidthOption{ CommonOption: NewCommonOption(WidthOptionName, NewIntValue(val)), From 6a0793700c17e1151a44cd618acbd1251cea5818 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Tue, 12 Nov 2024 00:15:03 +0100 Subject: [PATCH 19/28] chore: update docs --- cli/commands/flags.go | 10 ++-- docs/_docs/02_features/custom-log-format.md | 53 +++++++++++++++++++++ docs/_docs/04_reference/cli-options.md | 40 ++++++++++++++-- 3 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 docs/_docs/02_features/custom-log-format.md diff --git a/cli/commands/flags.go b/cli/commands/flags.go index ca4e8de9ec..9e6270584d 100644 --- a/cli/commands/flags.go +++ b/cli/commands/flags.go @@ -358,7 +358,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { level, err := log.ParseLevel(val) if err != nil { - return errors.Errorf("flag --%s, %w", TerragruntLogLevelFlagName, err) + return cli.NewExitError(errors.Errorf("flag --%s, %w", TerragruntLogLevelFlagName, err), 1) } opts.Logger.SetOptions(log.WithLevel(level)) @@ -424,7 +424,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Action: func(_ *cli.Context, val string) error { phs, err := format.ParseFormat(val) if err != nil { - return errors.Errorf("flag --%s, invalid format %q, %v", TerragruntLogFormatFlagName, val, err) + return cli.NewExitError(errors.Errorf("flag --%s, invalid format %q, %v", TerragruntLogFormatFlagName, val, err), 1) } if opts.DisableLog || opts.DisableLogFormatting || opts.JSONLogFormat { @@ -504,7 +504,11 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Destination: &opts.StrictControls, Usage: "Enables specific strict controls. For a list of available controls, see https://terragrunt.gruntwork.io/docs/reference/strict-mode .", Action: func(ctx *cli.Context, val []string) error { - return strict.StrictControls.ValidateControlNames(val) + if err := strict.StrictControls.ValidateControlNames(val); err != nil { + cli.NewExitError(err, 1) + } + + return nil }, }, // Terragrunt Provider Cache flags diff --git a/docs/_docs/02_features/custom-log-format.md b/docs/_docs/02_features/custom-log-format.md new file mode 100644 index 0000000000..f8420a9bb8 --- /dev/null +++ b/docs/_docs/02_features/custom-log-format.md @@ -0,0 +1,53 @@ +--- +layout: collection-browser-doc +title: Custom Log Format +category: features +categories_url: features +excerpt: Learn how to use terragrunt provider cache. +tags: ["log"] +order: 280 +nav_title: Documentation +nav_title_link: /docs/ +--- + +## Custom Log Format + +Using this `--terragrunt-log-custom-format ` flag you can specify which information you want to output. The format string consists of placeholders and text. The simplest example: + + +```shell +--terragrunt-log-custom-format "%time %level %msg" +``` + +Output: + +```shell +10:09:19.809 debug Running command: tofu --version +``` + +The placeholders have preset names: + +* `%time` - current time + +* `%interval` - seconds has passed since Terragrunt started + +* `%level` - log level + +* `%prefix` - path to working directory + +* `%tfpath` - path to TF executable file + +* `%msg` - log message + +Everything else is treated as plain text, for example: + + +```shell +--terragrunt-log-custom-format "time=%time level=%level message=%msg" +``` + +Output: + +```shell +time=00:10:44.716 level=debug message=Running command: tofu --version +``` diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md index 68c042aea8..246c0b7d66 100644 --- a/docs/_docs/04_reference/cli-options.md +++ b/docs/_docs/04_reference/cli-options.md @@ -60,6 +60,8 @@ This page documents the CLI commands and options available with Terragrunt: - [terragrunt-debug](#terragrunt-debug) - [terragrunt-log-level](#terragrunt-log-level) - [terragrunt-log-disable](#terragrunt-log-disable) + - [terragrunt-log-format](#terragrunt-log-format) + - [terragrunt-log-custom-format](#terragrunt-log-custom-format) - [terragrunt-log-show-abs-paths](#terragrunt-log-show-abs-paths) - [terragrunt-no-color](#terragrunt-no-color) - [terragrunt-check](#terragrunt-check) @@ -77,7 +79,7 @@ This page documents the CLI commands and options available with Terragrunt: - [terragrunt-fail-on-state-bucket-creation](#terragrunt-fail-on-state-bucket-creation) - [terragrunt-disable-bucket-update](#terragrunt-disable-bucket-update) - [terragrunt-disable-command-validation](#terragrunt-disable-command-validation) - - [terragrunt-json-log](#terragrunt-json-log) + - [terragrunt-json-log](#terragrunt-json-log) (DEPRECATED: use [terragrunt-log-format](#terragrunt-log-format)) - [terragrunt-tf-logs-to-json](#terragrunt-tf-logs-to-json) - [terragrunt-provider-cache](#terragrunt-provider-cache) - [terragrunt-provider-cache-dir](#terragrunt-provider-cache-dir) @@ -87,7 +89,7 @@ This page documents the CLI commands and options available with Terragrunt: - [terragrunt-provider-cache-registry-names](#terragrunt-provider-cache-registry-names) - [terragrunt-out-dir](#terragrunt-out-dir) - [terragrunt-json-out-dir](#terragrunt-json-out-dir) - - [terragrunt-disable-log-formatting](#terragrunt-disable-log-formatting) + - [terragrunt-disable-log-formatting](#terragrunt-disable-log-formatting) (DEPRECATED: use [terragrunt-log-format](#terragrunt-log-format)) - [terragrunt-forward-tf-stdout](#terragrunt-forward-tf-stdout) - [terragrunt-no-destroy-dependencies-check](#terragrunt-no-destroy-dependencies-check) @@ -764,6 +766,8 @@ prefix `--terragrunt-` (e.g., `--terragrunt-config`). The currently available op - [terragrunt-parallelism](#terragrunt-parallelism) - [terragrunt-debug](#terragrunt-debug) - [terragrunt-log-level](#terragrunt-log-level) + - [terragrunt-log-format](#terragrunt-log-format) + - [terragrunt-log-custom-format](#terragrunt-log-custom-format) - [terragrunt-log-disable](#terragrunt-log-disable) - [terragrunt-log-show-abs-paths](#terragrunt-log-show-abs-paths) - [terragrunt-no-color](#terragrunt-no-color) @@ -782,7 +786,7 @@ prefix `--terragrunt-` (e.g., `--terragrunt-config`). The currently available op - [terragrunt-fail-on-state-bucket-creation](#terragrunt-fail-on-state-bucket-creation) - [terragrunt-disable-bucket-update](#terragrunt-disable-bucket-update) - [terragrunt-disable-command-validation](#terragrunt-disable-command-validation) - - [terragrunt-json-log](#terragrunt-json-log) + - [terragrunt-json-log](#terragrunt-json-log) (DEPRECATED: use [terragrunt-log-format](#terragrunt-log-format)) - [terragrunt-tf-logs-to-json](#terragrunt-tf-logs-to-json) - [terragrunt-provider-cache](#terragrunt-provider-cache) - [terragrunt-provider-cache-dir](#terragrunt-provider-cache-dir) @@ -792,7 +796,7 @@ prefix `--terragrunt-` (e.g., `--terragrunt-config`). The currently available op - [terragrunt-provider-cache-registry-names](#terragrunt-provider-cache-registry-names) - [terragrunt-out-dir](#terragrunt-out-dir) - [terragrunt-json-out-dir](#terragrunt-json-out-dir) - - [terragrunt-disable-log-formatting](#terragrunt-disable-log-formatting) + - [terragrunt-disable-log-formatting](#terragrunt-disable-log-formatting) (DEPRECATED: use [terragrunt-log-format](#terragrunt-log-format)) - [terragrunt-forward-tf-stdout](#terragrunt-forward-tf-stdout) - [terragrunt-no-destroy-dependencies-check](#terragrunt-no-destroy-dependencies-check) @@ -1098,11 +1102,33 @@ When passed it, sets logging level for terragrunt. All supported levels are: Where the first two control the logging of Terraform/OpenTofu output. +### terragrunt-log-format + +**CLI Arg**: `--terragrunt-log-format`
+**Environment Variable**: `TERRAGRUNT_LOG_FORMAT`
+**Requires an argument**: `--terragrunt-log-format `
+ +There are four log format presets: + +- `pretty` (this is the default) +- `bare` (old Terragrunt log) +- `json` +- `key-value` + +### terragrunt-log-custom-format + +**CLI Arg**: `--terragrunt-log-custom-format`
+**Environment Variable**: `TERRAGRUNT_LOG_CUSTOM_FORMAT`
+**Requires an argument**: `--terragrunt-log-custom-format `
+ +This allows you to specify which information you want to output. It works a little bit like printf format. + +Make sure to read [Custom Log Format](https://terragrunt.gruntwork.io/docs/features/custom-log-format/) for syntax details. + ### terragrunt-log-disable **CLI Arg**: `--terragrunt-log-disable`
**Environment Variable**: `TERRAGRUNT_LOG_DISABLE`
-**Requires an argument**: `--terragrunt-log-disable`
Disable logging. This flag also enables [terragrunt-forward-tf-stdout](#terragrunt-forward-tf-stdout). @@ -1334,6 +1360,8 @@ When this flag is set, Terragrunt will not validate the terraform command, which ### terragrunt-json-log +DEPRECATED: Use [terragrunt-log-format](#terragrunt-log-format). + **CLI Arg**: `--terragrunt-json-log`
**Environment Variable**: `TERRAGRUNT_JSON_LOG` (set to `true`)
@@ -1471,6 +1499,8 @@ Other credential configurations will be supported in the future, but until then, ### terragrunt-disable-log-formatting +DEPRECATED: Use [terragrunt-log-format](#terragrunt-log-format). + **CLI Arg**: `--terragrunt-disable-log-formatting`
**Environment Variable**: `TERRAGRUNT_DISABLE_LOG_FORMATTING`
From f63de5543702f800ae7ee7297f2bf33533f78d86 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Tue, 12 Nov 2024 16:23:23 +0100 Subject: [PATCH 20/28] chore: code improvements, doc update --- docs/_docs/02_features/custom-log-format.md | 193 +++++++++++++++++++- pkg/log/format/format.go | 8 +- pkg/log/format/options/color.go | 119 +++++++----- pkg/log/format/options/content.go | 6 +- pkg/log/format/options/level_format.go | 5 +- pkg/log/format/options/path_format.go | 26 +-- pkg/log/format/options/time_format.go | 2 +- pkg/log/format/placeholders/common.go | 2 + pkg/log/format/placeholders/placeholder.go | 7 +- 9 files changed, 288 insertions(+), 80 deletions(-) diff --git a/docs/_docs/02_features/custom-log-format.md b/docs/_docs/02_features/custom-log-format.md index f8420a9bb8..21da02c96a 100644 --- a/docs/_docs/02_features/custom-log-format.md +++ b/docs/_docs/02_features/custom-log-format.md @@ -12,7 +12,13 @@ nav_title_link: /docs/ ## Custom Log Format -Using this `--terragrunt-log-custom-format ` flag you can specify which information you want to output. The format string consists of placeholders and text. The simplest example: +Using this `--terragrunt-log-custom-format ` flag you can specify which information you want to output. + + +### Placeholders + + +The format string consists of placeholders and text. Placeholders start with the `%` sign. The simplest example: ```shell @@ -25,21 +31,33 @@ Output: 10:09:19.809 debug Running command: tofu --version ``` -The placeholders have preset names: +The double sign `%%` displays the percent sign as plain text. + +```shell +--terragrunt-log-custom-format "%time %level %%msg" +``` + +Output: + +```shell +10:09:19.809 debug %msg +``` + +Placeholders have preset names: -* `%time` - current time +* `%time` - Current time. -* `%interval` - seconds has passed since Terragrunt started +* `%interval` - Seconds has passed since Terragrunt started. -* `%level` - log level +* `%level` - Log level. -* `%prefix` - path to working directory +* `%prefix` - Path to working directory. -* `%tfpath` - path to TF executable file +* `%tfpath` - Path to TF executable file. -* `%msg` - log message +* `%msg` - Log message. -Everything else is treated as plain text, for example: +Any other text is considered as plain text, for example: ```shell @@ -51,3 +69,160 @@ Output: ```shell time=00:10:44.716 level=debug message=Running command: tofu --version ``` + +A placeholder is just a value, to format this value you need to pass options to the placeholder. It has the following syntax: + +`%placeholder-name(option-name=option-value, option-name=option-value,...)` + +```shell +--terragrunt-log-custom-format "%time(format='Y-m-d H:i:sv') %level(format=short,case=upper) %msg" +``` + +Output: + +```shell +2024-11-12 11:52:20.214 DEB Running command: tofu --version +``` + +Even if you don't pass options, the empty brackets are added implicitly. Thus `%time` equals `%time()`. If you need to add brackets as plain text after a placeholder with no options and without space, you need to explicitly specify empty brackets first, otherwise, they will be treated as invalid options. + + +```shell +--terragrunt-log-custom-format "%level()(%time()(%msg))" +``` + +Output: + +```shell +debug(12:15:48.355(Running command: tofu --version)) +``` + +You can format plain text as well by using an unnamed placeholder. + + +```shell +--terragrunt-log-custom-format "%(content='time=',color=magenta)%time %(content='level=',color=light-blue)%level %(content='msg=',color=green)%msg" +``` + +Output: + +```shell +time=12:33:08.513 level=debug msg=Running command: tofu --version +``` + +*Unfortunately, it is not possible to display color in a Markdown document, but in the above output, `time=` is colored magenta, `level=` is colored light blue and `msg=` is colored green.* + + +### Options + +Options can be divided into common ones, which can be passed to any placeholder, and specific ones for each placeholder. + +Common options: + +* `content=` - Sets a placeholder value, typically used to set the initial value of an unnamed placeholder. + +* `case=[upper|lower|capitalize]` - Sets the case of the text. + +* `width=` - Sets the column width. + +* `align=[left|center|right]` - Aligns content relative to the edges of the column, used in conjunction with `width`. + +* `prefix=` - Appends a content prefix. + +* `suffix=`- Prepends a content suffix. + +* `escape=[json]` - Escapes content for use as a value in a JSON string. + +* `color=[red|white|yellow|green|cayn|magenta|blue|...]` - Sets the color for the content. + +Specific options for placeholders: + +* `%level` + + * `format=[tiny|short]` - Shortens the log level names `stdout`, `stderr`, `error`, `warn`, `info`, `debug`, `trcace` to 1 and 3 characters. + + * `tiny` - `std`, `err`, `wrn`, `inf`, `deb`, `trc` + + * `short` - `s`, `e`, `w`, `i`, `d`, `t` + +* `%time` + + * `format=` - Sets the time format. + + Persets formats: + + * `date-time` - Example: 2006-01-02 15:04:05 + + * `date-only` - Example: 2006-01-02 + + * `time-only` - Example: 15:04:05 + + * `rfc3339` - Example: 2006-01-02T15:04:05Z07:00 + + * `rfc3339-nano` - Example: 2006-01-02T15:04:05.999999999Z07:00 + + Characters formats: + + * `H` - 24-hour format of an hour with leading zeros, 00 to 23 + + * `h` - 12-hour format of an hour with leading zeros, 01 to 12 + + * `g` - 12-hour format of an hour without leading zeros, 1 to 12 + + * `i` - Minutes with leading zeros, 00 to 59 + + * `s` - Seconds with leading zeros, 00 to 59 + + * `v` - Milliseconds, example: .654 + + * `u` - Microseconds, example: .654321 + + * `Y` - A full numeric representation of a year, examples: 1999, 2003 + + * `y` - A two digit representation of a year, examples: 99 or 03 + + * `m` - Numeric representation of a month, with leading zeros, 01 to 12 + + * `n` - Numeric representation of a month, without leading zeros, 1 to 12 + + * `M` - A short textual representation of a month, three letters, Jan to Dec + + * `d` - Day of the month, 2 digits with leading zeros, 01 to 31 + + * `j` - Day of the month without leading zeros, 1 to 31 + + * `D` - A textual representation of a day, three letters, Mon to Sun + + * `A` - Uppercase Ante meridiem and Post meridiem, AM or PM + + * `a` - Lowercase Ante meridiem and Post meridiem, am or pm + + * `T` - Timezone abbreviation, examples: EST, MDT + + * `P` - Difference to Greenwich time (GMT) with colon between hours and minutes, example: +02:00 + + * `O` - Difference to Greenwich time (GMT) without colon between hours and minutes, example: +0200 + +* `%prefix` + + * `path=[relative|short-relative|short]` + + * `relative` - Outputs a relative path to the working directory. + + * `short-relative` - Outputs a relative path to the working directory, trims the leading slash `./` and hides the working directory path `.` + + * `short` - Outputs a abosolute path, but hides the working directory path. + +* `%tfpath` + + * `path=[filename|dir]` + + * `filename` - Outputs the name of the executable file. + + * `dir` - Outputs the directory name of the executable file. + +* `%msg` + + * `path=[relative]` + + * `relative` - Converts all absolute paths to relative ones to the working directory. diff --git a/pkg/log/format/format.go b/pkg/log/format/format.go index a7be3a1217..b4b1ae98f7 100644 --- a/pkg/log/format/format.go +++ b/pkg/log/format/format.go @@ -31,7 +31,7 @@ func NewBareFormat() Placeholders { PlainText(" "), Message(), Field(WorkDirKeyName, - PathFormat(ModulePath), + PathFormat(ShortPath), Prefix("\t prefix=["), Suffix("] "), ), @@ -52,7 +52,7 @@ func NewPrettyFormat() Placeholders { ), PlainText(" "), Field(WorkDirKeyName, - PathFormat(RelativeModulePath), + PathFormat(ShortRelativePath), Prefix("["), Suffix("] "), Color(GradientColor), @@ -81,7 +81,7 @@ func NewJSONFormat() Placeholders { ), PlainText(`", "prefix":"`), Field(WorkDirKeyName, - PathFormat(ModulePath), + PathFormat(ShortPath), Escape(JSONEscape), ), PlainText(`", "tfpath":"`), @@ -110,7 +110,7 @@ func NewKeyValueFormat() Placeholders { ), Field(WorkDirKeyName, Prefix(" prefix="), - PathFormat(RelativeModulePath), + PathFormat(ShortRelativePath), ), Field(TFPathKeyName, Prefix(" tfpath="), diff --git a/pkg/log/format/options/color.go b/pkg/log/format/options/color.go index 15cd83a05a..f40ded328a 100644 --- a/pkg/log/format/options/color.go +++ b/pkg/log/format/options/color.go @@ -1,48 +1,44 @@ package options import ( + "strconv" + "strings" + + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/mgutz/ansi" "github.com/puzpuzpuz/xsync/v3" + "golang.org/x/exp/maps" ) // ColorOptionName is the option name. const ColorOptionName = "color" const ( - NoneColor ColorValue = iota + NoneColor ColorValue = iota + 255 DisableColor RedColor WhiteColor YellowColor GreenColor + BlueColor CyanColor BlueHColor BlackHColor + MagentaColor AutoColor GradientColor - - Color66 - Color67 - Color95 - Color96 - Color102 - Color103 - Color108 - Color109 - Color138 - Color139 - Color144 - Color145 ) var ( - colorList = NewMapValue(map[ColorValue]string{ //nolint:gochecknoglobals + colorList = NewColorList(map[ColorValue]string{ //nolint:gochecknoglobals RedColor: "red", WhiteColor: "white", YellowColor: "yellow", GreenColor: "green", CyanColor: "cyan", + MagentaColor: "magenta", + BlueColor: "blue", BlueHColor: "light-blue", BlackHColor: "light-black", AutoColor: "auto", @@ -51,29 +47,46 @@ var ( }) colorScheme = ColorScheme{ //nolint:gochecknoglobals - RedColor: "red", - WhiteColor: "white", - YellowColor: "yellow", - GreenColor: "green", - CyanColor: "cyan", - BlueHColor: "blue+h", - BlackHColor: "black+h", - - Color66: "66", - Color67: "67", - Color95: "95", - Color96: "96", - Color102: "102", - Color103: "103", - Color108: "108", - Color109: "109", - Color138: "138", - Color139: "139", - Color144: "144", - Color145: "145", + RedColor: "red", + WhiteColor: "white", + YellowColor: "yellow", + GreenColor: "green", + CyanColor: "cyan", + BlueColor: "blue", + BlueHColor: "blue+h", + BlackHColor: "black+h", + MagentaColor: "magenta", } ) +type ColorList struct { + MapValue[ColorValue] +} + +func NewColorList(list map[ColorValue]string) ColorList { + return ColorList{ + MapValue: NewMapValue(list), + } +} + +func (val *ColorList) Set(v ColorValue) *ColorList { + return &ColorList{MapValue: *val.MapValue.Set(v)} +} + +func (val *ColorList) Parse(str string) error { + if num, err := strconv.Atoi(str); err == nil && num >= 0 && num <= 255 { + val.value = ColorValue(byte(num)) + + return nil + } + + if err := val.MapValue.Parse(str); err != nil { + return errors.Errorf("available values: 0..255,%s", strings.Join(maps.Values(val.list), ",")) + } + + return nil +} + type ColorScheme map[ColorValue]ColorStyle func (scheme ColorScheme) Compile() compiledColorScheme { @@ -83,6 +96,12 @@ func (scheme ColorScheme) Compile() compiledColorScheme { compiled[name] = val.ColorFunc() } + for i := range 255 { + s := strconv.Itoa(i) + + compiled[ColorValue(i)] = ColorStyle(s).ColorFunc() + } + return compiled } @@ -94,7 +113,7 @@ func (val ColorStyle) ColorFunc() ColorFunc { type ColorFunc func(string) string -type ColorValue byte +type ColorValue int type compiledColorScheme map[ColorValue]ColorFunc @@ -108,6 +127,10 @@ type ColorOption struct { func (color *ColorOption) Format(data *Data, str string) (string, error) { value := color.value.Get() + if value == NoneColor { + return str, nil + } + if value == DisableColor || data.DisableColors { return log.RemoveAllASCISeq(str), nil } @@ -141,18 +164,18 @@ var ( // https://user-images.githubusercontent.com/995050/47952855-ecb12480-df75-11e8-89d4-ac26c50e80b9.png // https://www.hackitu.de/termcolor256/ defaultAutoColorValues = []ColorValue{ //nolint:gochecknoglobals - Color66, - Color67, - Color95, - Color96, - Color102, - Color103, - Color108, - Color109, - Color138, - Color139, - Color144, - Color145, + 66, + 67, + 95, + 96, + 102, + 103, + 108, + 109, + 138, + 139, + 144, + 145, } ) diff --git a/pkg/log/format/options/content.go b/pkg/log/format/options/content.go index c1ad7fc93c..ffc89a1997 100644 --- a/pkg/log/format/options/content.go +++ b/pkg/log/format/options/content.go @@ -9,7 +9,11 @@ type ContentOption struct { // Format implements `Option` interface. func (option *ContentOption) Format(_ *Data, str string) (string, error) { - return option.value.Get(), nil + if val := option.value.Get(); val != "" { + return val, nil + } + + return str, nil } // Content creates the option that sets the content. diff --git a/pkg/log/format/options/level_format.go b/pkg/log/format/options/level_format.go index 918f300dca..f96fcc6ce2 100644 --- a/pkg/log/format/options/level_format.go +++ b/pkg/log/format/options/level_format.go @@ -4,15 +4,14 @@ package options const LevelFormatOptionName = "format" const ( - LevelFormatTiny LevelFormatValue = iota + LevelFormatFull LevelFormatValue = iota LevelFormatShort - LevelFormatFull + LevelFormatTiny ) var levelFormatList = NewMapValue(map[LevelFormatValue]string{ //nolint:gochecknoglobals LevelFormatTiny: "tiny", LevelFormatShort: "short", - LevelFormatFull: "full", }) type LevelFormatValue byte diff --git a/pkg/log/format/options/path_format.go b/pkg/log/format/options/path_format.go index 499269beac..6f0465ca6d 100644 --- a/pkg/log/format/options/path_format.go +++ b/pkg/log/format/options/path_format.go @@ -17,18 +17,18 @@ const PathFormatOptionName = "path" const ( NonePath PathFormatValue = iota RelativePath - RelativeModulePath - ModulePath + ShortRelativePath + ShortPath FilenamePath DirectoryPath ) var pathFormatList = NewMapValue(map[PathFormatValue]string{ //nolint:gochecknoglobals - RelativePath: "relative", - RelativeModulePath: "relative-module", - ModulePath: "module", - FilenamePath: "filename", - DirectoryPath: "dir", + RelativePath: "relative", + ShortRelativePath: "short-relative", + ShortPath: "short", + FilenamePath: "filename", + DirectoryPath: "dir", }) type PathFormatValue byte @@ -46,23 +46,23 @@ func (option *PathFormatOption) Format(data *Data, str string) (string, error) { } return data.RelativePather.ReplaceAbsPaths(str), nil - case RelativeModulePath: + case ShortRelativePath: + if str == data.BaseDir { + return "", nil + } + if data.RelativePather == nil { break } str = data.RelativePather.ReplaceAbsPaths(str) - if str == log.CurDir { - return "", nil - } - if strings.HasPrefix(str, log.CurDirWithSeparator) { return str[len(log.CurDirWithSeparator):], nil } return str, nil - case ModulePath: + case ShortPath: if str == data.BaseDir { return "", nil } diff --git a/pkg/log/format/options/time_format.go b/pkg/log/format/options/time_format.go index 5404635779..d77afa2f06 100644 --- a/pkg/log/format/options/time_format.go +++ b/pkg/log/format/options/time_format.go @@ -104,7 +104,7 @@ func (val TimeFormatValue) Value(str string) string { return str } -func (val TimeFormatValue) Parse(str string) error { +func (val *TimeFormatValue) Parse(str string) error { val.value = timeFormatList.Value(str) return nil diff --git a/pkg/log/format/placeholders/common.go b/pkg/log/format/placeholders/common.go index 4e41cc348b..b37766928c 100644 --- a/pkg/log/format/placeholders/common.go +++ b/pkg/log/format/placeholders/common.go @@ -10,11 +10,13 @@ import ( // WithCommonOptions is a set of common options that are used in all placeholders. func WithCommonOptions(opts ...options.Option) options.Options { return options.Options(append(opts, + options.Content(""), options.Case(options.NoneCase), options.Width(0), options.Align(options.NoneAlign), options.Prefix(""), options.Suffix(""), + options.Escape(options.NoneEscape), options.Color(options.NoneColor), )) } diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go index deddec7014..cdb576e5b5 100644 --- a/pkg/log/format/placeholders/placeholder.go +++ b/pkg/log/format/placeholders/placeholder.go @@ -99,6 +99,11 @@ func Parse(str string) (Placeholders, error) { } } + if next != len(str) { + placeholder := PlainText(str[next:]) + placeholders = append(placeholders, placeholder) + } + return placeholders, nil } @@ -108,7 +113,7 @@ func newPlaceholders() Placeholders { Time(), Level(), Message(), - Field(WorkDirKeyName, options.PathFormat(options.NonePath, options.RelativePath, options.RelativeModulePath, options.ModulePath)), + Field(WorkDirKeyName, options.PathFormat(options.NonePath, options.RelativePath, options.ShortRelativePath, options.ShortPath)), Field(TFPathKeyName, options.PathFormat(options.NonePath, options.FilenamePath, options.DirectoryPath)), } } From 1f9918e800bf5f64ff7bf7ebaceb049cbe50686e Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Tue, 12 Nov 2024 16:33:15 +0100 Subject: [PATCH 21/28] chore: fix tests --- cli/commands/flags.go | 2 +- docs/_docs/02_features/custom-log-format.md | 7 ------- test/integration_test.go | 4 ++-- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/cli/commands/flags.go b/cli/commands/flags.go index 9e6270584d..9d1841708b 100644 --- a/cli/commands/flags.go +++ b/cli/commands/flags.go @@ -505,7 +505,7 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags { Usage: "Enables specific strict controls. For a list of available controls, see https://terragrunt.gruntwork.io/docs/reference/strict-mode .", Action: func(ctx *cli.Context, val []string) error { if err := strict.StrictControls.ValidateControlNames(val); err != nil { - cli.NewExitError(err, 1) + return cli.NewExitError(err, 1) } return nil diff --git a/docs/_docs/02_features/custom-log-format.md b/docs/_docs/02_features/custom-log-format.md index 21da02c96a..4eb3ae7d03 100644 --- a/docs/_docs/02_features/custom-log-format.md +++ b/docs/_docs/02_features/custom-log-format.md @@ -14,13 +14,10 @@ nav_title_link: /docs/ Using this `--terragrunt-log-custom-format ` flag you can specify which information you want to output. - ### Placeholders - The format string consists of placeholders and text. Placeholders start with the `%` sign. The simplest example: - ```shell --terragrunt-log-custom-format "%time %level %msg" ``` @@ -59,7 +56,6 @@ Placeholders have preset names: Any other text is considered as plain text, for example: - ```shell --terragrunt-log-custom-format "time=%time level=%level message=%msg" ``` @@ -86,7 +82,6 @@ Output: Even if you don't pass options, the empty brackets are added implicitly. Thus `%time` equals `%time()`. If you need to add brackets as plain text after a placeholder with no options and without space, you need to explicitly specify empty brackets first, otherwise, they will be treated as invalid options. - ```shell --terragrunt-log-custom-format "%level()(%time()(%msg))" ``` @@ -99,7 +94,6 @@ debug(12:15:48.355(Running command: tofu --version)) You can format plain text as well by using an unnamed placeholder. - ```shell --terragrunt-log-custom-format "%(content='time=',color=magenta)%time %(content='level=',color=light-blue)%level %(content='msg=',color=green)%msg" ``` @@ -112,7 +106,6 @@ time=12:33:08.513 level=debug msg=Running command: tofu --version *Unfortunately, it is not possible to display color in a Markdown document, but in the above output, `time=` is colored magenta, `level=` is colored light blue and `msg=` is colored green.* - ### Options Options can be divided into common ones, which can be passed to any placeholder, and specific ones for each placeholder. diff --git a/test/integration_test.go b/test/integration_test.go index bfae94da23..841e58ff66 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -138,7 +138,7 @@ func TestLogCustomFormatOutput(t *testing.T) { }, }, { - logCustomFormat: "%interval %level(case=upper) %prefix(path=relative-module,prefix='[',suffix='] ')%msg(path=relative)", + logCustomFormat: "%interval %level(case=upper) %prefix(path=short-relative,prefix='[',suffix='] ')%msg(path=relative)", expectedOutputRegs: []*regexp.Regexp{ regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG Terragrunt Version:")), regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" DEBUG [dep] Module ./dep must wait for 0 dependencies to finish")), @@ -146,7 +146,7 @@ func TestLogCustomFormatOutput(t *testing.T) { }, }, { - logCustomFormat: "%interval%(content=' plain-text ')%level(case=upper,width=6) %prefix(path=relative-module,suffix=' ')%tfpath(suffix=': ')%msg(path=relative)", + logCustomFormat: "%interval%(content=' plain-text ')%level(case=upper,width=6) %prefix(path=short-relative,suffix=' ')%tfpath(suffix=': ')%msg(path=relative)", expectedOutputRegs: []*regexp.Regexp{ regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" plain-text DEBUG Terragrunt Version:")), regexp.MustCompile(`\d{4}` + regexp.QuoteMeta(" plain-text STDOUT dep "+wrappedBinary()+": Initializing the backend...")), From 42c18cf99e002bda8d3a1d86135fdb28b6420b60 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Tue, 12 Nov 2024 22:10:42 +0100 Subject: [PATCH 22/28] chore: doc update --- docs/_docs/02_features/custom-log-format.md | 42 ++++++++++++- pkg/log/format/format.go | 4 +- pkg/log/format/options/color.go | 67 ++++++++++++++------- pkg/log/format/options/option.go | 2 +- pkg/log/format/placeholders/level.go | 4 +- 5 files changed, 91 insertions(+), 28 deletions(-) diff --git a/docs/_docs/02_features/custom-log-format.md b/docs/_docs/02_features/custom-log-format.md index 4eb3ae7d03..93b3ff5cc2 100644 --- a/docs/_docs/02_features/custom-log-format.md +++ b/docs/_docs/02_features/custom-log-format.md @@ -120,14 +120,24 @@ Common options: * `align=[left|center|right]` - Aligns content relative to the edges of the column, used in conjunction with `width`. -* `prefix=` - Appends a content prefix. +* `prefix=` - Prepends the prefix to the content. If the content of the placeholder is empty, the prefix will not be prepended. -* `suffix=`- Prepends a content suffix. +* `suffix=`- Appends the suffix to the content. If the content of the placeholder is empty, the suffix will not be appended. * `escape=[json]` - Escapes content for use as a value in a JSON string. * `color=[red|white|yellow|green|cayn|magenta|blue|...]` - Sets the color for the content. + * `1..255` - Specifies a color using a [number](https://www.hackitu.de/termcolor256/), 1 to 255 + + * `red|white|yellow|green|cyan|magenta|blue|light-blue|light-black|light-red|light-green|light-yellow|light-magenta|light-cyan|light-white` - Specifies a color using a word + + * `gradient` - Specifies to use a new color each time the placeholder contents change. + + * `preset` - Specifies to use preset colors. For example, each log level name has its own preset color. + + * `disable` - Disables color, also removes colors set in terraform/tofu output. + Specific options for placeholders: * `%level` @@ -219,3 +229,31 @@ Specific options for placeholders: * `path=[relative]` * `relative` - Converts all absolute paths to relative ones to the working directory. + +### Examples + +The examples below replicate the formats specified with `--terragrunt-log-format`. They can be useful if you need to change existing formats to suit your needs. + +`--terragrunt-log-format pretty` + +```shell +--terragrunt-log-custom-format "%time(color=light-black) %level(case=upper,width=6,color=preset) %prefix(path=short-relative,color=gradient,suffix=' ')%tfpath(color=cyan,suffix=': ')%msg(path=relative)" +``` + +`--terragrunt-log-format bare` + +```shell +--terragrunt-log-custom-format "%level(case=upper,width=4)[%interval] %msg %prefix(path=short,prefix='prefix=')" +``` + +`--terragrunt-log-format key-value` + +```shell +--terragrunt-log-custom-format "time=%time(format=rfc3339) level=%level prefix=%prefix(path=short-relative) tfpath=%tfpath(path=filename) msg=%msg(path=relative,color=disable)" +``` + +`--terragrunt-log-format json` + +```shell +--terragrunt-log-custom-format '{"time":"%time(format=rfc3339,escape=json)", "level":"%level(escape=json)", "prefix":"%prefix(path=short-relative,escape=json)", "tfpath":"%tfpath(path=filename,escape=json)", "msg":"%msg(path=relative,escape=json,color=disable)"}' +``` diff --git a/pkg/log/format/format.go b/pkg/log/format/format.go index b4b1ae98f7..8275a1a6f7 100644 --- a/pkg/log/format/format.go +++ b/pkg/log/format/format.go @@ -42,13 +42,13 @@ func NewPrettyFormat() Placeholders { return Placeholders{ Time( TimeFormat(fmt.Sprintf("%s:%s:%s%s", Hour24Zero, MinZero, SecZero, MilliSec)), - Color(BlackHColor), + Color(LightBlackColor), ), PlainText(" "), Level( Width(6), //nolint:mnd Case(UpperCase), - Color(AutoColor), + Color(PresetColor), ), PlainText(" "), Field(WorkDirKeyName, diff --git a/pkg/log/format/options/color.go b/pkg/log/format/options/color.go index f40ded328a..12ffe2bd8f 100644 --- a/pkg/log/format/options/color.go +++ b/pkg/log/format/options/color.go @@ -17,45 +17,70 @@ const ColorOptionName = "color" const ( NoneColor ColorValue = iota + 255 DisableColor + GradientColor + PresetColor + + BlackColor RedColor WhiteColor YellowColor GreenColor BlueColor CyanColor - BlueHColor - BlackHColor MagentaColor - AutoColor - GradientColor + + LightBlueColor + LightBlackColor + LightRedColor + LightGreenColor + LightYellowColor + LightMagentaColor + LightCyanColor + LightWhiteColor ) var ( colorList = NewColorList(map[ColorValue]string{ //nolint:gochecknoglobals - RedColor: "red", - WhiteColor: "white", - YellowColor: "yellow", - GreenColor: "green", - CyanColor: "cyan", - MagentaColor: "magenta", - BlueColor: "blue", - BlueHColor: "light-blue", - BlackHColor: "light-black", - AutoColor: "auto", + PresetColor: "preset", GradientColor: "gradient", DisableColor: "disable", - }) - colorScheme = ColorScheme{ //nolint:gochecknoglobals + BlackColor: "black", RedColor: "red", WhiteColor: "white", YellowColor: "yellow", GreenColor: "green", CyanColor: "cyan", - BlueColor: "blue", - BlueHColor: "blue+h", - BlackHColor: "black+h", MagentaColor: "magenta", + BlueColor: "blue", + + LightBlueColor: "light-blue", + LightBlackColor: "light-black", + LightRedColor: "light-red", + LightGreenColor: "light-green", + LightYellowColor: "light-yellow", + LightMagentaColor: "light-magenta", + LightCyanColor: "light-cyan", + LightWhiteColor: "light-white", + }) + + colorScheme = ColorScheme{ //nolint:gochecknoglobals + BlackColor: "black", + RedColor: "red", + WhiteColor: "white", + YellowColor: "yellow", + GreenColor: "green", + CyanColor: "cyan", + BlueColor: "blue", + MagentaColor: "magenta", + LightBlueColor: "blue+h", + LightBlackColor: "black+h", + LightRedColor: "red+h", + LightGreenColor: "green+h", + LightYellowColor: "yellow+h", + LightMagentaColor: "magenta+h", + LightCyanColor: "cyan+h", + LightWhiteColor: "white+h", } ) @@ -135,8 +160,8 @@ func (color *ColorOption) Format(data *Data, str string) (string, error) { return log.RemoveAllASCISeq(str), nil } - if value == AutoColor && data.AutoColorFn != nil { - value = data.AutoColorFn() + if value == PresetColor && data.PresetColorFn != nil { + value = data.PresetColorFn() } if value == GradientColor && color.gradientColor != nil { diff --git a/pkg/log/format/options/option.go b/pkg/log/format/options/option.go index c9628d54c4..75d92e6a30 100644 --- a/pkg/log/format/options/option.go +++ b/pkg/log/format/options/option.go @@ -31,7 +31,7 @@ type Data struct { BaseDir string DisableColors bool RelativePather *RelativePather - AutoColorFn func() ColorValue + PresetColorFn func() ColorValue } // Options is a set of Options. diff --git a/pkg/log/format/placeholders/level.go b/pkg/log/format/placeholders/level.go index bd998618d7..01288676eb 100644 --- a/pkg/log/format/placeholders/level.go +++ b/pkg/log/format/placeholders/level.go @@ -13,7 +13,7 @@ var levlAutoColorFunc = func(level log.Level) options.ColorValue { case log.TraceLevel: return options.WhiteColor case log.DebugLevel: - return options.BlueHColor + return options.LightBlueColor case log.InfoLevel: return options.GreenColor case log.WarnLevel: @@ -36,7 +36,7 @@ type level struct { // Format implements `Placeholder` interface. func (level *level) Format(data *options.Data) (string, error) { newData := *data - newData.AutoColorFn = func() options.ColorValue { + newData.PresetColorFn = func() options.ColorValue { return levlAutoColorFunc(data.Level) } From 1a9dafde59bb97aa9a58853009d9df0eed1169f0 Mon Sep 17 00:00:00 2001 From: Levko Burburas <62853952+levkohimins@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:56:32 +0100 Subject: [PATCH 23/28] Apply suggestions from code review Co-authored-by: Yousif Akbar <11247449+yhakbar@users.noreply.github.com> --- docs/_docs/02_features/custom-log-format.md | 52 +++++++++++++-------- docs/_docs/04_reference/cli-options.md | 2 +- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/docs/_docs/02_features/custom-log-format.md b/docs/_docs/02_features/custom-log-format.md index 93b3ff5cc2..5bbab6e04a 100644 --- a/docs/_docs/02_features/custom-log-format.md +++ b/docs/_docs/02_features/custom-log-format.md @@ -16,7 +16,9 @@ Using this `--terragrunt-log-custom-format ` flag you can specify which ### Placeholders -The format string consists of placeholders and text. Placeholders start with the `%` sign. The simplest example: +The format string consists of placeholders and text. Placeholders start with the `%` sign. + +e.g. ```shell --terragrunt-log-custom-format "%time %level %msg" @@ -28,7 +30,9 @@ Output: 10:09:19.809 debug Running command: tofu --version ``` -The double sign `%%` displays the percent sign as plain text. +To escape the `%` character, use `%%`. + +e.g. ```shell --terragrunt-log-custom-format "%time %level %%msg" @@ -44,17 +48,19 @@ Placeholders have preset names: * `%time` - Current time. -* `%interval` - Seconds has passed since Terragrunt started. +* `%interval` - Seconds elapsed since Terragrunt started. * `%level` - Log level. -* `%prefix` - Path to working directory. +* `%prefix` - Path to the working directory were Terragrunt is running. -* `%tfpath` - Path to TF executable file. +* `%tfpath` - Path to the OpenTofu/Terraform executable (as defined by [terragrunt-tfpath](https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-tfpath)). * `%msg` - Log message. -Any other text is considered as plain text, for example: +Any other text is considered plain text. + +e.g. ```shell --terragrunt-log-custom-format "time=%time level=%level message=%msg" @@ -66,10 +72,14 @@ Output: time=00:10:44.716 level=debug message=Running command: tofu --version ``` -A placeholder is just a value, to format this value you need to pass options to the placeholder. It has the following syntax: +Using the placeholder as shown above will display the value simply. If you would like to format the value, you can pass options to the placeholder. + +Placeholder formatting uses the following syntax: `%placeholder-name(option-name=option-value, option-name=option-value,...)` +e.g. + ```shell --terragrunt-log-custom-format "%time(format='Y-m-d H:i:sv') %level(format=short,case=upper) %msg" ``` @@ -80,7 +90,9 @@ Output: 2024-11-12 11:52:20.214 DEB Running command: tofu --version ``` -Even if you don't pass options, the empty brackets are added implicitly. Thus `%time` equals `%time()`. If you need to add brackets as plain text after a placeholder with no options and without space, you need to explicitly specify empty brackets first, otherwise, they will be treated as invalid options. +In this example, the timestamp (as referenced by the `%time` placeholder) has been formatted with the `format` string `Y-m-d H:i:sv`. Similarly, the log level (as referenced by the `%level` placeholder), has been formatted to use the `short` `format`, and `upper` `case`. + +Even if you don't pass options, the empty parenthesis are added implicitly. Thus `%time` is equivalent to `%time()`. If you need to add parenthesis as plain text immediately after a placeholder without space, you need to explicitly specify empty parenthesis, otherwise, they will be treated as invalid options. ```shell --terragrunt-log-custom-format "%level()(%time()(%msg))" @@ -142,7 +154,7 @@ Specific options for placeholders: * `%level` - * `format=[tiny|short]` - Shortens the log level names `stdout`, `stderr`, `error`, `warn`, `info`, `debug`, `trcace` to 1 and 3 characters. + * `format=[tiny|short]` - Specifies the format for log level names `stdout`, `stderr`, `error`, `warn`, `info`, `debug`, `trace`. * `tiny` - `std`, `err`, `wrn`, `inf`, `deb`, `trc` @@ -152,9 +164,9 @@ Specific options for placeholders: * `format=` - Sets the time format. - Persets formats: + Preset formats: - * `date-time` - Example: 2006-01-02 15:04:05 + * `date-time` - e.g. `2006-01-02 15:04:05` * `date-only` - Example: 2006-01-02 @@ -164,7 +176,7 @@ Specific options for placeholders: * `rfc3339-nano` - Example: 2006-01-02T15:04:05.999999999Z07:00 - Characters formats: + Custom format string characters: * `H` - 24-hour format of an hour with leading zeros, 00 to 23 @@ -172,11 +184,11 @@ Specific options for placeholders: * `g` - 12-hour format of an hour without leading zeros, 1 to 12 - * `i` - Minutes with leading zeros, 00 to 59 + * `i` - Minutes with leading zeros, `00` to `59` - * `s` - Seconds with leading zeros, 00 to 59 + * `s` - Seconds with leading zeros, `00` to `59` - * `v` - Milliseconds, example: .654 + * `v` - Milliseconds. e.g. `.654` * `u` - Microseconds, example: .654321 @@ -214,25 +226,25 @@ Specific options for placeholders: * `short-relative` - Outputs a relative path to the working directory, trims the leading slash `./` and hides the working directory path `.` - * `short` - Outputs a abosolute path, but hides the working directory path. + * `short` - Outputs an absolute path, but hides the working directory path. * `%tfpath` * `path=[filename|dir]` - * `filename` - Outputs the name of the executable file. + * `filename` - Outputs the name of the executable. - * `dir` - Outputs the directory name of the executable file. + * `dir` - Outputs the directory name of the executable. * `%msg` * `path=[relative]` - * `relative` - Converts all absolute paths to relative ones to the working directory. + * `relative` - Converts all absolute paths to paths relative to the working directory. ### Examples -The examples below replicate the formats specified with `--terragrunt-log-format`. They can be useful if you need to change existing formats to suit your needs. +The examples below replicate the preset formats specified with `--terragrunt-log-format`. They can be useful if you need to change existing formats to suit your needs. `--terragrunt-log-format pretty` diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md index 17964f460c..5cb6a68b94 100644 --- a/docs/_docs/04_reference/cli-options.md +++ b/docs/_docs/04_reference/cli-options.md @@ -1120,7 +1120,7 @@ Where the first two control the logging of Terraform/OpenTofu output. There are four log format presets: - `pretty` (this is the default) -- `bare` (old Terragrunt log) +- `bare` (old Terragrunt logging, pre-[v0.67.0](https://github.com/gruntwork-io/terragrunt/tree/v0.67.0)) - `json` - `key-value` From 84d015e5bf6af35a22ebb3eaff65945320bbc012 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Wed, 13 Nov 2024 14:51:32 +0100 Subject: [PATCH 24/28] chore: doc update --- docs/_docs/02_features/custom-log-format.md | 66 +++++++++++------- .../img/screenshots/custom-log-format-1.jpg | Bin 0 -> 29096 bytes pkg/log/format/options/level_format.go | 1 + pkg/log/format/placeholders/placeholder.go | 9 +++ 4 files changed, 49 insertions(+), 27 deletions(-) create mode 100644 docs/assets/img/screenshots/custom-log-format-1.jpg diff --git a/docs/_docs/02_features/custom-log-format.md b/docs/_docs/02_features/custom-log-format.md index 5bbab6e04a..d1c1829e51 100644 --- a/docs/_docs/02_features/custom-log-format.md +++ b/docs/_docs/02_features/custom-log-format.md @@ -58,6 +58,10 @@ Placeholders have preset names: * `%msg` - Log message. +* `%t` - Tab. + +* `%n` - Newline. + Any other text is considered plain text. e.g. @@ -94,6 +98,8 @@ In this example, the timestamp (as referenced by the `%time` placeholder) has be Even if you don't pass options, the empty parenthesis are added implicitly. Thus `%time` is equivalent to `%time()`. If you need to add parenthesis as plain text immediately after a placeholder without space, you need to explicitly specify empty parenthesis, otherwise, they will be treated as invalid options. +e.g. + ```shell --terragrunt-log-custom-format "%level()(%time()(%msg))" ``` @@ -106,6 +112,8 @@ debug(12:15:48.355(Running command: tofu --version)) You can format plain text as well by using an unnamed placeholder. +e.g. + ```shell --terragrunt-log-custom-format "%(content='time=',color=magenta)%time %(content='level=',color=light-blue)%level %(content='msg=',color=green)%msg" ``` @@ -118,6 +126,8 @@ time=12:33:08.513 level=debug msg=Running command: tofu --version *Unfortunately, it is not possible to display color in a Markdown document, but in the above output, `time=` is colored magenta, `level=` is colored light blue and `msg=` is colored green.* +[![screenshot](/assets/img/screenshots/custom-log-format-1.jpg){: width="50%" }](https://terragrunt.gruntwork.io/assets/img/screenshots/custom-log-format-1.jpg) + ### Options Options can be divided into common ones, which can be passed to any placeholder, and specific ones for each placeholder. @@ -144,7 +154,7 @@ Common options: * `red|white|yellow|green|cyan|magenta|blue|light-blue|light-black|light-red|light-green|light-yellow|light-magenta|light-cyan|light-white` - Specifies a color using a word - * `gradient` - Specifies to use a new color each time the placeholder contents change. + * `gradient` - Specifies to use a different color each time the placeholder content changes. * `preset` - Specifies to use preset colors. For example, each log level name has its own preset color. @@ -154,11 +164,13 @@ Specific options for placeholders: * `%level` - * `format=[tiny|short]` - Specifies the format for log level names `stdout`, `stderr`, `error`, `warn`, `info`, `debug`, `trace`. + * `format=[full|short|tiny]` - Specifies the format for log level names. + + * `full` - `stdout`, `stderr`, `error`, `warn`, `info`, `debug`, `trace` - * `tiny` - `std`, `err`, `wrn`, `inf`, `deb`, `trc` + * `short` - `std`, `err`, `wrn`, `inf`, `deb`, `trc` - * `short` - `s`, `e`, `w`, `i`, `d`, `t` + * `tiny` - `s`, `e`, `w`, `i`, `d`, `t` * `%time` @@ -168,21 +180,21 @@ Specific options for placeholders: * `date-time` - e.g. `2006-01-02 15:04:05` - * `date-only` - Example: 2006-01-02 + * `date-only` - e.g. `2006-01-02` - * `time-only` - Example: 15:04:05 + * `time-only` - e.g. `15:04:05` - * `rfc3339` - Example: 2006-01-02T15:04:05Z07:00 + * `rfc3339` - e.g. `2006-01-02T15:04:05Z07:00` - * `rfc3339-nano` - Example: 2006-01-02T15:04:05.999999999Z07:00 + * `rfc3339-nano` - e.g. `2006-01-02T15:04:05.999999999Z07:00` Custom format string characters: - * `H` - 24-hour format of an hour with leading zeros, 00 to 23 + * `H` - 24-hour format of an hour with leading zeros, `00` to `23` - * `h` - 12-hour format of an hour with leading zeros, 01 to 12 + * `h` - 12-hour format of an hour with leading zeros, `01` to `12` - * `g` - 12-hour format of an hour without leading zeros, 1 to 12 + * `g` - 12-hour format of an hour without leading zeros, `1` to `12` * `i` - Minutes with leading zeros, `00` to `59` @@ -190,33 +202,33 @@ Specific options for placeholders: * `v` - Milliseconds. e.g. `.654` - * `u` - Microseconds, example: .654321 + * `u` - Microseconds, e.g. `.654321` - * `Y` - A full numeric representation of a year, examples: 1999, 2003 + * `Y` - A full numeric representation of a year, e.g. `1999`, `2003` - * `y` - A two digit representation of a year, examples: 99 or 03 + * `y` - A two digit representation of a year, e.g. `99`, `03` - * `m` - Numeric representation of a month, with leading zeros, 01 to 12 + * `m` - Numeric representation of a month, with leading zeros, `01` to `12` - * `n` - Numeric representation of a month, without leading zeros, 1 to 12 + * `n` - Numeric representation of a month, without leading zeros, `1` to `12` - * `M` - A short textual representation of a month, three letters, Jan to Dec + * `M` - A short textual representation of a month, three letters, `Jan` to `Dec` - * `d` - Day of the month, 2 digits with leading zeros, 01 to 31 + * `d` - Day of the month, 2 digits with leading zeros, `01` to `31` - * `j` - Day of the month without leading zeros, 1 to 31 + * `j` - Day of the month without leading zeros, `1` to `31` - * `D` - A textual representation of a day, three letters, Mon to Sun + * `D` - A textual representation of a day, three letters, `Mon` to `Sun` - * `A` - Uppercase Ante meridiem and Post meridiem, AM or PM + * `A` - Uppercase Ante meridiem and Post meridiem, `AM` or `PM` - * `a` - Lowercase Ante meridiem and Post meridiem, am or pm + * `a` - Lowercase Ante meridiem and Post meridiem, `am` or `pm` - * `T` - Timezone abbreviation, examples: EST, MDT + * `T` - Timezone abbreviation, e.g. `EST`, `MDT` - * `P` - Difference to Greenwich time (GMT) with colon between hours and minutes, example: +02:00 + * `P` - Difference to Greenwich time (GMT) with colon between hours and minutes, e.g. `+02:00` - * `O` - Difference to Greenwich time (GMT) without colon between hours and minutes, example: +0200 + * `O` - Difference to Greenwich time (GMT) without colon between hours and minutes, e.g. `+0200` * `%prefix` @@ -242,7 +254,7 @@ Specific options for placeholders: * `relative` - Converts all absolute paths to paths relative to the working directory. -### Examples +### Presets The examples below replicate the preset formats specified with `--terragrunt-log-format`. They can be useful if you need to change existing formats to suit your needs. @@ -255,7 +267,7 @@ The examples below replicate the preset formats specified with `--terragrunt-log `--terragrunt-log-format bare` ```shell ---terragrunt-log-custom-format "%level(case=upper,width=4)[%interval] %msg %prefix(path=short,prefix='prefix=')" +--terragrunt-forward-tf-stdout --terragrunt-log-custom-format "%level(case=upper,width=4)[%interval] %msg %prefix(path=short,prefix='prefix=[',suffix=']')" ``` `--terragrunt-log-format key-value` diff --git a/docs/assets/img/screenshots/custom-log-format-1.jpg b/docs/assets/img/screenshots/custom-log-format-1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7cd00655cc05f7a6c50f3d8f95d508fbe0d578fd GIT binary patch literal 29096 zcmbTd2Ut^0)Giu|^o}4sNRci|??sd%Qly1mq#FT|79fCv^bS%41QZYv1Jb4U-g^lx zq4$IuA;8V|{r~yzIp^NzKL5EZ$=>rMvnR7=_N;l=yVky)yIlp)JXhCJ2jJlW07t@N5YpbEyZ=~~ zi2kJ=F_-6Eu@A{#Nw}X>v@;ltqj z@%#_9aL519?0?gX7N-|JAt3=F@jrUu;rsogI4$9w`;Up}R9_O?dD3%8Ja zJ1Mug0gB<(yKyo`9ti|5`XAN))$G5gSm^&R&HhWV|64COKn;NZuMptl6Wk#nAh<(x z2PZ@%ME?j08Ogsw_MbxWuTcFX)c+;7xJK}BZ4eR?664-9gu5;EZWjO) z1bDcciGUWM47dr|o4+B$if16HW87L++_qeNj~SY{yrX;Uom76B$`0av$)Ak_Rosjo zR$y&WaM?DoA!Wu;6=g%~7B#YWJ>&5%%MZ0Irq9NAmVC@z0-Hp2<4)KVgnF|cdg=t! zG*3=8>{d4{_M+4S5Afv6ajN&Sw@BM7=$QtiWG-=pGm`W}^Crv6$wjBCZXdB8F zO1);?i4rikUu!h|;O~TL`SznArE8ywU^eUYUCk5ILrwLU4X&@he942Dm8I9rdd>9P zN2ELM?{vJ>(Gm?1DRvG*IAu-D==dq<>HTh;_Bg0+R#KMRJvNHqA*7tpY&U-QmPm^r z>ueO`d=&HKncDY`yD7hJ0pPlMw)Y=48c81kc>v9N?q}UtX(0M7Rybr=$nh3XFwv~s z%OO1Z<|`%@_U%`orMBNCbDAik*S&8h6R1lf{CjeE8OT}ZmYi% zOYys^%U-tHO-M`R+lHIHh{YH>JC9=TJeM;o&}A-zYZFb~ zH%=>~%u(JQe9QJ73*5;%wxPzHApAllt<*OMpUo=G``zg-8Up#zR8|f=LOz`s2Eek>X(ovPf*F>HnnPM3lF%6{(YSt<6T%J9Q}R7k#QE?oU`;I2dO`s_qH$at-| zEKkwGwIP0&S@^*5i_vQ4cqYM4lEBH1T+uJ}z--yC@c~sm0|iR!uUgu!vlX<~u30)$irvXa2*L1}rD$>0_%vu0zvY>Dc%SgHf zczdBZ$_Y&tUAXc*2W@3c2>5Oo%hEX3G$8wF)Q+xGFJ?VFQ1;wouQuA|6bvWqbRrIHrsa zIpY=DJTS0=*71tz4aawe^5gf$-OxfcG5%j6fnCeuW8WuEM;TPlzeu@~(JGjlj4(v2 z)l40{g@sZ*HVS5iDx&Xo>#^(v-$7NY9@WT79eLIAbG>XZEAl`j@Z6cZq!N28)PyMw zLSOE+w-Ci5SI+aW+{yDmjD`}NqtATywHIxh$gw{}So;k;@l(v%(fo-8M(7)s6Wx-E z^Z)`0Fr(d-4?mxm@0T_|`!;UGlggvMc*oFt%i;5z{TKF|!vL4y$q))95{&ffQKvFJ zlwHe1z0~WMnbq!zLUs+`WTEDZJz8JQHCO4(j`zL{OR2-oP|=AS8jRYxV+*DeP28rB zfg5Ib7d)v=0ADrIvMJl%d!btQmWs*G61xQH4yfyGC4q`ehS(>D@8m;OZ{`!9H_X7b zLiS6uLk^s?&XmS7eU+WAWOz1TL_a=0D^FKq3ZA$HFe%Zv5Oi2GwA6%9jlP>rah+WV zf=xFa|DmlC*Je&7!zT@t8-9VJBG?`-W=(w72#B3UvcJ# zq_)p1%f@>La|T&|BnZ^iI%38SebhOXm@1+|H)-!W8 z-vZA2P$3Z#qBI!p?i=9PH&oGVf`YX3uU$C_@{Mk1zsI&WmZlu$QZl-aRAmT}O^qro zm}_8LE6oBV@JjE70c_oS{o_@l)RXPZ^GK*Wvy#awiRJ!Mzd68fACAfzT+`?t=4;QI zo;2P5nYz5|N1_7RhjEJyRwuo+z{6{Af3y0*Mtxfjw10J_xw9;Z0(Is^uAo98k(~{W zGq8NC?hwU6n@;;dxK-iM;O6t z;XzmcU@4k5N%`UT=Eid;Oa}5gq-$VVZ2Zls8*tSlOtMk?^S4?T9{DMC?nYtVozI^T zk4Wvf?RkF?AoaqQDKKDU{F7UN0j6|~2`iMpJ;+ws%_ge!HKZwTalYrLq2FMHpNvjh zQm4$RG8~9F3RPl)x;LSUJC_u8vUq%1pyN*_w1nJqm#jW#srwGnr;dnE#{6|^nZ9lf zSx1)L0`}<(Zvi`8`o!JFf!dSqXYsm|nv;`54J}zRT$XFzDZh=Gl?r2apwy_NUS+y7 zvuZuSwx9XgbrZE0k5uw!_WN<~M^p~d0Rbt1eoh0Z()2--B$~L>{Jc;KzGGhWWeEO3 zMw3qin$V1ge6Mshja9x9#>!x7zTXVq+((J;y|JFunQj7Ie^me_?}C1`w$w%IeX3A7 z1{fv0^&=8G!3v?(5Qhb*S)_ARFze#uhWc^QjVg_oLj$m=9R5);0h3;9Pn)kcj5RA! zCbxjpr4YxuqdO#Ote@Avk~0tk<*iY$Rnt4lS@ZMR)pu>6T$=PiJ;g##0dEROeX z#mAU)UM%Ef`_;qm((*3}Pt>7$>G1c1h?!*=EZf5+; z4G%~o?aw``LUkSbQh}o8*x7t?DL+5Pdv9&brTP!PIK=;^Vy7}%U1y_RiMh<^$rVTQ zB`P}&x_Nc`y}G&)dPLlYe|DVJ++*J2LZxmVYt`@~ghWb) z@XGOhc{$>Zj)Vh=9XW(s#;fKrGjg1+!-FKf` zEy3S2Xz`iz7iq7kCyVNE|pw4 zw>p|j7b_L`Gxvx~B#EuJl^FOPY}gq>iyLWT(A42CoMj5CJzho*?6MAKnprt9B|Xo+ zn?kK{v1e5;O>N5;7Qx2~%E&c{@v9RRG>4~HjvN_7c4n=(VE!dC?QYk83iv=0NW@x4 zDrT8g|56e*u%P`YSu9m70`UD{l}4S_eG#2A`9^^oNvYi#kQe*rAk0-xt($$Qd)&8a z>$PAh&4||ST{bG+JAuHqz$mCHb&L|7^&ca&PxRG;wd43Fqhjs-+f-CRZ|~@!d3i7) zsPc{#a;q0|-wTJ!URNYXXngYb9hZJ$@hrRlN-l5RI0CRJO%u!6o zEL0Jh7a7cBrEFClByL>c{n@zVcb|OnRD9oFayv6|QIO7>Krp*1Di6_=%QIO6UJ|;J ziCWRg<%gxC>Mm81#H5e3w^9O}$7W-oW@sGdYjSf&WCN+@mMtE9aG42qJdGBfc*?}| z!GSQ@*+Sm#wu2=o(A4W>TDeK5_ue@V&DyJ`A+n(KgtYx!C0c7R^If-_Q& zBX~G{VJqga`|LDWfJQji$di_CUj9zk^RQC-9H$nu+M0%@)Pd^oEvWNr0sBNtvP%N6 z&XrK`Hsh6YY@Jpjf~Xge_vSZ)#<|b5f_4Gs99&KgRjI_d1-x-IELln6f(-V&AfWB@ zlf{c19M_X^f#&gFoa$!bl6!~Hyh;EPSd1_s)V3NG8`bIfu*YvH=(RvZdE0#EHch?RDahkSAQ{_FV{Ou($-laD9@ZRQeS9||H5fZXw!{x)C z(WU&+`Y}fSio(*xTCO5=R=){-3exWil7G|(nzAzhsmWD+Vm1)2ey5~#|?6fTC~64_I2x0E3jOXUH`rX zI2^^dC|~D8?bmDwFabit4|asDM|hg56OfRD>MKamBe+B z^Ra^y;m_28wS=0(TL4M*wX^kg^bP-ZE45wvUYAh0>&fdebNmajZRF$MZ}h$tn1xl@ zbGy8P46FI&gIj>NfY88U#^z$nopK*Et#i1$9pptayNJH#hqlF{PPp+MmmGf%asi=tX#4&*Q01ipAGCzIbP{~_BN<6~mifj%y)Td={9G!LJ zc10#U4mCLJqP*Xp3HCz1-)wl*Dp$>Rs|xmb&#CZ~w!%dQG&ni^-8)_o_X9)?;VlX? zdZd^XQMx?hmORn(zU;oMN6aDOWA>r)TQa?}y^{OOvS{DW_8w@y4!Y_v{T8(Ff4IS; zoUi_MgKJv&F~u)YpaW(fWV7I%`uYmSN~SqFcJqEl70tQ zf{~|8Db0?)3gW4th)wT3Kfns)oWYyDao%xIHZJDYiFj*mw2(RFmU}FtzGL0%0zWPvGLm@}R0WqTgoHM|o$vxL2&g|qd+KkxT9rBb zl*Km|DVn@CiXYOnisRH*LLsW2mOVGc7mfs%oUk0+rXZOg6DLE7@q; zTDAV^3128uAa#f{sg;|K3h%&@#dYCh42Qg{nW+O!oGLIaaDTzQ;Ct}U@%JLINP!O0 zlZ*6aQYEWOW5!!%WOXC{9g6wxSXEa!6B3~QQVXmo*xy#?>c2zKNs*u&7 zpc)m={2W^8nN#31l&TH;_EY}m&Nwp1=n>}Kxg&|b_bYdT? zQT)2jV3crTpSEAIc~Xo1t1n)^tntpSl6#*1tmusZR#9#??f`0SXi_sb)0J1XxycqW zz_+r-9sv3QwMOwp-EiM5o8JP$6&`~{hL=X8Y#-ViJbhIB$>DP!@oV*ZC;Q1g8=Z_- zMdop@`^{Qrk2b&EoF-NdJg)4rTQhfka8z^9X!H7U2o$snk;tps>H=Xp%b<+oZnjf3 z*F>)4(}#97F{b(47opnG-1_X#f3t?!UT!w7z!@jId{^kb>(J7j&C-a%lkM7g#xF&S z7K`l)&yVx#MZs51^(OjqAD@i`qqkbYtqeOHQrCq|{zwrb(?em$78(s<%fdW|Xp#A; z3ySg71*1rU~po@C81{@@xUlB$4vE1e=ix%)u{LM@8ExP$E&)z zkL5xqwy(^+T6=>H-%9+}5TVHzvGJhBP5hPIAi0cCGn;LaDsiGUP6NML;P26H zCMXAUjVV^Gxc}ELwgGQXn&ZLnzfBd7-UZ_Q$(1%{1^MBukiWH1g>4=kKwtN*a%IwL zz&ovc>gW=*`iW*zkFVYDhGtG6S`9em&R+EJrm{+vH*VxdRr^~dh;TdS^l6sVictAz4$FyR}S`Z zgUq~Hy3=7X$gQqu8ehOkM_N{Tu6q6Dutg55HQ7^B96L&6Ix@aHl#y)l;%j~~k|cb_ zdGP0Y5A4G%JGlNj!q8GLHu#YT9qDYRE8@TsQg9$k8yg~gc#p%G6+3zaT?wq+J-G$U zxtk)cf8GF_elh3_53FA{=j;+AHSgjLV$H5U-v~kZD_hVS(cZCvB6l7R(Q0uNIKIOq zXAcs|IHy>vtf#FtLyxR9;@uv;X>u%|U+Nmy^9b=y;l^iXcC=L`{z@FPv5ntd^y^|) zf8GKxX(`w9J}JK{2QT%#tfC&nk2mo4L7v3b_hLGelnOvPbt=A z+;zdKyLpJdt6Y#g<-E0VFwK$R$5YF#BVg*@K?qz)gxzH$)>J)wg-`X%_nGvd6)!NtkG&r8+Ui>I64 zFeNBHWMsSFZ)F;ZkP3^}KZl)tD8{YK7YVQ64?Zl3iHQ|P^vZMx_O_S*71vwfzG;Mj zZUMTIJoLjQA?=EX_AOpwJIXZ1ZhmZkwIAd#rv=@usIfz}A~ zmULqgc!7U@yh+Z_gTr}UvsAhE&S7<&9Hd0et>`OY%a*tdHu=`c?QShXA1 zQx4ZmE&+eCZyQm}uKSUx!AqWRtS(s~)1>Pi zriaseafj^$gajTvt(B2INB9u)lb)>a$sU0Pu8qO-N?$_2LFZPzfPva$`5$L*+}Qid z3=w*5dNDwjSJMw$fIG?7<}SG)xI15CU3Cn#vR_7hak6-$>s9^EaM1Xb!fRmFekIfl zwbcRR>za|)Sq<{xb8|Xe(0s@luTRh0^jJVEjJA&ANoZ#?1oXd7j|3jhwPH{r#Oh-n zo)aU963>O)jLm)>e$5gpwRoQw=rAV#U0vNMX>N$M(e^dUh+2vf69?eDF2&Ma625GMe*o8FFLcv!BAD~%!lCSOx{i#v;H7}G zQ`?~yOf*97*~P(~tB0>ch^t+OS7L8CUZY=Mz97iR(3#3n^<$wv>=pkz5;XH(NgJ)v znaB(gS4C|_L$z&%Pj3N?@aE;LP17&Mp;Ba)JqaW4Q>2cJm!=#DEER*?u3Rv&I?b4_ z76KHu#XTOi_M#FsT*8L$jX^(dJAB}55O~0FtOM{1p=#m9z!Az(y|D^-KjKA=O>^-c zN3bR?^~ZNQ>~MK~7!lvGF%Dh|sJR71DQKgLyUeRY-PBAlL0^}xp3~Z&@a1Y}B%kLS zvu38g2e7|qKurggUpZ^=bgrghpPMg-L|o!?f9RC+@BRdQS^rMnL7#ZIl~Rcw`UKNn zriD0)3{c3bS)~2^{eB9+ZIqAyKVW!vtcoI+NROcct-T8B28QW5EGv)4fBF8MX~9}e z7-W*c8t^ff$a_&PjYmqhM`$}F;u{X8*_Fp=BiMUQI5;O0DmiZf7T1Xzi9*3E*@Gu_ zTeZ^8@l!4I_e!q_A^Vupx05zq4IWtTWk0om$LCcQmxyfs>fe^;WO%hZX)=!Dq#9=lsv4J|aH~lLFtQ}9(R;O!PZpI&ll(4ktZC&Y( zNNXL&b~d21{(pQbhI=@aU_8m5HszfY(Z-M5%V8P+)nG2v@RVb(&aT^tlmZuC~QT5jxx zbulI?y>#3fZxix2-oC46DsS;l#0wq;R;?TW5!4a)-eO9P(JdLJE{HRiGLc{V!L#qs za{j)XQppFSnGEgF2j

{KaIhYB`iHSxhn&qis{0pUu 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 Date: Wed, 13 Nov 2024 15:09:52 +0100 Subject: [PATCH 25/28] chore: doc update --- docs/_docs/02_features/custom-log-format.md | 30 ++++++++++++++++++++- pkg/log/format/placeholders/placeholder.go | 5 ++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docs/_docs/02_features/custom-log-format.md b/docs/_docs/02_features/custom-log-format.md index d1c1829e51..ea31500b30 100644 --- a/docs/_docs/02_features/custom-log-format.md +++ b/docs/_docs/02_features/custom-log-format.md @@ -96,7 +96,35 @@ Output: In this example, the timestamp (as referenced by the `%time` placeholder) has been formatted with the `format` string `Y-m-d H:i:sv`. Similarly, the log level (as referenced by the `%level` placeholder), has been formatted to use the `short` `format`, and `upper` `case`. -Even if you don't pass options, the empty parenthesis are added implicitly. Thus `%time` is equivalent to `%time()`. If you need to add parenthesis as plain text immediately after a placeholder without space, you need to explicitly specify empty parenthesis, otherwise, they will be treated as invalid options. +Even if you don't pass options, the empty parenthesis are added implicitly. Thus `%time` is equivalent to `%time()`. Parenthesis are considered part of the syntax for specifying parameters to placeholders by default. Any parenthesis following a placeholder will be interpreted as specifying the parameters for the placeholder function. + +e.g. + +```shell +--terragrunt-log-custom-format "%time(plain-text)" +``` + +Output: + +```shell +invalid option name "plain-text" for placeholder "time" +``` + +If you would like to escape parentheses so that they appear as plain text in logs, make sure to use empty parentheses after a placeholder so that the following parentheses are not evaluated as specifying parameters for the placeholder function. + +e.g. + +```shell +--terragrunt-log-custom-format "%time()(plain-text)" +``` + +Output: + +```shell +12:33:08.513(plain-text) +``` + +If you need to add parenthesis as plain text immediately after a placeholder without space, you need to explicitly specify empty parenthesis, otherwise, they will be treated as invalid options. e.g. diff --git a/pkg/log/format/placeholders/placeholder.go b/pkg/log/format/placeholders/placeholder.go index 3fb78e4d1b..427881543b 100644 --- a/pkg/log/format/placeholders/placeholder.go +++ b/pkg/log/format/placeholders/placeholder.go @@ -2,7 +2,6 @@ package placeholders import ( - "fmt" "strings" "github.com/gruntwork-io/terragrunt/internal/errors" @@ -153,9 +152,9 @@ func parsePlaceholder(str string, registered Placeholders) (Placeholder, int, er switch str[0] { case 't': - return PlainText(fmt.Sprintf("\t")), next, nil + return PlainText("\t"), next, nil case 'n': - return PlainText(fmt.Sprintf("\n")), next, nil + return PlainText("\n"), next, nil } break From a3bfee4c6f3dcdf92e357fac205750d9a2faaa65 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Wed, 13 Nov 2024 15:12:04 +0100 Subject: [PATCH 26/28] chore: doc update --- docs/_docs/02_features/custom-log-format.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/_docs/02_features/custom-log-format.md b/docs/_docs/02_features/custom-log-format.md index ea31500b30..0ed6833388 100644 --- a/docs/_docs/02_features/custom-log-format.md +++ b/docs/_docs/02_features/custom-log-format.md @@ -12,7 +12,9 @@ nav_title_link: /docs/ ## Custom Log Format -Using this `--terragrunt-log-custom-format ` flag you can specify which information you want to output. +Using the `--terragrunt-log-custom-format ` flag you can customize the way Terragrunt logs with total control over the logging format. + +The argument passed to this flag is a Terragrunt native format string that has special syntax, as described below. ### Placeholders From 15a8c6c45334546b26e48b7df26c05fb296fbb38 Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Wed, 13 Nov 2024 18:01:32 +0100 Subject: [PATCH 27/28] chore: doc update --- docs/_docs/02_features/custom-log-format.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/_docs/02_features/custom-log-format.md b/docs/_docs/02_features/custom-log-format.md index 0ed6833388..882cdd6676 100644 --- a/docs/_docs/02_features/custom-log-format.md +++ b/docs/_docs/02_features/custom-log-format.md @@ -126,20 +126,6 @@ Output: 12:33:08.513(plain-text) ``` -If you need to add parenthesis as plain text immediately after a placeholder without space, you need to explicitly specify empty parenthesis, otherwise, they will be treated as invalid options. - -e.g. - -```shell ---terragrunt-log-custom-format "%level()(%time()(%msg))" -``` - -Output: - -```shell -debug(12:15:48.355(Running command: tofu --version)) -``` - You can format plain text as well by using an unnamed placeholder. e.g. From f42ba5169f8844825d1d568aeb133bfbf90ee24e Mon Sep 17 00:00:00 2001 From: Levko Burburas Date: Mon, 18 Nov 2024 19:24:39 +0100 Subject: [PATCH 28/28] chore: fixes after reviewing --- docs/_docs/04_reference/cli-options.md | 2 +- options/options.go | 4 ++-- pkg/log/helper.go | 5 +++-- pkg/log/options.go | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md index 82d9ea7a8c..656ef2f428 100644 --- a/docs/_docs/04_reference/cli-options.md +++ b/docs/_docs/04_reference/cli-options.md @@ -1139,7 +1139,7 @@ There are four log format presets: **Environment Variable**: `TERRAGRUNT_LOG_CUSTOM_FORMAT`
**Requires an argument**: `--terragrunt-log-custom-format `
-This allows you to specify which information you want to output. It works a little bit like printf format. +This allows you to customize logging however you like. Make sure to read [Custom Log Format](https://terragrunt.gruntwork.io/docs/features/custom-log-format/) for syntax details. diff --git a/options/options.go b/options/options.go index eaedc3833e..fc37cbef91 100644 --- a/options/options.go +++ b/options/options.go @@ -129,7 +129,7 @@ type TerragruntOptions struct { // Disable Terragrunt colors DisableLogColors bool - // Output Terragrunt logs in JSON formatter + // Output Terragrunt logs in JSON format JSONLogFormat bool // Disable replacing full paths in logs with short relative paths @@ -147,7 +147,7 @@ type TerragruntOptions struct { // If true, logs will be displayed in formatter key/value, by default logs are formatted in human-readable formatter. DisableLogFormatting bool - // Wrap Terraform logs in JSON formatter + // Wrap Terraform logs in JSON format TerraformLogsToJSON bool // ValidateStrict mode for the validate-inputs command diff --git a/pkg/log/helper.go b/pkg/log/helper.go index 030ae93da5..cb0f3bbe3b 100644 --- a/pkg/log/helper.go +++ b/pkg/log/helper.go @@ -16,11 +16,12 @@ type Entry struct { Fields Fields } -type logruFormatter struct { +// fromLogrusFormatter converts call from logrus.Formatter interface to our long.Formatter interface. +type fromLogrusFormatter struct { Formatter } -func (f *logruFormatter) Format(parent *logrus.Entry) ([]byte, error) { +func (f *fromLogrusFormatter) Format(parent *logrus.Entry) ([]byte, error) { entry := &Entry{ Entry: parent, Level: FromLogrusLevel(parent.Level), diff --git a/pkg/log/options.go b/pkg/log/options.go index 109a7c7d12..be98b79e8e 100644 --- a/pkg/log/options.go +++ b/pkg/log/options.go @@ -26,7 +26,7 @@ func WithOutput(output io.Writer) Option { // WithFormatter sets the logger formatter. func WithFormatter(formatter Formatter) Option { return func(logger *logger) { - logger.Logger.SetFormatter(&logruFormatter{Formatter: formatter}) + logger.Logger.SetFormatter(&fromLogrusFormatter{Formatter: formatter}) } }