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 }