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

Issue:
0.67.0 makes STDOUT output in log format
## Describe the bug

terragrunt started printing its normal output in log format

## Steps To Reproduce

Steps to reproduce the behavior, code snippets and examples which can be used to reproduce the issue.

```hcl
terragrunt version v0.67.0
❯ \terragrunt plan         
13:13:16.454 STDOUT terraform: data.terraform_remote_state.infrastructure_network: Reading...
13:13:17.808 STDOUT terraform: data.terraform_remote_state.infrastructure_network: Read complete after 2s
13:13:17.870 STDOUT terraform: Terraform used the selected providers to generate the following execution
13:13:17.870 STDOUT terraform: plan. Resource actions are indicated with the following symbols:
13:13:17.870 STDOUT terraform:   + create
13:13:17.870 STDOUT terraform: Terraform will perform the following actions:
13:13:17.870 STDOUT terraform:   # aws_s3_bucket.bucket will be created
13:13:17.870 STDOUT terraform:   + resource "aws_s3_bucket" "bucket" {
13:13:17.870 STDOUT terraform:       + acceleration_status         = (known after apply)
13:13:17.870 STDOUT terraform:       + acl                         = (known after apply)
13:13:17.870 STDOUT terraform:       + arn                         = (known after apply)
13:13:17.870 STDOUT terraform:       + bucket                      = "my-bucket"
13:13:17.870 STDOUT terraform:       + bucket_domain_name          = (known after apply)
13:13:17.870 STDOUT terraform:       + bucket_prefix               = (known after apply)
13:13:17.870 STDOUT terraform:       + bucket_regional_domain_name = (known after apply)
13:13:17.870 STDOUT terraform:       + force_destroy               = false
13:13:17.870 STDOUT terraform:       + hosted_zone_id              = (known after apply)
13:13:17.870 STDOUT terraform:       + id                          = (known after apply)
13:13:17.870 STDOUT terraform:       + object_lock_enabled         = (known after apply)
13:13:17.870 STDOUT terraform:       + policy                      = (known after apply)
13:13:17.870 STDOUT terraform:       + region                      = (known after apply)
13:13:17.870 STDOUT terraform:       + request_payer               = (known after apply)
13:13:17.870 STDOUT terraform:       + tags_all                    = {
13:13:17.870 STDOUT terraform:           + "email"          = "FILL_THIS_IN"
13:13:17.870 STDOUT terraform:           + "product"        = "terraforms"
13:13:17.870 STDOUT terraform:           + "provisioned_by" = "terraform"
13:13:17.870 STDOUT terraform:           + "service"        = "foo"
13:13:17.870 STDOUT terraform:           + "terraform_path" = "foo"
13:13:17.870 STDOUT terraform:         }
13:13:17.870 STDOUT terraform:       + website_domain              = (known after apply)
13:13:17.870 STDOUT terraform:       + website_endpoint            = (known after apply)
13:13:17.870 STDOUT terraform:       + cors_rule (known after apply)
13:13:17.870 STDOUT terraform:       + grant (known after apply)
13:13:17.870 STDOUT terraform:       + lifecycle_rule (known after apply)
13:13:17.870 STDOUT terraform:       + logging (known after apply)
13:13:17.870 STDOUT terraform:       + object_lock_configuration (known after apply)
13:13:17.870 STDOUT terraform:       + replication_configuration (known after apply)
13:13:17.870 STDOUT terraform:       + server_side_encryption_configuration (known after apply)
13:13:17.870 STDOUT terraform:       + versioning (known after apply)
13:13:17.870 STDOUT terraform:       + website (known after apply)
13:13:17.870 STDOUT terraform:     }
13:13:17.870 STDOUT terraform: Plan: 1 to add, 0 to change, 0 to destroy.
13:13:17.870 STDOUT terraform: 
13:13:17.870 STDOUT terraform: 
13:13:17.870 STDOUT terraform: ─────────────────────────────────────────────────────────────────────────────
13:13:17.870 STDOUT terraform: Note: You didn't use the -out option to save this plan, so Terraform can't
13:13:17.870 STDOUT terraform: guarantee to take exactly these actions if you run "terraform apply" now.
```


## Expected behavior

A clear and concise description of what you expected to happen.
```
terragrunt version v0.66.9
❯ \terragrunt plan
data.terraform_remote_state.infrastructure_network: Reading...
data.terraform_remote_state.infrastructure_network: Read complete after 2s

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.bucket will be created
  + resource "aws_s3_bucket" "bucket" {
      + acceleration_status         = (known after apply)
      + acl                         = (known after apply)
      + arn                         = (known after apply)
      + bucket                      = "my-bucket"
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = (known after apply)
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + object_lock_enabled         = (known after apply)
      + policy                      = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + tags_all                    = {
          + "email"          = "FILL_THIS_IN"
          + "product"        = "terraforms"
          + "provisioned_by" = "terraform"
          + "service"        = "foo"
          + "terraform_path" = "foo"
        }
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + cors_rule (known after apply)

      + grant (known after apply)

      + lifecycle_rule (known after apply)

      + logging (known after apply)

      + object_lock_configuration (known after apply)

      + replication_configuration (known after apply)

      + server_side_encryption_configuration (known after apply)

      + versioning (known after apply)

      + website (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.
```

## Nice to haves

- [ ] Terminal output
- [ ] Screenshots

## Versions

- Terragrunt version: v0.67.0
- OpenTofu/Terraform version: 1.9.5
- Environment details (Ubuntu 20.04, Windows 10, etc.): ubuntu 22.04

## Additional context

In order to get things back to how they were, it seems one is forced to use `--terragrunt-log-level error --terragrunt-forward-tf-stdout `. Is that what we are supposed to do? One should have to opt-in to the new STDOUT format rather than being forced to opt-out to maintain the status quo.




Discussion / hints:
Hey @grimm26 ,

Thanks for creating this issue.

Yes, you will need to set 
[terragrunt-forward-tf-stdout](https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-forward-tf-stdout) in order to have logs emitted the way that they were prior to `v0.67.0`.

This is not a bug. We believe that this gives better context for Terragrunt users by default, and is an appropriate change to include in a breaking release. 

We could have done a better job communicating how to preserve legacy behavior in the release notes, and I've updated them accordingly.

Thanks again for sharing your thoughts. If you have any enhancements you would like to see to how Terragrunt emits logs, please feel free to create an issue detailing them!
> Hey @grimm26 ,
> 
> Thanks for creating this issue.
> 
> Yes, you will need to set [terragrunt-forward-tf-stdout](https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-forward-tf-stdout) in order to have logs emitted the way that they were prior to `v0.67.0`.
> 
> This is not a bug. We believe that this gives better context for Terragrunt users by default, and is an appropriate change to include in a breaking release.
> 
> We could have done a better job communicating how to preserve legacy behavior in the release notes, and I've updated them accordingly.
> 
> Thanks again for sharing your thoughts. If you have any enhancements you would like to see to how Terragrunt emits logs, please feel free to create an issue detailing them!

 [terragrunt-forward-tf-stdout](https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-forward-tf-stdout) doesn't seem to do the trick.

