Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Wrap TF stdout and stderr in JSON #3602

Merged
merged 21 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions cli/commands/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions cli/deprecated_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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())

Expand All @@ -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())

Expand All @@ -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
},
},
Expand Down
17 changes: 11 additions & 6 deletions cli/provider_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍🏽

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.
Expand Down
2 changes: 1 addition & 1 deletion config/dependency.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion configstack/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion configstack/running_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down
10 changes: 5 additions & 5 deletions docs/_docs/02_features/custom-log-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ 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)).
* `%tf-path` - Path to the OpenTofu/Terraform executable (as defined by [terragrunt-tfpath](https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-tfpath)).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure to call out this change in the release notes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I will call this out. Thanks.


* `%msg` - Log message.

Expand Down Expand Up @@ -256,7 +256,7 @@ Specific options for placeholders:

* `short` - Outputs an absolute path, but hides the working directory path.

* `%tfpath`
* `%tf-path`

* `path=[filename|dir]`

Expand All @@ -277,7 +277,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`
Expand All @@ -289,11 +289,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) tfpath=%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)", "tfpath":"%tf-path(path=filename,escape=json)", "msg":"%msg(path=relative,escape=json,color=disable)"}'
```
9 changes: 6 additions & 3 deletions docs/_docs/04_reference/cli-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1483,7 +1483,10 @@ DEPRECATED: Use [terragrunt-log-format](#terragrunt-log-format).

When this flag is set, Terragrunt will output its logs in JSON format.

### terragrunt-tf-logs-to-json
# 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/>
Expand Down
26 changes: 16 additions & 10 deletions internal/strict/strict.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this error is user facing, and it's more likely to involve multiple sentences don't you think it's better to have them be full sentences? If so, we should just add a nolint for whatever made you adjust this.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, maybe you know a special way to convert the error messages to proper casing before they are reported to users?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I did what you suggested.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, maybe you know a special way to convert the error messages to proper casing before they are reported to users?

Ah no, I don't know of any ready-made function that could do this, only manual message conversion, but I don't think it's worth it.

Error: errors.Errorf("the `%s` command is no longer supported. Use `terragrunt run-all apply` instead", SpinUp),
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),
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),
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),
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),
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),
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),
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),
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),
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),
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),
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.
Expand Down
5 changes: 0 additions & 5 deletions options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
24 changes: 17 additions & 7 deletions pkg/log/format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(`, "workingDir":"`),
Suffix(`"`),
Escape(JSONEscape),
),
PlainText(`", "tfpath":"`),
Field(TFPathKeyName,
Prefix(`, "tfpath":"`),
Suffix(`"`),
PathFormat(FilenamePath),
Escape(JSONEscape),
),
PlainText(`", "msg":"`),
Field(TFCmdArgsKeyName,
Prefix(`, "executedCommandArgs":[`),
Suffix(`]`),
Escape(JSONEscape),
),
Message(
Prefix(`, "msg":"`),
Suffix(`"`),
PathFormat(RelativePath),
Color(DisableColor),
Escape(JSONEscape),
),
PlainText(`"}`),
PlainText(`}`),
}
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/log/format/formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package format

import (
"bytes"
"sync"

"github.com/gruntwork-io/terragrunt/internal/errors"
"github.com/gruntwork-io/terragrunt/pkg/log"
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
Loading