gomcp/internal/infrastructure/tracing/middleware.go

83 lines
2.3 KiB
Go

package tracing
import (
"fmt"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
// HTTPMiddleware creates spans for each HTTP request.
// Extracts trace context from incoming headers and sets span attributes.
func HTTPMiddleware(next http.Handler) http.Handler {
tracer := otel.Tracer("sentinel-soc/http")
propagator := otel.GetTextMapPropagator()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract trace context from incoming headers.
ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
spanName := fmt.Sprintf("%s %s", r.Method, r.URL.Path)
ctx, span := tracer.Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithAttributes(
attribute.String("http.method", r.Method),
attribute.String("http.url", r.URL.String()),
attribute.String("http.target", r.URL.Path),
attribute.String("http.user_agent", r.UserAgent()),
attribute.String("net.host.name", r.Host),
),
)
defer span.End()
// Wrap response writer to capture status code.
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(sw, r.WithContext(ctx))
span.SetAttributes(
attribute.Int("http.status_code", sw.status),
)
if sw.status >= 400 {
span.SetAttributes(attribute.Bool("error", true))
}
})
}
// statusWriter captures the HTTP status code for span attributes.
// Implements http.Flusher to support SSE/streaming through middleware chain.
type statusWriter struct {
http.ResponseWriter
status int
wroteHeader bool
}
func (sw *statusWriter) WriteHeader(code int) {
if !sw.wroteHeader {
sw.status = code
sw.wroteHeader = true
}
sw.ResponseWriter.WriteHeader(code)
}
func (sw *statusWriter) Write(b []byte) (int, error) {
if !sw.wroteHeader {
sw.wroteHeader = true
}
return sw.ResponseWriter.Write(b)
}
// Flush delegates to the underlying ResponseWriter if it supports http.Flusher.
// Required for SSE streaming endpoints to work through the middleware chain.
func (sw *statusWriter) Flush() {
if f, ok := sw.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// Unwrap returns the underlying ResponseWriter for Go 1.20+ ResponseController.
func (sw *statusWriter) Unwrap() http.ResponseWriter {
return sw.ResponseWriter
}