docs/tracing.md
This document describes how to use and implement tracing in OpenTofu Core using OpenTelemetry.
[!NOTE]
For background on the design decisions and motivation behind OpenTofu's tracing implementation, see the OpenTelemetry Tracing RFC.
[!NOTE] If you are upgrading any dependent libraries which pull in a new OTEL version, you MUST update the semconv version in tracing/init.go to the latest version. Failing to do this will result in an error "Could not initialize telemetry: failed to create resource: error detecting resource: conflicting Schema URL". This sets the maximum supported schema version in our OTEL context. Semconv is backwards compatible with older versions, but the newest must be specified.
OpenTofu provides distributed tracing capabilities via OpenTelemetry to help end users understand the execution flow and performance characteristics of OpenTofu operations. Tracing is particularly useful for:
Tracing in OpenTofu is strictly opt-in and disabled by default. It's designed to have minimal overhead when disabled and to provide valuable insights when enabled.
[!IMPORTANT]
OpenTofu's tracing functionality refers only to OpenTelemetry traces for local debugging and analysis. No telemetry or usage data is sent to external servers, and no data leaves your environment unless you explicitly configure an external collector.
To enable tracing in OpenTofu:
OTEL_TRACES_EXPORTER=otlpExample configuration for a local Jaeger collector:
export OTEL_TRACES_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_INSECURE=true
For a complete list of configuration options, refer to the OpenTelemetry Documentation.
To quickly spin up a local Jaeger instance with OTLP support:
docker run -d --rm --name jaeger \
-p 16686:16686 \
-p 4317:4317 \
-p 4318:4318 \
-p 5778:5778 \
-p 9411:9411 \
jaegertracing/jaeger:2.5.0
Then configure OpenTofu as shown above and access the Jaeger UI at http://localhost:16686.
[!NOTE]
For Contributors: When adding tracing to OpenTofu, remember that the primary audience is end users who need to understand performance, not developers. Add spans sparingly to avoid polluting traces with too much detail.
import (
"github.com/opentofu/opentofu/internal/tracing"
"github.com/opentofu/opentofu/internal/tracing/traceattrs"
otelAttr "go.opentelemetry.io/otel/attribute" // Note the alias
)
func SomeFunction(ctx context.Context) error {
// Create a new span
ctx, span := tracing.Tracer().Start(ctx, "Human readable operation name")
defer span.End()
// Add attributes to provide context
span.SetAttributes(otelAttr.String("opentofu.some.attribute", "value"))
// Using predefined attributes from traceattrs package
span.SetAttributes(otelAttr.String(traceattrs.ProviderAddress, "hashicorp/aws"))
// Your function logic here...
// If an error occurs
if err != nil {
tracing.SetSpanError(span, err)
return err
}
return nil
}
[!TIP]
We should use theotelAttralias for OpenTelemetry's attribute package to clearly distinguish it from OpenTofu's trace attribute constants in thetraceattrspackage. This convention makes the code more readable and prevents import conflicts.
internal/tracing/traceattrs// Good attribute names
"opentofu.provider.address" // For provider addresses
"opentofu.module.source" // For module sources
"opentofu.operation.target_count" // For operation-specific counts
Use the tracing.SetSpanError helper to consistently record errors:
if err != nil {
tracing.SetSpanError(span, err)
return err
}
This helper supports various error types including standard errors, strings, and OpenTofu diagnostics.