diff --git a/cli/commands/flags.go b/cli/commands/flags.go
index 4d959f264..095d8afb1 100644
--- a/cli/commands/flags.go
+++ b/cli/commands/flags.go
@@ -68,9 +68,6 @@ const (
TerragruntDebugFlagName = "terragrunt-debug"
TerragruntDebugEnvName = "TERRAGRUNT_DEBUG"
- TerragruntTfLogJSONFlagName = "terragrunt-tf-logs-to-json"
- TerragruntTfLogJSONEnvName = "TERRAGRUNT_TF_JSON_LOG"
-
TerragruntModulesThatIncludeFlagName = "terragrunt-modules-that-include"
TerragruntModulesThatIncludeEnvName = "TERRAGRUNT_MODULES_THAT_INCLUDE"
@@ -401,12 +398,6 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags {
return nil
},
},
- &cli.BoolFlag{
- Name: TerragruntTfLogJSONFlagName,
- EnvVar: TerragruntTfLogJSONEnvName,
- Destination: &opts.TerraformLogsToJSON,
- Usage: "If specified, Terragrunt will wrap Terraform stdout and stderr in JSON.",
- },
&cli.BoolFlag{
Name: TerragruntUsePartialParseConfigCacheFlagName,
EnvVar: TerragruntUsePartialParseConfigCacheEnvName,
@@ -439,8 +430,11 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags {
return nil
}
- if val == format.BareFormatName {
+ switch val {
+ case format.BareFormatName:
opts.ForwardTFStdout = true
+ case format.JSONFormatName:
+ opts.JSONLogFormat = true
}
opts.LogFormatter.SetFormat(phs)
diff --git a/cli/deprecated_flags.go b/cli/deprecated_flags.go
index 8f2030683..52a9ba36d 100644
--- a/cli/deprecated_flags.go
+++ b/cli/deprecated_flags.go
@@ -19,6 +19,9 @@ const (
TerragruntJSONLogFlagName = "terragrunt-json-log"
TerragruntJSONLogEnvName = "TERRAGRUNT_JSON_LOG"
+
+ TerragruntTfLogJSONFlagName = "terragrunt-tf-logs-to-json"
+ TerragruntTfLogJSONEnvName = "TERRAGRUNT_TF_JSON_LOG"
)
// NewDeprecatedFlags creates and returns deprecated flags.
@@ -39,6 +42,7 @@ func NewDeprecatedFlags(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.",
+ Hidden: true,
Action: func(_ *cli.Context, _ bool) error {
opts.LogFormatter.SetFormat(format.NewKeyValueFormat())
@@ -59,6 +63,7 @@ func NewDeprecatedFlags(opts *options.TerragruntOptions) cli.Flags {
EnvVar: TerragruntJSONLogEnvName,
Destination: &opts.JSONLogFormat,
Usage: "If specified, Terragrunt will output its logs in JSON format.",
+ Hidden: true,
Action: func(_ *cli.Context, _ bool) error {
opts.LogFormatter.SetFormat(format.NewJSONFormat())
@@ -71,6 +76,24 @@ func NewDeprecatedFlags(opts *options.TerragruntOptions) cli.Flags {
opts.Logger.Warnf(warn)
}
+ return nil
+ },
+ },
+ &cli.BoolFlag{
+ Name: TerragruntTfLogJSONFlagName,
+ EnvVar: TerragruntTfLogJSONEnvName,
+ Usage: "If specified, Terragrunt will wrap Terraform stdout and stderr in JSON.",
+ Hidden: true,
+ Action: func(_ *cli.Context, _ bool) error {
+ 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/cli/provider_cache.go b/cli/provider_cache.go
index 5d74382b3..a1969f881 100644
--- a/cli/provider_cache.go
+++ b/cli/provider_cache.go
@@ -333,10 +333,11 @@ func (cache *ProviderCache) createLocalCLIConfig(ctx context.Context, opts *opti
func runTerraformCommand(ctx context.Context, opts *options.TerragruntOptions, args []string, envs map[string]string) (*util.CmdOutput, error) {
// We use custom writer in order to trap the log from `terraform providers lock -platform=provider-cache` command, which terraform considers an error, but to us a success.
- errWriter := util.NewTrapWriter(opts.ErrWriter, httpStatusCacheProviderReg)
+ errWriter := util.NewTrapWriter(opts.ErrWriter)
// add -no-color flag to args if it was set in Terragrunt arguments
- if util.ListContainsElement(opts.TerraformCliArgs, terraform.FlagNameNoColor) {
+ if util.ListContainsElement(opts.TerraformCliArgs, terraform.FlagNameNoColor) &&
+ !util.ListContainsElement(args, terraform.FlagNameNoColor) {
args = append(args, terraform.FlagNameNoColor)
}
@@ -350,14 +351,18 @@ func runTerraformCommand(ctx context.Context, opts *options.TerragruntOptions, a
cloneOpts.WorkingDir = opts.WorkingDir
cloneOpts.TerraformCliArgs = args
cloneOpts.Env = envs
- cloneOpts.ForwardTFStdout = true
+ output, err := shell.RunTerraformCommandWithOutput(ctx, cloneOpts, cloneOpts.TerraformCliArgs...)
// If the Terraform error matches `httpStatusCacheProviderReg` we ignore it and hide the log from users, otherwise we process the error as is.
- if output, err := shell.RunTerraformCommandWithOutput(ctx, cloneOpts, cloneOpts.TerraformCliArgs...); err != nil && len(errWriter.Msgs()) == 0 {
- return output, err
+ if err != nil && httpStatusCacheProviderReg.Match(output.Stderr.Bytes()) {
+ return new(util.CmdOutput), nil
+ }
+
+ if err := errWriter.Flush(); err != nil {
+ return nil, err
}
- return nil, nil
+ return output, err
}
// providerCacheEnvironment returns TF_* name/value ENVs, which we use to force terraform processes to make requests through our cache server (proxy) instead of making direct requests to the origin servers.
diff --git a/config/dependency.go b/config/dependency.go
index 9b6eb7f6a..9e1245e3d 100644
--- a/config/dependency.go
+++ b/config/dependency.go
@@ -1051,7 +1051,7 @@ func runTerragruntOutputJSON(ctx *ParsingContext, targetConfig string) ([]byte,
newOpts := *ctx.TerragruntOptions
// explicit disable json formatting and prefixing to read json output
newOpts.ForwardTFStdout = false
- newOpts.TerraformLogsToJSON = false
+ newOpts.JSONLogFormat = false
newOpts.Writer = stdoutBufferWriter
ctx = ctx.WithTerragruntOptions(&newOpts)
diff --git a/configstack/module.go b/configstack/module.go
index e104e3ed4..4ecccf0ae 100644
--- a/configstack/module.go
+++ b/configstack/module.go
@@ -256,7 +256,7 @@ func FindWhereWorkingDirIsIncluded(ctx context.Context, opts *options.Terragrunt
cfgOptions.OriginalTerragruntConfigPath = opts.OriginalTerragruntConfigPath
cfgOptions.TerraformCommand = opts.TerraformCommand
cfgOptions.NonInteractive = true
- cfgOptions.Logger.SetOptions(log.WithHooks(NewForceLogLevelHook(log.DebugLevel)))
+ cfgOptions.Logger = opts.Logger.WithOptions(log.WithHooks(NewForceLogLevelHook(log.DebugLevel)))
// build stack from config directory
stack, err := FindStackInSubfolders(ctx, cfgOptions, WithChildTerragruntConfig(terragruntConfig))
diff --git a/configstack/running_module.go b/configstack/running_module.go
index bd7077499..af41621e3 100644
--- a/configstack/running_module.go
+++ b/configstack/running_module.go
@@ -140,7 +140,7 @@ func (module *RunningModule) runNow(ctx context.Context, rootOptions *options.Te
stdout := bytes.Buffer{}
jsonOptions.ForwardTFStdout = true
- jsonOptions.TerraformLogsToJSON = false
+ jsonOptions.JSONLogFormat = false
jsonOptions.Writer = &stdout
jsonOptions.TerraformCommand = terraform.CommandNameShow
jsonOptions.TerraformCliArgs = []string{terraform.CommandNameShow, "-json", module.Module.planFile(rootOptions)}
diff --git a/docs/_docs/02_features/custom-log-format.md b/docs/_docs/02_features/custom-log-format.md
index 882cdd667..52ffc4a1e 100644
--- a/docs/_docs/02_features/custom-log-format.md
+++ b/docs/_docs/02_features/custom-log-format.md
@@ -56,10 +56,12 @@ Placeholders have preset names:
* `%prefix` - Path to the working directory were Terragrunt is running.
-* `%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.
+* `%tf-path` - Path to the OpenTofu/Terraform executable (as defined by [terragrunt-tfpath](https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-tfpath)).
+
+* `%tf-command-args` - Arguments of the executed OpenTofu/Terraform command.
+
* `%t` - Tab.
* `%n` - Newline.
@@ -256,7 +258,7 @@ Specific options for placeholders:
* `short` - Outputs an absolute path, but hides the working directory path.
-* `%tfpath`
+* `%tf-path`
* `path=[filename|dir]`
@@ -277,7 +279,7 @@ The examples below replicate the preset formats specified with `--terragrunt-log
`--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-custom-format "%time(color=light-black) %level(case=upper,width=6,color=preset) %prefix(path=short-relative,color=gradient,suffix=' ')%tf-path(color=cyan,suffix=': ')%msg(path=relative)"
```
`--terragrunt-log-format bare`
@@ -289,11 +291,11 @@ The examples below replicate the preset formats specified with `--terragrunt-log
`--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-custom-format "time=%time(format=rfc3339) level=%level prefix=%prefix(path=short-relative) tf-path=%tf-path(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)"}'
+--terragrunt-log-custom-format '{"time":"%time(format=rfc3339,escape=json)", "level":"%level(escape=json)", "prefix":"%prefix(path=short-relative,escape=json)", "tf-path":"%tf-path(path=filename,escape=json)", "msg":"%msg(path=relative,escape=json,color=disable)"}'
```
diff --git a/docs/_docs/04_reference/cli-options.md b/docs/_docs/04_reference/cli-options.md
index 99934a7ef..81dcc7b5c 100644
--- a/docs/_docs/04_reference/cli-options.md
+++ b/docs/_docs/04_reference/cli-options.md
@@ -82,7 +82,7 @@ This page documents the CLI commands and options available with Terragrunt:
- [terragrunt-disable-bucket-update](#terragrunt-disable-bucket-update)
- [terragrunt-disable-command-validation](#terragrunt-disable-command-validation)
- [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-tf-logs-to-json](#terragrunt-tf-logs-to-json) (DEPRECATED: use [terragrunt-log-format](#terragrunt-log-format))
- [terragrunt-provider-cache](#terragrunt-provider-cache)
- [terragrunt-provider-cache-dir](#terragrunt-provider-cache-dir)
- [terragrunt-provider-cache-hostname](#terragrunt-provider-cache-hostname)
@@ -809,7 +809,7 @@ prefix `--terragrunt-` (e.g., `--terragrunt-config`). The currently available op
- [terragrunt-disable-bucket-update](#terragrunt-disable-bucket-update)
- [terragrunt-disable-command-validation](#terragrunt-disable-command-validation)
- [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-tf-logs-to-json](#terragrunt-tf-logs-to-json) (DEPRECATED: use [terragrunt-log-format](#terragrunt-log-format))
- [terragrunt-provider-cache](#terragrunt-provider-cache)
- [terragrunt-provider-cache-dir](#terragrunt-provider-cache-dir)
- [terragrunt-provider-cache-hostname](#terragrunt-provider-cache-hostname)
@@ -1485,6 +1485,9 @@ When this flag is set, Terragrunt will output its logs in JSON format.
### terragrunt-tf-logs-to-json
+DEPRECATED: Use [terragrunt-log-format](#terragrunt-log-format). OpenTofu/Terraform `stdout` and `stderr` is wrapped in JSON by default with `--terragurnt-log-format json` flag if `--terragrunt-forward-tf-stdout` flag is not specified.
+In other words, the previous behavior with the `--terragrunt-json-log --terragrunt-tf-logs-to-json` flags is now equivalent to `--terragrunt-log-format json` and the previous behavior with the `--terragrunt-json-log` is now equivalent to `--terragrunt-log-format json --terragrunt-forward-tf-stdout`.
+
**CLI Arg**: `--terragrunt-tf-logs-to-json`
**Environment Variable**: `TERRAGRUNT_TF_JSON_LOG` (set to `true`)
diff --git a/internal/strict/strict.go b/internal/strict/strict.go
index 893dd71f0..dbfa26bcf 100644
--- a/internal/strict/strict.go
+++ b/internal/strict/strict.go
@@ -47,6 +47,8 @@ const (
DisableLogFormatting = "terragrunt-disable-log-formatting"
// JSONLog is the control that prevents the deprecated `--terragrunt-json-log` flag from being used.
JSONLog = "terragrunt-json-log"
+ // TfLogJSON is the control that prevents the deprecated `--terragrunt-tf-logs-to-json` flag from being used.
+ TfLogJSON = "terragrunt-tf-logs-to-json"
)
// GetStrictControl returns the strict control with the given name.
@@ -83,45 +85,49 @@ type Controls map[string]Control
//nolint:lll,gochecknoglobals,stylecheck
var StrictControls = Controls{
SpinUp: {
- Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all apply` instead.", SpinUp),
+ Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all apply` instead.", SpinUp), //nolint:revive
Warning: fmt.Sprintf("The `%s` command is deprecated and will be removed in a future version. Use `terragrunt run-all apply` instead.", SpinUp),
},
TearDown: {
- Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all destroy` instead.", TearDown),
+ Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all destroy` instead.", TearDown), //nolint:revive
Warning: fmt.Sprintf("The `%s` command is deprecated and will be removed in a future version. Use `terragrunt run-all destroy` instead.", TearDown),
},
PlanAll: {
- Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all plan` instead.", PlanAll),
+ Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all plan` instead.", PlanAll), //nolint:revive
Warning: fmt.Sprintf("The `%s` command is deprecated and will be removed in a future version. Use `terragrunt run-all plan` instead.", PlanAll),
},
ApplyAll: {
- Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all apply` instead.", ApplyAll),
+ Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all apply` instead.", ApplyAll), //nolint:revive
Warning: fmt.Sprintf("The `%s` command is deprecated and will be removed in a future version. Use `terragrunt run-all apply` instead.", ApplyAll),
},
DestroyAll: {
- Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all destroy` instead.", DestroyAll),
+ Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all destroy` instead.", DestroyAll), //nolint:revive
Warning: fmt.Sprintf("The `%s` command is deprecated and will be removed in a future version. Use `terragrunt run-all destroy` instead.", DestroyAll),
},
OutputAll: {
- Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all output` instead.", OutputAll),
+ Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all output` instead.", OutputAll), //nolint:revive
Warning: fmt.Sprintf("The `%s` command is deprecated and will be removed in a future version. Use `terragrunt run-all output` instead.", OutputAll),
},
ValidateAll: {
- Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all validate` instead.", ValidateAll),
+ Error: errors.Errorf("The `%s` command is no longer supported. Use `terragrunt run-all validate` instead.", ValidateAll), //nolint:revive
Warning: fmt.Sprintf("The `%s` command is deprecated and will be removed in a future version. Use `terragrunt run-all validate` instead.", ValidateAll),
},
SkipDependenciesInputs: {
- 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),
+ 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), //nolint:revive
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),
+ Error: errors.Errorf("The `--%s` flag is no longer supported. Use `--terragrunt-log-format=key-value` instead.", DisableLogFormatting), //nolint:revive
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),
+ Error: errors.Errorf("The `--%s` flag is no longer supported. Use `--terragrunt-log-format=json` instead.", JSONLog), //nolint:revive
Warning: fmt.Sprintf("The `--%s` flag is deprecated and will be removed in a future version. Use `--terragrunt-log-format=json` instead.", JSONLog),
},
+ TfLogJSON: {
+ Error: errors.Errorf("The `--%s` flag is no longer supported. Use `--terragrunt-log-format=json` instead.", TfLogJSON), //nolint:revive
+ Warning: fmt.Sprintf("The `--%s` flag is deprecated and will be removed in a future version. Use `--terragrunt-log-format=json` instead.", TfLogJSON),
+ },
}
// Names returns the names of all strict controls.
diff --git a/options/options.go b/options/options.go
index be9d054b2..1260d7e50 100644
--- a/options/options.go
+++ b/options/options.go
@@ -147,9 +147,6 @@ 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 format
- TerraformLogsToJSON bool
-
// ValidateStrict mode for the validate-inputs command
ValidateStrict bool
@@ -469,7 +466,6 @@ func NewTerragruntOptionsWithWriters(stdout, stderr io.Writer) *TerragruntOption
ForwardTFStdout: false,
JSONOut: DefaultJSONOutName,
TerraformImplementation: UnknownImpl,
- TerraformLogsToJSON: false,
JSONDisableDependentModules: false,
RunTerragrunt: func(ctx context.Context, opts *TerragruntOptions) error {
return errors.New(ErrRunTerragruntCommandNotSet)
@@ -618,7 +614,6 @@ func (opts *TerragruntOptions) Clone(terragruntConfigPath string) (*TerragruntOp
FailIfBucketCreationRequired: opts.FailIfBucketCreationRequired,
DisableBucketUpdate: opts.DisableBucketUpdate,
TerraformImplementation: opts.TerraformImplementation,
- TerraformLogsToJSON: opts.TerraformLogsToJSON,
GraphRoot: opts.GraphRoot,
ScaffoldVars: opts.ScaffoldVars,
ScaffoldVarFiles: opts.ScaffoldVarFiles,
diff --git a/pkg/log/format/format.go b/pkg/log/format/format.go
index 8275a1a6f..9adf9c252 100644
--- a/pkg/log/format/format.go
+++ b/pkg/log/format/format.go
@@ -70,32 +70,42 @@ func NewPrettyFormat() Placeholders {
func NewJSONFormat() Placeholders {
return Placeholders{
- PlainText(`{"time":"`),
+ PlainText(`{`),
Time(
+ Prefix(`"time":"`),
+ Suffix(`"`),
TimeFormat(RFC3339),
Escape(JSONEscape),
),
- PlainText(`", "level":"`),
Level(
+ Prefix(`, "level":"`),
+ Suffix(`"`),
Escape(JSONEscape),
),
- PlainText(`", "prefix":"`),
Field(WorkDirKeyName,
- PathFormat(ShortPath),
+ Prefix(`, "working-dir":"`),
+ Suffix(`"`),
Escape(JSONEscape),
),
- PlainText(`", "tfpath":"`),
Field(TFPathKeyName,
+ Prefix(`, "tf-path":"`),
+ Suffix(`"`),
PathFormat(FilenamePath),
Escape(JSONEscape),
),
- PlainText(`", "msg":"`),
+ Field(TFCmdArgsKeyName,
+ Prefix(`, "tf-command-args":[`),
+ Suffix(`]`),
+ Escape(JSONEscape),
+ ),
Message(
+ Prefix(`, "msg":"`),
+ Suffix(`"`),
PathFormat(RelativePath),
Color(DisableColor),
Escape(JSONEscape),
),
- PlainText(`"}`),
+ PlainText(`}`),
}
}
@@ -113,7 +123,7 @@ func NewKeyValueFormat() Placeholders {
PathFormat(ShortRelativePath),
),
Field(TFPathKeyName,
- Prefix(" tfpath="),
+ Prefix(" tf-path="),
PathFormat(FilenamePath),
),
Message(
diff --git a/pkg/log/format/formatter.go b/pkg/log/format/formatter.go
index 09a0691e6..73af5288f 100644
--- a/pkg/log/format/formatter.go
+++ b/pkg/log/format/formatter.go
@@ -2,6 +2,7 @@ package format
import (
"bytes"
+ "sync"
"github.com/gruntwork-io/terragrunt/internal/errors"
"github.com/gruntwork-io/terragrunt/pkg/log"
@@ -17,6 +18,7 @@ type Formatter struct {
placeholders placeholders.Placeholders
disableColors bool
relativePather *options.RelativePather
+ mu sync.Mutex
}
// NewFormatter returns a new Formatter instance with default values.
@@ -47,6 +49,9 @@ func (formatter *Formatter) Format(entry *log.Entry) ([]byte, error) {
return nil, err
}
+ formatter.mu.Lock()
+ defer formatter.mu.Unlock()
+
if str != "" {
if _, err := buf.WriteString(str); err != nil {
return nil, errors.New(err)
diff --git a/pkg/log/format/options/align.go b/pkg/log/format/options/align.go
index b55522961..a2bc5c214 100644
--- a/pkg/log/format/options/align.go
+++ b/pkg/log/format/options/align.go
@@ -27,7 +27,9 @@ type AlignOption struct {
}
// Format implements `Option` interface.
-func (option *AlignOption) Format(_ *Data, str string) (string, error) {
+func (option *AlignOption) Format(_ *Data, val any) (any, error) {
+ str := toString(val)
+
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 c2ee92c0f..f812e8429 100644
--- a/pkg/log/format/options/case.go
+++ b/pkg/log/format/options/case.go
@@ -30,7 +30,9 @@ type CaseOption struct {
}
// Format implements `Option` interface.
-func (option *CaseOption) Format(_ *Data, str string) (string, error) {
+func (option *CaseOption) Format(_ *Data, val any) (any, error) {
+ str := toString(val)
+
switch option.value.Get() {
case UpperCase:
return strings.ToUpper(str), nil
diff --git a/pkg/log/format/options/color.go b/pkg/log/format/options/color.go
index 12ffe2bd8..1e88fce21 100644
--- a/pkg/log/format/options/color.go
+++ b/pkg/log/format/options/color.go
@@ -149,8 +149,11 @@ type ColorOption struct {
}
// Format implements `Option` interface.
-func (color *ColorOption) Format(data *Data, str string) (string, error) {
- value := color.value.Get()
+func (color *ColorOption) Format(data *Data, val any) (any, error) {
+ var (
+ str = toString(val)
+ value = color.value.Get()
+ )
if value == NoneColor {
return str, nil
diff --git a/pkg/log/format/options/content.go b/pkg/log/format/options/content.go
index ffc89a199..e91dfdbe8 100644
--- a/pkg/log/format/options/content.go
+++ b/pkg/log/format/options/content.go
@@ -8,12 +8,12 @@ type ContentOption struct {
}
// Format implements `Option` interface.
-func (option *ContentOption) Format(_ *Data, str string) (string, error) {
+func (option *ContentOption) Format(_ *Data, val any) (any, error) {
if val := option.value.Get(); val != "" {
return val, nil
}
- return str, nil
+ return val, nil
}
// Content creates the option that sets the content.
diff --git a/pkg/log/format/options/escape.go b/pkg/log/format/options/escape.go
index b27d09029..80edd56f1 100644
--- a/pkg/log/format/options/escape.go
+++ b/pkg/log/format/options/escape.go
@@ -25,18 +25,18 @@ type EscapeOption struct {
}
// Format implements `Option` interface.
-func (option *EscapeOption) Format(_ *Data, str string) (string, error) {
+func (option *EscapeOption) Format(_ *Data, val any) (any, error) {
if option.value.Get() != JSONEscape {
- return str, nil
+ return val, nil
}
- b, err := json.Marshal(str)
+ jsonStr, err := json.Marshal(val)
if err != nil {
return "", errors.New(err)
}
// Trim the beginning and trailing " character.
- return string(b[1 : len(b)-1]), nil
+ return string(jsonStr[1 : len(jsonStr)-1]), nil
}
// Escape creates the option to escape text.
diff --git a/pkg/log/format/options/level_format.go b/pkg/log/format/options/level_format.go
index 28821c3fc..f373868e7 100644
--- a/pkg/log/format/options/level_format.go
+++ b/pkg/log/format/options/level_format.go
@@ -22,7 +22,7 @@ type LevelFormatOption struct {
}
// Format implements `Option` interface.
-func (format *LevelFormatOption) Format(data *Data, _ string) (string, error) {
+func (format *LevelFormatOption) Format(data *Data, _ any) (any, error) {
switch format.value.Get() {
case LevelFormatTiny:
return data.Level.TinyName(), nil
diff --git a/pkg/log/format/options/option.go b/pkg/log/format/options/option.go
index 75d92e6a3..8ded8a60e 100644
--- a/pkg/log/format/options/option.go
+++ b/pkg/log/format/options/option.go
@@ -20,7 +20,7 @@ type Option interface {
// Name returns the name of the option.
Name() string
// Format formats the given string.
- Format(data *Data, str string) (string, error)
+ Format(data *Data, val any) (any, error)
// ParseValue parses and sets the value of the option.
ParseValue(str string) error
}
@@ -76,15 +76,15 @@ func (opts Options) Merge(withOpts ...Option) Options {
}
// Format returns the formatted value.
-func (opts Options) Format(data *Data, str string) (string, error) {
+func (opts Options) Format(data *Data, val any) (string, error) {
var err error
for _, opt := range opts {
- str, err = opt.Format(data, str)
- if str == "" || err != nil {
+ val, err = opt.Format(data, val)
+ if val == "" || err != nil {
return "", err
}
}
- return str, nil
+ return toString(val), nil
}
diff --git a/pkg/log/format/options/path_format.go b/pkg/log/format/options/path_format.go
index 6f0465ca6..168b25b46 100644
--- a/pkg/log/format/options/path_format.go
+++ b/pkg/log/format/options/path_format.go
@@ -38,7 +38,9 @@ type PathFormatOption struct {
}
// Format implements `Option` interface.
-func (option *PathFormatOption) Format(data *Data, str string) (string, error) {
+func (option *PathFormatOption) Format(data *Data, val any) (any, error) {
+ str := toString(val)
+
switch option.value.Get() {
case RelativePath:
if data.RelativePather == nil {
@@ -47,21 +49,11 @@ func (option *PathFormatOption) Format(data *Data, str string) (string, error) {
return data.RelativePather.ReplaceAbsPaths(str), nil
case ShortRelativePath:
- if str == data.BaseDir {
- return "", nil
- }
-
if data.RelativePather == nil {
break
}
- str = data.RelativePather.ReplaceAbsPaths(str)
-
- if strings.HasPrefix(str, log.CurDirWithSeparator) {
- return str[len(log.CurDirWithSeparator):], nil
- }
-
- return str, nil
+ return option.shortRelativePath(data, str), nil
case ShortPath:
if str == data.BaseDir {
return "", nil
@@ -75,7 +67,21 @@ func (option *PathFormatOption) Format(data *Data, str string) (string, error) {
case NonePath:
}
- return str, nil
+ return val, nil
+}
+
+func (option *PathFormatOption) shortRelativePath(data *Data, str string) string {
+ if str == data.BaseDir {
+ return ""
+ }
+
+ str = data.RelativePather.ReplaceAbsPaths(str)
+
+ if strings.HasPrefix(str, log.CurDirWithSeparator) {
+ return str[len(log.CurDirWithSeparator):]
+ }
+
+ return str
}
// PathFormat creates the option to format the paths.
diff --git a/pkg/log/format/options/prefix.go b/pkg/log/format/options/prefix.go
index 9f19a1d52..5b82fa0ff 100644
--- a/pkg/log/format/options/prefix.go
+++ b/pkg/log/format/options/prefix.go
@@ -8,8 +8,8 @@ type PrefixOption struct {
}
// Format implements `Option` interface.
-func (option *PrefixOption) Format(_ *Data, str string) (string, error) {
- return option.value.Get() + str, nil
+func (option *PrefixOption) Format(_ *Data, val any) (any, error) {
+ return option.value.Get() + toString(val), nil
}
// Prefix creates the option to add a prefix to the text.
diff --git a/pkg/log/format/options/suffix.go b/pkg/log/format/options/suffix.go
index 25194bedf..c0d54b640 100644
--- a/pkg/log/format/options/suffix.go
+++ b/pkg/log/format/options/suffix.go
@@ -8,8 +8,8 @@ type SuffixOption struct {
}
// Format implements `Option` interface.
-func (option *SuffixOption) Format(_ *Data, str string) (string, error) {
- return str + option.value.Get(), nil
+func (option *SuffixOption) Format(_ *Data, val any) (any, error) {
+ return toString(val) + option.value.Get(), nil
}
// Suffix creates the option to add a suffix to the text.
diff --git a/pkg/log/format/options/time_format.go b/pkg/log/format/options/time_format.go
index d77afa2f0..5ad85e890 100644
--- a/pkg/log/format/options/time_format.go
+++ b/pkg/log/format/options/time_format.go
@@ -115,7 +115,7 @@ type TimeFormatOption struct {
}
// Format implements `Option` interface.
-func (option *TimeFormatOption) Format(data *Data, _ string) (string, error) {
+func (option *TimeFormatOption) Format(data *Data, _ any) (any, error) {
return data.Time.Format(option.value.Get()), nil
}
diff --git a/pkg/log/format/options/util.go b/pkg/log/format/options/util.go
new file mode 100644
index 000000000..7692fa759
--- /dev/null
+++ b/pkg/log/format/options/util.go
@@ -0,0 +1,17 @@
+package options
+
+import (
+ "fmt"
+ "strings"
+)
+
+func toString(val any) string {
+ switch val := val.(type) {
+ case string:
+ return val
+ case []string:
+ return strings.Join(val, " ")
+ }
+
+ return fmt.Sprintf("%v", val)
+}
diff --git a/pkg/log/format/options/width.go b/pkg/log/format/options/width.go
index 199ff0ac9..07d38bf61 100644
--- a/pkg/log/format/options/width.go
+++ b/pkg/log/format/options/width.go
@@ -14,7 +14,9 @@ type WidthOption struct {
}
// Format implements `Option` interface.
-func (option *WidthOption) Format(_ *Data, str string) (string, error) {
+func (option *WidthOption) Format(_ *Data, val any) (any, error) {
+ str := toString(val)
+
width := option.value.Get()
if width == 0 {
return str, nil
diff --git a/pkg/log/format/placeholders/common.go b/pkg/log/format/placeholders/common.go
index b37766928..8b5a5afb3 100644
--- a/pkg/log/format/placeholders/common.go
+++ b/pkg/log/format/placeholders/common.go
@@ -11,12 +11,12 @@ import (
func WithCommonOptions(opts ...options.Option) options.Options {
return options.Options(append(opts,
options.Content(""),
+ options.Escape(options.NoneEscape),
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/field.go b/pkg/log/format/placeholders/field.go
index 6989b7a44..8008b15b4 100644
--- a/pkg/log/format/placeholders/field.go
+++ b/pkg/log/format/placeholders/field.go
@@ -6,8 +6,9 @@ import (
const (
WorkDirKeyName = "prefix"
- DownloadDirKeyName = "downloaddir"
- TFPathKeyName = "tfpath"
+ DownloadDirKeyName = "download-dir"
+ TFPathKeyName = "tf-path"
+ TFCmdArgsKeyName = "tf-command-args"
)
type fieldPlaceholder struct {
@@ -17,9 +18,7 @@ type fieldPlaceholder struct {
// 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.Format(data, val)
- }
+ return field.opts.Format(data, val)
}
return "", nil
diff --git a/shell/git.go b/shell/git.go
index 575778fee..02cfae6ec 100644
--- a/shell/git.go
+++ b/shell/git.go
@@ -36,6 +36,7 @@ func GitTopLevelDir(ctx context.Context, terragruntOptions *options.TerragruntOp
return "", err
}
+ opts.Logger = terragruntOptions.Logger.Clone()
opts.Env = terragruntOptions.Env
opts.Writer = &stdout
opts.ErrWriter = &stderr
@@ -66,6 +67,7 @@ func GitRepoTags(ctx context.Context, opts *options.TerragruntOptions, gitRepo *
return nil, err
}
+ gitOpts.Logger = opts.Logger.Clone()
gitOpts.Env = opts.Env
gitOpts.Writer = &stdout
gitOpts.ErrWriter = &stderr
diff --git a/shell/run_shell_cmd.go b/shell/run_shell_cmd.go
index c6c48b98b..2b87a1d3e 100644
--- a/shell/run_shell_cmd.go
+++ b/shell/run_shell_cmd.go
@@ -133,25 +133,35 @@ func RunShellCommandWithOutput(
errWriter = opts.ErrWriter
)
- if opts.JSONLogFormat && opts.TerraformLogsToJSON {
- logger := opts.Logger.WithField("workingDir", opts.WorkingDir).WithField("executedCommandArgs", args)
- 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(placeholders.TFPathKeyName, filepath.Base(opts.TerraformPath))
-
- outWriter = writer.New(
- writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))),
- writer.WithDefaultLevel(log.StdoutLevel),
- writer.WithMsgSeparator(logMsgSeparator),
- )
-
- errWriter = writer.New(
- writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))),
- writer.WithDefaultLevel(log.StderrLevel),
- writer.WithMsgSeparator(logMsgSeparator),
- writer.WithParseFunc(terraform.ParseLogFunc(tfLogMsgPrefix, false)),
- )
+ if command == opts.TerraformPath && !opts.ForwardTFStdout {
+ logger := opts.Logger.
+ WithField(placeholders.TFPathKeyName, filepath.Base(opts.TerraformPath)).
+ WithField(placeholders.TFCmdArgsKeyName, args)
+
+ if opts.JSONLogFormat && !cli.Args(args).Normalize(cli.SingleDashFlag).Contains(terraform.FlagNameJSON) {
+ outWriter = writer.New(
+ writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))),
+ writer.WithDefaultLevel(log.StdoutLevel),
+ )
+
+ errWriter = writer.New(
+ writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))),
+ writer.WithDefaultLevel(log.StderrLevel),
+ )
+ } else if !shouldForceForwardTFStdout(args) {
+ outWriter = writer.New(
+ writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))),
+ writer.WithDefaultLevel(log.StdoutLevel),
+ writer.WithMsgSeparator(logMsgSeparator),
+ )
+
+ errWriter = writer.New(
+ writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))),
+ writer.WithDefaultLevel(log.StderrLevel),
+ writer.WithMsgSeparator(logMsgSeparator),
+ writer.WithParseFunc(terraform.ParseLogFunc(tfLogMsgPrefix, false)),
+ )
+ }
}
var (
diff --git a/shell/run_shell_cmd_output_test.go b/shell/run_shell_cmd_output_test.go
index 37a249a12..c3ce3fcf8 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 tfpath=%s msg=%s", prefix, filepath.Base(terraformPath), line))
+ prefixedOutput = append(prefixedOutput, fmt.Sprintf("prefix=%s tf-path=%s msg=%s", prefix, filepath.Base(terraformPath), line))
}
logFormatter := format.NewFormatter(format.NewKeyValueFormat())
diff --git a/test/integration_serial_test.go b/test/integration_serial_test.go
index d12442340..a2cb0f3ba 100644
--- a/test/integration_serial_test.go
+++ b/test/integration_serial_test.go
@@ -11,6 +11,7 @@ import (
"os"
"path"
"path/filepath"
+ "regexp"
"runtime"
"strings"
"testing"
@@ -705,3 +706,30 @@ func TestParseTFLog(t *testing.T) {
assert.Contains(t, stderr, "INFO ["+prefixName+"] "+wrappedBinary()+`: TF_LOG: Go runtime version`)
}
}
+
+func TestTerragruntTerraformOutputJson(t *testing.T) {
+ tmpEnvPath := helpers.CopyEnvironment(t, testFixtureInitError)
+ helpers.CleanupTerraformFolder(t, tmpEnvPath)
+ testPath := util.JoinPath(tmpEnvPath, testFixtureInitError)
+
+ _, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt apply --no-color --terragrunt-json-log --terragrunt-tf-logs-to-json --terragrunt-non-interactive --terragrunt-working-dir "+testPath)
+ require.Error(t, err)
+
+ // Sometimes, this is the error returned by AWS.
+ if !strings.Contains(stderr, "Error: Failed to get existing workspaces: operation error S3: ListObjectsV2, https response error StatusCode: 301") {
+ assert.Regexp(t, `"msg":".*`+regexp.QuoteMeta("Initializing the backend..."), stderr)
+ }
+
+ // check if output can be extracted in json
+ jsonStrings := strings.Split(stderr, "\n")
+ for _, jsonString := range jsonStrings {
+ if len(jsonString) == 0 {
+ continue
+ }
+ var output map[string]interface{}
+ err = json.Unmarshal([]byte(jsonString), &output)
+ require.NoErrorf(t, err, "Failed to parse json %s", jsonString)
+ assert.NotNil(t, output["level"])
+ assert.NotNil(t, output["time"])
+ }
+}
diff --git a/test/integration_test.go b/test/integration_test.go
index 5ae1323bf..29291aeec 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -215,7 +215,7 @@ func TestLogCustomFormatOutput(t *testing.T) {
},
},
{
- logCustomFormat: "%interval%(content=' plain-text ')%level(case=upper,width=6) %prefix(path=short-relative,suffix=' ')%tfpath(suffix=': ')%msg(path=relative)",
+ logCustomFormat: "%interval%(content=' plain-text ')%level(case=upper,width=6) %prefix(path=short-relative,suffix=' ')%tf-path(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...")),
@@ -234,6 +234,9 @@ func TestLogCustomFormatOutput(t *testing.T) {
tmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogFormatter)
rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter)
+ rootPath, err := filepath.EvalSymlinks(rootPath)
+ require.NoError(t, err)
+
_, stderr, err := helpers.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)
@@ -372,7 +375,7 @@ func TestLogFormatKeyValueOutput(t *testing.T) {
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=stdout prefix="+prefixName+" tf-path="+wrappedBinary()+" msg=Initializing provider plugins...\n")
assert.Contains(t, stderr, "level=debug prefix="+prefixName+" msg=Reading Terragrunt config file at ./"+prefixName+"/terragrunt.hcl\n")
}
})
@@ -3758,35 +3761,6 @@ func TestLogFormatJSONOutput(t *testing.T) {
}
}
-func TestTerragruntTerraformOutputJson(t *testing.T) {
- t.Parallel()
-
- tmpEnvPath := helpers.CopyEnvironment(t, testFixtureInitError)
- helpers.CleanupTerraformFolder(t, tmpEnvPath)
- testPath := util.JoinPath(tmpEnvPath, testFixtureInitError)
-
- _, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt apply --no-color --terragrunt-json-log --terragrunt-tf-logs-to-json --terragrunt-forward-tf-stdout --terragrunt-non-interactive --terragrunt-working-dir "+testPath)
- require.Error(t, err)
-
- // Sometimes, this is the error returned by AWS.
- if !strings.Contains(stderr, "Error: Failed to get existing workspaces: operation error S3: ListObjectsV2, https response error StatusCode: 301") {
- assert.Contains(t, stderr, `"msg":"Initializing the backend..."`)
- }
-
- // check if output can be extracted in json
- jsonStrings := strings.Split(stderr, "\n")
- for _, jsonString := range jsonStrings {
- if len(jsonString) == 0 {
- continue
- }
- var output map[string]interface{}
- err = json.Unmarshal([]byte(jsonString), &output)
- require.NoErrorf(t, err, "Failed to parse json %s", jsonString)
- assert.NotNil(t, output["level"])
- assert.NotNil(t, output["time"])
- }
-}
-
func TestTerragruntOutputFromDependencyLogsJson(t *testing.T) {
t.Parallel()
diff --git a/util/trap_writer.go b/util/trap_writer.go
index 4bdb2d3f0..81acacdd6 100644
--- a/util/trap_writer.go
+++ b/util/trap_writer.go
@@ -2,48 +2,41 @@ package util
import (
"io"
- "regexp"
-)
-
-const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
-// regexp matches ansi characters getting from a shell output, used for colors etc.
-var ansiReg = regexp.MustCompile(ansi)
+ "github.com/gruntwork-io/terragrunt/internal/errors"
+)
-// TrapWriter intercepts any messages matching `reg` received from the `writer` output, but passes all others.
+// TrapWriter intercepts any messages received from the `writer` output.
// Used when necessary to filter logs from terraform.
type TrapWriter struct {
- writer io.Writer
- reg *regexp.Regexp
- trappedMsgs []string
+ writer io.Writer
+ msgs [][]byte
}
// NewTrapWriter returns a new TrapWriter instance.
-func NewTrapWriter(writer io.Writer, reg *regexp.Regexp) *TrapWriter {
+func NewTrapWriter(writer io.Writer) *TrapWriter {
return &TrapWriter{
writer: writer,
- reg: reg,
}
}
-// Msgs returns the intercepted messages.
-func (trap *TrapWriter) Msgs() []string {
- return trap.trappedMsgs
-}
+// Flush flushes intercepted messages to the writer.
+func (trap *TrapWriter) Flush() error {
+ for _, msg := range trap.msgs {
+ if _, err := trap.writer.Write(msg); err != nil {
+ return errors.New(err)
+ }
+ }
-// Clear clears all intercepted messages.
-func (trap *TrapWriter) Clear() {
- trap.trappedMsgs = nil
+ return nil
}
// Write implements `io.Writer` interface.
-func (trap *TrapWriter) Write(msg []byte) (int, error) {
- msgWithoutAnsi := ansiReg.ReplaceAll(msg, []byte(""))
+func (trap *TrapWriter) Write(d []byte) (int, error) {
+ msg := make([]byte, len(d))
+ copy(msg, d)
- if trap.reg.Match(msgWithoutAnsi) {
- trap.trappedMsgs = append(trap.trappedMsgs, string(msg))
- return len(msg), nil
- }
+ trap.msgs = append(trap.msgs, msg)
- return trap.writer.Write(msg)
+ return len(msg), nil
}