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
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user