```
❯ \terragrunt --terragrunt-forward-tf-stdout plan
16:12:23.091 INFO   Retrieved output from terragrunt.hcl
16:12:26.268 STDOUT terraform: ╷
16:12:26.268 STDOUT terraform: │ Error: Reference to undeclared input variable
16:12:26.268 STDOUT terraform: │ 
16:12:26.268 STDOUT terraform: │   on locals.tf line 13, in locals:
16:12:26.268 STDOUT terraform: │   13:     terraform_path = var.terraform_path
16:12:26.268 STDOUT terraform: │ 
16:12:26.268 STDOUT terraform: │ An input variable with the name "terraform_path" has not been declared.
16:12:26.268 STDOUT terraform: │ This variable can be declared with a variable "terraform_path" {} block.
16:12:26.268 STDOUT terraform: ╵
16:12:26.290 ERROR  terraform invocation failed in /home/mkeisler/git/terraforms/production_643927032162/netcredit/production/payment_schedule_generator error=[/home/mkeisler/git/terraforms/production_643927032162/netcredit/production/payment_schedule_generator] exit status 1
16:12:26.290 ERROR  1 error occurred:
	* [/home/mkeisler/git/terraforms/production_643927032162/netcredit/production/payment_schedule_generator] exit status 1
```


Do you only see this happen when there's an error in Terraform execution?

If you could share the code that you're running, or a sanitized version, it might help us understand what the issue is that you're experiencing.
```
❯ l
drwxr-xr-x 3 mkeisler mkeisler 4.0 KB Wed Aug 28 13:01:51 2024  .terraform
.rw-r--r-- 1 mkeisler mkeisler 1.3 KB Wed Aug 28 13:01:52 2024  .terraform.lock.hcl
.rw-rw-r-- 1 mkeisler mkeisler  38 B  Wed Aug 28 16:30:20 2024  main.tf
.rw-rw-r-- 1 mkeisler mkeisler   0 B  Wed Aug 28 13:02:10 2024  terragrunt.hcl
❯ cat main.tf
locals {
  foo = var.terraform_path
}
❯ cat terragrunt.hcl
❯ \terragrunt --terragrunt-forward-tf-stdout plan
16:30:51.164 INFO   Retrieved output from terragrunt.hcl
16:30:51.606 STDOUT terraform: ╷
16:30:51.606 STDOUT terraform: │ Error: Reference to undeclared input variable
16:30:51.606 STDOUT terraform: │ 
16:30:51.606 STDOUT terraform: │   on main.tf line 2, in locals:
16:30:51.606 STDOUT terraform: │    2:   foo = var.terraform_path
16:30:51.606 STDOUT terraform: │ 
16:30:51.606 STDOUT terraform: │ An input variable with the name "terraform_path" has not been declared.
16:30:51.606 STDOUT terraform: │ This variable can be declared with a variable "terraform_path" {} block.
16:30:51.606 STDOUT terraform: ╵
16:30:51.611 ERROR  terraform invocation failed in /tmp/tf error=[/tmp/tf] exit status 1
16:30:51.611 ERROR  1 error occurred:
	* [/tmp/tf] exit status 1


  /tmp/tf     
```
and with no error:
```
❯ cat main.tf
locals {
  foo = "bar"
}

output "foo" {
  value = local.foo
}
❯ \terragrunt --terragrunt-forward-tf-stdout plan
16:32:39.178 INFO   Retrieved output from terragrunt.hcl
16:32:39.625 INFO   Retrieved output from terragrunt.hcl

Changes to Outputs:
  + foo = "bar"

You can apply this plan to save these new output values to the Terraform
state, without changing any real infrastructure.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.
```
Hmmm. That does seem unexpected. Do you mind updating the bug report accordingly and reopening the issue? 

To be clear, enriching stdout from OpenTofu/Terraform by default is not a bug, but failing to allow users to opt-out of that enrichment is. 
This is quite the change, is there no way to make it an env var so it can be disabled across all terragrunt commands instead of one every command?

The following no longer works - `terragrunt state pull > state.json` as the log is not valid json.

On top of that now my terminal is full of `14:30:56.137 STDOUT terraform: ` which really adds very little.
> This is quite the change, is there no way to make it an env var so it can be disabled across all terragrunt commands instead of one every command?
> 
> The following no longer works - `terragrunt state pull > state.json` as the log is not valid json.
> 
> On top of that now my terminal is full of `14:30:56.137 STDOUT terraform: ` which really adds very little.

That is another issue we will look to address. That should not happen by default.

Note that you can set `TERRAGRUNT_FORWARD_TF_STDOUT` in addition, per the [docs](https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-forward-tf-stdout).

We have received feedback from users that once you scale up Terragrunt usage, it can be really confusing to deal with multiple concurrent runs, which is the main benefit of the enriched logs.

Would you prefer that the logs are designed differently?
>We have received feedback from users that once you scale up Terragrunt usage, it can be really confusing to deal with multiple concurrent runs, which is the main benefit of the enriched logs.
>
>Would you prefer that the logs are designed differently?

I would perhaps consider enabling by default the new log format for `run-all`, but disabling it by default for all other commands since they are by definition dealing with a single run.

> I would perhaps consider enabling by default the new log format for `run-all`, but disabling it by default for all other commands since they are by definition dealing with a single run.

You could create two aliases to the `terragrunt` command with and without the flag `--terragrunt-forward-tf-stdout`

I think its presumed that if you have `ERROR` in the console log its going to stderr just as `INFO` is going to stdout.

I'm just a bit surprised, there's many issues both filed here also on the hashicorp discussion boards where the ask is for less logging with people coming up with workarounds.

Either way, as you've said we can set `TERRAGRUNT_FORWARD_TF_STDOUT` which works.
> > I would perhaps consider enabling by default the new log format for `run-all`, but disabling it by default for all other commands since they are by definition dealing with a single run.
> 
> You could create two aliases to the `terragrunt` command with and without the flag `--terragrunt-forward-tf-stdout`

Lol that hurts my soul
@lorengordon ,

The problem with only enabling the enriched logs for `run-all` is two fold:
1. There can still be ambiguity as to what's happening when running without `run-all`. Users using `run_cmd`, hooks, or `dependency` blocks, etc will have log output different than directly forwarding stdout/stderr from OpenTofu/Terraform.
2. Changing the log output between `run-all` and not using `run-all` can be more complicated to deal with by users that want to perform post-processing on the stdout/stderr from Terragrunt in CI, etc.

