(metrics *telemetry.Metrics, options ...MiddlewareOption)
| 46 | } |
| 47 | |
| 48 | func MetricTelemetryMiddleware(metrics *telemetry.Metrics, options ...MiddlewareOption) func(huma.Context, func(huma.Context)) { |
| 49 | config := &middlewareConfig{ |
| 50 | skipPaths: make(map[string]bool), |
| 51 | } |
| 52 | |
| 53 | for _, opt := range options { |
| 54 | opt(config) |
| 55 | } |
| 56 | |
| 57 | return func(ctx huma.Context, next func(huma.Context)) { |
| 58 | path := ctx.URL().Path |
| 59 | |
| 60 | // Skip instrumentation for specified paths |
| 61 | // extract the last part of the path to match against skipPaths |
| 62 | pathParts := strings.Split(path, "/") |
| 63 | pathToMatch := "/" + pathParts[len(pathParts)-1] |
| 64 | if config.skipPaths[pathToMatch] || config.skipPaths[path] { |
| 65 | next(ctx) |
| 66 | return |
| 67 | } |
| 68 | |
| 69 | start := time.Now() |
| 70 | method := ctx.Method() |
| 71 | routePath := getRoutePath(ctx) |
| 72 | |
| 73 | next(ctx) |
| 74 | |
| 75 | duration := time.Since(start).Seconds() |
| 76 | statusCode := ctx.Status() |
| 77 | |
| 78 | // If the client disconnected before the handler finished, the handler |
| 79 | // likely converted the resulting context.Canceled into a huma 5xx and |
| 80 | // tried to write a response to a closed socket. NGINX records that |
| 81 | // case as a 499 (client closed). Without this remap we count it as a |
| 82 | // server error: a single ServiceNow-style burst that times out a |
| 83 | // few thousand list-servers requests inflates http_errors_total even |
| 84 | // though no client ever saw a 5xx, and the availability alert fires |
| 85 | // on what is effectively just slow responses. |
| 86 | // |
| 87 | // Only context.Canceled is remapped — context.DeadlineExceeded would |
| 88 | // indicate a server-side timeout we set ourselves and should still |
| 89 | // count as a server error if/when we add per-request deadlines. |
| 90 | if reqErr := ctx.Context().Err(); reqErr != nil && errors.Is(reqErr, context.Canceled) { |
| 91 | statusCode = statusClientClosed |
| 92 | } |
| 93 | |
| 94 | // Combine common and custom attributes |
| 95 | attrs := []attribute.KeyValue{ |
| 96 | attribute.String("method", method), |
| 97 | attribute.String("path", routePath), |
| 98 | attribute.Int("status_code", statusCode), |
| 99 | } |
| 100 | |
| 101 | // Record metrics |
| 102 | metrics.Requests.Add(ctx.Context(), 1, metric.WithAttributes(attrs...)) |
| 103 | |
| 104 | // Skip the error counter for client-closed requests so the availability |
| 105 | // metric reflects server-visible errors only. |
searching dependent graphs…