Repository: gruntwork-io/terragrunt (language: go)

Issue:
TestTerragruntJsonPlanJsonOutput is flaky when using provider cache
## Describe the bug

The `TestTerragruntJsonPlanJsonOutput` test is flaky when run with provider cache.

The error received looks like this:

```
=== NAME  TestTerragruntJsonPlanJsonOutput/terragrunt_with_--terragrunt-json-log_--terragrunt-tf-logs-to-json_--terragrunt-forward-tf-stdout
    integration_test.go:3942: 
        	Error Trace:	/home/circleci/project/test/integration_test.go:3942
        	Error:      	Received unexpected error:
        	            	2 errors occurred:
        	            	
        	            	* Failed to execute "tofu init" in /tmp/terragrunt-test2983432252/fixtures/out-dir/dependency
        	            	  [31m[31m╷[0m[0m
        	            	  [31m│[0m [0m[1m[31mError: [0m[0m[1mFailed to install provider[0m
        	            	  [31m│[0m [0m
        	            	  [31m│[0m [0m[0mError while installing hashicorp/local v2.5.2: could not query provider
        	            	  [31m│[0m [0mregistry for registry.opentofu.org/hashicorp/local: 423 Locked
        	            	  [31m╵[0m[0m
        	            	  [0m[0m
        	            	  
        	            	  exit status 1
        	            	
        	            	* Failed to execute "tofu init" in /tmp/terragrunt-test2983432252/fixtures/out-dir/dependency
        	            	  [31m[31m╷[0m[0m
        	            	  [31m│[0m [0m[1m[31mError: [0m[0m[1mFailed to install provider[0m
        	            	  [31m│[0m [0m
        	            	  [31m│[0m [0m[0mError while installing hashicorp/local v2.5.2: could not query provider
        	            	  [31m│[0m [0mregistry for registry.opentofu.org/hashicorp/local: 423 Locked
        	            	  [31m╵[0m[0m
        	            	  [0m[0m
        	            	  
        	            	  exit status 1
        	Test:       	TestTerragruntJsonPlanJsonOutput/terragrunt_with_--terragrunt-json-log_--terragrunt-tf-logs-to-json_--terragrunt-forward-tf-stdout
```

## Steps To Reproduce

Run the test with provider cache enabled.

## Expected behavior

Test works consistently, and the `423 Locked` error is silently ignored, as it is an expected issue in normal provider cache usage.

## Nice to haves

- [x] Terminal output
- [ ] Screenshots

## Versions

- Terragrunt version: `main`
- OpenTofu version: `v1.7.1`
- Environment details (Ubuntu 20.04, Windows 10, etc.): MacOS latest.

## Additional context

This is largely an issue in CircleCI, as it results in flaky failures.



Discussion / hints:

Fix patch:
diff --git a/cli/commands/flags.go b/cli/commands/flags.go
index 4d959f264d..095d8afb16 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 8f2030683c..52a9ba36d6 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 5d74382b3f..a1969f8815 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 9b6eb7f6a4..9e1245e3d9 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 e104e3ed41..4ecccf0ae0 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 bd7077499a..af41621e3a 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 882cdd6676..52ffc4a1ed 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 35e9343d80..9ac672e64c 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`<br/>
 **Environment Variable**: `TERRAGRUNT_TF_JSON_LOG` (set to `true`)<br/>
 
diff --git a/internal/strict/strict.go b/internal/strict/strict.go
index 893dd71f04..dbfa26bcf1 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 be9d054b2d..1260d7e50b 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 8275a1a6f7..9adf9c2526 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 09a0691e60..73af5288f0 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 b555229618..a2bc5c214c 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 c2ee92c0fa..f812e8429b 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 12ffe2bd8f..1e88fce218 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 ffc89a1997..e91dfdbe82 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 b27d090297..80edd56f14 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 28821c3fc4..f373868e73 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 75d92e6a30..8ded8a60e4 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 6f0465ca6d..168b25b460 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 9f19a1d525..5b82fa0ff0 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 25194bedf3..c0d54b6408 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 d77afa2f06..5ad85e8900 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 0000000000..7692fa7599
--- /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 199ff0ac9d..07d38bf612 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 b37766928c..8b5a5afb3b 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 6989b7a44c..8008b15b49 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 575778fee9..02cfae6ec3 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 c6c48b98bd..2b87a1d3e1 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/util/trap_writer.go b/util/trap_writer.go
index 4bdb2d3f08..81acacdd6e 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
 }


Test patch:
diff --git a/shell/run_shell_cmd_output_test.go b/shell/run_shell_cmd_output_test.go
index 37a249a129..c3ce3fcf8e 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 d124423404..a2cb0f3bad 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 5ae1323bf0..29291aeecf 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()