141 lines
5.3 KiB
Go
141 lines
5.3 KiB
Go
// 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)
|
|
}
|