Repository: gruntwork-io/terragrunt

Issue:
`find_in_parent_folders` throws errors and warnings in some cases when used from a dependency
**Describe the bug**
`find_in_parent_folders` used in dependencies throws errors and warnings. It seems that in dependencies, instead of being searched from the dependency, `find_in_parent_folders` is searching from the current project instead and throws errors.

This appeared in 0.55.2 and does not appear in 0.55.1.

The project still works but error messages appear which pollute the output:

```
ERRO[0000] Error: Error in function call

ERRO[0000]   on /<REDACTED>/foo/test/terragrunt.hcl line 4, in include "foo":
ERRO[0000]    4: include "foo" { path = find_in_parent_folders("foo.hcl") }
ERRO[0000]
ERRO[0000] Call to function "find_in_parent_folders" failed:
ParentFileNotFoundError: Could not find a foo.hcl in any
of the parent folders of
/<REDACTED>/bar/terragrunt.hcl.
Cause: Traversed all the way to the root..

ERRO[0000] Error: Unsuitable value type

ERRO[0000]   on /<REDACTED>/foo/test/terragrunt.hcl line 4, in include "foo":
ERRO[0000]    4: include "foo" { path = find_in_parent_folders("foo.hcl") }
ERRO[0000]
ERRO[0000] Unsuitable value: value must be known

WARN[0000] Error reading partial config  for dependency foo: /<REDACTED>/foo/test/terragrunt.hcl:4,24-47: Error in function call; Call to function "find_in_parent_folders" failed: ParentFileNotFoundError: Could not find a foo.hcl in any of the parent folders of /<REDACTED>/foo/test/terragrunt.hcl. Cause: Traversed all the way to the root.., and 1 other diagnostic(s)
```

**To Reproduce**
You need the following tree:
* `foo/`
  * `test/`
    * `terragrunt.hcl`
  * `foo.hcl`
* `bar/`
  * `terragrunt.hcl`

`foo/test/terragrunt.hcl`:
```hcl
include "foo" {
  path = find_in_parent_folders("foo.hcl")
}
```

`bar/terragrunt.hcl`:
```hcl
dependency "foo" {
  config_path = find_in_parent_folders("foo/test")
}
```

**Expected behavior**
Planning the `bar/` project should work without any warnings or errors.

**Versions**
- Terragrunt version: 0.55.2
- Terraform version: 1.3.6
- Environment details (Ubuntu 20.04, Windows 10, etc.): MacOS 14.1.1 (23B81)

**Additional context**
Works in 0.55.1

Hints from issue comments:
@perplexa: I am having a similar problem with `get_original_terragrunt_dir` now, which always returns the path from terragrunt.hcl of the invoking directory, rather than the path from the terragrunt.hcl that includes the `get_original_terragrunt_dir` call.

It breaks all sorts of dependencies that have different nesting levels than the current terragrunt.hcl.

I suspect the breaking change was introduced in https://github.com/gruntwork-io/terragrunt/commit/8af1118c2c0703f4a1e7fd81c72a8027beac307f

edit: seems the commit I linked wasn't the cause of it, because that's been included longer. It also broke in 0.55.2 for me. Not having that error in 0.55.1

in my case, a construct like `yamldecode(sops_decrypt_file("${get_original_terragrunt_dir()}/../../../../${local.secret_path}"))` works when called from `owner/terragrunt.hcl`, but when i have a relative `../notowner/somepath/terragrunt.hcl` that has a dependency on `../../owner/terragrunt.hcl` i get the wrong path in `get_original_terragrunt_dir` and my sops_decrypt_file call fails.

edit2: has been fixed with https://github.com/gruntwork-io/terragrunt/releases/tag/v0.55.6@denis256: Will take a look for a fix, most probably it is caused by HCL parsing code which looks for `skip` flag in files@denis256: I suspect the issue was fixed as part of https://github.com/gruntwork-io/terragrunt/releases/tag/v0.55.4  at least in my tests it does not occurring now

Please confirm from your side@seanmacisaac: I had this yesterday evening using 0.55.4, found this issue and downgraded to 0.55.1, and it worked.  We have dependencies that use find_in_partent_folders as described in this issue.@denis256: Additional fix included since: https://github.com/gruntwork-io/terragrunt/releases/tag/v0.55.6@yadamovych: Hi everyone,

I'm still experiencing this issue with find_in_parent_folders in the latest version.

In the root of our Git repository, there are folders for deployment and infrastructure. When I execute one of the deployment configurations, it contains a dependency on infrastructure.

```
dependency "eks" {
  config_path = "../../../../../../../infrastructure/accounts/qa-dev/eu-central-1/devx/customer-features/eks/k8s-main"
}
```

The dependent infrastructure module includes a template file:
```
include "template" {
  path = "infrastructure/template/general/stage/customer-features/eks/k8s/terragrunt.hcl"
}
```

The Terragrunt configuration template includes a local block that utilizes the find_in_parent_folders function, which leads to Issues following Terraform version 0.55.1.
```
locals {
  account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl"))
  region_vars  = read_terragrunt_config(find_in_parent_folders("regional.hcl"))
  stage_vars   = read_terragrunt_config(find_in_parent_folders("stage.hcl"))
}
```
Error:

ERRO[0001] Error: Error in function call
ERRO[0001]   on saas-test1/infrastructure/template/main/general/stage/network/terragrunt.hcl line 6, in locals: 
ERRO[0001]    6:   account_vars = read_terragrunt_config(find_in_parent_folders("account.hcl")) 
ERRO[0001] Call to function "find_in_parent_folders" failed: ParentFileNotFoundError: Could not find an account.hcl in any of the parent folders of saas-test1/infrastructure/template/main/general/stage/network/terragrunt.hcl. Traversed all the way to the root.

Config files are located here:
```
saas-test1/infrastructure/accounts/qa-dev/eu-central-1/devx/stage.hcl
saas-test1/infrastructure/accounts/qa-dev/eu-central-1/regional.hcl
saas-test1/infrastructure/accounts/qa-dev/account.hcl
```
Could someone please assist in resolving this?

Thanks for your help!@2xburnt: same problem in 0.67.4 find_in_parent_folders searches the path relative to the current *.hcl file not the originating terragrunt.hcl@ErezWeiss: same here - did you solve it?@masonhuemmer: same issue

Solution patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 9e03dbb3..ad353b3b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -11,7 +11,7 @@ env: &env
 
 defaults: &defaults
   docker:
-    - image: 087285199408.dkr.ecr.us-east-1.amazonaws.com/circle-ci-test-image-base:go1.21-tf1.5-tg39.1-pck1.8-ci50.7
+    - image: 087285199408.dkr.ecr.us-east-1.amazonaws.com/circle-ci-test-image-base:go1.21.7-tf1.5-tg39.1-pck1.8-ci50.7
 
 install_tofu: &install_tofu
   name: Install OpenTofu
