egress: suppress per-request HTTP metrics on /ws to fix SigNoz cardinality spike#365
Merged
Merged
Conversation
…lowup The SigNoz vendor pinged us about a 2x ingestion spike on May 20 caused specifically by the unbounded-egress service. Root cause: otelhttp's default http.server.duration.* histogram set carries net.sock.peer.addr, net.sock.peer.port, and http.user_agent attributes — every WebSocket upgrade creates a fresh time series across .bucket / .count / .max / .min / .sum, plus content-length families, plus histogram bucket boundaries. For a service handling thousands of upgrades per day this explodes into thousands of stale time series per day per metric. Attach a noop MeterProvider to the /ws handler so otelhttp no longer emits any http.server.* histograms here. Tracing/span propagation still works (the handler still wraps for context). The four ObservableUpDownCounters declared above (concurrent-websockets, concurrent-quic-connections, concurrent-quic-streams, ingress-bytes) already cover the only signals worth keeping for this endpoint: a single /ws route doesn't need per-request duration histograms because every request is a GET that returns 101 Switching Protocols, so status_code / method are constant and the duration is the upgrade handshake (rarely interesting in isolation). A follow-up will add a defensive AttributeFilter view in getlantern/telemetry's EnableOTELMetrics so the same cardinality explosion can't sneak back in via a different handler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR reduces OpenTelemetry HTTP server metric cardinality for the unbounded-egress WebSocket upgrade endpoint by preventing otelhttp from emitting per-request http.server.* metrics on /ws, while keeping span/context propagation intact. This aligns with the service’s usage pattern (long-lived WebSockets) and addresses the reported SigNoz ingestion spike caused by high-cardinality attributes on upgrade requests.
Changes:
- Attach a noop
MeterProviderspecifically to the/wsotelhttp.NewHandlerwrapper to suppresshttp.server.*metrics emission for WebSocket upgrades. - Add inline documentation explaining the high-cardinality mechanism and why suppressing these metrics is acceptable given existing custom concurrency/ingress counters.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the 2× SigNoz ingestion spike the vendor flagged on May 20 (Slack thread), traced to the `unbounded-egress` service.
`otelhttp.NewHandler` on the `/ws` upgrade endpoint emits `http.server.duration.*` histograms with these attributes:
Every WebSocket upgrade creates a fresh time series across all five histogram components (`.bucket`, `.count`, `.max`, `.min`, `.sum`), plus the `http.server.request_content_length.` / `http.server.response_content_length.` families. For a service with the throughput pattern of unbounded-egress this lands as thousands of stale time series per day per metric — exactly the 2× ingestion shape SigNoz observed.
SigNoz query confirms the volume: `http.server.duration.count` from `service.name = 'unbounded-egress'` is reporting 2,087 samples/sec rate (~180M samples/day) for a service that has 1–7 concurrent WebSocket connections. The arithmetic doesn't match — almost all of that volume is new-series-per-connection, not per-sample value.
Fix
Attach a noop `MeterProvider` to the `/ws` handler so `otelhttp` doesn't emit any `http.server.*` histograms there. Tracing/span propagation still works — the handler still wraps the context. The four `ObservableUpDownCounters` declared above (`concurrent-websockets`, `concurrent-quic-connections`, `concurrent-quic-streams`, `ingress-bytes`) already cover the only signals worth keeping for this endpoint.
What we lose
Per-request HTTP latency/size histograms scoped to this exact `/ws` route. On a single upgrade endpoint where every request is GET → 101 Switching Protocols, these were:
So we lose ~zero useful information and reclaim the SigNoz budget.
Follow-up
Going to open a defensive PR on `getlantern/telemetry` next, adding a metric `View` with an `AttributeFilter` that strips `net.sock.peer.`, `http.client_ip`, and `http.user_agent` from `http.server.` metrics at the SDK layer. That way if another service in the org adds `otelhttp.NewHandler` later, the cardinality explosion can't recur.
Test plan
cc Reflog (replying in the Slack thread once this lands)
🤖 Generated with Claude Code