To be clear on our response here: 
- We definitely need to improve our handling of stderr from OpenTofu/Terraform, and we're actively working on that. 
- This is not the end of our logging improvements, but the beginning. We will be introducing additional capabilities to customize the logs emitted here, so that users can chose to disable the timestamp, loglevel, etc if they want less logged. Your feedback is heavily encouraged so that we can get the logs to emit the right amount of data for your use-case.
Personally I just want the relative path to the working dir, and all the new stuff is just noise. Mostly what `TERRAGRUNT_INCLUDE_MODULE_PREFIX` did, but just the relative path.
I get the idea of having the ability to turn on extra debug logging, but totally agree that the new format is messy and has a lot of extraneous / unnecessary information.

And, if I set `TERRAGRUNT_DISABLE_LOG_FORMATTING`, I actually get _more_ stuff width-wise on the left of the terminal for the entire plan when I run `tg plan`, e.g.,
```
time=2024-09-14T11:36:59-07:00 level=stdout prefix=. binary=tofu msg=
```
vs
```
11:35:48.140 STDOUT tofu:
```

that's almost 70 characters _just_ of terragrunt's messages, where it used to just show the raw terraform plan output.

Setting `TERRAGRUNT_FORWARD_TF_STDOUT` does seem to more or less get the old / desired behavior.
We are working on custom formatting where you can specify which data will be displayed.
> We are working on custom formatting where you can specify what data will be displayed.

Totally get that; IMO, it would have been better with hindsight to preserve the older behavior (even with changes behind the scenes to the logging system), and make the new behavior opt-in. I appreciate that this is an open source project (and pre-1.0, though it's been around forever), but it is a big project with a lot of users.

Even aside from anyone who's parsing the output explicitly, the new behavior is a very dramatic change from the previous behavior.

> Either way, as you've said we can set TERRAGRUNT_FORWARD_TF_STDOUT which works.

Even with both of those vars set, some of the stuff from terragrunt still shows up in the new format
```
% tg init        
time=2024-09-14T11:49:42-07:00 level=info prefix=. msg=Retrieved output from tofu
```

**Edit**: seems like setting `TERRAGRUNT_LOG_DISABLE` gets closer to the old behavior -- not sure if there are any risks there, but seems to help. In my quick tests, it didn't seem to suppress logs related to errors, so I'm guessing the downsides should be relatively minimal.

More broadly, while I get that it's nice to have a unified library for logging and stdout / stderr under the hood, logging and CLI output serve very different purposes and needs. IMO, the default formatting should reflect that.

Having an error from a command you're running in realtime that includes loglevel / severity, timestamps, etc. seems like confusing in a way that it's not when it's the output of a daemon / system process.
Also, I just want to say, in all fairness, for `terragrunt --run-all` (which I don't usually use locally, but which we use in CI) _is_ a lot easier to parse now. While I'd love to have the mentioned customization, I can definitely see what the goal was on that side of things, so thanks for that (especially as a know a big overhaul like this is a lot of work).
@wyardley, Thanks for the feedback, it really means a lot.

> Even with both of those vars set, some of the stuff from terragrunt still shows up in the new format
> 
> ```
> % tg init        
> time=2024-09-14T11:49:42-07:00 level=info prefix=. msg=Retrieved output from tofu
> ```

This is a bug, this log should be output to `stderr`, I will fix it on Monday. After fixing it you can try TERRAGRUNT_FORWARD_TF_STDOUT again, I think you will get what you need then.


> **Edit**: seems like setting `TERRAGRUNT_LOG_DISABLE` gets closer to the old behavior -- not sure if there are any risks there, but seems to help. In my quick tests, it didn't seem to suppress logs related to errors, so I'm guessing the downsides should be relatively minimal.

This flag only suppresses the TG log, but displays Terraform/OpenTofu stderr/stdout as is. We shouldn't even call it Terraform/OpenTofu stderr/stdout as a _log_. The real log is enabled by TF_LOG=info/debug/warn/error


> it would have been better with hindsight to preserve the older behavior (even with changes behind the scenes to the logging system)

Once we implement the ability to customize the log, there will be presets as well. I think we will create one that will replicate the behavior of the old log.

Fix patch:
diff --git a/pkg/cli/args.go b/pkg/cli/args.go
index ffd9951286..94ddf85a5a 100644
--- a/pkg/cli/args.go
+++ b/pkg/cli/args.go
@@ -5,6 +5,8 @@ import (
 	"strings"
 )
 
+const tailMinArgsLen = 2
+
 const (
 	SingleDashFlag NormalizeActsType = iota
 	DoubleDashFlag
@@ -42,9 +44,7 @@ func (args Args) Last() string {
 // Tail returns the rest of the arguments (not the first one)
 // or else an empty string slice
 func (args Args) Tail() Args {
-	const minArgsLen = 2
-
-	if args.Len() < minArgsLen {
+	if args.Len() < tailMinArgsLen {
 		return []string{}
 	}
 
diff --git a/shell/run_shell_cmd.go b/shell/run_shell_cmd.go
index 1a368a1b50..1c25a913eb 100644
--- a/shell/run_shell_cmd.go
+++ b/shell/run_shell_cmd.go
@@ -153,7 +153,7 @@ func RunShellCommandWithOutput(
 				logger := opts.Logger.WithField(format.TFBinaryKeyName, filepath.Base(opts.TerraformPath))
 
 				outWriter = writer.New(
-					writer.WithLogger(logger.WithOptions(log.WithOutput(outWriter))),
+					writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))),
 					writer.WithDefaultLevel(log.StdoutLevel),
 					writer.WithMsgSeparator(logMsgSeparator),
 				)
@@ -162,7 +162,7 @@ func RunShellCommandWithOutput(
 					writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))),
 					writer.WithDefaultLevel(log.StderrLevel),
 					writer.WithMsgSeparator(logMsgSeparator),
-					writer.WithParseFunc(terraform.ParseLogFunc(tfLogMsgPrefix)),
+					writer.WithParseFunc(terraform.ParseLogFunc(tfLogMsgPrefix, false)),
 				)
 			}
 		}
@@ -453,5 +453,18 @@ func shouldForceForwardTFStdout(args cli.Args) bool {
 		terraform.CommandNameConsole,
 	}
 
-	return collections.ListContainsElement(tfCommands, args.CommandName()) || args.Tail().Contains(terraform.FlagNameJSON)
+	tfFlags := []string{
+		terraform.FlagNameJSON,
+		terraform.FlagNameVersion,
+		terraform.FlagNameHelpLong,
+		terraform.FlagNameHelpShort,
+	}
+
+	for _, flag := range tfFlags {
+		if args.Normalize(cli.SingleDashFlag).Contains(flag) {
+			return true
+		}
+	}
+
+	return collections.ListContainsElement(tfCommands, args.CommandName())
 }
diff --git a/terraform/log.go b/terraform/log.go
index 944f8245f8..2a7e6cb45d 100644
--- a/terraform/log.go
+++ b/terraform/log.go
@@ -5,10 +5,13 @@ import (
 	"strings"
 	"time"
 
+	"github.com/gruntwork-io/go-commons/errors"
 	"github.com/gruntwork-io/terragrunt/pkg/log"
 	"github.com/gruntwork-io/terragrunt/pkg/log/writer"
 )
 
