opentofu/telemetry.go
Martin Atkins 7bee77bdd3 command: Start of propagating OpenTelemetry context
Several times over the years we've considered adding tracing
instrumentation to Terraform, since even when running in isolation as a
CLI program it has a "distributed system-like" structure, with lots of
concurrent internal work and also some work delegated to provider plugins
that are essentially temporarily-running microservices.

However, it's always felt a bit overwhelming to do it because much of
Terraform predates the Go context.Context idiom and so it's tough to get
a clean chain of context.Context values all the way down the stack without
disturbing a lot of existing APIs.

This commit aims to just get that process started by establishing how a
context can propagate from "package main" into the command package,
focusing initially on "terraform init" and some other commands that share
some underlying functions with that command.

OpenTelemetry has emerged as a de-facto industry standard and so this uses
its API directly, without any attempt to hide it behind an abstraction.
The OpenTelemetry API is itself already an adapter layer, so we should be
able to swap in any backend that uses comparable concepts. For now we just
discard the tracing reports by default, and allow users to opt in to
delivering traces over OTLP by setting an environment variable when
running Terraform (the environment variable was established in an earlier
commit, so this commit builds on that.)

When tracing collection is enabled, every Terraform CLI run will generate
at least one overall span representing the command that was run. Some
commands might also create child spans, but most currently do not.
2023-07-14 10:24:10 -07:00

88 lines
3.3 KiB
Go

package main
import (
"context"
"os"
"github.com/hashicorp/terraform/version"
"go.opentelemetry.io/contrib/exporters/autoexport"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.opentelemetry.io/otel/trace"
)
// If this environment variable is set to "otlp" when running Terraform CLI
// then we'll enable an experimental OTLP trace exporter.
//
// BEWARE! This is not a committed external interface.
//
// Everything about this is experimental and subject to change in future
// releases. Do not depend on anything about the structure of this output.
// This mechanism might be removed altogether if a different strategy seems
// better based on experience with this experiment.
const openTelemetryExporterEnvVar = "OTEL_TRACES_EXPORTER"
// tracer is the OpenTelemetry tracer to use for traces in package main only.
var tracer trace.Tracer
func init() {
tracer = otel.Tracer("github.com/hashicorp/terraform")
}
// openTelemetryInit initializes the optional OpenTelemetry exporter.
//
// By default we don't export telemetry information at all, since Terraform is
// a CLI tool and so we don't assume we're running in an environment with
// a telemetry collector available.
//
// However, for those running Terraform in automation we allow setting
// the standard OpenTelemetry environment variable OTEL_TRACES_EXPORTER=otlp
// to enable an OTLP exporter, which is in turn configured by all of the
// standard OTLP exporter environment variables:
//
// https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options
//
// We don't currently support any other telemetry export protocols, because
// OTLP has emerged as a de-facto standard and each other exporter we support
// means another relatively-heavy external dependency. OTLP happens to use
// protocol buffers and gRPC, which Terraform would depend on for other reasons
// anyway.
func openTelemetryInit() error {
// We'll check the environment variable ourselves first, because the
// "autoexport" helper we're about to use is built under the assumption
// that exporting should always be enabled and so will expect to find
// an OTLP server on localhost if no environment variables are set at all.
if os.Getenv(openTelemetryExporterEnvVar) != "otlp" {
return nil // By default we just discard all telemetry calls
}
otelResource := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("Terraform CLI"),
semconv.ServiceVersionKey.String(version.Version),
)
// If the environment variable was set to explicitly enable telemetry
// then we'll enable it, using the "autoexport" library to automatically
// handle the details based on the other OpenTelemetry standard environment
// variables.
exp, err := autoexport.NewSpanExporter(context.Background())
if err != nil {
return err
}
sp := sdktrace.NewSimpleSpanProcessor(exp)
provider := sdktrace.NewTracerProvider(
sdktrace.WithSpanProcessor(sp),
sdktrace.WithResource(otelResource),
)
otel.SetTracerProvider(provider)
pgtr := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})
otel.SetTextMapPropagator(pgtr)
return nil
}