Skip to content

egress: suppress per-request HTTP metrics on /ws to fix SigNoz cardinality spike#365

Merged
myleshorton merged 1 commit into
mainfrom
fisk/strip-otelhttp-metrics-on-ws
May 21, 2026
Merged

egress: suppress per-request HTTP metrics on /ws to fix SigNoz cardinality spike#365
myleshorton merged 1 commit into
mainfrom
fisk/strip-otelhttp-metrics-on-ws

Conversation

@myleshorton

Copy link
Copy Markdown
Contributor

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:

Attribute Cardinality
`net.sock.peer.addr` unique per peer
`net.sock.peer.port` unique per TCP connection (ephemeral port)
`http.user_agent` high cardinality
`http.client_ip` unique per peer
`le` per histogram bucket boundary

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:

  • Method: always GET (no useful variance)
  • Status code: always 101 (no useful variance)
  • Duration: handshake-only, not the WebSocket lifetime
  • Size: tiny request body, tiny response body, no useful signal

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

  • `go build ./...` clean
  • `go test ./egress/...` passes (2.047s, 0 failures)
  • Deploy unbounded-egress with this fix and confirm SigNoz `http.server.duration.*` series for `service.name = 'unbounded-egress'` drop to near-zero within the periodic-reader interval (60s default)
  • Confirm `concurrent-websockets` and the other custom counters continue to report normally

cc Reflog (replying in the Slack thread once this lands)

🤖 Generated with Claude Code

…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>
Copilot AI review requested due to automatic review settings May 21, 2026 16:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 MeterProvider specifically to the /ws otelhttp.NewHandler wrapper to suppress http.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.

@myleshorton
myleshorton merged commit 71a580e into main May 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants