Open-Source Wikis

/

Terraform

/

Systems

/

Cross-cutting: diagnostics, logging, telemetry

hashicorp/terraform

Cross-cutting: diagnostics, logging, telemetry

Active contributors: Martin Atkins, James Bardin.

Purpose

A handful of small, plumbing-style packages knit the rest of the codebase together. None of them is a "feature"; together they're how every other package handles errors, logs, traces, and other process-level concerns.

Diagnostics — internal/tfdiags/

The tfdiags package defines Diagnostic and Diagnostics, the ubiquitous result type. A Diagnostic carries:

  • SeverityError or Warning.
  • Summary — a short one-line description.
  • Detail — optional longer explanation.
  • Source — optional *hcl.Range so the diagnostic can be displayed against the user's source code.
  • Extra — an open-ended typed payload, used by the command/format package to attach things like "wrong-type" details that the renderer can show specially.

Diagnostics is []Diagnostic with helpers:

diags = diags.Append(otherDiags)        // accepts another Diagnostics, *Diagnostic, error, hcl.Diagnostics
diags = diags.Append(tfdiags.Sourceless(severity, summary, detail))
if diags.HasErrors() { return nil, diags }

The package supports several diagnostic flavors:

  • tfdiags.Sourceless — no source range.
  • tfdiags.AttributeValue — points to a specific attribute via traversal path.
  • tfdiags.WholeContainingBody(rng, ...) — whole-block.
  • tfdiags.AsHCLDiagnostic — round-trip with hcl.Diagnostics.

The conversion methods tfdiags.Diagnostics.ForRPC and .ToHCLDiagnostics are used at process boundaries (gRPC, JSON output).

The whole package is small (~20 files) and its API is stable. Almost every package in the codebase imports it.

Logging — internal/logging/

Terraform uses Go's standard log package with severity-prefixed messages. internal/logging/ configures it:

  • logging.LogOutput() returns the io.Writer log lines go to, respecting TF_LOG, TF_LOG_CORE, TF_LOG_PROVIDER, and TF_LOG_PATH.
  • logging.PanicHandler() is the deferred recover used in main.go and various places where a panic shouldn't crash the process silently. It logs the panic and writes a crash.log file with the goroutine dump.
  • logging.PluginPanics() returns all panics captured from provider plugin processes — used in main.go to emit them as errors at the end of an exit-non-zero run.
  • logging.RegisterSink(io.Writer) adds an additional log destination, used to plumb logs into a temp file when running via dlv.

The standard message shape is:

2026/04/29 18:31:16 [TRACE] terraform.Context: starting plan walk

Severities used are [TRACE], [DEBUG], [INFO], [WARN], [ERROR]. Trace-level messages are common in hot paths and are silenced by default.

Telemetry — OpenTelemetry plumbing

telemetry.go (root package) and the corresponding init code in main.go set up an OpenTelemetry tracer. The tracer is opt-in: by default no exporter is configured and no spans are emitted anywhere.

Set TERRAFORM_OPENTELEMETRY_EXPORTER=otlp (and the standard OTLP env vars for the collector address) to enable export. The CLI emits one root span per command invocation, plus child spans for major phases (graph build, walk, provider RPCs).

internal/rpcapi/telemetry.go adds OTel hooks to the rpcapi server so that gRPC clients (HCP Terraform) can correlate their own traces with Terraform Core's.

Working directory — internal/command/workdir/

workdir.Dir is the abstraction over .terraform/, .terraform.tfstate, and the TF_DATA_DIR override. It exposes typed paths for:

  • Plugin cache directory.
  • Provider cache directory.
  • Module cache directory.
  • The dependency lock file.
  • The terraform.tfstate.d/<workspace> workspace state files (for the local backend).

Every command that needs to read or write under .terraform/ does so through workdir.Dir, never via raw paths. This keeps the data-dir override behavior consistent.

Module installation — internal/initwd/

initwd orchestrates module installation: walking the configuration to find module blocks, resolving each source (registry, local path, Git, HTTP, S3, …), copying the resolved content into .terraform/modules/<key>/, and writing the manifest at .terraform/modules/modules.json. It uses internal/getmodules/ (which wraps hashicorp/go-getter) for the actual transfer.

The companion internal/configs/configload/ package consumes the manifest produced by initwd and feeds the on-disk module tree to internal/configs.Parser.

Service discovery — internal/httpclient/, internal/registry/

httpclient.TerraformUserAgent(version) produces the User-Agent string used in every HTTP call.

internal/registry/ implements the bits of the Terraform Registry API not covered by internal/getproviders/ — primarily module-level lookups used by internal/initwd/.

Tiny helpers used in a few places:

  • didyoumean.NameSuggestion(name, candidates) — Levenshtein-based suggestion. Used by the unknown-command handler in main.go and a few "field not found" diagnostics.
  • replacefile.AtomicRename / replacefile.AtomicallyOverwrite — file rename operations that work across filesystems and platforms (state writes, lock-file updates).
  • copy.CopyDir, copy.CopyFile — unified directory copy used by module installation and similar workflows.

Refactoring — internal/refactoring/

Helpers for moved {} and removed {} blocks: validates that from and to addresses make sense, computes the resulting in-state moves before plan walk, and produces friendly error messages when a moved cannot be applied.

internal/promising/

Promise-based scheduler used by internal/stacks/stackruntime/. See stacks for context.

Why these are cross-cutting

None of these packages is part of any particular feature, but every feature uses them. Knowing they exist saves you from reinventing them; reading their source is also a quick way to understand the code conventions of the project.

Entry points for modification

  • New diagnostic flavors: extend tfdiags. Be careful — diagnostics flow into every output path (CLI, JSON, gRPC), and a new Extra payload type needs to be handled by every renderer.
  • New log severities: extend internal/logging/. The existing severities cover almost any need; introducing a new one is rarely a good idea.
  • New telemetry spans: just add tracer.Start calls in the code path you want to instrument. Use the existing tracer instance.
  • New paths under .terraform/: add typed accessors to workdir.Dir, never use raw paths.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Cross-cutting: diagnostics, logging, telemetry – Terraform wiki | Factory