package tracing import ( "context" "fmt" "net/http" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "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" ) var tracer trace.Tracer // Config holds tracing configuration type Config struct { ServiceName string ServiceVersion string Endpoint string // OTLP endpoint (e.g., "localhost:4317") Enabled bool } // InitTracer initializes the OpenTelemetry tracer func InitTracer(config Config) (func(context.Context) error, error) { if !config.Enabled { // Return no-op shutdown function return func(context.Context) error { return nil }, nil } ctx := context.Background() // Create OTLP exporter client := otlptracegrpc.NewClient( otlptracegrpc.WithEndpoint(config.Endpoint), otlptracegrpc.WithInsecure(), // Use WithTLSCredentials in production ) exporter, err := otlptrace.New(ctx, client) if err != nil { return nil, fmt.Errorf("failed to create OTLP exporter: %w", err) } // Create resource res, err := resource.New(ctx, resource.WithAttributes( semconv.ServiceNameKey.String(config.ServiceName), semconv.ServiceVersionKey.String(config.ServiceVersion), ), ) if err != nil { return nil, fmt.Errorf("failed to create resource: %w", err) } // Create trace provider tp := sdktrace.NewTracerProvider( sdktrace.WithBatcher(exporter), sdktrace.WithResource(res), sdktrace.WithSampler(sdktrace.AlwaysSample()), ) // Set global trace provider otel.SetTracerProvider(tp) // Set global propagator otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( propagation.TraceContext{}, propagation.Baggage{}, )) // Get tracer tracer = tp.Tracer(config.ServiceName) // Return shutdown function return tp.Shutdown, nil } // Middleware returns an HTTP middleware that creates spans for requests func Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if tracer == nil { next.ServeHTTP(w, r) return } // Extract context from request headers ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)) // Start span ctx, span := tracer.Start(ctx, r.Method+" "+r.URL.Path, trace.WithSpanKind(trace.SpanKindServer), trace.WithAttributes( semconv.HTTPMethodKey.String(r.Method), semconv.HTTPURLKey.String(r.URL.String()), semconv.HTTPTargetKey.String(r.URL.Path), semconv.HTTPSchemeKey.String(r.URL.Scheme), semconv.NetHostNameKey.String(r.Host), ), ) defer span.End() // Create new request with updated context r = r.WithContext(ctx) next.ServeHTTP(w, r) }) } // StartSpan starts a new span with the given name func StartSpan(ctx context.Context, name string, attrs ...attribute.KeyValue) (context.Context, trace.Span) { if tracer == nil { return ctx, trace.SpanFromContext(ctx) } return tracer.Start(ctx, name, trace.WithAttributes(attrs...)) } // SpanFromContext returns the current span from the context func SpanFromContext(ctx context.Context) trace.Span { return trace.SpanFromContext(ctx) } // AddEvent adds an event to the current span func AddEvent(ctx context.Context, name string, attrs ...attribute.KeyValue) { span := trace.SpanFromContext(ctx) span.AddEvent(name, trace.WithAttributes(attrs...)) } // SetAttributes sets attributes on the current span func SetAttributes(ctx context.Context, attrs ...attribute.KeyValue) { span := trace.SpanFromContext(ctx) span.SetAttributes(attrs...) } // RecordError records an error on the current span func RecordError(ctx context.Context, err error) { if err == nil { return } span := trace.SpanFromContext(ctx) span.RecordError(err) }