package observability import ( "context" "net/http" "github.com/google/uuid" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "go.opentelemetry.io/otel/trace" ) // ContextKey is the type for context keys to avoid collisions. type ContextKey string const ( // RequestIDKey is the key for the request ID in the context. RequestIDKey ContextKey = "request_id" // LoggerContextKey is the key for the logger in the context. LoggerContextKey ContextKey = "logger" ) // responseWriter is a wrapper around http.ResponseWriter to capture the status code. type responseWriter struct { http.ResponseWriter statusCode int } func (rw *responseWriter) WriteHeader(code int) { rw.statusCode = code rw.ResponseWriter.WriteHeader(code) } // RequestIDMiddleware generates a unique request ID and adds it to the request context. func RequestIDMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestID := uuid.New().String() ctx := context.WithValue(r.Context(), RequestIDKey, requestID) w.Header().Set("X-Request-ID", requestID) next.ServeHTTP(w, r.WithContext(ctx)) }) } // LoggingMiddleware creates a request-scoped logger and injects it into the context. func LoggingMiddleware(log *Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Start with a logger that has trace and span IDs. requestLogger := log.Ctx(r.Context()) // Add request_id to logger context. if reqID, ok := r.Context().Value(RequestIDKey).(string); ok { requestLogger = requestLogger.With("request_id", reqID) } // Add the logger to the context. ctx := context.WithValue(r.Context(), LoggerContextKey, requestLogger) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // LoggerFromContext retrieves the request-scoped logger from the context. // If no logger is found, it returns a default logger. func LoggerFromContext(ctx context.Context) *Logger { if logger, ok := ctx.Value(LoggerContextKey).(*Logger); ok { return logger } // Fallback to a default logger if none is found in context. return NewLogger("tercul-fallback", "development") } // TracingMiddleware creates a new OpenTelemetry span for each request. func TracingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := otel.GetTextMapPropagator().Extract(r.Context(), propagation.HeaderCarrier(r.Header)) tracer := otel.Tracer("http-server") ctx, span := tracer.Start(ctx, "HTTP "+r.Method+" "+r.URL.Path, trace.WithAttributes( semconv.HTTPMethodKey.String(r.Method), semconv.HTTPURLKey.String(r.URL.String()), )) defer span.End() rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} next.ServeHTTP(rw, r.WithContext(ctx)) span.SetAttributes(attribute.Int("http.status_code", rw.statusCode)) }) }