diff --git a/cli/app.go b/cli/app.go
index 78cf5882..589d265a 100644
--- a/cli/app.go
+++ b/cli/app.go
@@ -1,6 +1,7 @@
 package cli
 
 import (
+	"context"
 	"fmt"
 	"io"
 	"os"
@@ -8,6 +9,8 @@ import (
 	"sort"
 	"strings"
 
+	"github.com/gruntwork-io/terragrunt/telemetry"
+
 	"github.com/gruntwork-io/terragrunt/cli/commands/graph"
 
 	"github.com/gruntwork-io/terragrunt/cli/commands/scaffold"
@@ -62,8 +65,8 @@ func NewApp(writer io.Writer, errWriter io.Writer) *cli.App {
 		deprecatedCommands(opts),
 		terragruntCommands(opts)...)
 	app.Before = beforeAction(opts)
-	app.CommonBefore = initialSetup(opts)           // all commands run this function before running their own `Action` function
-	app.DefaultCommand = terraform.NewCommand(opts) // by default, if no terragrunt command is specified, run the Terraform command
+	app.CommonBefore = initialSetup(opts)                                   // all commands run this function before running their own `Action` function
+	app.DefaultCommand = telemetryCommand(opts, terraform.NewCommand(opts)) // by default, if no terragrunt command is specified, run the Terraform command
 	app.OsExiter = osExiter
 
 	return app
@@ -72,29 +75,49 @@ func NewApp(writer io.Writer, errWriter io.Writer) *cli.App {
 // This set of commands is also used in unit tests
 func terragruntCommands(opts *options.TerragruntOptions) cli.Commands {
 	cmds := cli.Commands{
-		runall.NewCommand(opts),             // run-all
-		terragruntinfo.NewCommand(opts),     // terragrunt-info
-		validateinputs.NewCommand(opts),     // validate-inputs
-		graphdependencies.NewCommand(opts),  // graph-dependencies
-		hclfmt.NewCommand(opts),             // hclfmt
-		renderjson.NewCommand(opts),         // render-json
-		awsproviderpatch.NewCommand(opts),   // aws-provider-patch
-		outputmodulegroups.NewCommand(opts), // output-module-groups
-		catalog.NewCommand(opts),            // catalog
-		scaffold.NewCommand(opts),           // scaffold
-		graph.NewCommand(opts),              // graph
+		telemetryCommand(opts, runall.NewCommand(opts)),             // run-all
+		telemetryCommand(opts, terragruntinfo.NewCommand(opts)),     // terragrunt-info
+		telemetryCommand(opts, validateinputs.NewCommand(opts)),     // validate-inputs
+		telemetryCommand(opts, graphdependencies.NewCommand(opts)),  // graph-dependencies
+		telemetryCommand(opts, hclfmt.NewCommand(opts)),             // hclfmt
+		telemetryCommand(opts, renderjson.NewCommand(opts)),         // render-json
+		telemetryCommand(opts, awsproviderpatch.NewCommand(opts)),   // aws-provider-patch
+		telemetryCommand(opts, outputmodulegroups.NewCommand(opts)), // output-module-groups
+		telemetryCommand(opts, catalog.NewCommand(opts)),            // catalog
+		telemetryCommand(opts, scaffold.NewCommand(opts)),           // scaffold
+		telemetryCommand(opts, graph.NewCommand(opts)),              // graph
 	}
 
 	sort.Sort(cmds)
 
 	// add terraform command `*` after sorting to put the command at the end of the list in the help.
-	cmds.Add(terraform.NewCommand(opts))
+	cmds.Add(telemetryCommand(opts, terraform.NewCommand(opts)))
 
 	return cmds
 }
 
+// Wrap CLI command execution with setting of telemetry context and labels, if telemetry is disabled, just run the command.
+func telemetryCommand(opts *options.TerragruntOptions, cmd *cli.Command) *cli.Command {
+	fn := cmd.Action
+	cmd.Action = func(ctx *cli.Context) error {
+		return telemetry.Telemetry(opts, fmt.Sprintf("%s %s", ctx.Command.Name, opts.TerraformCommand), map[string]interface{}{
+			"terraformCommand": opts.TerraformCommand,
+			"args":             opts.TerraformCliArgs,
+			"dir":              opts.WorkingDir,
+		}, func(childCtx context.Context) error {
+			ctx.Context = childCtx
+			opts.CtxTelemetryCtx = childCtx
+			return fn(ctx)
+		})
+	}
+
+	return cmd
+}
+
 func beforeAction(opts *options.TerragruntOptions) func(ctx *cli.Context) error {
 	return func(ctx *cli.Context) error {
+		// setting current context to the options
+		opts.CtxTelemetryCtx = ctx
 		// show help if the args are not specified.
 		if !ctx.Args().Present() {
 			err := cli.ShowAppHelp(ctx)
diff --git a/cli/commands/catalog/module/repo.go b/cli/commands/catalog/module/repo.go
index e269943e..34c1eb9f 100644
--- a/cli/commands/catalog/module/repo.go
+++ b/cli/commands/catalog/module/repo.go
@@ -192,7 +192,7 @@ func (repo *Repo) clone(ctx context.Context) error {
 	}
 	repo.cloneURL = sourceUrl.String()
 
-	log.Infof("Cloning repository %q to temprory directory %q", repo.cloneURL, repo.path)
+	log.Infof("Cloning repository %q to temporary directory %q", repo.cloneURL, repo.path)
 
 	if err := getter.Get(repo.path, strings.Trim(sourceUrl.String(), "/"), getter.WithContext(ctx)); err != nil {
 		return errors.WithStackTrace(err)
diff --git a/cli/commands/terraform/action.go b/cli/commands/terraform/action.go
index c46865e1..637a54a6 100644
--- a/cli/commands/terraform/action.go
+++ b/cli/commands/terraform/action.go
@@ -1,6 +1,7 @@
 package terraform
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -11,6 +12,8 @@ import (
 	"sync"
 	"time"
 
+	"github.com/gruntwork-io/terragrunt/telemetry"
+
 	"github.com/gruntwork-io/terragrunt/terraform"
 
 	"github.com/gruntwork-io/gruntwork-cli/collections"
@@ -169,7 +172,13 @@ func runTerraform(terragruntOptions *options.TerragruntOptions, target *Target)
 	}
 
 	if sourceUrl != "" {
-		updatedTerragruntOptions, err = downloadTerraformSource(sourceUrl, terragruntOptions, terragruntConfig)
+		err = telemetry.Telemetry(terragruntOptions, "download_terraform_source", map[string]interface{}{
+			"sourceUrl": sourceUrl,
+		}, func(childCtx context.Context) error {
+			updatedTerragruntOptions, err = downloadTerraformSource(sourceUrl, terragruntOptions, terragruntConfig)
+			return err
+		})
+
 		if err != nil {
 			return err
 		}
diff --git a/cli/commands/terraform/hook.go b/cli/commands/terraform/hook.go
index 6db55b4b..2e6ba45e 100644
--- a/cli/commands/terraform/hook.go
+++ b/cli/commands/terraform/hook.go
@@ -1,10 +1,13 @@
 package terraform
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"sync"
 
+	"github.com/gruntwork-io/terragrunt/telemetry"
+
 	"github.com/gruntwork-io/terragrunt/config"
 	"github.com/gruntwork-io/terragrunt/options"
 	"github.com/gruntwork-io/terragrunt/shell"
@@ -88,7 +91,12 @@ func processHooks(hooks []config.Hook, terragruntOptions *options.TerragruntOpti
 	for _, curHook := range hooks {
 		allPreviousErrors := multierror.Append(previousExecErrors, errorsOccured)
 		if shouldRunHook(curHook, terragruntOptions, allPreviousErrors) {
-			err := runHook(terragruntOptions, terragruntConfig, curHook)
+			err := telemetry.Telemetry(terragruntOptions, fmt.Sprintf("hook_%s", curHook.Name), map[string]interface{}{
+				"hook": curHook.Name,
+				"dir":  curHook.WorkingDir,
+			}, func(childCtx context.Context) error {
+				return runHook(terragruntOptions, terragruntConfig, curHook)
+			})
 			if err != nil {
 				errorsOccured = multierror.Append(errorsOccured, err)
 			}
diff --git a/cli/deprecated_commands.go b/cli/deprecated_commands.go
index 65526f9b..2594aee1 100644
--- a/cli/deprecated_commands.go
+++ b/cli/deprecated_commands.go
@@ -72,7 +72,7 @@ func deprecatedCommands(opts *options.TerragruntOptions) cli.Commands {
 			Hidden: true,
 			Action: runFunc(opts),
 		}
-		commands = append(commands, command)
+		commands = append(commands, telemetryCommand(opts, command))
 	}
 
 	return commands
diff --git a/config/config.go b/config/config.go
index fd0611ac..55525d41 100644
--- a/config/config.go
+++ b/config/config.go
@@ -9,6 +9,8 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/gruntwork-io/terragrunt/telemetry"
+
 	"github.com/mitchellh/mapstructure"
 
 	"github.com/hashicorp/go-getter"
@@ -49,6 +51,7 @@ const (
 	MetadataIamAssumeRoleSessionName    = "iam_assume_role_session_name"
 	MetadataInputs                      = "inputs"
 	MetadataLocals                      = "locals"
+	MetadataLocal                       = "local"
 	MetadataCatalog                     = "catalog"
 	MetadataGenerateConfigs             = "generate"
 	MetadataRetryableErrors             = "retryable_errors"
@@ -662,23 +665,32 @@ func ReadTerragruntConfig(terragruntOptions *options.TerragruntOptions) (*Terrag
 	terragruntOptions.Logger.Debugf("Reading Terragrunt config file at %s", terragruntOptions.TerragruntConfigPath)
 
 	ctx := NewParsingContext(context.Background(), terragruntOptions)
-	return ParseConfigFile(ctx, terragruntOptions.TerragruntConfigPath, nil)
+	return ParseConfigFile(terragruntOptions, ctx, terragruntOptions.TerragruntConfigPath, nil)
 }
 
 // Parse the Terragrunt config file at the given path. If the include parameter is not nil, then treat this as a config
 // included in some other config file when resolving relative paths.
-func ParseConfigFile(ctx *ParsingContext, configPath string, includeFromChild *IncludeConfig) (*TerragruntConfig, error) {
-	// Parse the HCL file into an AST body that can be decoded multiple times later without having to re-parse
-	file, err := hclparse.NewParser().WithOptions(ctx.ParserOptions...).ParseFromFile(configPath)
-	if err != nil {
-		return nil, err
-	}
+func ParseConfigFile(opts *options.TerragruntOptions, ctx *ParsingContext, configPath string, includeFromChild *IncludeConfig) (*TerragruntConfig, error) {
+
+	var config *TerragruntConfig
+	err := telemetry.Telemetry(opts, "parse_config_file", map[string]interface{}{
+		"config_path": configPath,
+	}, func(childCtx context.Context) error {
+		// Parse the HCL file into an AST body that can be decoded multiple times later without having to re-parse
+		file, err := hclparse.NewParser().WithOptions(ctx.ParserOptions...).ParseFromFile(configPath)
+		if err != nil {
+			return err
+		}
 
-	config, err := ParseConfig(ctx, file, includeFromChild)
+		config, err = ParseConfig(ctx, file, includeFromChild)
+		if err != nil {
+			return err
+		}
+		return nil
+	})
 	if err != nil {
 		return nil, err
 	}
-
 	return config, nil
 }
 
diff --git a/config/config_helpers.go b/config/config_helpers.go
index 273e9723..f6665f04 100644
--- a/config/config_helpers.go
+++ b/config/config_helpers.go
@@ -188,11 +188,11 @@ func createTerragruntEvalContext(ctx *ParsingContext, configPath string) (*hcl.E
 	}
 	evalCtx.Variables = map[string]cty.Value{}
 	if ctx.Locals != nil {
-		evalCtx.Variables["local"] = *ctx.Locals
+		evalCtx.Variables[MetadataLocal] = *ctx.Locals
 	}
 
 	if ctx.DecodedDependencies != nil {
-		evalCtx.Variables["dependency"] = *ctx.DecodedDependencies
+		evalCtx.Variables[MetadataDependency] = *ctx.DecodedDependencies
 	}
 	if ctx.TrackInclude != nil && len(ctx.TrackInclude.CurrentList) > 0 {
 		// For each include block, check if we want to expose the included config, and if so, add under the include
@@ -516,7 +516,7 @@ func getWorkingDir(ctx *ParsingContext) (string, error) {
 		FuncNameGetWorkingDir: wrapVoidToEmptyStringAsFuncImpl(),
 	}
 
-	terragruntConfig, err := ParseConfigFile(ctx, ctx.TerragruntOptions.TerragruntConfigPath, nil)
+	terragruntConfig, err := ParseConfigFile(ctx.TerragruntOptions, ctx, ctx.TerragruntOptions.TerragruntConfigPath, nil)
 	if err != nil {
 		return "", err
 	}
@@ -591,7 +591,7 @@ func readTerragruntConfig(ctx *ParsingContext, configPath string, defaultVal *ct
 
 	// We update the ctx of terragruntOptions to the config being read in.
 	ctx = ctx.WithTerragruntOptions(ctx.TerragruntOptions.Clone(targetConfig))
-	config, err := ParseConfigFile(ctx, targetConfig, nil)
+	config, err := ParseConfigFile(ctx.TerragruntOptions, ctx, targetConfig, nil)
 	if err != nil {
 		return cty.NilVal, err
 	}
diff --git a/config/config_partial.go b/config/config_partial.go
index 9149a754..2a4430f8 100644
--- a/config/config_partial.go
+++ b/config/config_partial.go
@@ -293,6 +293,15 @@ func PartialParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChi
 		case TerragruntInputs:
 			decoded := terragruntInputs{}
 
+			if _, ok := evalParsingContext.Variables[MetadataDependency]; !ok {
+				// Decode just the `dependency` blocks, retrieving the outputs from the target terragrunt config in the process.
+				retrievedOutputs, err := decodeAndRetrieveOutputs(ctx, file)
+				if err != nil {
+					return nil, err
+				}
+				evalParsingContext.Variables[MetadataDependency] = *retrievedOutputs
+			}
+
 			if err := file.Decode(&decoded, evalParsingContext); err != nil {
 				diagErr, ok := errors.Unwrap(err).(hcl.Diagnostics)
 
diff --git a/config/dependency.go b/config/dependency.go
index f51218f4..1f4fed87 100644
--- a/config/dependency.go
+++ b/config/dependency.go
@@ -177,6 +177,10 @@ func decodeAndRetrieveOutputs(ctx *ParsingContext, file *hclparse.File) (*cty.Va
 		return nil, err
 	}
 
+	if err := checkForDependencyBlockCycles(ctx, file.ConfigPath, decodedDependency); err != nil {
+		return nil, err
+	}
+
 	// Mark skipped dependencies as disabled
 	updatedDependencies := terragruntDependency{}
 	for _, dep := range decodedDependency.Dependencies {
@@ -186,8 +190,7 @@ func decodeAndRetrieveOutputs(ctx *ParsingContext, file *hclparse.File) (*cty.Va
 				WithDecodeList(TerragruntFlags, TerragruntInputs).
 				WithTerragruntOptions(cloneTerragruntOptionsForDependency(ctx, depPath))
 
-			depConfig, err := PartialParseConfigFile(depCtx, depPath, nil)
-			if err == nil {
+			if depConfig, err := PartialParseConfigFile(depCtx, depPath, nil); err == nil {
 				if depConfig.Skip {
 					ctx.TerragruntOptions.Logger.Debugf("Skipping outputs reading for disabled dependency %s", dep.Name)
 					dep.Enabled = new(bool)
@@ -200,7 +203,7 @@ func decodeAndRetrieveOutputs(ctx *ParsingContext, file *hclparse.File) (*cty.Va
 				dep.Inputs = &inputsCty
 
 			} else {
-				ctx.TerragruntOptions.Logger.Warnf("Error reading partial config  for dependency %s: %v", dep.Name, err)
+				ctx.TerragruntOptions.Logger.Warnf("Error reading partial config for dependency %s: %v", dep.Name, err)
 			}
 		}
 
@@ -217,10 +220,6 @@ func decodeAndRetrieveOutputs(ctx *ParsingContext, file *hclparse.File) (*cty.Va
 		decodedDependency = *mergedDecodedDependency
 	}
 
-	if err := checkForDependencyBlockCycles(ctx, file.ConfigPath, decodedDependency); err != nil {
-		return nil, err
-	}
-
 	return dependencyBlocksToCtyValue(ctx, decodedDependency.Dependencies)
 }
 
diff --git a/config/include.go b/config/include.go
index 9bf96073..760013be 100644
--- a/config/include.go
+++ b/config/include.go
@@ -89,7 +89,7 @@ func parseIncludedConfig(ctx *ParsingContext, includedConfig *IncludeConfig) (*T
 		return PartialParseConfigFile(ctx, includePath, includedConfig)
 	}
 
-	return ParseConfigFile(ctx, includePath, includedConfig)
+	return ParseConfigFile(ctx.TerragruntOptions, ctx, includePath, includedConfig)
 }
 
 // handleInclude merges the included config into the current config depending on the merge strategy specified by the
diff --git a/config/parsing_context.go b/config/parsing_context.go
index d984698f..32e52322 100644
--- a/config/parsing_context.go
+++ b/config/parsing_context.go
@@ -25,8 +25,7 @@ type ParsingContext struct {
 
 	// DecodedDependencies are references of other terragrunt config. This contains the following attributes that map to
 	// various fields related to that config:
-	// - outputs: The map of outputs from the terraform state obtained by running `terragrunt output` on that target
-	//            config.
+	// - outputs: The map of outputs from the terraform state obtained by running `terragrunt output` on that target config.
 	DecodedDependencies *cty.Value
 
 	// PartialParseDecodeList is the list of sections that are being decoded in the current config. This can be used to
diff --git a/configstack/running_module.go b/configstack/running_module.go
index b7e36df5..0d5c0f08 100644
--- a/configstack/running_module.go
+++ b/configstack/running_module.go
@@ -1,9 +1,14 @@
 package configstack
 
 import (
+	"context"
 	"fmt"
 	"sync"
 
+	"github.com/gruntwork-io/terragrunt/options"
+
+	"github.com/gruntwork-io/terragrunt/telemetry"
+
 	"github.com/gruntwork-io/go-commons/errors"
 	"github.com/gruntwork-io/terragrunt/shell"
 	"github.com/hashicorp/go-multierror"
@@ -56,33 +61,33 @@ func newRunningModule(module *TerraformModule) *runningModule {
 // Run the given map of module path to runningModule. To "run" a module, execute the RunTerragrunt command in its
 // TerragruntOptions object. The modules will be executed in an order determined by their inter-dependencies, using
 // as much concurrency as possible.
-func RunModules(modules []*TerraformModule, parallelism int) error {
+func RunModules(opts *options.TerragruntOptions, modules []*TerraformModule, parallelism int) error {
 	runningModules, err := toRunningModules(modules, NormalOrder)
 	if err != nil {
 		return err
 	}
-	return runModules(runningModules, parallelism)
+	return runModules(opts, runningModules, parallelism)
 }
 
 // Run the given map of module path to runningModule. To "run" a module, execute the RunTerragrunt command in its
 // TerragruntOptions object. The modules will be executed in the reverse order of their inter-dependencies, using
 // as much concurrency as possible.
-func RunModulesReverseOrder(modules []*TerraformModule, parallelism int) error {
+func RunModulesReverseOrder(opts *options.TerragruntOptions, modules []*TerraformModule, parallelism int) error {
 	runningModules, err := toRunningModules(modules, ReverseOrder)
 	if err != nil {
 		return err
 	}
-	return runModules(runningModules, parallelism)
+	return runModules(opts, runningModules, parallelism)
 }
 
 // Run the given map of module path to runningModule. To "run" a module, execute the RunTerragrunt command in its
 // TerragruntOptions object. The modules will be executed without caring for inter-dependencies.
-func RunModulesIgnoreOrder(modules []*TerraformModule, parallelism int) error {
+func RunModulesIgnoreOrder(opts *options.TerragruntOptions, modules []*TerraformModule, parallelism int) error {
 	runningModules, err := toRunningModules(modules, IgnoreOrder)
 	if err != nil {
 		return err
 	}
-	return runModules(runningModules, parallelism)
+	return runModules(opts, runningModules, parallelism)
 }
 
 // Convert the list of modules to a map from module path to a runningModule struct. This struct contains information
@@ -163,7 +168,7 @@ func removeFlagExcluded(modules map[string]*runningModule) map[string]*runningMo
 // Run the given map of module path to runningModule. To "run" a module, execute the RunTerragrunt command in its
 // TerragruntOptions object. The modules will be executed in an order determined by their inter-dependencies, using
 // as much concurrency as possible.
-func runModules(modules map[string]*runningModule, parallelism int) error {
+func runModules(opts *options.TerragruntOptions, modules map[string]*runningModule, parallelism int) error {
 	var waitGroup sync.WaitGroup
 	var semaphore = make(chan struct{}, parallelism) // Make a semaphore from a buffered channel
 
@@ -171,7 +176,7 @@ func runModules(modules map[string]*runningModule, parallelism int) error {
 		waitGroup.Add(1)
 		go func(module *runningModule) {
 			defer waitGroup.Done()
-			module.runModuleWhenReady(semaphore)
+			module.runModuleWhenReady(opts, semaphore)
 		}(module)
 	}
 
@@ -194,14 +199,26 @@ func collectErrors(modules map[string]*runningModule) error {
 }
 
 // Run a module once all of its dependencies have finished executing.
-func (module *runningModule) runModuleWhenReady(semaphore chan struct{}) {
-	err := module.waitForDependencies()
+func (module *runningModule) runModuleWhenReady(opts *options.TerragruntOptions, semaphore chan struct{}) {
+
+	err := telemetry.Telemetry(opts, "wait_for_module_ready", map[string]interface{}{
+		"path":             module.Module.Path,
+		"terraformCommand": module.Module.TerragruntOptions.TerraformCommand,
+	}, func(childCtx context.Context) error {
+		return module.waitForDependencies()
+	})
+
 	semaphore <- struct{}{} // Add one to the buffered channel. Will block if parallelism limit is met
 	defer func() {
 		<-semaphore // Remove one from the buffered channel
 	}()
 	if err == nil {
-		err = module.runNow()
+		err = telemetry.Telemetry(opts, "run_module", map[string]interface{}{
+			"path":             module.Module.Path,
+			"terraformCommand": module.Module.TerragruntOptions.TerraformCommand,
+		}, func(childCtx context.Context) error {
+			return module.runNow()
+		})
 	}
 	module.moduleFinished(err)
 }
diff --git a/configstack/stack.go b/configstack/stack.go
index c9e4e1a8..1aadf352 100644
--- a/configstack/stack.go
+++ b/configstack/stack.go
@@ -124,11 +124,11 @@ func (stack *Stack) Run(terragruntOptions *options.TerragruntOptions) error {
 
 	switch {
 	case terragruntOptions.IgnoreDependencyOrder:
-		return RunModulesIgnoreOrder(stack.Modules, terragruntOptions.Parallelism)
+		return RunModulesIgnoreOrder(terragruntOptions, stack.Modules, terragruntOptions.Parallelism)
 	case stackCmd == "destroy":
-		return RunModulesReverseOrder(stack.Modules, terragruntOptions.Parallelism)
+		return RunModulesReverseOrder(terragruntOptions, stack.Modules, terragruntOptions.Parallelism)
 	default:
-		return RunModules(stack.Modules, terragruntOptions.Parallelism)
+		return RunModules(terragruntOptions, stack.Modules, terragruntOptions.Parallelism)
 	}
 }
 
diff --git a/docs/_docs/02_features/debugging.md b/docs/_docs/02_features/debugging.md
index bb1af029..b043d6ff 100644
--- a/docs/_docs/02_features/debugging.md
+++ b/docs/_docs/02_features/debugging.md
@@ -136,3 +136,159 @@ in dependency and local variable resolution.
 https://github.com/gruntwork-io/terragrunt/blob/eb692a83bee285b0baaaf4b271c66230f99b6358/docs/_docs/02_features/debugging.md
 for thoughts on other potential features to implement.
 -->
+
+### OpenTelemetry integration
+
+Terragrunt can be configured to emit telemetry in [OpenTelemetry](https://opentelemetry.io/) format, traces and metrics.
+
+Concepts:
+* [OpenTelemetry](https://opentelemetry.io/)
+* [Traces](https://opentelemetry.io/docs/concepts/signals/traces/)
+* [Metrics](https://opentelemetry.io/docs/concepts/signals/metrics/)
+* [Jaeger](https://www.jaegertracing.io/)
+
+Tracing configuration:
+* `TERRAGRUNT_TELEMETRY_TRACE_EXPORTER` - traces exporter type to be used. Currently supported values are:
+  * `none` - no trace exporting, default value.
+  * `console` - to export traces to console as JSON
+  * `otlpHttp` - to export traces to an OpenTelemetry collector over HTTP [otlptracehttp](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp)
+  * `otlpGrpc` - to export traces over gRPC [otlptracegrpc](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc)
+  * `http` - to export traces to a custom HTTP endpoint using [otlptracehttp](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp)
+* `TERRAGRUNT_TELEMERTY_TRACE_EXPORTER_HTTP_ENDPOINT` - in case of `http` exporter, this is the endpoint to which traces will be sent.
+* `TERRAGRUNT_TELEMERTY_TRACE_EXPORTER_INSECURE_ENDPOINT` - if set to true, the exporter will not validate the server's certificate, helpful for local traces collection.
+
+Metrics configuration:
+* `TERRAGRUNT_TELEMETRY_METRIC_EXPORTER` - metrics exporter type to be used. Currently supported values are:
+  * `none` - no metric exporting, default value.
+  * `console` - write metrics to console as JSONs.
+  * `otlpHttp` - export metrics to an OpenTelemetry collector over HTTP [otlpmetrichttp](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp)
+  * `grpcHttp` - export metrics to an OpenTelemetry collector over gRPC [otlpmetricgrpc](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc)
+* `TERRAGRUNT_TELEMERTY_METRIC_EXPORTER_INSECURE_ENDPOINT` - if set to true, the exporter will not validate the server's certificate, helpful for local metrics collection.
+
+## Example configurations for trace collection
+
+Collection of examples how to configure Terragrunt to emit traces and metrics in OpenTelemetry format.
+
+### Example traces collection with Jaeger
+* Start a Jaeger instance with docker:
+```bash
+docker run --rm --name jaeger -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:1.54.0
+```
+* Verify that UI is available at http://localhost:16686/
+* Define environment variables for Terragrunt to report traces to Jaeger:
+```bash
+export TERRAGRUNT_TELEMETRY_TRACE_EXPORTER=http
+export TERRAGRUNT_TELEMERTY_TRACE_EXPORTER_HTTP_ENDPOINT=localhost:4318
+export TERRAGRUNT_TELEMERTY_TRACE_EXPORTER_INSECURE_ENDPOINT=true
+```
+* Run terragrunt
+* Verify that traces are available in Jaeger UI
+
+### Configurations to collect traces in Grafana Tempo
+
+* Start a Grafana Tempo instance [example](https://grafana.com/docs/tempo/latest/getting-started/docker-example/)
+* Define environment variables for Terragrunt to report traces to Tempo:
+```bash
+export TERRAGRUNT_TELEMETRY_TRACE_EXPORTER=otlpHttp
+# Replace with your tempo instance
+export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
+export TERRAGRUNT_TELEMERTY_TRACE_EXPORTER_INSECURE_ENDPOINT=true
+````
+* Run terragrunt
+* Check for traces in Tempo UI for service "terragrunt"
+
+### Example traces collection in console
+* Set env variable to enable telemetry:
+```bash
+export TERRAGRUNT_TELEMETRY_TRACE_EXPORTER=console
+```
+* Run terragrunt
+* Check produced traces in console, like:
+```json
+{"Name":"run_bash","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"f91587247524593b","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:30.564217484Z","EndTime":"2024-02-08T12:32:31.570666395Z","Attributes":[{"Key":"command","Value":{"Type":"STRING","Value":"bash"}},{"Key":"args","Value":{"Type":"STRING","Value":"[-c sleep 1]"}},{"Key":"dir","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod2"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":0,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}}
+{"Name":"parse_config_file","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"d2823047fb469bdf","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:30.380054129Z","EndTime":"2024-02-08T12:32:31.570899286Z","Attributes":[{"Key":"config_path","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod2/terragrunt.hcl"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":0,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}}
+{"Name":"run_terraform","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"152d873a18559f07","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:31.57161757Z","EndTime":"2024-02-08T12:32:31.688157882Z","Attributes":[{"Key":"command","Value":{"Type":"STRING","Value":"terraform"}},{"Key":"args","Value":{"Type":"STRING","Value":"[init]"}},{"Key":"dir","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod2"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":0,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}}
+{"Name":"run_terraform","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"29341bdb65f66b1e","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:31.688240673Z","EndTime":"2024-02-08T12:32:31.793377642Z","Attributes":[{"Key":"command","Value":{"Type":"STRING","Value":"terraform"}},{"Key":"args","Value":{"Type":"STRING","Value":"[apply -auto-approve -input=false]"}},{"Key":"dir","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod2"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":0,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}}
+{"Name":"run_module","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"8a01522bc65e0f1b","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:30.290680776Z","EndTime":"2024-02-08T12:32:31.793392803Z","Attributes":[{"Key":"path","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod2"}},{"Key":"terraformCommand","Value":{"Type":"STRING","Value":"apply"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":0,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}}
+{"Name":"run-all apply","SpanContext":{"TraceID":"bdf3cb9078706b7f0b4f1d92428eedc0","SpanID":"b0b007770f852066","TraceFlags":"01","TraceState":"","Remote":false},"Parent":{"TraceID":"00000000000000000000000000000000","SpanID":"0000000000000000","TraceFlags":"00","TraceState":"","Remote":false},"SpanKind":1,"StartTime":"2024-02-08T12:32:26.388519019Z","EndTime":"2024-02-08T12:32:31.793405603Z","Attributes":[{"Key":"terraformCommand","Value":{"Type":"STRING","Value":"apply"}},{"Key":"args","Value":{"Type":"STRING","Value":"[apply]"}},{"Key":"dir","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test"}}],"Events":null,"Links":null,"Status":{"Code":"Unset","Description":""},"DroppedAttributes":0,"DroppedEvents":0,"DroppedLinks":0,"ChildSpanCount":28,"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-29-g66bfa07b756e-dirty"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.0"}}],"InstrumentationLibrary":{"Name":"terragrunt","Version":"","SchemaURL":""}}
+```
+
+### Collection of metrics with OpenTelemetry collector and Prometheus
+
+* Start OpenTelemetry collector with Prometheus receiver
+Example setup through `docker-compose.yml`:
+```yaml
+version: '3'
+services:
+  otel-collector:
+    image: otel/opentelemetry-collector:0.94.0
+    volumes:
+      - ./otel-collector-config.yaml:/etc/otelcol/config.yaml
+    ports:
+      - "4317:4317" # OTLP gRPC receiver
+      - "4318:4318" # OTLP HTTP receiver
+      - "8889:8889" # Prometheus exporter
+  prometheus:
+    image: prom/prometheus:v2.45.3
+    volumes:
+      - ./prometheus.yml:/etc/prometheus/prometheus.yml
+    ports:
+      - "9090:9090"
+    depends_on:
+      - otel-collector
+```
+OpenTelemetry collector configuration `otel-collector-config.yaml`:
+```yaml
+receivers:
+  otlp:
+    protocols:
+      grpc:
+      http:
+
+processors:
+  batch:
+
+exporters:
+  prometheus:
+    endpoint: "0.0.0.0:8889" # Prometheus exporter endpoint
+
+service:
+  pipelines:
+    metrics:
+      receivers: [otlp]
+      processors: [batch]
+      exporters: [prometheus]
+```
+Prometheus configuration file `prometheus.yml`:
+```yaml
+global:
+  scrape_interval: 15s
+
+scrape_configs:
+  - job_name: 'opentelemetry'
+    scrape_interval: 5s
+    static_configs:
+      - targets: ['otel-collector:8889']
+
+```
+* Confirm that Prometheus is available at http://localhost:9090/
+* Define environment variables for Terragrunt to report metrics to OpenTelemetry collector:
+```bash
+export TERRAGRUNT_TELEMETRY_METRIC_EXPORTER=grpcHttp
+export TERRAGRUNT_TELEMERTY_METRIC_EXPORTER_INSECURE_ENDPOINT=true
+export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
+```
+* Run terragrunt
+* Verify that metrics are available in Prometheus UI
+
+Example configuration to export metrics to console:
+* Set env variable to enable telemetry:
+```bash
+export TERRAGRUNT_TELEMETRY_METRIC_EXPORTER=console
+```
+* Run terragrunt
+* In output will be printed messages like:
+```json
+{"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-41-g7185318bb11b"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.1"}}],"ScopeMetrics":[]}
+{"Resource":[{"Key":"service.name","Value":{"Type":"STRING","Value":"terragrunt"}},{"Key":"service.version","Value":{"Type":"STRING","Value":"v0.55.0-41-g7185318bb11b"}},{"Key":"telemetry.sdk.language","Value":{"Type":"STRING","Value":"go"}},{"Key":"telemetry.sdk.name","Value":{"Type":"STRING","Value":"opentelemetry"}},{"Key":"telemetry.sdk.version","Value":{"Type":"STRING","Value":"1.23.1"}}],"ScopeMetrics":[{"Scope":{"Name":"terragrunt","Version":"","SchemaURL":""},"Metrics":[{"Name":"run_bash_duration","Description":"","Unit":"","Data":{"DataPoints":[{"Attributes":[{"Key":"args","Value":{"Type":"STRING","Value":"[-c sleep 2]"}},{"Key":"command","Value":{"Type":"STRING","Value":"bash"}},{"Key":"dir","Value":{"Type":"STRING","Value":"/projects/gruntwork/terragrunt-tests/trace-test/mod3"}}],"StartTime":"2024-02-12T14:38:14.85578658Z","Time":"2024-02-12T14:38:17.853165589Z","Count":1,"Bounds":[0,5,10,25,50,75,100,250,500,750,1000,2500,5000,7500,10000],"BucketCounts":[0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],"Min":2005,"Max":2005,"Sum":2005}],"Temporality":"CumulativeTemporality"}},{"Name":"run_bash_success_count","Description":"","Unit":"","Data":{"DataPoints":[{"Attributes":[],"StartTime":"2024-02-12T14:38:16.860878555Z","Time":"2024-02-12T14:38:17.853169359Z","Value":1}],"Temporality":"CumulativeTemporality","IsMonotonic":true}}]}]}
+```
diff --git a/go.mod b/go.mod
index 26020c6c..6e4617bf 100644
--- a/go.mod
+++ b/go.mod
@@ -30,14 +30,14 @@ require (
 	github.com/zclconf/go-cty v1.13.2
 	go.mozilla.org/sops/v3 v3.7.3
 	golang.org/x/crypto v0.17.0 // indirect
-	golang.org/x/oauth2 v0.13.0
-	golang.org/x/sync v0.4.0
-	golang.org/x/sys v0.15.0
+	golang.org/x/oauth2 v0.15.0
+	golang.org/x/sync v0.5.0
+	golang.org/x/sys v0.16.0
 	google.golang.org/api v0.149.0
 )
 
 require (
-	cloud.google.com/go v0.110.10 // indirect
+	cloud.google.com/go v0.111.0 // indirect
 	github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/snappy v0.0.4 // indirect
@@ -52,7 +52,7 @@ require (
 	github.com/terraform-linters/tflint v0.47.0
 	github.com/ulikunitz/xz v0.5.11 // indirect
 	golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
-	google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
+	google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect
 )
 
 require (
@@ -65,17 +65,29 @@ require (
 	github.com/gruntwork-io/go-commons v0.17.1
 	github.com/gruntwork-io/gruntwork-cli v0.7.0
 	github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
+	github.com/pkg/errors v0.9.1
 	github.com/posener/complete v1.2.3
 	github.com/urfave/cli/v2 v2.25.5
+	go.opentelemetry.io/otel v1.23.1
+	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1
+	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0
+	go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.23.1
+	go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.22.0
+	go.opentelemetry.io/otel/metric v1.23.1
+	go.opentelemetry.io/otel/sdk v1.23.1
+	go.opentelemetry.io/otel/sdk/metric v1.23.1
+	go.opentelemetry.io/otel/trace v1.23.1
 	golang.org/x/term v0.15.0
 	golang.org/x/text v0.14.0
 	gopkg.in/ini.v1 v1.67.0
 )
 
 require (
-	cloud.google.com/go/compute v1.23.1 // indirect
+	cloud.google.com/go/compute v1.23.3 // indirect
 	cloud.google.com/go/compute/metadata v0.2.3 // indirect
-	cloud.google.com/go/iam v1.1.3 // indirect
+	cloud.google.com/go/iam v1.1.5 // indirect
 	filippo.io/age v1.0.0 // indirect
 	github.com/AlecAivazis/survey/v2 v2.3.4 // indirect
 	github.com/Azure/azure-sdk-for-go v63.3.0+incompatible // indirect
@@ -109,6 +121,7 @@ require (
 	github.com/bmatcuk/doublestar v1.1.5 // indirect
 	github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
 	github.com/cenkalti/backoff/v3 v3.2.2 // indirect
+	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
 	github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
@@ -117,6 +130,8 @@ require (
 	github.com/docker/go-units v0.5.0 // indirect
 	github.com/fatih/color v1.15.0 // indirect
 	github.com/frankban/quicktest v1.14.5 // indirect
+	github.com/go-logr/logr v1.4.1 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect
 	github.com/go-sql-driver/mysql v1.5.0 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
@@ -132,6 +147,7 @@ require (
 	github.com/gookit/color v1.5.0 // indirect
 	github.com/gorilla/css v1.0.0 // indirect
 	github.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-hclog v1.5.0 // indirect
 	github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
@@ -154,7 +170,6 @@ require (
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/jstemmer/go-junit-report v1.0.0 // indirect
 	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
-	github.com/kr/pretty v0.3.1 // indirect
 	github.com/lib/pq v1.10.5 // indirect
 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
@@ -175,7 +190,6 @@ require (
 	github.com/olekukonko/tablewriter v0.0.5 // indirect
 	github.com/owenrumney/go-sarif v1.1.1 // indirect
 	github.com/pierrec/lz4 v2.6.1+incompatible // indirect
-	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/pterm/pterm v0.12.41 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
@@ -198,16 +212,17 @@ require (
 	github.com/zclconf/go-cty-yaml v1.0.3 // indirect
 	go.mozilla.org/gopgagent v0.0.0-20170926210634-4d7ea76ff71a // indirect
 	go.opencensus.io v0.24.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.1.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
 	golang.org/x/mod v0.12.0 // indirect
-	golang.org/x/net v0.17.0 // indirect
-	golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
-	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
-	google.golang.org/grpc v1.59.0 // indirect
-	google.golang.org/protobuf v1.31.0 // indirect
+	golang.org/x/net v0.19.0 // indirect
+	google.golang.org/appengine v1.6.8 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect
+	google.golang.org/grpc v1.61.0 // indirect
+	google.golang.org/protobuf v1.32.0 // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
 	gopkg.in/urfave/cli.v1 v1.20.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index 0b8220f8..d305928f 100644
--- a/go.sum
+++ b/go.sum
@@ -32,8 +32,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9
 cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
 cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
 cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
-cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
-cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic=
+cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM=
+cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU=
 cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
 cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=
 cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=
@@ -70,8 +70,8 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz
 cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
 cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
 cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
-cloud.google.com/go/compute v1.23.1 h1:V97tBoDaZHb6leicZ1G6DLK2BAaZLJ/7+9BB/En3hR0=
-cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78=
+cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
+cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
 cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
 cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
 cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=
@@ -111,8 +111,8 @@ cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y97
 cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=
 cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
 cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc=
-cloud.google.com/go/iam v1.1.3 h1:18tKG7DzydKWUnLjonWcJO6wjSCAtzh4GcRKlH/Hrzc=
-cloud.google.com/go/iam v1.1.3/go.mod h1:3khUlaBXfPKKe7huYgEpDn6FtgRyMEqbkvBxrQyY5SE=
+cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
+cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
 cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
 cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=
 cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=
@@ -346,6 +346,8 @@ github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH
 github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
 github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M=
 github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -451,6 +453,11 @@ github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9p
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
+github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
 github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
 github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
 github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
@@ -615,6 +622,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
 github.com/gruntwork-io/boilerplate v0.5.7 h1:Is6idcUj+HklxCtho+6K3cJde3tNUnVOqWlEWZr6ZwM=
 github.com/gruntwork-io/boilerplate v0.5.7/go.mod h1:MNsJ5fJSld7HMOHtORL6C8JvKE1ySSq4RjQREU5MwZQ=
 github.com/gruntwork-io/go-commons v0.17.1 h1:2KS9wAqrgeOTWj33DSHzDNJ1FCprptWdLFqej+wB8x0=
@@ -976,8 +985,9 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -1111,10 +1121,38 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
 go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/otel v1.23.1 h1:Za4UzOqJYS+MUczKI320AtqZHZb7EqxO00jAHE0jmQY=
+go.opentelemetry.io/otel v1.23.1/go.mod h1:Td0134eafDLcTS4y+zQ26GE8u3dEuRBiBCTUIRHaikA=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1 h1:ZqRWZJGHXV/1yCcEEVJ6/Uz2JtM79DNS8OZYa3vVY/A=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1/go.mod h1:D7ynngPWlGJrqyGSDOdscuv7uqttfCE3jcBvffDv9y4=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1 h1:q/Nj5/2TZRIt6PderQ9oU0M00fzoe8UZuINGw6ETGTw=
+go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.23.1/go.mod h1:DTE9yAu6r08jU3xa68GiSeI7oRcSEQ2RpKbbQGO+dWM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.0 h1:D/cXD+03/UOphyyT87NX6h+DlU+BnplN6/P6KJwsgGc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.23.0/go.mod h1:L669qRGbPBwLcftXLFnTVFO6ES/GyMAvITLdvRjEAIM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.0 h1:VZrBiTXzP3FErizsdF1JQj0qf0yA8Ktt6LAcjUhZqbc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.0/go.mod h1:xkkwo777b9MEfsyD1yUZa4g+7MCqqWAP3r2tTSZePRc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.23.1 h1:C8r95vDR125t815KD+b1tI0Fbc1pFnwHTBxkbIZ6Szc=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.23.1/go.mod h1:Qr0qomr64jentMtOjWMbtYeJMSuMSlsPEjmnRA2sWZ4=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.22.0 h1:zr8ymM5OWWjjiWRzwTfZ67c905+2TMHYp2lMJ52QTyM=
+go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.22.0/go.mod h1:sQs7FT2iLVJ+67vYngGJkPe1qr39IzaBzaj9IDNNY8k=
+go.opentelemetry.io/otel/metric v1.23.1 h1:PQJmqJ9u2QaJLBOELl1cxIdPcpbwzbkjfEyelTl2rlo=
+go.opentelemetry.io/otel/metric v1.23.1/go.mod h1:mpG2QPlAfnK8yNhNJAxDZruU9Y1/HubbC+KyH8FaCWI=
+go.opentelemetry.io/otel/sdk v1.23.1 h1:O7JmZw0h76if63LQdsBMKQDWNb5oEcOThG9IrxscV+E=
+go.opentelemetry.io/otel/sdk v1.23.1/go.mod h1:LzdEVR5am1uKOOwfBWFef2DCi1nu3SA8XQxx2IerWFk=
+go.opentelemetry.io/otel/sdk/metric v1.23.1 h1:T9/8WsYg+ZqIpMWwdISVVrlGb/N0Jr1OHjR/alpKwzg=
+go.opentelemetry.io/otel/sdk/metric v1.23.1/go.mod h1:8WX6WnNtHCgUruJ4TJ+UssQjMtpxkpX0zveQC8JG/E0=
+go.opentelemetry.io/otel/trace v1.23.1 h1:4LrmmEd8AU2rFvU1zegmvqW7+kWarxtNOPyeL6HmYY8=
+go.opentelemetry.io/otel/trace v1.23.1/go.mod h1:4IpnpJFwr1mo/6HL8XIPJaE9y0+u1KcVmuW7dwFSVrI=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
+go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1242,8 +1280,8 @@ golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfS
 golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1269,8 +1307,8 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri
 golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
 golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
 golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A=
-golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
-golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
+golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
+golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1285,8 +1323,8 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
-golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1375,8 +1413,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
-golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -1398,6 +1436,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
@@ -1530,8 +1569,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
 google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -1636,12 +1676,12 @@ google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqw
 google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
 google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
 google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
-google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b h1:+YaDE2r2OG8t/z5qmsh7Y+XXwCbvadxxZ0YY6mTdrVA=
-google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI=
-google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b h1:CIC2YMXmIhYw6evmhPxBKJ4fmLbOFtXQN/GV3XOZR8k=
-google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:IBQ646DjkDkvUIsVq/cc03FUFQ9wbZu7yE396YcL870=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b h1:ZlWIi1wSK56/8hn4QcBp/j9M7Gt3U/3hZw3mC7vDICo=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:swOH3j0KzcDDgGUWr+SNpyTen5YrXjS3eyPzFYKc6lc=
+google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
+google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY=
+google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917 h1:rcS6EyEaoCO52hQDupoSfrxI3R6C2Tq741is7X8OvnM=
+google.golang.org/genproto/googleapis/api v0.0.0-20240102182953-50ed04b92917/go.mod h1:CmlNWB9lSezaYELKS5Ym1r44VrrbPUa7JTvw+6MbpJ0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
 google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -1679,8 +1719,8 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu
 google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
 google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
 google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
-google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
-google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
+google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
+google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -1697,13 +1737,14 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
 google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
+google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
diff --git a/options/options.go b/options/options.go
index ac43975f..cec44478 100644
--- a/options/options.go
+++ b/options/options.go
@@ -59,6 +59,9 @@ const (
 
 // TerragruntOptions represents options that configure the behavior of the Terragrunt program
 type TerragruntOptions struct {
+	// Context for collection of telemetry data
+	CtxTelemetryCtx context.Context
+
 	// Location of the Terragrunt config file
 	TerragruntConfigPath string
 
@@ -410,6 +413,7 @@ func (opts *TerragruntOptions) Clone(terragruntConfigPath string) *TerragruntOpt
 	// during xxx-all commands (e.g., apply-all, plan-all). See https://github.com/gruntwork-io/terragrunt/issues/367
 	// for more info.
 	return &TerragruntOptions{
+		CtxTelemetryCtx:                opts.CtxTelemetryCtx,
 		TerragruntConfigPath:           terragruntConfigPath,
 		OriginalTerragruntConfigPath:   opts.OriginalTerragruntConfigPath,
 		TerraformPath:                  opts.TerraformPath,
diff --git a/pkg/cli/app.go b/pkg/cli/app.go
index f56ba213..4141ad32 100644
--- a/pkg/cli/app.go
+++ b/pkg/cli/app.go
@@ -1,9 +1,14 @@
 package cli
 
 import (
+	"context"
 	"os"
 	"strings"
 
+	"github.com/gruntwork-io/go-commons/env"
+
+	"github.com/gruntwork-io/terragrunt/telemetry"
+
 	"github.com/gruntwork-io/go-commons/errors"
 	"github.com/urfave/cli/v2"
 )
@@ -109,6 +114,24 @@ func (app *App) Run(arguments []string) error {
 		args := Args(parentCtx.Args().Slice())
 		ctx := newContext(parentCtx.Context, app)
 
+		// configure telemetry integration
+		err := telemetry.InitTelemetry(ctx, &telemetry.TelemetryOptions{
+			Vars:       env.Parse(os.Environ()),
+			AppName:    app.Name,
+			AppVersion: app.Version,
+			Writer:     app.Writer,
+			ErrWriter:  app.ErrWriter,
+		})
+		if err != nil {
+			return err
+		}
+		defer func(ctx context.Context) {
+			err := telemetry.ShutdownTelemetry(ctx)
+			if err != nil {
+				_, _ = app.ErrWriter.Write([]byte(err.Error()))
+			}
+		}(ctx)
+
 		if app.Autocomplete {
 			if err := app.setupAutocomplete(args); err != nil {
 				return app.handleExitCoder(err)
@@ -123,9 +146,7 @@ func (app *App) Run(arguments []string) error {
 				ctx.shellComplete = true
 			}
 		}
-
-		err := cmd.Run(ctx, args.Normalize(SingleDashFlag))
-		return err
+		return cmd.Run(ctx, args.Normalize(SingleDashFlag))
 	}
 
 	return app.App.Run(arguments)
diff --git a/shell/run_shell_cmd.go b/shell/run_shell_cmd.go
index 9ecc75d8..f11b7998 100644
--- a/shell/run_shell_cmd.go
+++ b/shell/run_shell_cmd.go
@@ -2,6 +2,7 @@ package shell
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"io"
 	"net/url"
@@ -13,6 +14,8 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/gruntwork-io/terragrunt/telemetry"
+
 	"github.com/hashicorp/go-version"
 
 	"golang.org/x/text/cases"
@@ -88,100 +91,108 @@ func RunShellCommandWithOutput(
 	command string,
 	args ...string,
 ) (*CmdOutput, error) {
-	// Terrafrom `init` command with the plugin cache directory is not guaranteed to be concurrency safe.
-	// The provider installer's behavior in environments with multiple terraform init calls is undefined.
-	// Thus, terraform `init` commands must be executed sequentially, even if `--terragrunt-parallelism` is greater than 1.
-	if command == "terraform" && collections.ListContainsElement(args, "init") && terraform.IsPluginCacheUsed() {
-		defer terraformInitMutex.Unlock()
-		terraformInitMutex.Lock()
-	}
-
-	terragruntOptions.Logger.Debugf("Running command: %s %s", command, strings.Join(args, " "))
-	if suppressStdout {
-		terragruntOptions.Logger.Debugf("Command output will be suppressed.")
+	var output *CmdOutput = nil
+	var commandDir = workingDir
+	if workingDir == "" {
+		commandDir = terragruntOptions.WorkingDir
 	}
+	err := telemetry.Telemetry(terragruntOptions, fmt.Sprintf("run_%s", command), map[string]interface{}{
+		"command": command,
+		"args":    fmt.Sprintf("%v", args),
+		"dir":     commandDir,
+	}, func(childCtx context.Context) error {
+
+		// Terrafrom `init` command with the plugin cache directory is not guaranteed to be concurrency safe.
+		// The provider installer's behavior in environments with multiple terraform init calls is undefined.
+		// Thus, terraform `init` commands must be executed sequentially, even if `--terragrunt-parallelism` is greater than 1.
+		if command == "terraform" && collections.ListContainsElement(args, "init") && terraform.IsPluginCacheUsed() {
+			defer terraformInitMutex.Unlock()
+			terraformInitMutex.Lock()
+		}
 
-	var stdoutBuf bytes.Buffer
-	var stderrBuf bytes.Buffer
-
-	cmd := exec.Command(command, args...)
-
-	// TODO: consider adding prefix from terragruntOptions logger to stdout and stderr
-	cmd.Env = toEnvVarsList(terragruntOptions.Env)
+		terragruntOptions.Logger.Debugf("Running command: %s %s", command, strings.Join(args, " "))
+		if suppressStdout {
+			terragruntOptions.Logger.Debugf("Command output will be suppressed.")
+		}
 
-	var errWriter = terragruntOptions.ErrWriter
-	var outWriter = terragruntOptions.Writer
+		var stdoutBuf bytes.Buffer
+		var stderrBuf bytes.Buffer
 
-	// redirect output through logger with json wrapping
-	if terragruntOptions.JsonLogFormat && terragruntOptions.TerraformLogsToJson {
-		outWriter = terragruntOptions.Logger.Logger.WithField("workingDir", terragruntOptions.WorkingDir).WithField("executedCommandArgs", args).Writer()
-		errWriter = terragruntOptions.Logger.Logger.WithField("workingDir", terragruntOptions.WorkingDir).WithField("executedCommandArgs", args).WriterLevel(logrus.ErrorLevel)
-	}
+		cmd := exec.Command(command, args...)
 
-	var prefix = ""
-	if terragruntOptions.IncludeModulePrefix {
-		prefix = terragruntOptions.OutputPrefix
-	}
+		// TODO: consider adding prefix from terragruntOptions logger to stdout and stderr
+		cmd.Env = toEnvVarsList(terragruntOptions.Env)
 
-	if workingDir == "" {
-		cmd.Dir = terragruntOptions.WorkingDir
-	} else {
-		cmd.Dir = workingDir
-	}
+		var errWriter = terragruntOptions.ErrWriter
+		var outWriter = terragruntOptions.Writer
 
-	// Inspired by https://blog.kowalczyk.info/article/wOYk/advanced-command-execution-in-go-with-osexec.html
-	cmdStderr := io.MultiWriter(withPrefix(errWriter, prefix), &stderrBuf)
-	var cmdStdout io.Writer
-	if !suppressStdout {
-		cmdStdout = io.MultiWriter(withPrefix(outWriter, prefix), &stdoutBuf)
-	} else {
-		cmdStdout = io.MultiWriter(&stdoutBuf)
-	}
+		// redirect output through logger with json wrapping
+		if terragruntOptions.JsonLogFormat && terragruntOptions.TerraformLogsToJson {
+			outWriter = terragruntOptions.Logger.Logger.WithField("workingDir", terragruntOptions.WorkingDir).WithField("executedCommandArgs", args).Writer()
+			errWriter = terragruntOptions.Logger.Logger.WithField("workingDir", terragruntOptions.WorkingDir).WithField("executedCommandArgs", args).WriterLevel(logrus.ErrorLevel)
+		}
 
-	// If we need to allocate a ptty for the command, route through the ptty routine. Otherwise, directly call the
-	// command.
-	if allocatePseudoTty {
-		if err := runCommandWithPTTY(terragruntOptions, cmd, cmdStdout, cmdStderr); err != nil {
-			return nil, err
+		var prefix = ""
+		if terragruntOptions.IncludeModulePrefix {
+			prefix = terragruntOptions.OutputPrefix
 		}
-	} else {
-		cmd.Stdin = os.Stdin
-		cmd.Stdout = cmdStdout
-		cmd.Stderr = cmdStderr
-		if err := cmd.Start(); err != nil {
-			// bad path, binary not executable, &c
-			return nil, errors.WithStackTrace(err)
+		cmd.Dir = commandDir
+
+		// Inspired by https://blog.kowalczyk.info/article/wOYk/advanced-command-execution-in-go-with-osexec.html
+		cmdStderr := io.MultiWriter(withPrefix(errWriter, prefix), &stderrBuf)
+		var cmdStdout io.Writer
+		if !suppressStdout {
+			cmdStdout = io.MultiWriter(withPrefix(outWriter, prefix), &stdoutBuf)
+		} else {
+			cmdStdout = io.MultiWriter(&stdoutBuf)
 		}
-	}
 
-	// Make sure to forward signals to the subcommand.
-	cmdChannel := make(chan error) // used for closing the signals forwarder goroutine
-	signalChannel := NewSignalsForwarder(forwardSignals, cmd, terragruntOptions.Logger, cmdChannel)
-	defer func(signalChannel *SignalsForwarder) {
-		err := signalChannel.Close()
-		if err != nil {
-			terragruntOptions.Logger.Warnf("Error closing signal channel: %v", err)
+		// If we need to allocate a ptty for the command, route through the ptty routine. Otherwise, directly call the
+		// command.
+		if allocatePseudoTty {
+			if err := runCommandWithPTTY(terragruntOptions, cmd, cmdStdout, cmdStderr); err != nil {
+				return err
+			}
+		} else {
+			cmd.Stdin = os.Stdin
+			cmd.Stdout = cmdStdout
+			cmd.Stderr = cmdStderr
+			if err := cmd.Start(); err != nil {
+				// bad path, binary not executable, &c
+				return errors.WithStackTrace(err)
+			}
 		}
-	}(&signalChannel)
 
-	err := cmd.Wait()
-	cmdChannel <- err
+		// Make sure to forward signals to the subcommand.
+		cmdChannel := make(chan error) // used for closing the signals forwarder goroutine
+		signalChannel := NewSignalsForwarder(forwardSignals, cmd, terragruntOptions.Logger, cmdChannel)
+		defer func(signalChannel *SignalsForwarder) {
+			err := signalChannel.Close()
+			if err != nil {
+				terragruntOptions.Logger.Warnf("Error closing signal channel: %v", err)
+			}
+		}(&signalChannel)
 
-	cmdOutput := CmdOutput{
-		Stdout: stdoutBuf.String(),
-		Stderr: stderrBuf.String(),
-	}
+		err := cmd.Wait()
+		cmdChannel <- err
 
-	if err != nil {
-		err = ProcessExecutionError{
-			Err:        err,
-			StdOut:     stdoutBuf.String(),
-			Stderr:     stderrBuf.String(),
-			WorkingDir: cmd.Dir,
+		cmdOutput := CmdOutput{
+			Stdout: stdoutBuf.String(),
+			Stderr: stderrBuf.String(),
 		}
-	}
 
-	return &cmdOutput, errors.WithStackTrace(err)
+		if err != nil {
+			err = ProcessExecutionError{
+				Err:        err,
+				StdOut:     stdoutBuf.String(),
+				Stderr:     stderrBuf.String(),
+				WorkingDir: cmd.Dir,
+			}
+		}
+		output = &cmdOutput
+		return errors.WithStackTrace(err)
+	})
+	return output, err
 }
 
 func toEnvVarsList(envVarsAsMap map[string]string) []string {
diff --git a/telemetry/metrics.go b/telemetry/metrics.go
new file mode 100644
index 00000000..9ae3d8e9
--- /dev/null
+++ b/telemetry/metrics.go
@@ -0,0 +1,152 @@
+package telemetry
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
+
+	"github.com/gruntwork-io/go-commons/env"
+	"github.com/gruntwork-io/terragrunt/options"
+	"github.com/pkg/errors"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
+	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
+	otelmetric "go.opentelemetry.io/otel/metric"
+	"go.opentelemetry.io/otel/sdk/metric"
+
+	"go.opentelemetry.io/otel/sdk/resource"
+	semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
+)
+
+type metricsExporterType string
+
+const (
+	noneMetricsExporterType     metricsExporterType = "none"
+	consoleMetricsExporterType  metricsExporterType = "console"
+	oltpHttpMetricsExporterType metricsExporterType = "otlpHttp"
+	grpcHttpMetricsExporterType metricsExporterType = "grpcHttp"
+
+	ErrorsCounter = "errors"
+
+	readerInterval = 1 * time.Second
+)
+
+var metricNameCleanPattern = regexp.MustCompile(`[^A-Za-z0-9_.-/]`)
+var multipleUnderscoresPattern = regexp.MustCompile(`_+`)
+
+// Time - collect time for function execution
+func Time(opts *options.TerragruntOptions, name string, attrs map[string]interface{}, fn func(childCtx context.Context) error) error {
+	ctx := opts.CtxTelemetryCtx
+	if ctx == nil || metricExporter == nil {
+		return fn(ctx)
+	}
+
+	metricAttrs := mapToAttributes(attrs)
+	histogram, err := meter.Int64Histogram(cleanMetricName(fmt.Sprintf("%s_duration", name)))
+	if err != nil {
+		return errors.WithStack(err)
+	}
+	startTime := time.Now()
+	err = fn(ctx)
+	histogram.Record(ctx, time.Since(startTime).Milliseconds(), otelmetric.WithAttributes(metricAttrs...))
+	if err != nil {
+		// count errors
+		Count(opts, ErrorsCounter, 1)
+		Count(opts, fmt.Sprintf("%s_errors", name), 1)
+	} else {
+		Count(opts, fmt.Sprintf("%s_success", name), 1)
+	}
+	return err
+}
+
+// Count - add to counter provided value
+func Count(opts *options.TerragruntOptions, name string, value int64) {
+	ctx := opts.CtxTelemetryCtx
+	if ctx == nil || metricExporter == nil {
+		return
+	}
+	counter, err := meter.Int64Counter(cleanMetricName(fmt.Sprintf("%s_count", name)))
+	if err != nil {
+		return
+	}
+	counter.Add(ctx, value)
+}
+
+// configureMetricsCollection - configure the metrics collection
+func configureMetricsCollection(ctx context.Context, opts *TelemetryOptions) error {
+	exporter, err := newMetricsExporter(ctx, opts)
+	if err != nil {
+		return errors.WithStack(err)
+	}
+	metricExporter = exporter
+	if metricExporter == nil {
+		return nil
+	}
+	provider, err := newMetricsProvider(opts, metricExporter)
+	if err != nil {
+		return errors.WithStack(err)
+	}
+	metricProvider = provider
+	otel.SetMeterProvider(metricProvider)
+	// configure app meter
+	meter = otel.GetMeterProvider().Meter(opts.AppName)
+	return nil
+}
+
+// newMetricsExporter - create a new exporter based on the telemetry options.
+func newMetricsExporter(ctx context.Context, opts *TelemetryOptions) (metric.Exporter, error) {
+	exporterType := metricsExporterType(env.GetString(opts.Vars["TERRAGRUNT_TELEMETRY_METRIC_EXPORTER"], string(noneMetricsExporterType)))
+	insecure := env.GetBool(opts.Vars["TERRAGRUNT_TELEMERTY_METRIC_EXPORTER_INSECURE_ENDPOINT"], false)
+	switch exporterType {
+	case oltpHttpMetricsExporterType:
+		var config []otlpmetrichttp.Option
+		if insecure {
+			config = append(config, otlpmetrichttp.WithInsecure())
+		}
+		return otlpmetrichttp.New(ctx, config...)
+	case grpcHttpMetricsExporterType:
+		var config []otlpmetricgrpc.Option
+		if insecure {
+			config = append(config, otlpmetricgrpc.WithInsecure())
+		}
+		return otlpmetricgrpc.New(ctx, config...)
+	case consoleMetricsExporterType:
+		return stdoutmetric.New(stdoutmetric.WithWriter(opts.Writer))
+	default:
+		return nil, nil
+
+	}
+}
+
+// newMetricsProvider - create a new metrics provider.
+func newMetricsProvider(opts *TelemetryOptions, exp metric.Exporter) (*metric.MeterProvider, error) {
+	r, err := resource.Merge(
+		resource.Default(),
+		resource.NewWithAttributes(
+			semconv.SchemaURL,
+			semconv.ServiceName(opts.AppName),
+			semconv.ServiceVersion(opts.AppVersion),
+		),
+	)
+
+	if err != nil {
+		return nil, errors.WithStack(err)
+	}
+
+	meterProvider := metric.NewMeterProvider(
+		metric.WithResource(r),
+		metric.WithReader(metric.NewPeriodicReader(exp, metric.WithInterval(readerInterval))),
+	)
+	return meterProvider, nil
+}
+
+// cleanMetricName - clean metric name from invalid characters.
+func cleanMetricName(metricName string) string {
+	cleanedName := metricNameCleanPattern.ReplaceAllString(metricName, "_")
+	cleanedName = multipleUnderscoresPattern.ReplaceAllString(cleanedName, "_")
+	return strings.Trim(cleanedName, "_")
+}
diff --git a/telemetry/telemetry.go b/telemetry/telemetry.go
new file mode 100644
index 00000000..6c26bdb0
--- /dev/null
+++ b/telemetry/telemetry.go
@@ -0,0 +1,107 @@
+package telemetry
+
+import (
+	"context"
+	"fmt"
+	"io"
+
+	"github.com/gruntwork-io/terragrunt/options"
+
+	"github.com/pkg/errors"
+	otelmetric "go.opentelemetry.io/otel/metric"
+	"go.opentelemetry.io/otel/sdk/metric"
+
+	"go.opentelemetry.io/otel/attribute"
+
+	oteltrace "go.opentelemetry.io/otel/sdk/trace"
+	sdktrace "go.opentelemetry.io/otel/sdk/trace"
+	"go.opentelemetry.io/otel/trace"
+)
+
+// TelemetryOptions - options for telemetry provider.
+type TelemetryOptions struct {
+	Vars       map[string]string
+	AppName    string
+	AppVersion string
+	Writer     io.Writer
+	ErrWriter  io.Writer
+}
+
+var spanExporter oteltrace.SpanExporter
+var traceProvider *sdktrace.TracerProvider
+var rootTracer trace.Tracer
+
+var meter otelmetric.Meter
+var metricProvider *metric.MeterProvider
+var metricExporter metric.Exporter
+
+// InitTelemetry - initialize the telemetry provider.
+func InitTelemetry(ctx context.Context, opts *TelemetryOptions) error {
+
+	if err := configureTraceCollection(ctx, opts); err != nil {
+		return errors.WithStack(err)
+	}
+
+	if err := configureMetricsCollection(ctx, opts); err != nil {
+		return errors.WithStack(err)
+	}
+
+	return nil
+}
+
+// ShutdownTelemetry - shutdown the telemetry provider.
+func ShutdownTelemetry(ctx context.Context) error {
+	if traceProvider != nil {
+		if err := traceProvider.Shutdown(ctx); err != nil {
+			return errors.WithStack(err)
+		}
+		traceProvider = nil
+	}
+	if metricProvider != nil {
+		if err := metricProvider.Shutdown(ctx); err != nil {
+			return errors.WithStack(err)
+		}
+		metricProvider = nil
+	}
+	return nil
+}
+
+// Telemetry - collect telemetry from function execution - metrics and traces.
+func Telemetry(opts *options.TerragruntOptions, name string, attrs map[string]interface{}, fn func(childCtx context.Context) error) error {
+	// wrap telemetry collection with trace and time metric
+	return Trace(opts, name, attrs, func(childCtx context.Context) error {
+		opts.CtxTelemetryCtx = childCtx
+		return Time(opts, name, attrs, fn)
+	})
+}
+
+// mapToAttributes - convert map to attributes to pass to span.SetAttributes.
+func mapToAttributes(data map[string]interface{}) []attribute.KeyValue {
+	var attrs []attribute.KeyValue
+	for k, v := range data {
+		switch val := v.(type) {
+		case string:
+			attrs = append(attrs, attribute.String(k, val))
+		case int:
+			attrs = append(attrs, attribute.Int64(k, int64(val)))
+		case int64:
+			attrs = append(attrs, attribute.Int64(k, val))
+		case float64:
+			attrs = append(attrs, attribute.Float64(k, val))
+		case bool:
+			attrs = append(attrs, attribute.Bool(k, val))
+		default:
+			attrs = append(attrs, attribute.String(k, fmt.Sprintf("%v", val)))
+		}
+	}
+	return attrs
+}
+
+// ErrorMissingEnvVariable error for missing environment variable.
+type ErrorMissingEnvVariable struct {
+	Vars []string
+}
+
+func (e *ErrorMissingEnvVariable) Error() string {
+	return fmt.Sprintf("missing environment variable: %v", e.Vars)
+}
diff --git a/telemetry/traces.go b/telemetry/traces.go
new file mode 100644
index 00000000..3625aeff
--- /dev/null
+++ b/telemetry/traces.go
@@ -0,0 +1,137 @@
+package telemetry
+
+import (
+	"context"
+
+	"github.com/gruntwork-io/terragrunt/options"
+
+	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
+
+	"github.com/gruntwork-io/go-commons/env"
+	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
+
+	"github.com/pkg/errors"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
+	"go.opentelemetry.io/otel/sdk/resource"
+	sdktrace "go.opentelemetry.io/otel/sdk/trace"
+	semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
+	"go.opentelemetry.io/otel/trace"
+)
+
+type traceExporterType string
+
+const (
+	noneTraceExporterType     traceExporterType = "none"
+	consoleTraceExporterType  traceExporterType = "console"
+	otlpHttpTraceExporterType traceExporterType = "otlpHttp"
+	otlpGrpcTraceExporterType traceExporterType = "otlpGrpc"
+	httpTraceExporterType     traceExporterType = "http"
+)
+
+// Trace - collect traces for method execution
+func Trace(opts *options.TerragruntOptions, name string, attrs map[string]interface{}, fn func(childCtx context.Context) error) error {
+	ctx := opts.CtxTelemetryCtx
+	if spanExporter == nil || traceProvider == nil || ctx == nil { // invoke function without tracing
+		return fn(ctx)
+	}
+	childCtx, span := openSpan(ctx, name, attrs)
+	defer func() {
+		span.End()
+	}()
+	err := fn(childCtx)
+	if err != nil {
+		// record error in span
+		span.RecordError(err)
+	}
+
+	return err
+}
+
+// configureTraceCollection - configure the traces collection
+func configureTraceCollection(ctx context.Context, opts *TelemetryOptions) error {
+	exp, err := newTraceExporter(ctx, opts)
+	if err != nil {
+		return errors.WithStack(err)
+	}
+	if exp == nil { // no exporter
+		return nil
+	}
+	spanExporter = exp
+	traceProvider, err = newTraceProvider(opts, spanExporter)
+	if err != nil {
+		return errors.WithStack(err)
+	}
+	otel.SetTracerProvider(traceProvider)
+	rootTracer = traceProvider.Tracer(opts.AppName)
+	return nil
+}
+
+// newTraceProvider - create a new trace provider with terragrunt version.
+func newTraceProvider(opts *TelemetryOptions, exp sdktrace.SpanExporter) (*sdktrace.TracerProvider, error) {
+	r, err := resource.Merge(
+		resource.Default(),
+		resource.NewWithAttributes(
+			semconv.SchemaURL,
+			semconv.ServiceName(opts.AppName),
+			semconv.ServiceVersion(opts.AppVersion),
+		),
+	)
+
+	if err != nil {
+		return nil, errors.WithStack(err)
+	}
+
+	return sdktrace.NewTracerProvider(
+		sdktrace.WithBatcher(exp),
+		sdktrace.WithResource(r),
+	), nil
+}
+
+// newTraceExporter - create a new exporter based on the telemetry options.
+func newTraceExporter(ctx context.Context, opts *TelemetryOptions) (sdktrace.SpanExporter, error) {
+	exporterType := traceExporterType(env.GetString(opts.Vars["TERRAGRUNT_TELEMETRY_TRACE_EXPORTER"], string(noneTraceExporterType)))
+	insecure := env.GetBool(opts.Vars["TERRAGRUNT_TELEMERTY_TRACE_EXPORTER_INSECURE_ENDPOINT"], false)
+	switch exporterType {
+	case httpTraceExporterType:
+		endpoint := env.GetString(opts.Vars["TERRAGRUNT_TELEMERTY_TRACE_EXPORTER_HTTP_ENDPOINT"], "")
+		if endpoint == "" {
+			return nil, &ErrorMissingEnvVariable{
+				Vars: []string{"TERRAGRUNT_TELEMERTY_TRACE_EXPORTER_HTTP_ENDPOINT"},
+			}
+		}
+		endpointOpt := otlptracehttp.WithEndpoint(endpoint)
+		config := []otlptracehttp.Option{endpointOpt}
+		if insecure {
+			config = append(config, otlptracehttp.WithInsecure())
+		}
+		return otlptracehttp.New(ctx, config...)
+	case otlpHttpTraceExporterType:
+		var config []otlptracehttp.Option
+		if insecure {
+			config = append(config, otlptracehttp.WithInsecure())
+		}
+		return otlptracehttp.New(ctx, config...)
+	case otlpGrpcTraceExporterType:
+		var config []otlptracegrpc.Option
+		if insecure {
+			config = append(config, otlptracegrpc.WithInsecure())
+		}
+		return otlptracegrpc.New(ctx, config...)
+	case consoleTraceExporterType:
+		return stdouttrace.New(stdouttrace.WithWriter(opts.Writer))
+	default:
+		return nil, nil
+	}
+}
+
+// openSpan - create a new span with attributes.
+func openSpan(ctx context.Context, name string, attrs map[string]interface{}) (context.Context, trace.Span) {
+	if traceProvider == nil {
+		return ctx, nil
+	}
+	childCtx, span := rootTracer.Start(ctx, name)
+	// convert attrs map to span.SetAttributes
+	span.SetAttributes(mapToAttributes(attrs)...)
+	return childCtx, span
+}



Test patch:
diff --git a/configstack/running_module_test.go b/configstack/running_module_test.go
index 969381ad..0985ba9f 100644
--- a/configstack/running_module_test.go
+++ b/configstack/running_module_test.go
@@ -496,7 +496,10 @@ func testToRunningModules(t *testing.T, modules []*TerraformModule, order Depend
 func TestRunModulesNoModules(t *testing.T) {
 	t.Parallel()
 
-	err := RunModules([]*TerraformModule{}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 }
 
@@ -511,7 +514,10 @@ func TestRunModulesOneModuleSuccess(t *testing.T) {
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "a", nil, &aRan),
 	}
 
-	err := RunModules([]*TerraformModule{moduleA}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 	assert.True(t, aRan)
 }
@@ -528,7 +534,10 @@ func TestRunModulesOneModuleAssumeAlreadyRan(t *testing.T) {
 		AssumeAlreadyApplied: true,
 	}
 
-	err := RunModules([]*TerraformModule{moduleA}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 	assert.False(t, aRan)
 }
@@ -544,7 +553,10 @@ func TestRunModulesReverseOrderOneModuleSuccess(t *testing.T) {
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "a", nil, &aRan),
 	}
 
-	err := RunModulesReverseOrder([]*TerraformModule{moduleA}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModulesReverseOrder(opts, []*TerraformModule{moduleA}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 	assert.True(t, aRan)
 }
@@ -560,7 +572,10 @@ func TestRunModulesIgnoreOrderOneModuleSuccess(t *testing.T) {
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "a", nil, &aRan),
 	}
 
-	err := RunModulesIgnoreOrder([]*TerraformModule{moduleA}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModulesIgnoreOrder(opts, []*TerraformModule{moduleA}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 	assert.True(t, aRan)
 }
@@ -577,7 +592,10 @@ func TestRunModulesOneModuleError(t *testing.T) {
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "a", expectedErrA, &aRan),
 	}
 
-	err := RunModules([]*TerraformModule{moduleA}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrA)
 	assert.True(t, aRan)
 }
@@ -594,7 +612,10 @@ func TestRunModulesReverseOrderOneModuleError(t *testing.T) {
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "a", expectedErrA, &aRan),
 	}
 
-	err := RunModulesReverseOrder([]*TerraformModule{moduleA}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModulesReverseOrder(opts, []*TerraformModule{moduleA}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrA)
 	assert.True(t, aRan)
 }
@@ -611,7 +632,10 @@ func TestRunModulesIgnoreOrderOneModuleError(t *testing.T) {
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "a", expectedErrA, &aRan),
 	}
 
-	err := RunModulesIgnoreOrder([]*TerraformModule{moduleA}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModulesIgnoreOrder(opts, []*TerraformModule{moduleA}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrA)
 	assert.True(t, aRan)
 }
@@ -643,7 +667,10 @@ func TestRunModulesMultipleModulesNoDependenciesSuccess(t *testing.T) {
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "c", nil, &cRan),
 	}
 
-	err := RunModules([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 
 	assert.True(t, aRan)
@@ -678,7 +705,10 @@ func TestRunModulesMultipleModulesNoDependenciesSuccessNoParallelism(t *testing.
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "c", nil, &cRan),
 	}
 
-	err := RunModules([]*TerraformModule{moduleA, moduleB, moduleC}, 1)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA, moduleB, moduleC}, 1)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 
 	assert.True(t, aRan)
@@ -713,7 +743,10 @@ func TestRunModulesReverseOrderMultipleModulesNoDependenciesSuccess(t *testing.T
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "c", nil, &cRan),
 	}
 
-	err := RunModulesReverseOrder([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModulesReverseOrder(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 
 	assert.True(t, aRan)
@@ -748,7 +781,10 @@ func TestRunModulesIgnoreOrderMultipleModulesNoDependenciesSuccess(t *testing.T)
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "c", nil, &cRan),
 	}
 
-	err := RunModulesIgnoreOrder([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModulesIgnoreOrder(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 
 	assert.True(t, aRan)
@@ -784,7 +820,10 @@ func TestRunModulesMultipleModulesNoDependenciesOneFailure(t *testing.T) {
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "c", nil, &cRan),
 	}
 
-	err := RunModules([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, optsErr := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, optsErr)
+
+	err := RunModules(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrB)
 
 	assert.True(t, aRan)
@@ -822,7 +861,10 @@ func TestRunModulesMultipleModulesNoDependenciesMultipleFailures(t *testing.T) {
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "c", expectedErrC, &cRan),
 	}
 
-	err := RunModules([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrA, expectedErrB, expectedErrC)
 
 	assert.True(t, aRan)
@@ -857,7 +899,10 @@ func TestRunModulesMultipleModulesWithDependenciesSuccess(t *testing.T) {
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "c", nil, &cRan),
 	}
 
-	err := RunModules([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 
 	assert.True(t, aRan)
@@ -901,7 +946,10 @@ func TestRunModulesMultipleModulesWithDependenciesWithAssumeAlreadyRanSuccess(t
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "d", nil, &dRan),
 	}
 
-	err := RunModules([]*TerraformModule{moduleA, moduleB, moduleC, moduleD}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA, moduleB, moduleC, moduleD}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 
 	assert.True(t, aRan)
@@ -937,7 +985,10 @@ func TestRunModulesReverseOrderMultipleModulesWithDependenciesSuccess(t *testing
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "c", nil, &cRan),
 	}
 
-	err := RunModulesReverseOrder([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModulesReverseOrder(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 
 	assert.True(t, aRan)
@@ -972,7 +1023,10 @@ func TestRunModulesIgnoreOrderMultipleModulesWithDependenciesSuccess(t *testing.
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "c", nil, &cRan),
 	}
 
-	err := RunModulesIgnoreOrder([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModulesIgnoreOrder(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assert.Nil(t, err, "Unexpected error: %v", err)
 
 	assert.True(t, aRan)
@@ -1010,7 +1064,10 @@ func TestRunModulesMultipleModulesWithDependenciesOneFailure(t *testing.T) {
 
 	expectedErrC := DependencyFinishedWithError{moduleC, moduleB, expectedErrB}
 
-	err := RunModules([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrB, expectedErrC)
 
 	assert.True(t, aRan)
@@ -1052,7 +1109,10 @@ func TestRunModulesMultipleModulesWithDependenciesOneFailureIgnoreDependencyErro
 		TerragruntOptions: terragruntOptionsC,
 	}
 
-	err := RunModules([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrB)
 
 	assert.True(t, aRan)
@@ -1090,7 +1150,10 @@ func TestRunModulesReverseOrderMultipleModulesWithDependenciesOneFailure(t *test
 
 	expectedErrA := DependencyFinishedWithError{moduleA, moduleB, expectedErrB}
 
-	err := RunModulesReverseOrder([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModulesReverseOrder(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrB, expectedErrA)
 
 	assert.False(t, aRan)
@@ -1126,7 +1189,10 @@ func TestRunModulesIgnoreOrderMultipleModulesWithDependenciesOneFailure(t *testi
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "c", nil, &cRan),
 	}
 
-	err := RunModulesIgnoreOrder([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModulesIgnoreOrder(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrB)
 
 	assert.True(t, aRan)
@@ -1165,7 +1231,10 @@ func TestRunModulesMultipleModulesWithDependenciesMultipleFailures(t *testing.T)
 	expectedErrB := DependencyFinishedWithError{moduleB, moduleA, expectedErrA}
 	expectedErrC := DependencyFinishedWithError{moduleC, moduleB, expectedErrB}
 
-	err := RunModules([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrA, expectedErrB, expectedErrC)
 
 	assert.True(t, aRan)
@@ -1201,7 +1270,10 @@ func TestRunModulesIgnoreOrderMultipleModulesWithDependenciesMultipleFailures(t
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "c", nil, &cRan),
 	}
 
-	err := RunModulesIgnoreOrder([]*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModulesIgnoreOrder(opts, []*TerraformModule{moduleA, moduleB, moduleC}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrA)
 
 	assert.True(t, aRan)
@@ -1260,7 +1332,10 @@ func TestRunModulesMultipleModulesWithDependenciesLargeGraphAllSuccess(t *testin
 		TerragruntOptions: optionsWithMockTerragruntCommand(t, "f", nil, &fRan),
 	}
 
-	err := RunModules([]*TerraformModule{moduleA, moduleB, moduleC, moduleD, moduleE, moduleF}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA, moduleB, moduleC, moduleD, moduleE, moduleF}, options.DefaultParallelism)
 	assert.Nil(t, err)
 
 	assert.True(t, aRan)
@@ -1335,7 +1410,10 @@ func TestRunModulesMultipleModulesWithDependenciesLargeGraphPartialFailure(t *te
 	expectedErrD := DependencyFinishedWithError{moduleD, moduleC, expectedErrC}
 	expectedErrF := DependencyFinishedWithError{moduleF, moduleD, expectedErrD}
 
-	err := RunModules([]*TerraformModule{moduleA, moduleB, moduleC, moduleD, moduleE, moduleF, moduleG}, options.DefaultParallelism)
+	opts, err := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, err)
+
+	err = RunModules(opts, []*TerraformModule{moduleA, moduleB, moduleC, moduleD, moduleE, moduleF, moduleG}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrC, expectedErrD, expectedErrF)
 
 	assert.True(t, aRan)
@@ -1402,7 +1480,10 @@ func TestRunModulesReverseOrderMultipleModulesWithDependenciesLargeGraphPartialF
 	expectedErrB := DependencyFinishedWithError{moduleB, moduleC, expectedErrC}
 	expectedErrA := DependencyFinishedWithError{moduleA, moduleB, expectedErrB}
 
-	err := RunModulesReverseOrder([]*TerraformModule{moduleA, moduleB, moduleC, moduleD, moduleE, moduleF}, options.DefaultParallelism)
+	opts, optsErr := options.NewTerragruntOptionsForTest("")
+	assert.NoError(t, optsErr)
+
+	err := RunModulesReverseOrder(opts, []*TerraformModule{moduleA, moduleB, moduleC, moduleD, moduleE, moduleF}, options.DefaultParallelism)
 	assertMultiErrorContains(t, err, expectedErrC, expectedErrB, expectedErrA)
 
 	assert.False(t, aRan)
diff --git a/telemetry/metrics_test.go b/telemetry/metrics_test.go
new file mode 100644
index 00000000..5b997123
--- /dev/null
+++ b/telemetry/metrics_test.go
@@ -0,0 +1,130 @@
+package telemetry
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"testing"
+
+	"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
+
+	"github.com/stretchr/testify/assert"
+	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
+	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
+)
+
+func TestNewMetricsExporter(t *testing.T) {
+	ctx := context.Background()
+
+	stdout, err := stdoutmetric.New()
+	assert.NoError(t, err)
+
+	tests := []struct {
+		name         string
+		exporterType string
+		insecure     bool
+		expectedType interface{}
+		expectNil    bool
+	}{
+		{
+			name:         "OTLP HTTP Exporter",
+			exporterType: "otlpHttp",
+			insecure:     false,
+			expectedType: (*otlpmetrichttp.Exporter)(nil),
+		},
+		{
+			name:         "gRPC HTTP Exporter",
+			exporterType: "grpcHttp",
+			insecure:     true,
+			expectedType: (*otlpmetricgrpc.Exporter)(nil),
+		},
+		{
+			name:         "Console Exporter",
+			exporterType: "console",
+			insecure:     false,
+			expectedType: stdout,
+		},
+		{
+			name:         "None Exporter",
+			exporterType: "none",
+			insecure:     false,
+			expectNil:    true,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			opts := &TelemetryOptions{
+				Vars: map[string]string{
+					"TERRAGRUNT_TELEMETRY_METRIC_EXPORTER":                   tt.exporterType,
+					"TERRAGRUNT_TELEMERTY_METRIC_EXPORTER_INSECURE_ENDPOINT": fmt.Sprintf("%v", tt.insecure),
+				},
+				Writer: io.Discard,
+			}
+
+			exporter, err := newMetricsExporter(ctx, opts)
+			assert.NoError(t, err)
+
+			if tt.expectNil {
+				assert.Nil(t, exporter)
+			} else {
+				assert.IsType(t, tt.expectedType, exporter)
+			}
+		})
+	}
+}
+
+func TestCleanMetricName(t *testing.T) {
+	testCases := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "Normal case",
+			input:    "metricName_1.2-34",
+			expected: "metricName_1.2_34",
+		},
+		{
+			name:     "Starts with invalid characters",
+			input:    "!@#metricName",
+			expected: "metricName",
+		},
+		{
+			name:     "Ends with invalid characters",
+			input:    "metricName!@#",
+			expected: "metricName",
+		},
+		{
+			name:     "Only invalid characters",
+			input:    "!@#$%^&*()",
+			expected: "",
+		},
+		{
+			name:     "Empty string",
+			input:    "",
+			expected: "",
+		},
+		{
+			name:     "Leading underscore from replacement",
+			input:    "!metricName",
+			expected: "metricName",
+		},
+		{
+			name:     "Multiple replacements",
+			input:    "metric!@#Name",
+			expected: "metric_Name",
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			result := cleanMetricName(tc.input)
+			assert.Equal(t, tc.expected, result)
+		})
+	}
+}
diff --git a/telemetry/traces_test.go b/telemetry/traces_test.go
new file mode 100644
index 00000000..81c53bfa
--- /dev/null
+++ b/telemetry/traces_test.go
@@ -0,0 +1,104 @@
+package telemetry
+
+import (
+	"context"
+	"io"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
+	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
+	"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
+)
+
+func TestNewTraceExporter(t *testing.T) {
+	ctx := context.Background()
+
+	http, err := otlptracehttp.New(ctx)
+	assert.NoError(t, err)
+
+	grpc, err := otlptracegrpc.New(ctx)
+	assert.NoError(t, err)
+
+	stdoutrace, err := stdouttrace.New()
+	assert.NoError(t, err)
+
+	tests := []struct {
+		name             string
+		telemetryOptions *TelemetryOptions
+		expectedType     interface{}
+		expectError      bool
+	}{
+		{
+			name: "HTTP Trace Exporter",
+			telemetryOptions: &TelemetryOptions{
+				Vars: map[string]string{
+					"TERRAGRUNT_TELEMETRY_TRACE_EXPORTER": "otlpHttp",
+				},
+				Writer: io.Discard,
+			},
+			expectedType: http,
+			expectError:  false,
+		},
+		{
+			name: "Custom HTTP endpoint",
+			telemetryOptions: &TelemetryOptions{
+				Vars: map[string]string{
+					"TERRAGRUNT_TELEMETRY_TRACE_EXPORTER":               "http",
+					"TERRAGRUNT_TELEMERTY_TRACE_EXPORTER_HTTP_ENDPOINT": "http://localhost:4317",
+				},
+				Writer: io.Discard,
+			},
+			expectedType: http,
+			expectError:  false,
+		},
+		{
+			name: "Custom HTTP endpoint without endpoint",
+			telemetryOptions: &TelemetryOptions{
+				Vars: map[string]string{
+					"TERRAGRUNT_TELEMETRY_TRACE_EXPORTER": "http",
+				},
+				Writer: io.Discard,
+			},
+			expectedType: http,
+			expectError:  true,
+		},
+		{
+			name: "Grpc Trace Exporter",
+			telemetryOptions: &TelemetryOptions{
+				Vars: map[string]string{
+					"TERRAGRUNT_TELEMETRY_TRACE_EXPORTER": "otlpGrpc",
+				},
+				Writer: io.Discard,
+			},
+			expectedType: grpc,
+			expectError:  false,
+		},
+		{
+			name: "Console Trace Exporter",
+			telemetryOptions: &TelemetryOptions{
+				Vars: map[string]string{
+					"TERRAGRUNT_TELEMETRY_TRACE_EXPORTER": "console",
+				},
+				Writer: io.Discard,
+			},
+			expectedType: stdoutrace,
+			expectError:  false,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			exporter, err := newTraceExporter(ctx, tt.telemetryOptions)
+
+			if tt.expectError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.IsType(t, tt.expectedType, exporter)
+			}
+		})
+	}
+}
diff --git a/test/fixture-inputs-from-dependency/app/main.tf b/test/fixture-inputs-from-dependency/app/main.tf
index 488bb9a8..7608fb29 100644
--- a/test/fixture-inputs-from-dependency/app/main.tf
+++ b/test/fixture-inputs-from-dependency/app/main.tf
@@ -27,3 +27,23 @@ output "baz" {
   value       = var.baz
   description = "Test baz value"
 }
+
+variable "dep-output-test" {
+  type    = string
+  default = "empty"
+}
+
+output "dep-output-test" {
+  value       = var.dep-output-test
+  description = "Test dep-output-test value"
+}
+
+variable "dep-cluster-id" {
+  type    = string
+  default = "empty"
+}
+
+output "dep-cluster-id" {
+  value       = var.dep-cluster-id
+  description = "Test dep-cluster-id value"
+}
diff --git a/test/fixture-inputs-from-dependency/app/terragrunt.hcl b/test/fixture-inputs-from-dependency/app/terragrunt.hcl
index c7aaabb8..dd634732 100644
--- a/test/fixture-inputs-from-dependency/app/terragrunt.hcl
+++ b/test/fixture-inputs-from-dependency/app/terragrunt.hcl
@@ -2,12 +2,15 @@ dependencies {
   paths = ["../dependency"]
 }
 
-dependency "test" {
+dependency "dep" {
   config_path = "../dependency"
 }
 
 inputs = {
-  foo = dependency.test.inputs.foo
-  bar = dependency.test.inputs.bar
-  baz = dependency.test.inputs.baz
+  foo = dependency.dep.inputs.foo
+  bar = dependency.dep.inputs.bar
+  baz = dependency.dep.inputs.baz
+
+  dep-output-test = dependency.dep.outputs.test
+  dep-cluster-id  = dependency.dep.outputs.cluster-id
 }
diff --git a/test/fixture-inputs-from-dependency/cluster/main.tf b/test/fixture-inputs-from-dependency/cluster/main.tf
new file mode 100644
index 00000000..9194624c
--- /dev/null
+++ b/test/fixture-inputs-from-dependency/cluster/main.tf
@@ -0,0 +1,3 @@
+output "id" {
+  value = "test-id-value"
+}
diff --git a/test/fixture-inputs-from-dependency/cluster/terragrunt.hcl b/test/fixture-inputs-from-dependency/cluster/terragrunt.hcl
new file mode 100644
index 00000000..bb7b160d
--- /dev/null
+++ b/test/fixture-inputs-from-dependency/cluster/terragrunt.hcl
@@ -0,0 +1 @@
+# Intentionally empty
diff --git a/test/fixture-inputs-from-dependency/dependency/main.tf b/test/fixture-inputs-from-dependency/dependency/main.tf
index 2bbf6adb..bf6cac75 100644
--- a/test/fixture-inputs-from-dependency/dependency/main.tf
+++ b/test/fixture-inputs-from-dependency/dependency/main.tf
@@ -7,3 +7,13 @@ output "test" {
   value       = var.test
   description = "Test value"
 }
+
+variable "cluster-id" {
+  type    = string
+  default = "empty"
+}
+
+output "cluster-id" {
+  value       = var.cluster-id
+  description = "Test cluster-id value"
+}
diff --git a/test/fixture-inputs-from-dependency/dependency/terragrunt.hcl b/test/fixture-inputs-from-dependency/dependency/terragrunt.hcl
index 5e762c3d..ab9c9aa1 100644
--- a/test/fixture-inputs-from-dependency/dependency/terragrunt.hcl
+++ b/test/fixture-inputs-from-dependency/dependency/terragrunt.hcl
@@ -6,6 +6,11 @@ include "local" {
   path = "local.hcl"
 }
 
+dependency "cluster" {
+  config_path = "../cluster"
+}
+
 inputs = {
-  foo = "dependency-input-foo-value"
+  foo        = "dependency-input-foo-value"
+  cluster-id = dependency.cluster.outputs.id
 }
diff --git a/test/fixture-skip-dependencies/first/foo.hcl b/test/fixture-skip-dependencies/first/foo.hcl
new file mode 100644
index 00000000..e69de29b
diff --git a/test/fixture-skip-dependencies/first/terragrunt.hcl b/test/fixture-skip-dependencies/first/terragrunt.hcl
index 49129f0e..d428343c 100644
--- a/test/fixture-skip-dependencies/first/terragrunt.hcl
+++ b/test/fixture-skip-dependencies/first/terragrunt.hcl
@@ -4,6 +4,10 @@ terraform {
   source = "../module"
 }
 
+include "foo" {
+  path = "foo.hcl"
+}
+
 inputs = {
   input = "first"
 }
\ No newline at end of file
diff --git a/test/integration_serial_test.go b/test/integration_serial_test.go
index 1f479c50..faf0df32 100644
--- a/test/integration_serial_test.go
+++ b/test/integration_serial_test.go
@@ -359,3 +359,104 @@ func TestTerragruntWorksWithImpersonateGCSBackend(t *testing.T) {
 	}
 	assert.True(t, ownerEmail, "Identity email should match the impersonated account")
 }
+
+func TestTerragruntProduceTelemetryTraces(t *testing.T) {
+	t.Setenv("TERRAGRUNT_TELEMETRY_TRACE_EXPORTER", "console")
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_HOOKS_BEFORE_AND_AFTER_PATH)
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_HOOKS_BEFORE_AND_AFTER_PATH)
+	rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_HOOKS_BEFORE_AND_AFTER_PATH)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath), &stdout, &stderr)
+	assert.NoError(t, err)
+
+	output := stdout.String()
+
+	// check that output have Telemetry json output
+	assert.Contains(t, output, "\"SpanContext\":")
+	assert.Contains(t, output, "\"TraceID\":")
+	assert.Contains(t, output, "\"Name\":\"hook_after_hook_1\"")
+	assert.Contains(t, output, "\"Name\":\"hook_after_hook_2\"")
+}
+
+func TestTerragruntProduceTelemetryMetrics(t *testing.T) {
+	t.Setenv("TERRAGRUNT_TELEMETRY_METRIC_EXPORTER", "console")
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_HOOKS_BEFORE_AND_AFTER_PATH)
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_HOOKS_BEFORE_AND_AFTER_PATH)
+	rootPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_HOOKS_BEFORE_AND_AFTER_PATH)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply -no-color -auto-approve --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath), &stdout, &stderr)
+	assert.NoError(t, err)
+
+	// sleep for a bit to allow the metrics to be flushed
+	time.Sleep(1 * time.Second)
+
+	output := stdout.String()
+
+	// check that output have Telemetry json output
+	assert.Contains(t, output, "{\"Name\":\"hook_after_hook_2_duration\"")
+	assert.Contains(t, output, "{\"Name\":\"run_")
+	assert.Contains(t, output, ",\"IsMonotonic\":true}}")
+}
+
+func TestTerragruntOutputJson(t *testing.T) {
+	// no parallel test execution since JSON output is global
+	defer func() {
+		util.DisableJsonFormat()
+	}()
+
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_NOT_EXISTING_SOURCE)
+	cleanupTerraformFolder(t, tmpEnvPath)
+	testPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_NOT_EXISTING_SOURCE)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply --terragrunt-json-log --terragrunt-non-interactive --terragrunt-working-dir %s", testPath), &stdout, &stderr)
+	assert.Error(t, err)
+
+	var output map[string]interface{}
+
+	err = json.Unmarshal(stderr.Bytes(), &output)
+	assert.NoError(t, err)
+
+	assert.Contains(t, output["msg"], "Downloading Terraform configurations from git::https://github.com/gruntwork-io/terragrunt.git?ref=v0.9.9")
+}
+
+func TestTerragruntTerraformOutputJson(t *testing.T) {
+	// no parallel test execution since JSON output is global
+	defer func() {
+		util.DisableJsonFormat()
+	}()
+
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_INIT_ERROR)
+	cleanupTerraformFolder(t, tmpEnvPath)
+	testPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_INIT_ERROR)
+
+	stdout := bytes.Buffer{}
+	stderr := bytes.Buffer{}
+
+	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply --no-color --terragrunt-json-log --terragrunt-tf-logs-to-json --terragrunt-non-interactive --terragrunt-working-dir %s", testPath), &stdout, &stderr)
+	assert.Error(t, err)
+
+	assert.Contains(t, stderr.String(), "\"level\":\"info\",\"msg\":\"Initializing the backend...")
+
+	// check if output can be extracted in json
+	jsonStrings := strings.Split(stderr.String(), "\n")
+	for _, jsonString := range jsonStrings {
+		if len(jsonString) == 0 {
+			continue
+		}
+		var output map[string]interface{}
+		err = json.Unmarshal([]byte(jsonString), &output)
+		assert.NoErrorf(t, err, "Failed to parse json %s", jsonString)
+		assert.NotNil(t, output["level"])
+		assert.NotNil(t, output["level"])
+		assert.NotNil(t, output["time"])
+	}
+}
diff --git a/test/integration_test.go b/test/integration_test.go
index 428c20ac..35ff00d5 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -959,9 +959,11 @@ func TestTerragruntInputsFromDependency(t *testing.T) {
 	runTerragruntRedirectOutput(t, fmt.Sprintf("terragrunt output --terragrunt-non-interactive --terragrunt-working-dir %s", appTerragruntPath), &stdout, &stderr)
 
 	expectedOutpus := map[string]string{
-		"bar": "dependency-parent-bar-value",
-		"baz": "dependency-include-input-baz-value",
-		"foo": "dependency-input-foo-value",
+		"bar":             "dependency-parent-bar-value",
+		"baz":             "dependency-include-input-baz-value",
+		"foo":             "dependency-input-foo-value",
+		"dep-cluster-id":  "test-id-value",
+		"dep-output-test": "test-value",
 	}
 
 	output := stdout.String()
@@ -6516,63 +6518,6 @@ func TestTerragruntUpdatePolicy(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-func TestTerragruntOutputJson(t *testing.T) {
-	// no parallel test execution since JSON output is global
-	defer func() {
-		util.DisableJsonFormat()
-	}()
-
-	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_NOT_EXISTING_SOURCE)
-	cleanupTerraformFolder(t, tmpEnvPath)
-	testPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_NOT_EXISTING_SOURCE)
-
-	stdout := bytes.Buffer{}
-	stderr := bytes.Buffer{}
-
-	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply --terragrunt-json-log --terragrunt-non-interactive --terragrunt-working-dir %s", testPath), &stdout, &stderr)
-	assert.Error(t, err)
-
-	var output map[string]interface{}
-
-	err = json.Unmarshal(stderr.Bytes(), &output)
-	assert.NoError(t, err)
-
-	assert.Contains(t, output["msg"], "Downloading Terraform configurations from git::https://github.com/gruntwork-io/terragrunt.git?ref=v0.9.9")
-}
-
-func TestTerragruntTerraformOutputJson(t *testing.T) {
-	// no parallel test execution since JSON output is global
-	defer func() {
-		util.DisableJsonFormat()
-	}()
-
-	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_INIT_ERROR)
-	cleanupTerraformFolder(t, tmpEnvPath)
-	testPath := util.JoinPath(tmpEnvPath, TEST_FIXTURE_INIT_ERROR)
-
-	stdout := bytes.Buffer{}
-	stderr := bytes.Buffer{}
-
-	err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply --no-color --terragrunt-json-log --terragrunt-tf-logs-to-json --terragrunt-non-interactive --terragrunt-working-dir %s", testPath), &stdout, &stderr)
-	assert.Error(t, err)
-
-	assert.Contains(t, stderr.String(), "\"level\":\"info\",\"msg\":\"Initializing the backend...")
-
-	// check if output can be extracted in json
-	jsonStrings := strings.Split(stderr.String(), "\n")
-	for _, jsonString := range jsonStrings {
-		if len(jsonString) == 0 {
-			continue
-		}
-		var output map[string]interface{}
-		err = json.Unmarshal([]byte(jsonString), &output)
-		assert.NoErrorf(t, err, "Failed to parse json %s", jsonString)
-		assert.NotNil(t, output["level"])
-		assert.NotNil(t, output["level"])
-		assert.NotNil(t, output["time"])
-	}
-}
-
 func TestTerragruntDestroyGraph(t *testing.T) {
 	t.Parallel()
 
@@ -6712,6 +6657,10 @@ func TestTerragruntSkipDependenciesWithSkipFlag(t *testing.T) {
 
 	output := fmt.Sprintf("%s %s", stderr.String(), stdout.String())
 
+	assert.NotContains(t, output, "Error reading partial config for dependency")
+	assert.NotContains(t, output, "Call to function \"find_in_parent_folders\" failed")
+	assert.NotContains(t, output, "ParentFileNotFoundError")
+
 	assert.Contains(t, output, "first/terragrunt.hcl due to skip = true")
 	assert.Contains(t, output, "second/terragrunt.hcl due to skip = true")
 	// check that no test_file.txt was created in module directory