+const parseLogNumberOfValues = 4
+
 var (
 	// logTimestampFormat is TF_LOG timestamp formats.
 	logTimestampFormat = "2006-01-02T15:04:05.000Z0700"
@@ -22,41 +25,51 @@ var (
 	tfLogTimeLevelMsgReg = regexp.MustCompile(`(?i)(^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}\S*)\s*\[(trace|debug|warn|info|error)\]\s*(.+\S)$`)
 )
 
-func ParseLogFunc(msgPrefix string) writer.WriterParseFunc {
+// ParseLogFunc wraps `ParseLog` to add msg prefix and bypasses the the parse erorr if `returnError` is false,
+// since returning the error for `log/writer` will cause TG to fall with a `broken pipe` error.
+func ParseLogFunc(msgPrefix string, returnError bool) writer.WriterParseFunc {
 	return func(str string) (msg string, ptrTime *time.Time, ptrLevel *log.Level, err error) {
-		return ParseLog(msgPrefix, str)
+		if msg, ptrTime, ptrLevel, err = ParseLog(str); err != nil {
+			if returnError {
+				return str, nil, nil, err
+			}
+
+			return str, nil, nil, nil
+		}
+
+		return msgPrefix + msg, ptrTime, ptrLevel, nil
 	}
 }
 
-func ParseLog(msgPrefix, str string) (msg string, ptrTime *time.Time, ptrLevel *log.Level, err error) {
-	const numberOfValues = 4
-
+func ParseLog(str string) (msg string, ptrTime *time.Time, ptrLevel *log.Level, err error) {
 	if !tfLogTimeLevelMsgReg.MatchString(str) {
-		return str, nil, nil, nil
+		return str, nil, nil, errors.Errorf("could not parse string %q: does not match a known format", str)
 	}
 
 	match := tfLogTimeLevelMsgReg.FindStringSubmatch(str)
-	if len(match) != numberOfValues {
-		return str, nil, nil, nil
+	if len(match) != parseLogNumberOfValues {
+		return str, nil, nil, errors.Errorf("could not parse string %q: does not match a known format", str)
 	}
 
 	timeStr, levelStr, msg := match[1], match[2], match[3]
 
 	if levelStr != "" {
-		if level, err := log.ParseLevel(strings.ToLower(levelStr)); err == nil {
-			ptrLevel = &level
-		} else {
-			msg = "[" + levelStr + "] " + msg
+		level, err := log.ParseLevel(strings.ToLower(levelStr))
+		if err != nil {
+			return str, nil, nil, errors.Errorf("could not parse level %q: %w", levelStr, err)
 		}
+
+		ptrLevel = &level
 	}
 
 	if timeStr != "" {
-		if time, err := time.Parse(logTimestampFormat, timeStr); err == nil {
-			ptrTime = &time
-		} else {
-			msg = timeStr + " " + msg
+		time, err := time.Parse(logTimestampFormat, timeStr)
+		if err != nil {
+			return str, nil, nil, errors.Errorf("could not parse time %q: %w", timeStr, err)
 		}
+
+		ptrTime = &time
 	}
 
-	return msgPrefix + msg, ptrTime, ptrLevel, nil
+	return msg, ptrTime, ptrLevel, nil
 }
diff --git a/terraform/terraform.go b/terraform/terraform.go
index f22c1686b7..14bc742a85 100644
--- a/terraform/terraform.go
+++ b/terraform/terraform.go
@@ -25,8 +25,11 @@ const (
 	CommandNameShow           = "show"
 	CommandNameVersion        = "version"
 
-	FlagNameJSON    = "-json"
-	FlagNameNoColor = "-no-color"
+	FlagNameHelpLong  = "-help"
+	FlagNameHelpShort = "-h"
+	FlagNameVersion   = "-version"
+	FlagNameJSON      = "-json"
+	FlagNameNoColor   = "-no-color"
 	// `apply -destroy` is alias for `destroy`
 	FlagNameDestroy = "-destroy"
 


Test patch:
diff --git a/test/integration_auto_retry_test.go b/test/integration_auto_retry_test.go
index d5d7a64b34..ff459b026a 100644
--- a/test/integration_auto_retry_test.go
+++ b/test/integration_auto_retry_test.go
@@ -31,7 +31,7 @@ func TestAutoRetryBasicRerun(t *testing.T) {
 	out := new(bytes.Buffer)
 	rootPath := copyEnvironment(t, testFixtureAutoRetryRerun)
 	modulePath := util.JoinPath(rootPath, testFixtureAutoRetryRerun)
-	err := runTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+modulePath, out, os.Stderr)
+	err := runTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+modulePath, out, os.Stderr)
 
 	require.NoError(t, err)
 	assert.Contains(t, out.String(), "Apply complete!")
@@ -55,7 +55,7 @@ func TestAutoRetryExhaustRetries(t *testing.T) {
 	out := new(bytes.Buffer)
 	rootPath := copyEnvironment(t, testFixtureAutoRetryExhaust)
 	modulePath := util.JoinPath(rootPath, testFixtureAutoRetryExhaust)
-	err := runTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+modulePath, out, os.Stderr)
+	err := runTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+modulePath, out, os.Stderr)
 
 	require.Error(t, err)
 	assert.Contains(t, out.String(), "Failed to load backend")
@@ -68,7 +68,7 @@ func TestAutoRetryCustomRetryableErrors(t *testing.T) {
 	out := new(bytes.Buffer)
 	rootPath := copyEnvironment(t, testFixtureAutoRetryCustomErrors)
 	modulePath := util.JoinPath(rootPath, testFixtureAutoRetryCustomErrors)
-	err := runTerragruntCommand(t, "terragrunt apply --auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+modulePath, out, os.Stderr)
+	err := runTerragruntCommand(t, "terragrunt apply --auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+modulePath, out, os.Stderr)
 
 	require.NoError(t, err)
 	assert.Contains(t, out.String(), "My own little error")
@@ -101,7 +101,7 @@ func TestAutoRetryCustomRetryableErrorsFailsWhenRetryableErrorsNotSet(t *testing
 	out := new(bytes.Buffer)
 	rootPath := copyEnvironment(t, testFixtureAutoRetryCustomErrorsNotSet)
 	modulePath := util.JoinPath(rootPath, testFixtureAutoRetryCustomErrorsNotSet)
-	err := runTerragruntCommand(t, "terragrunt apply --auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+modulePath, out, os.Stderr)
+	err := runTerragruntCommand(t, "terragrunt apply --auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+modulePath, out, os.Stderr)
 
 	require.Error(t, err)
 	assert.Contains(t, out.String(), "My own little error")
@@ -137,7 +137,7 @@ func TestAutoRetryApplyAllDependentModuleRetries(t *testing.T) {
 	out := new(bytes.Buffer)
 	rootPath := copyEnvironment(t, testFixtureAutoRetryApplyAllRetries)
 	modulePath := util.JoinPath(rootPath, testFixtureAutoRetryApplyAllRetries)
-	err := runTerragruntCommand(t, "terragrunt apply-all -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+modulePath, out, os.Stderr)
+	err := runTerragruntCommand(t, "terragrunt apply-all -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+modulePath, out, os.Stderr)
 
 	require.NoError(t, err)
 	s := out.String()
@@ -154,7 +154,7 @@ func TestAutoRetryConfigurableRetries(t *testing.T) {
 	stderr := new(bytes.Buffer)
 	rootPath := copyEnvironment(t, testFixtureAutoRetryConfigurableRetries)
 	modulePath := util.JoinPath(rootPath, testFixtureAutoRetryConfigurableRetries)
-	err := runTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+modulePath, stdout, stderr)
+	err := runTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+modulePath, stdout, stderr)
 	sleeps := regexp.MustCompile("Sleeping 0s before retrying.").FindAllStringIndex(stderr.String(), -1)
 
 	require.NoError(t, err)
diff --git a/test/integration_destroy_test.go b/test/integration_destroy_test.go
index 6956ece08b..4992c2848e 100644
--- a/test/integration_destroy_test.go
+++ b/test/integration_destroy_test.go
@@ -40,7 +40,7 @@ func TestTerragruntDestroyOrder(t *testing.T) {
 
 	runTerragrunt(t, "terragrunt run-all apply --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
 
-	stdout, _, err := runTerragruntCommandWithOutput(t, "terragrunt run-all destroy --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
+	stdout, _, err := runTerragruntCommandWithOutput(t, "terragrunt run-all destroy --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath)
 	require.NoError(t, err)
 	assert.Regexp(t, regexp.MustCompile(`(?smi)(?:(Module E|Module D|Module B).*){3}(?:(Module A|Module C).*){2}`), stdout)
 }
@@ -54,7 +54,7 @@ func TestTerragruntApplyDestroyOrder(t *testing.T) {
 
 	runTerragrunt(t, "terragrunt run-all apply --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
 
-	stdout, _, err := runTerragruntCommandWithOutput(t, "terragrunt run-all apply -destroy --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
+	stdout, _, err := runTerragruntCommandWithOutput(t, "terragrunt run-all apply -destroy --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath)
 	require.NoError(t, err)
 	assert.Regexp(t, regexp.MustCompile(`(?smi)(?:(Module E|Module D|Module B).*){3}(?:(Module A|Module C).*){2}`), stdout)
 }
@@ -211,7 +211,7 @@ func TestPreventDestroyDependenciesIncludedConfig(t *testing.T) {
 			showStderr bytes.Buffer
 		)
 
-		err = runTerragruntCommand(t, "terragrunt show --terragrunt-non-interactive --terragrunt-working-dir "+modulePath, &showStdout, &showStderr)
+		err = runTerragruntCommand(t, "terragrunt show --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+modulePath, &showStdout, &showStderr)
 		logBufferContentsLineByLine(t, showStdout, "show stdout for "+modulePath)
 		logBufferContentsLineByLine(t, showStderr, "show stderr for "+modulePath)
 
diff --git a/test/integration_download_test.go b/test/integration_download_test.go
index 0523260ca7..36b01638e2 100644
--- a/test/integration_download_test.go
+++ b/test/integration_download_test.go
@@ -415,7 +415,7 @@ func TestTerragruntExternalDependencies(t *testing.T) {
 	rootPath := copyEnvironment(t, testFixtureExternalDependence)
 	modulePath := util.JoinPath(rootPath, testFixtureExternalDependence, "module-b")
 
-	err := runTerragruntCommand(t, "terragrunt apply-all --terragrunt-non-interactive --terragrunt-include-external-dependencies --terragrunt-working-dir "+modulePath, &applyAllStdout, &applyAllStderr)
+	err := runTerragruntCommand(t, "terragrunt apply-all --terragrunt-non-interactive --terragrunt-include-external-dependencies --terragrunt-forward-tf-stdout --terragrunt-working-dir "+modulePath, &applyAllStdout, &applyAllStderr)
 	logBufferContentsLineByLine(t, applyAllStdout, "apply-all stdout")
 	logBufferContentsLineByLine(t, applyAllStderr, "apply-all stderr")
 	applyAllStdoutString := applyAllStdout.String()
@@ -518,7 +518,7 @@ func TestPreventDestroyDependencies(t *testing.T) {
 			showStderr bytes.Buffer
 		)
 
-		err = runTerragruntCommand(t, "terragrunt show --terragrunt-non-interactive --terragrunt-working-dir "+modulePath, &showStdout, &showStderr)
+		err = runTerragruntCommand(t, "terragrunt show --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+modulePath, &showStdout, &showStderr)
 		logBufferContentsLineByLine(t, showStdout, "show stdout for "+modulePath)
 		logBufferContentsLineByLine(t, showStderr, "show stderr for "+modulePath)
 
diff --git a/test/integration_engine_test.go b/test/integration_engine_test.go
index 19e7084b9c..e6a2a4b393 100644
--- a/test/integration_engine_test.go
+++ b/test/integration_engine_test.go
@@ -33,7 +33,7 @@ var LocalEngineBinaryPath = "terragrunt-iac-engine-opentofu_rpc_" + testEngineVe
 func TestEngineLocalPlan(t *testing.T) {
 	rootPath := setupLocalEngine(t)
 
-	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir %s --terragrunt-log-level debug", rootPath))
+	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt plan --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s --terragrunt-log-level debug", rootPath))
 	require.NoError(t, err)
 
 	assert.Contains(t, stderr, LocalEngineBinaryPath+": plugin address")
@@ -45,7 +45,7 @@ func TestEngineLocalPlan(t *testing.T) {
 func TestEngineLocalApply(t *testing.T) {
 	rootPath := setupLocalEngine(t)
 
-	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath))
+	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s", rootPath))
 	require.NoError(t, err)
 
 	assert.Contains(t, stderr, LocalEngineBinaryPath+": plugin address")
@@ -61,7 +61,7 @@ func TestEngineOpentofu(t *testing.T) {
 	tmpEnvPath := copyEnvironment(t, testFixtureOpenTofuEngine)
 	rootPath := util.JoinPath(tmpEnvPath, testFixtureOpenTofuEngine)
 
-	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath))
+	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s", rootPath))
 	require.NoError(t, err)
 
 	assert.Contains(t, stderr, "starting plugin:")
@@ -77,7 +77,7 @@ func TestEngineRunAllOpentofu(t *testing.T) {
 	tmpEnvPath := copyEnvironment(t, testFixtureOpenTofuRunAll)
 	rootPath := util.JoinPath(tmpEnvPath, testFixtureOpenTofuRunAll)
 
-	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all apply -no-color -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath))
+	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all apply -no-color -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s", rootPath))
 	require.NoError(t, err)
 
 	assert.Contains(t, stderr, "starting plugin:")
@@ -94,7 +94,7 @@ func TestEngineRunAllOpentofuCustomPath(t *testing.T) {
 
 	cacheDir, rootPath := setupEngineCache(t)
 
-	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all apply -no-color -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath))
+	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all apply -no-color -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s", rootPath))
 	require.NoError(t, err)
 
 	assert.Contains(t, stderr, "starting plugin:")
@@ -123,7 +123,7 @@ func TestEngineDownloadOverHttp(t *testing.T) {
 		"__hardcoded_url__": fmt.Sprintf("https://github.com/gruntwork-io/terragrunt-engine-opentofu/releases/download/v0.0.4/terragrunt-iac-engine-opentofu_rpc_v0.0.4_%s_%s.zip", platform, arch),
 	})
 
-	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath))
+	stdout, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s", rootPath))
 	require.NoError(t, err)
 
 	assert.Contains(t, stderr, "starting plugin:")
diff --git a/test/integration_include_test.go b/test/integration_include_test.go
index 7e95b0020d..29e8b29485 100644
--- a/test/integration_include_test.go
+++ b/test/integration_include_test.go
@@ -105,7 +105,7 @@ func TestTerragruntRunAllModulesThatIncludeRestrictsSet(t *testing.T) {
 	err := runTerragruntCommand(
 		t,
 		fmt.Sprintf(
-			"terragrunt run-all plan --terragrunt-non-interactive --terragrunt-log-level debug --terragrunt-working-dir %s --terragrunt-modules-that-include alpha.hcl",
+			"terragrunt run-all plan --terragrunt-non-interactive --terragrunt-log-level debug --terragrunt-forward-tf-stdout --terragrunt-working-dir %s --terragrunt-modules-that-include alpha.hcl",
 			modulePath,
 		),
 		&stdout,
@@ -132,7 +132,7 @@ func TestTerragruntRunAllModulesWithPrefix(t *testing.T) {
 	stderr := bytes.Buffer{}
 	err := runTerragruntCommand(
 		t,
-		"terragrunt run-all plan --terragrunt-non-interactive --terragrunt-working-dir "+modulePath,
+		"terragrunt run-all plan --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+modulePath,
 		&stdout,
 		&stderr,
 	)
@@ -145,7 +145,7 @@ func TestTerragruntRunAllModulesWithPrefix(t *testing.T) {
 	assert.Contains(t, planOutput, "beta")
 	assert.Contains(t, planOutput, "charlie")
 
-	stdoutLines := strings.Split(planOutput, "\n")
+	stdoutLines := strings.Split(stderr.String(), "\n")
 	for _, line := range stdoutLines {
 		if strings.Contains(line, "alpha") {
 			assert.Contains(t, line, "prefix=a")
diff --git a/test/integration_serial_test.go b/test/integration_serial_test.go
index 5d59934e51..a67ecf7586 100644
--- a/test/integration_serial_test.go
+++ b/test/integration_serial_test.go
@@ -340,7 +340,7 @@ func TestTerragruntDownloadDir(t *testing.T) {
 
 func TestExtraArguments(t *testing.T) {
 	out := new(bytes.Buffer)
-	runTerragruntRedirectOutput(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+testFixtureExtraArgsPath, out, os.Stderr)
+	runTerragruntRedirectOutput(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+testFixtureExtraArgsPath, out, os.Stderr)
 	t.Log(out.String())
 	assert.Contains(t, out.String(), "Hello, World from dev!")
 }
@@ -348,14 +348,14 @@ func TestExtraArguments(t *testing.T) {
 func TestExtraArgumentsWithEnv(t *testing.T) {
 	out := new(bytes.Buffer)
 	t.Setenv("TF_VAR_env", "prod")
-	runTerragruntRedirectOutput(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+testFixtureExtraArgsPath, out, os.Stderr)
+	runTerragruntRedirectOutput(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+testFixtureExtraArgsPath, out, os.Stderr)
 	t.Log(out.String())
 	assert.Contains(t, out.String(), "Hello, World!")
 }
 
 func TestExtraArgumentsWithEnvVarBlock(t *testing.T) {
 	out := new(bytes.Buffer)
-	runTerragruntRedirectOutput(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+testFixtureEnvVarsBlockPath, out, os.Stderr)
+	runTerragruntRedirectOutput(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+testFixtureEnvVarsBlockPath, out, os.Stderr)
 	t.Log(out.String())
 	assert.Contains(t, out.String(), "I'm set in extra_arguments env_vars")
 }
@@ -363,7 +363,7 @@ func TestExtraArgumentsWithEnvVarBlock(t *testing.T) {
 func TestExtraArgumentsWithRegion(t *testing.T) {
 	out := new(bytes.Buffer)
 	t.Setenv("TF_VAR_region", "us-west-2")
-	runTerragruntRedirectOutput(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+testFixtureExtraArgsPath, out, os.Stderr)
+	runTerragruntRedirectOutput(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+testFixtureExtraArgsPath, out, os.Stderr)
 	t.Log(out.String())
 	assert.Contains(t, out.String(), "Hello, World from Oregon!")
 }
@@ -393,7 +393,7 @@ func TestPreserveEnvVarApplyAll(t *testing.T) {
 func TestPriorityOrderOfArgument(t *testing.T) {
 	out := new(bytes.Buffer)
 	injectedValue := "Injected-directly-by-argument"
-	runTerragruntRedirectOutput(t, fmt.Sprintf("terragrunt apply -auto-approve -var extra_var=%s --terragrunt-non-interactive --terragrunt-working-dir %s", injectedValue, testFixtureExtraArgsPath), out, os.Stderr)
+	runTerragruntRedirectOutput(t, fmt.Sprintf("terragrunt apply -auto-approve -var extra_var=%s --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s", injectedValue, testFixtureExtraArgsPath), out, os.Stderr)
 	t.Log(out.String())
 	// And the result value for test should be the injected variable since the injected arguments are injected before the suplied parameters,
 	// so our override of extra_var should be the last argument.
diff --git a/test/integration_test.go b/test/integration_test.go
index 03b5439826..aafab1a6c7 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -28,6 +28,7 @@ import (
 	"github.com/gruntwork-io/terragrunt/config"
 	"github.com/gruntwork-io/terragrunt/internal/view/diagnostic"
 	"github.com/gruntwork-io/terragrunt/options"
+	"github.com/gruntwork-io/terragrunt/pkg/log"
 	"github.com/gruntwork-io/terragrunt/remote"
 	"github.com/gruntwork-io/terragrunt/shell"
 	"github.com/gruntwork-io/terragrunt/util"
@@ -139,12 +140,12 @@ func TestLogWithAbsPath(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-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 --terragrunt-disable-log-formatting=false -no-color --terragrunt-no-color --terragrunt-working-dir "+rootPath)
 	require.NoError(t, err)
 
 	for _, prefixName := range []string{"app", "dep"} {
 		prefixName = filepath.Join(rootPath, prefixName)
-		assert.Contains(t, stdout, "STDOUT ["+prefixName+"] "+wrappedBinary()+": Initializing provider plugins...")
+		assert.Contains(t, stderr, "STDOUT ["+prefixName+"] "+wrappedBinary()+": Initializing provider plugins...")
 		assert.Contains(t, stderr, "DEBUG  ["+prefixName+"] Reading Terragrunt config file at "+prefixName+"/terragrunt.hcl")
 	}
 }
@@ -160,9 +161,11 @@ func TestLogFormatterPrettyOutput(t *testing.T) {
 	require.NoError(t, err)
 
 	for _, prefixName := range []string{"app", "dep"} {
-		assert.Contains(t, stdout, "STDOUT ["+prefixName+"] "+wrappedBinary()+": Initializing provider plugins...")
+		assert.Contains(t, stderr, "STDOUT ["+prefixName+"] "+wrappedBinary()+": Initializing provider plugins...")
 		assert.Contains(t, stderr, "DEBUG  ["+prefixName+"] Reading Terragrunt config file at ./"+prefixName+"/terragrunt.hcl")
 	}
+
+	assert.Empty(t, stdout)
 	assert.Contains(t, stderr, "DEBUG  Terragrunt Version:")
 }
 
@@ -173,11 +176,11 @@ func TestLogFormatterKeyValueOutput(t *testing.T) {
 	tmpEnvPath := copyEnvironment(t, testFixtureLogFormatter)
 	rootPath := util.JoinPath(tmpEnvPath, testFixtureLogFormatter)
 
-	stdout, 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-disable-log-formatting --terragrunt-working-dir "+rootPath)
 	require.NoError(t, err)
 
 	for _, prefixName := range []string{"app", "dep"} {
-		assert.Contains(t, stdout, "level=stdout prefix="+prefixName+" binary="+wrappedBinary()+" msg=Initializing provider plugins...\n")
+		assert.Contains(t, stderr, "level=stdout prefix="+prefixName+" binary="+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:")
@@ -193,7 +196,9 @@ func TestLogRawModuleOutput(t *testing.T) {
 	stdout, _, err := runTerragruntCommandWithOutput(t, "terragrunt run-all init -no-color --terragrunt-log-level debug --terragrunt-non-interactive  --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath)
 	require.NoError(t, err)
 
-	assert.Contains(t, strings.ReplaceAll(stdout, "\n", ""), "Initializing the backend...Initializing provider plugins...")
+	stdoutInline := strings.ReplaceAll(stdout, "\n", "")
+	assert.Contains(t, stdoutInline, "Initializing the backend...Initializing provider plugins...")
+	assert.NotRegexp(t, regexp.MustCompile(`(?i)(`+strings.Join(log.AllLevels.Names(), "|")+`)+`), stdoutInline)
 }
 
 func TestTerragruntExcludesFile(t *testing.T) {
@@ -430,7 +435,7 @@ func TestTerragruntInitOnce(t *testing.T) {
 	tmpEnvPath := copyEnvironment(t, testFixtureInitOnce)
 	rootPath := util.JoinPath(tmpEnvPath, testFixtureInitOnce)
 
-	stdout, _, err := runTerragruntCommandWithOutput(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
+	stdout, _, err := runTerragruntCommandWithOutput(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath)
 	require.NoError(t, err)
 	assert.Contains(t, stdout, "Initializing modules")
 
@@ -441,7 +446,7 @@ func TestTerragruntInitOnce(t *testing.T) {
 	err = os.WriteFile(cfgPath, bytes, 0644)
 	require.NoError(t, err)
 
-	stdout, _, err = runTerragruntCommandWithOutput(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir "+rootPath)
+	stdout, _, err = runTerragruntCommandWithOutput(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath)
 	require.NoError(t, err)
 	assert.NotContains(t, stdout, "Initializing modules", "init command executed more than once")
 }
@@ -814,7 +819,7 @@ func TestTerragruntExcludeExternalDependencies(t *testing.T) {
 	rootPath := copyEnvironment(t, testFixtureExternalDependence)
 	modulePath := util.JoinPath(rootPath, testFixtureExternalDependence, includedModule)
 
-	err := runTerragruntCommand(t, "terragrunt apply-all --terragrunt-non-interactive --terragrunt-ignore-external-dependencies --terragrunt-working-dir "+modulePath, &applyAllStdout, &applyAllStderr)
+	err := runTerragruntCommand(t, "terragrunt apply-all --terragrunt-non-interactive --terragrunt-ignore-external-dependencies --terragrunt-forward-tf-stdout --terragrunt-working-dir "+modulePath, &applyAllStdout, &applyAllStderr)
 	logBufferContentsLineByLine(t, applyAllStdout, "apply-all stdout")
 	logBufferContentsLineByLine(t, applyAllStderr, "apply-all stderr")
 	applyAllStdoutString := applyAllStdout.String()
@@ -857,7 +862,7 @@ func TestApplySkipFalse(t *testing.T) {
 	showStdout := bytes.Buffer{}
 	showStderr := bytes.Buffer{}
 
-	err := runTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+rootPath, &showStdout, &showStderr)
+	err := runTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath, &showStdout, &showStderr)
 	logBufferContentsLineByLine(t, showStdout, "show stdout")
 	logBufferContentsLineByLine(t, showStderr, "show stderr")
 
@@ -878,7 +883,7 @@ func TestApplyAllSkipTrue(t *testing.T) {
 	showStdout := bytes.Buffer{}
 	showStderr := bytes.Buffer{}
 
-	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply-all --terragrunt-non-interactive --terragrunt-working-dir %s --terragrunt-log-level info", rootPath), &showStdout, &showStderr)
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply-all --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s --terragrunt-log-level info", rootPath), &showStdout, &showStderr)
 	logBufferContentsLineByLine(t, showStdout, "show stdout")
 	logBufferContentsLineByLine(t, showStderr, "show stderr")
 
@@ -900,7 +905,7 @@ func TestApplyAllSkipFalse(t *testing.T) {
 	showStdout := bytes.Buffer{}
 	showStderr := bytes.Buffer{}
 
-	err := runTerragruntCommand(t, "terragrunt apply-all --terragrunt-non-interactive --terragrunt-working-dir "+rootPath, &showStdout, &showStderr)
+	err := runTerragruntCommand(t, "terragrunt apply-all --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath, &showStdout, &showStderr)
 	logBufferContentsLineByLine(t, showStdout, "show stdout")
 	logBufferContentsLineByLine(t, showStderr, "show stderr")
 
@@ -1750,14 +1755,14 @@ func TestDataDir(t *testing.T) {
 		stderr bytes.Buffer
 	)
 
-	err := runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir "+rootPath, &stdout, &stderr)
+	err := runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath, &stdout, &stderr)
 	require.NoError(t, err)
 	assert.Contains(t, stdout.String(), "Initializing provider plugins")
 
 	stdout = bytes.Buffer{}
 	stderr = bytes.Buffer{}
 
-	err = runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir "+rootPath, &stdout, &stderr)
+	err = runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath, &stdout, &stderr)
 	require.NoError(t, err)
 	assert.NotContains(t, stdout.String(), "Initializing provider plugins")
 }
@@ -2166,7 +2171,7 @@ func TestTerragruntGenerateAttr(t *testing.T) {
 
 	text := "test-terragrunt-generate-attr-hello-world"
 
-	stdout, _, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s -var text=\"%s\"", generateTestCase, text))
+	stdout, _, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir %s -var text=\"%s\"", generateTestCase, text))
 	require.NoError(t, err)
 	assert.Contains(t, stdout, text)
 }
@@ -2985,7 +2990,7 @@ func TestNoMultipleInitsWithoutSourceChange(t *testing.T) {
 	stdout := bytes.Buffer{}
 	stderr := bytes.Buffer{}
 
-	err := runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir "+testPath, &stdout, &stderr)
+	err := runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+testPath, &stdout, &stderr)
 	require.NoError(t, err)
 	// providers initialization during first plan
 	assert.Equal(t, 1, strings.Count(stdout.String(), "has been successfully initialized!"))
@@ -2993,7 +2998,7 @@ func TestNoMultipleInitsWithoutSourceChange(t *testing.T) {
 	stdout = bytes.Buffer{}
 	stderr = bytes.Buffer{}
 
-	err = runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir "+testPath, &stdout, &stderr)
+	err = runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+testPath, &stdout, &stderr)
 	require.NoError(t, err)
 	// no initialization expected for second plan run
 	// https://github.com/gruntwork-io/terragrunt/issues/1921
@@ -3018,7 +3023,7 @@ func TestAutoInitWhenSourceIsChanged(t *testing.T) {
 	stdout := bytes.Buffer{}
 	stderr := bytes.Buffer{}
 
-	err = runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir "+testPath, &stdout, &stderr)
+	err = runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+testPath, &stdout, &stderr)
 	require.NoError(t, err)
 	// providers initialization during first plan
 	assert.Equal(t, 1, strings.Count(stdout.String(), "has been successfully initialized!"))
@@ -3029,7 +3034,7 @@ func TestAutoInitWhenSourceIsChanged(t *testing.T) {
 	stdout = bytes.Buffer{}
 	stderr = bytes.Buffer{}
 
-	err = runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir "+testPath, &stdout, &stderr)
+	err = runTerragruntCommand(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+testPath, &stdout, &stderr)
 	require.NoError(t, err)
 	// auto initialization when source is changed
 	assert.Equal(t, 1, strings.Count(stdout.String(), "has been successfully initialized!"))
@@ -3045,7 +3050,7 @@ func TestNoColor(t *testing.T) {
 	stdout := bytes.Buffer{}
 	stderr := bytes.Buffer{}
 
-	err := runTerragruntCommand(t, "terragrunt plan -no-color --terragrunt-working-dir "+testPath, &stdout, &stderr)
+	err := runTerragruntCommand(t, "terragrunt plan -no-color --terragrunt-forward-tf-stdout --terragrunt-working-dir "+testPath, &stdout, &stderr)
 	require.NoError(t, err)
 	// providers initialization during first plan
 	assert.Equal(t, 1, strings.Count(stdout.String(), "has been successfully initialized!"))
@@ -3318,7 +3323,7 @@ func TestInitSkipCache(t *testing.T) {
 
 	require.NoError(
 		t,
-		runTerragruntCommand(t, "terragrunt plan --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-working-dir "+rootPath, &stdout, &stderr),
+		runTerragruntCommand(t, "terragrunt plan --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath, &stdout, &stderr),
 	)
 
 	// verify that init was invoked
@@ -3330,7 +3335,7 @@ func TestInitSkipCache(t *testing.T) {
 
 	require.NoError(
 		t,
-		runTerragruntCommand(t, "terragrunt plan --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-working-dir "+rootPath, &stdout, &stderr),
+		runTerragruntCommand(t, "terragrunt plan --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath, &stdout, &stderr),
 	)
 
 	// verify that init wasn't invoked second time since cache directories are ignored
@@ -3348,7 +3353,7 @@ func TestInitSkipCache(t *testing.T) {
 
 	require.NoError(
 		t,
-		runTerragruntCommand(t, "terragrunt plan --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-working-dir "+rootPath, &stdout, &stderr),
+		runTerragruntCommand(t, "terragrunt plan --terragrunt-log-level debug --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+rootPath, &stdout, &stderr),
 	)
 
 	// verify that init was invoked
@@ -3526,12 +3531,9 @@ func TestTerragruntInvokeTerraformTests(t *testing.T) {
 	cleanupTerraformFolder(t, tmpEnvPath)
 	testPath := util.JoinPath(tmpEnvPath, testFixtureTfTest)
 
-	stdout := bytes.Buffer{}
-	stderr := bytes.Buffer{}
-
-	err := runTerragruntCommand(t, "terragrunt test --terragrunt-non-interactive --terragrunt-working-dir "+testPath, &stdout, &stderr)
+	stdout, _, err := runTerragruntCommandWithOutput(t, "terragrunt test --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+testPath)
 	require.NoError(t, err)
-	assert.Contains(t, stdout.String(), "1 passed, 0 failed")
+	assert.Contains(t, stdout, "1 passed, 0 failed")
 }
 
 func TestTerragruntCommandsThatNeedInput(t *testing.T) {
@@ -3541,7 +3543,7 @@ func TestTerragruntCommandsThatNeedInput(t *testing.T) {
 	cleanupTerraformFolder(t, tmpEnvPath)
 	testPath := util.JoinPath(tmpEnvPath, testCommandsThatNeedInput)
 
-	stdout, _, err := runTerragruntCommandWithOutput(t, "terragrunt apply --terragrunt-non-interactive --terragrunt-working-dir "+testPath)
+	stdout, _, err := runTerragruntCommandWithOutput(t, "terragrunt apply --terragrunt-non-interactive --terragrunt-forward-tf-stdout --terragrunt-working-dir "+testPath)
 	require.NoError(t, err)
 	assert.Contains(t, stdout, "Apply complete")
 }
@@ -3703,7 +3705,7 @@ func TestTerragruntRunAllPlanAndShow(t *testing.T) {
 	_, _, err = runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all plan --terragrunt-non-interactive --terragrunt-log-level debug --terragrunt-working-dir %s --terragrunt-out-dir %s", testPath, tmpDir))
 	require.NoError(t, err)
 
-	stdout, _, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all show --terragrunt-non-interactive --terragrunt-log-level debug --terragrunt-working-dir %s --terragrunt-out-dir %s -no-color", testPath, tmpDir))
+	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