Files
opentf/internal/tracing/utils.go
Martin Atkins 0503163e28 tracing: Centralize our OpenTelemetry package imports
OpenTelemetry has various Go packages split across several Go modules that
often need to be carefully upgraded together. And in particular, we are
using the "semconv" package in conjunction with the OpenTelemetry SDK's
"resource" package in a way that requires that they both agree on which
version of the OpenTelemetry Semantic Conventions are being followed.

To help avoid "dependency hell" situations when upgrading, this centralizes
all of our direct calls into the OpenTelemetry SDK and tracing API into
packages under internal/tracing, by exposing a few thin wrapper functions
that other packages can use to access the same functionality indirectly.

We only use a relatively small subset of the OpenTelemetry library surface
area, so we don't need too many of these reexports and they should not
represent a significant additional maintenance burden.

For the semconv and resource interaction in particular this also factors
that out into a separate helper function with a unit test, so we should
notice quickly whenever they become misaligned. This complements the
end-to-end test previously added in opentofu/opentofu#3447 to give us
faster feedback about this particular problem, while the end-to-end test
has the broader scope of making sure there aren't any errors at all when
initializing OpenTelemetry tracing.

Finally, this also replaces the constants we previously had in package
traceaddrs with functions that return attribute.KeyValue values directly.
This matches the API style used by the OpenTelemetry semconv packages, and
makes the calls to these helpers from elsewhere in the system a little
more concise.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-30 13:27:10 -07:00

145 lines
4.6 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tracing
import (
"context"
"errors"
"log"
"runtime"
"strings"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
"github.com/opentofu/opentofu/internal/tfdiags"
)
func Tracer() trace.Tracer {
if !isTracingEnabled {
return otel.Tracer("")
}
pc, _, _, ok := runtime.Caller(1)
if !ok || runtime.FuncForPC(pc) == nil {
return otel.Tracer("")
}
// We use the import path of the caller function as the tracer name.
return otel.GetTracerProvider().Tracer(extractImportPath(runtime.FuncForPC(pc).Name()))
}
// SetSpanError sets the error or diagnostic information on the span.
// It accepts an error, a string, or a diagnostics object.
// It also sets the span status to Error and records the error or message.
func SetSpanError(span trace.Span, input any) {
if span == nil || input == nil {
return
}
switch v := input.(type) {
case error:
if v != nil {
span.SetStatus(codes.Error, v.Error())
span.RecordError(v)
}
case string:
if v != "" {
span.SetStatus(codes.Error, v)
span.RecordError(errors.New(v))
}
case tfdiags.Diagnostics: // Assuming Diagnostics is a custom type you have defined elsewhere
if v.HasErrors() { // Assuming IsEmpty() checks if the diagnostics object has content
span.SetStatus(codes.Error, v.Err().Error())
span.RecordError(v.Err())
}
default:
// Handle unsupported types gracefully
// TODO: Discuss if this should panic?
span.SetStatus(codes.Error, "ERROR: unsupported input type for SetSpanError.")
span.AddEvent("ERROR: unsupported input type for SetSpanError")
}
}
// ForceFlush ensures that all spans are exported to the collector before
// the application terminates. This is particularly important for CLI
// applications where the process exits immediately after the operation.
//
// This should be called before the application terminates to ensure
// all spans are exported properly.
func ForceFlush(timeout time.Duration) {
if !isTracingEnabled {
return
}
provider, ok := otel.GetTracerProvider().(*sdktrace.TracerProvider)
if !ok {
log.Printf("[TRACE] OpenTelemetry: tracer provider is not an SDK provider, can't force flush")
return
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
log.Printf("[TRACE] OpenTelemetry: flushing spans")
if err := provider.ForceFlush(ctx); err != nil {
log.Printf("[WARN] OpenTelemetry: error flushing spans: %v", err)
}
}
// extractImportPath extracts the import path from a full function name.
// the function names returned by runtime.FuncForPC(pc).Name() can be in the following formats
//
// main.(*MyType).MyMethod
// github.com/you/pkg.(*SomeType).Method-fm
// github.com/you/pkg.functionName
func extractImportPath(fullName string) string {
lastSlash := strings.LastIndex(fullName, "/")
if lastSlash == -1 {
// When there is no slash, then use everything before the first dot
if dot := strings.Index(fullName, "."); dot != -1 {
return fullName[:dot]
}
log.Printf("[WARN] unable to extract import path from function name: %q. Tracing may be incomplete. This is a bug in OpenTofu, please report it.", fullName)
return "unknown"
}
dotAfterSlash := strings.Index(fullName[lastSlash:], ".")
if dotAfterSlash == -1 {
log.Printf("[WARN] unable to extract import path from function name: %q. Tracing may be incomplete. This is a bug in OpenTofu, please report it.", fullName)
return "unknown"
}
return fullName[:lastSlash+dotAfterSlash]
}
// Span is an alias for [trace.Span] just to centralize all of our direct
// imports of OpenTelemetry packages into our tracing packages, to help
// avoid dependency hell.
type Span = trace.Span
// SpanFromContext returns the trace span asssociated with the given context,
// or nil if there is no associated span.
//
// This is a wrapper around [trace.SpanFromContext] just to centralize all of
// our imports of OpenTelemetry packages into our tracing packages, to help
// avoid dependency hell.
func SpanFromContext(ctx context.Context) trace.Span {
return trace.SpanFromContext(ctx)
}
// SpanAttributes wraps [trace.WithAttributes] just so that we can minimize
// how many different OpenTofu packages directly import the OpenTelemetry
// packages, because we tend to need to control which versions we're using
// quite closely to avoid dependency hell.
func SpanAttributes(attrs ...attribute.KeyValue) trace.SpanStartEventOption {
return trace.WithAttributes(attrs...)
}