Add internal/tracing: env-gated OTel setup, span/log helpers, decorators
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
140
internal/tracing/tracing.go
Normal file
140
internal/tracing/tracing.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Package tracing wires OpenTelemetry tracing into the operator: SDK setup
|
||||
// gated on standard OTEL_* environment variables, span helpers that keep the
|
||||
// logr/zap logging enriched with trace context, and decorators for the
|
||||
// reconciler, providers, HTTP server, and Kubernetes API transport.
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
"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.43.0"
|
||||
)
|
||||
|
||||
// maxQueueSize bounds the batch processor's span buffer; the default 2048
|
||||
// is oversized for this process (3 concurrent reconciles, tickers) and the
|
||||
// manager pod runs with a 128Mi memory limit.
|
||||
const maxQueueSize = 512
|
||||
|
||||
// Setup initializes the global tracing pipeline from OTEL_* environment
|
||||
// variables and returns a shutdown func that flushes pending spans.
|
||||
//
|
||||
// Tracing stays fully off — no exporter, no global provider, spans are
|
||||
// no-ops, logs carry no traceID — unless the environment opts in by setting
|
||||
// OTEL_TRACES_EXPORTER or an OTLP endpoint. OTEL_SDK_DISABLED=true and
|
||||
// OTEL_TRACES_EXPORTER=none force it off (the Go SDK does not implement
|
||||
// OTEL_SDK_DISABLED itself). Sampling, endpoints, TLS, and headers follow
|
||||
// the standard SDK/exporter env vars (OTEL_TRACES_SAMPLER, OTEL_EXPORTER_OTLP_*).
|
||||
func Setup(ctx context.Context, log logr.Logger, service, version string) (func(context.Context) error, error) {
|
||||
noop := func(context.Context) error { return nil }
|
||||
|
||||
exporterEnv := strings.ToLower(strings.TrimSpace(os.Getenv("OTEL_TRACES_EXPORTER")))
|
||||
endpointSet := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT") != "" ||
|
||||
os.Getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") != ""
|
||||
sdkDisabled := strings.EqualFold(strings.TrimSpace(os.Getenv("OTEL_SDK_DISABLED")), "true")
|
||||
|
||||
if sdkDisabled || exporterEnv == "none" || (exporterEnv == "" && !endpointSet) {
|
||||
log.Info("tracing disabled",
|
||||
"reason", disabledReason(sdkDisabled, exporterEnv))
|
||||
return noop, nil
|
||||
}
|
||||
|
||||
exp, expName, err := newExporter(ctx, exporterEnv)
|
||||
if err != nil {
|
||||
return noop, fmt.Errorf("tracing setup: %w", err)
|
||||
}
|
||||
|
||||
res, err := buildResource(ctx, service, version)
|
||||
if err != nil {
|
||||
if res == nil {
|
||||
return noop, fmt.Errorf("tracing setup: building resource: %w", err)
|
||||
}
|
||||
// Schema-URL conflicts between semconv versions still yield a usable
|
||||
// merged resource; keep it rather than losing tracing over metadata.
|
||||
log.V(1).Info("tracing resource merge conflict; continuing", "err", err.Error())
|
||||
}
|
||||
|
||||
tp := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithBatcher(exp, sdktrace.WithMaxQueueSize(maxQueueSize)),
|
||||
sdktrace.WithResource(res),
|
||||
)
|
||||
otel.SetTracerProvider(tp)
|
||||
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
|
||||
propagation.TraceContext{}, propagation.Baggage{}))
|
||||
otelLog := log.WithName("otel")
|
||||
otel.SetLogger(otelLog)
|
||||
// V(1), not Error: an unreachable collector fails every export cycle and
|
||||
// would otherwise spam the error log every few seconds.
|
||||
otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) {
|
||||
otelLog.V(1).Info("otel error", "err", err.Error())
|
||||
}))
|
||||
|
||||
log.Info("tracing enabled", "exporter", expName, "service", service)
|
||||
return tp.Shutdown, nil
|
||||
}
|
||||
|
||||
func disabledReason(sdkDisabled bool, exporterEnv string) string {
|
||||
switch {
|
||||
case sdkDisabled:
|
||||
return "OTEL_SDK_DISABLED=true"
|
||||
case exporterEnv == "none":
|
||||
return "OTEL_TRACES_EXPORTER=none"
|
||||
default:
|
||||
return "no OTEL_TRACES_EXPORTER or OTLP endpoint configured"
|
||||
}
|
||||
}
|
||||
|
||||
// newExporter hand-rolls the OTEL_TRACES_EXPORTER / OTEL_EXPORTER_OTLP_*
|
||||
// protocol selection instead of pulling in contrib's autoexport, which drags
|
||||
// metric/log/prometheus exporters into a trace-only binary.
|
||||
func newExporter(ctx context.Context, kind string) (sdktrace.SpanExporter, string, error) {
|
||||
switch kind {
|
||||
case "", "otlp":
|
||||
proto := strings.ToLower(strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_TRACES_PROTOCOL")))
|
||||
if proto == "" {
|
||||
proto = strings.ToLower(strings.TrimSpace(os.Getenv("OTEL_EXPORTER_OTLP_PROTOCOL")))
|
||||
}
|
||||
switch proto {
|
||||
case "", "http/protobuf":
|
||||
exp, err := otlptracehttp.New(ctx)
|
||||
return exp, "otlp/http", err
|
||||
case "grpc":
|
||||
exp, err := otlptracegrpc.New(ctx)
|
||||
return exp, "otlp/grpc", err
|
||||
default:
|
||||
return nil, "", fmt.Errorf("unsupported OTLP protocol %q (supported: http/protobuf, grpc)", proto)
|
||||
}
|
||||
case "console":
|
||||
exp, err := stdouttrace.New()
|
||||
return exp, "console", err
|
||||
default:
|
||||
return nil, "", fmt.Errorf("unsupported OTEL_TRACES_EXPORTER %q (supported: otlp, console, none)", kind)
|
||||
}
|
||||
}
|
||||
|
||||
// buildResource layers defaults < service identity < environment, so
|
||||
// OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES always win.
|
||||
func buildResource(ctx context.Context, service, version string) (*resource.Resource, error) {
|
||||
base, err := resource.Merge(resource.Default(), resource.NewWithAttributes(semconv.SchemaURL,
|
||||
semconv.ServiceName(service),
|
||||
semconv.ServiceVersion(version),
|
||||
))
|
||||
if err != nil {
|
||||
return base, err
|
||||
}
|
||||
env, err := resource.New(ctx, resource.WithFromEnv())
|
||||
if err != nil {
|
||||
return base, err
|
||||
}
|
||||
return resource.Merge(base, env)
|
||||
}
|
||||
Reference in New Issue
Block a user