Observability

Request IDs with app.set( "requestId", true ), and a Prometheus metrics endpoint with app.metrics().

Request IDs

bxsapp.set( "requestId", true )

Every request gets an id: it's available as req.id, echoed on the response as X-Request-Id, and added to the request log line and the unhandled-error log line, so one request can be followed across log lines and across nodes behind a load balancer:

plain[2026-09-20 21:02:38] [checkout-7f3a] GET /hello 127.0.0.1

A well-formed X-Request-Id from the client (up to 128 characters of letters, digits, ., _ and -) is reused, so an id minted at the edge carries through. Anything else is replaced with a generated one, since it's client-controlled and ends up in logs and a response header. app.set( "requestIdHeader", "X-Correlation-Id" ) renames the header. Off by default, so existing log output is unchanged.

Prometheus metrics (app.metrics())

bxsapp.metrics( { token: getSystemSetting( "METRICS_TOKEN" ), sources: [ stomp ] } )

app.metrics() adds a Prometheus-format endpoint (default GET /metrics) and starts counting requests. Call it before listen(), and before app.use() if scrapes shouldn't pass through sessions or rate limiting.

MetricTypeMeaning
boxexpress_http_requests_total{class}counterRequests by status class (2xx, 4xx, …)
boxexpress_http_request_duration_seconds_sum / _countsummaryTotal handling time and count, for averages and rates
boxexpress_http_active_connections, _active_requestsgaugeOpen connections; requests in flight
boxexpress_http_bytes_received_total, _bytes_sent_totalcounterFrom Undertow
boxexpress_websocket_connectionsgaugeOpen app.ws() connections
boxexpress_draininggauge1 once app.shutdown() has begun
boxexpress_scheduler_job_runs_total{job}, _failures_total, _skipped_total, _runningcounter / gaugePer scheduled job
boxexpress_scheduler_job_last_success_timestamp_seconds{job}, _last_duration_secondsgaugeAbsent until the job has succeeded once
boxexpress_cluster_peers, boxexpress_cluster_is_managergaugeWith clustering on. is_manager appears once an election has been checked, and reports the last check rather than triggering one
boxexpress_stomp_connections, _subscriptions, _messages_published_total, _messages_delivered_totalgauge / counterFor a STOMP broker passed in sources

A scheduled job that has quietly been failing shows up as a rising failure counter and a stale last-success time.

sources takes any object with a getMetrics() method returning { name, type, help, samples: [ { labels, value } ] }, so your own code can add metrics to the same endpoint. A source that throws is logged and skipped rather than failing the scrape.

token requires Authorization: Bearer <token>, compared in constant time. The numbers are operational detail, so don't leave the endpoint open on a public listener. Requests made through app.inject() aren't counted.

There are deliberately no per-route latency histograms: raw paths would blow up the number of series, and bucket boundaries are an app-specific choice.

Connector statistics

For the same HTTP-layer numbers as a struct rather than a scrape endpoint — a monitoring page inside your own app, say — see app.getConnectorStatistics() under App.