tenseleyflow/shithub / 0aba572

Browse files

Add OpenTelemetry SDK setup with OTLP HTTP exporter and HTTP middleware

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
0aba5722b5cd3830a4dbed837e3bc3484f8e1c02
Parents
bb6cfc1
Tree
e1d6bba

1 changed file

StatusFile+-
A internal/infra/tracing/tracing.go 117 0
internal/infra/tracing/tracing.goadded
@@ -0,0 +1,117 @@
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
+}