Files
Admin Bot f9addf945d
CI / Vet, test, build (push) Successful in 3m43s
CI / Build and push image (push) Successful in 1m33s
feat(tracing): add OpenTelemetry instrumentation to API gateway
- Add internal/tracing package with OTel tracer initialization
- HTTP middleware for server-side tracing (request/response attributes)
- Transport wrapper for client-side upstream call tracing
- Update proxy to use tracing transport
- Add OTEL_* env vars to k8s deployment

Traces flow: api-gateway -> otel-collector -> tempo -> grafana
2026-08-31 15:01:56 -07:00

77 lines
1.9 KiB
Go

package tracing
import (
"net/http"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/propagation"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
"go.opentelemetry.io/otel/trace"
)
// Transport wraps an http.RoundTripper with tracing.
type Transport struct {
base http.RoundTripper
}
// NewTransport creates a new tracing transport wrapper.
func NewTransport(base http.RoundTripper) *Transport {
if base == nil {
base = http.DefaultTransport
}
return &Transport{base: base}
}
// RoundTrip implements http.RoundTripper with tracing.
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
tracer := otel.Tracer(tracerName)
propagator := otel.GetTextMapPropagator()
// Start client span
spanName := "HTTP " + req.Method + " " + req.URL.Host + req.URL.Path
ctx, span := tracer.Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
semconv.HTTPRequestMethodKey.String(req.Method),
semconv.URLFull(req.URL.String()),
semconv.ServerAddress(req.URL.Host),
attribute.String("upstream.name", req.URL.Host),
),
)
defer span.End()
// Inject trace context into outgoing request headers
propagator.Inject(ctx, propagation.HeaderCarrier(req.Header))
// Perform the request
start := time.Now()
resp, err := t.base.RoundTrip(req.WithContext(ctx))
duration := time.Since(start)
// Record timing
span.SetAttributes(attribute.Float64("http.request.duration_ms", float64(duration.Milliseconds())))
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
// Record response attributes
span.SetAttributes(
semconv.HTTPResponseStatusCode(resp.StatusCode),
)
if resp.StatusCode >= 400 {
span.SetStatus(codes.Error, http.StatusText(resp.StatusCode))
} else {
span.SetStatus(codes.Ok, "")
}
return resp, nil
}