Go · 3433 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 // Package tracing wires the OpenTelemetry SDK and exposes a small
4 // HTTP-middleware helper. When tracing is disabled, all helpers are no-ops
5 // so callers don't need to branch.
6 package tracing
7
8 import (
9 "context"
10 "errors"
11 "fmt"
12 "net/http"
13 "net/url"
14 "time"
15
16 "go.opentelemetry.io/otel"
17 "go.opentelemetry.io/otel/attribute"
18 "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
19 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
20 "go.opentelemetry.io/otel/propagation"
21 "go.opentelemetry.io/otel/sdk/resource"
22 sdktrace "go.opentelemetry.io/otel/sdk/trace"
23 semconv "go.opentelemetry.io/otel/semconv/v1.27.0"
24 "go.opentelemetry.io/otel/trace"
25 )
26
27 // Config controls the SDK setup.
28 type Config struct {
29 Enabled bool
30 Endpoint string // OTLP HTTP endpoint, e.g. "http://otel-collector:4318"
31 SampleRate float64 // 0..1
32 ServiceName string
33 }
34
35 // Init constructs the tracer provider per cfg and installs it as the
36 // global OTel provider. Returns a shutdown function the caller invokes on
37 // exit. When cfg.Enabled is false, returns a no-op shutdown.
38 func Init(ctx context.Context, cfg Config) (func(context.Context) error, error) {
39 if !cfg.Enabled {
40 return func(context.Context) error { return nil }, nil
41 }
42 if cfg.Endpoint == "" {
43 return nil, errors.New("tracing: endpoint required when enabled")
44 }
45
46 u, err := url.Parse(cfg.Endpoint)
47 if err != nil {
48 return nil, fmt.Errorf("tracing: parse endpoint: %w", err)
49 }
50 opts := []otlptracehttp.Option{
51 otlptracehttp.WithEndpoint(u.Host),
52 otlptracehttp.WithURLPath(u.Path),
53 }
54 if u.Scheme != "https" {
55 opts = append(opts, otlptracehttp.WithInsecure())
56 }
57
58 exporter, err := otlptrace.New(ctx, otlptracehttp.NewClient(opts...))
59 if err != nil {
60 return nil, fmt.Errorf("tracing: exporter: %w", err)
61 }
62
63 res, err := resource.Merge(
64 resource.Default(),
65 resource.NewWithAttributes(
66 semconv.SchemaURL,
67 semconv.ServiceName(cfg.ServiceName),
68 ),
69 )
70 if err != nil {
71 return nil, fmt.Errorf("tracing: resource: %w", err)
72 }
73
74 provider := sdktrace.NewTracerProvider(
75 sdktrace.WithBatcher(
76 exporter,
77 sdktrace.WithBatchTimeout(5*time.Second),
78 ),
79 sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(cfg.SampleRate))),
80 sdktrace.WithResource(res),
81 )
82 otel.SetTracerProvider(provider)
83 otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
84 propagation.TraceContext{},
85 propagation.Baggage{},
86 ))
87
88 return func(ctx context.Context) error {
89 shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
90 defer cancel()
91 return provider.Shutdown(shutdownCtx)
92 }, nil
93 }
94
95 // Tracer returns a named tracer from the global provider. Safe to call when
96 // tracing is disabled (returns a no-op tracer).
97 func Tracer(name string) trace.Tracer {
98 return otel.Tracer(name)
99 }
100
101 // Middleware returns HTTP middleware that emits one span per request,
102 // linking it to the request_id when present.
103 func Middleware(next http.Handler) http.Handler {
104 tracer := Tracer("shithub.web")
105 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
106 ctx, span := tracer.Start(
107 r.Context(), r.Method+" "+r.URL.Path,
108 trace.WithSpanKind(trace.SpanKindServer),
109 trace.WithAttributes(
110 attribute.String("http.method", r.Method),
111 attribute.String("http.path", r.URL.Path),
112 ),
113 )
114 defer span.End()
115 next.ServeHTTP(w, r.WithContext(ctx))
116 })
117 }
118