package telemetry import ( "context" "io" "github.com/fujin/anthropic-proxy/internal/config" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" otellog "go.opentelemetry.io/otel/log/global" "go.opentelemetry.io/otel/sdk/log" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.26.0" ) // Setup initializes OpenTelemetry providers. It always creates a MeterProvider // so metrics can be recorded in-process. When cfg.ExportEnabled(), OTLP gRPC // exporters are additionally configured to push to the LGTM stack. // Returns a shutdown function and an optional io.Writer for the log bridge. func Setup(ctx context.Context, cfg config.TelemetryConfig) (shutdown func(context.Context) error, logWriter io.Writer, err error) { res, err := resource.New(ctx, resource.WithAttributes( semconv.ServiceName(cfg.ServiceName), ), ) if err != nil { return nil, nil, err } if !cfg.ExportEnabled() { // No export — set up in-memory meter provider only so metric // instruments are valid (they just don't export anywhere). mp := sdkmetric.NewMeterProvider(sdkmetric.WithResource(res)) otel.SetMeterProvider(mp) InitMetrics(mp.Meter(cfg.ServiceName)) return func(ctx context.Context) error { return mp.Shutdown(ctx) }, nil, nil } // Build exporter options traceOpts := []otlptracegrpc.Option{otlptracegrpc.WithEndpoint(cfg.Endpoint)} metricOpts := []otlpmetricgrpc.Option{ otlpmetricgrpc.WithEndpoint(cfg.Endpoint), otlpmetricgrpc.WithTemporalitySelector(sdkmetric.CumulativeTemporalitySelector), } logOpts := []otlploggrpc.Option{otlploggrpc.WithEndpoint(cfg.Endpoint)} if cfg.Insecure { traceOpts = append(traceOpts, otlptracegrpc.WithInsecure()) metricOpts = append(metricOpts, otlpmetricgrpc.WithInsecure()) logOpts = append(logOpts, otlploggrpc.WithInsecure()) } // Trace exporter traceExp, err := otlptracegrpc.New(ctx, traceOpts...) if err != nil { return nil, nil, err } tp := trace.NewTracerProvider( trace.WithBatcher(traceExp), trace.WithResource(res), ) otel.SetTracerProvider(tp) // Metric exporter metricExp, err := otlpmetricgrpc.New(ctx, metricOpts...) if err != nil { return nil, nil, err } mp := sdkmetric.NewMeterProvider( sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp)), sdkmetric.WithResource(res), ) otel.SetMeterProvider(mp) InitMetrics(mp.Meter(cfg.ServiceName)) // Log exporter logExp, err := otlploggrpc.New(ctx, logOpts...) if err != nil { return nil, nil, err } lp := log.NewLoggerProvider( log.WithProcessor(log.NewBatchProcessor(logExp)), log.WithResource(res), ) otellog.SetLoggerProvider(lp) bridge := &LogBridge{provider: lp} shutdownFn := func(ctx context.Context) error { var firstErr error if e := tp.Shutdown(ctx); e != nil && firstErr == nil { firstErr = e } if e := mp.Shutdown(ctx); e != nil && firstErr == nil { firstErr = e } if e := lp.Shutdown(ctx); e != nil && firstErr == nil { firstErr = e } return firstErr } return shutdownFn, bridge, nil }