]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
feat(ui-server): expose Prometheus /metrics on ws and mcp transports (#1921)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Tue, 23 Jun 2026 22:35:14 +0000 (00:35 +0200)
committerGitHub <noreply@github.com>
Tue, 23 Jun 2026 22:35:14 +0000 (00:35 +0200)
commitb56748a3f5e53e5513124f96939b54b425e5af22
tree6f833332ce5c2404fc807525ebc4d13ac67ccb36
parent18bd635178562dbbd80f92225a17452ad06f98c0
feat(ui-server): expose Prometheus /metrics on ws and mcp transports (#1921)

The opt-in Prometheus `/metrics` endpoint introduced by #1912 was wired
only into `UIHttpServer`. With `uiServer.type` set to `ws` or `mcp`, the
endpoint was silently disabled with a startup warning.

Move the metrics registry, the request handler, and the soft-cap
accounting from `UIHttpServer` up to `AbstractUIServer`, then expose
`/metrics` on the shared `httpServer` from `UIWebSocketServer` and
`UIMCPServer` via a single `tryServeMetrics` template method that
encapsulates the `metrics.enabled` gate, the path predicate, the
authentication step, and the scrape handler. The
`uiServer.metrics.{enabled,softSampleCap}` configuration block is
preserved; the gauge registry is re-used unchanged; and `accessPolicy`,
rate-limiting, and `authentication` apply identically on all three
transports.

The lifecycle is bracketed by a public `start()` template-method on
`AbstractUIServer`; subclasses implement the `attachTransport()`
protected hook and may override the symmetric `detachTransport()` hook.
A one-shot `transportAttached` guard is set BEFORE `attachTransport()`
runs so a throwing hook cannot leave a half-attached server admitting a
silent retry; on failure any partially-registered `'request'` /
`'upgrade'` listeners on `httpServer` are stripped and the guard is
rolled back. `metricsScrapeChain` is reseated to `Promise.resolve()`
at every `start()` so cycle N+1 never awaits cycle N's scheduled
registry clear; in `runMetricsScrape`, the per-scrape
`metricsSampleCount` is captured into a local snapshot BEFORE
`await registry.metrics()` yields, so a peer scrape dispatched after
a sync `stop()`/`start()` cannot overwrite the count the soft-cap
branch reads. The corresponding `stop()` is symmetrically defensive:
both `detachTransport()` and each `uiService.stop()` are wrapped in
per-iteration `try/catch` so a throwing override does not abort
downstream teardown (`stopHttpServer`, remaining services, handlers,
caches), and the `transportAttached = false` reset runs in a `finally`
block so a subsequent `start()` is never permanently locked out — any
other unexpected throw still propagates. A shared
`renderNotFoundAndDestroy(req, res)` helper consolidates the 404
fallback used by the WebSocket and MCP request listeners; the helper
delegates socket teardown to `destroyHttp1SocketIfPending(req)` which
skips `req.destroy()` on HTTP/2 streams (lifecycle owned by the
`Http2Stream`) and on already-complete HTTP/1.1 requests (keep-alive
pool).

No new configuration surface, no new listener: `/metrics` is served on
the same `Http2Server | Server` instance that already backs the
WebSocket protocol upgrade in `UIWebSocketServer` and the `/mcp` route
in `UIMCPServer`. HEAD responses emit an explicit `Content-Length`
equal to the GET body byte length (`Buffer.byteLength(body, 'utf8')`)
and an empty body, per RFC 9110 §9.3.2. A frozen
`METRICS_ALLOWED_LABEL_NAMES` tuple pins the canonical PII surface; the
`isMetricsAllowedLabelName` type-guard predicate is the O(1) lookup,
and a guardian spec asserts every gauge label name in the rendered
registry is admitted.

Operator-visible change: `uiServer.metrics.enabled=true` with
`type='ws'` or `'mcp'` now serves `/metrics` whereas the previous
release silently disabled it. Operators relying on the silent disable
must set `metrics.enabled=false` (or omit the block) and audit firewall
posture on the UI server port. `HEAD /metrics` (e.g. `curl -I`) returns
identical headers to `GET` (including `Content-Length`) with an empty
body. README updated.

Closes #1917
Refs #851, #1912
README.md
src/charging-station/ui-server/AbstractUIServer.ts
src/charging-station/ui-server/UIHttpServer.ts
src/charging-station/ui-server/UIMCPServer.ts
src/charging-station/ui-server/UIWebSocketServer.ts
src/utils/ConfigurationSchema.ts
tests/charging-station/ui-server/UIHttpServer.test.ts
tests/charging-station/ui-server/UIMCPServer.test.ts
tests/charging-station/ui-server/UIMetricsEndpoint.test.ts
tests/charging-station/ui-server/UIServerTestUtils.ts
tests/charging-station/ui-server/UIWebSocketServer.test.ts