package telemetry import ( "net/http" "time" "github.com/gin-gonic/gin" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.uber.org/zap" ) // tracerName names the instrumentation scope for backend HTTP spans. const tracerName = "scrabble/backend/server" // Middleware returns gin middleware that, for every request, opens a server // span, measures server-side latency, and emits a structured access log // correlated with the active trace. It uses the globally-registered tracer, so // spans are exported only when an exporter is configured, while the timing log // is always emitted. Probe paths (/healthz, /readyz) log at debug level to keep // the default log clean. func Middleware(logger *zap.Logger) gin.HandlerFunc { if logger == nil { logger = zap.NewNop() } tracer := otel.Tracer(tracerName) return func(c *gin.Context) { start := time.Now() route := c.FullPath() if route == "" { route = c.Request.URL.Path } ctx, span := tracer.Start(c.Request.Context(), c.Request.Method+" "+route) c.Request = c.Request.WithContext(ctx) c.Next() status := c.Writer.Status() elapsed := time.Since(start) span.SetAttributes( attribute.String("http.request.method", c.Request.Method), attribute.String("http.route", route), attribute.Int("http.response.status_code", status), ) if status >= http.StatusInternalServerError { span.SetStatus(codes.Error, http.StatusText(status)) } span.End() fields := []zap.Field{ zap.String("method", c.Request.Method), zap.String("path", route), zap.Int("status", status), zap.Duration("latency", elapsed), } fields = append(fields, TraceFieldsFromContext(ctx)...) if isProbePath(c.Request.URL.Path) { logger.Debug("http request", fields...) return } logger.Info("http request", fields...) } } // isProbePath reports whether path is one of the unauthenticated infrastructure // probes, whose access logs are demoted to debug level. func isProbePath(path string) bool { return path == "/healthz" || path == "/readyz" }