Metrics & the /metrics endpoint
Metrics are the third observability pillar alongside the structured logger and the OpenTelemetry-compatible tracer. DaloyJS ships a dependency-free Prometheus / OpenMetrics stack: a metrics registry (counters, gauges, histograms), RED (Rate / Errors / Duration) instrumentation for every route, and an opt-in, auth-guarded /metrics scrape route that inherits the same hardened posture as app.healthcheck().
Everything is built on Web-standard primitives (plus optional process.* gauges guarded for non-Node runtimes), so it runs unchanged on Node, Bun, Deno, and Cloudflare Workers.
- 01Request handledRED hook installed by app.metrics()
- 02Record seriesrequests_total, request_duration_seconds, in_flight
- 03Registry accumulateslow-cardinality {method, route, status} labels
- 04Prometheus scrapesGET /metrics, Bearer token + per-IP rate limit
- 05text/plainExposition renderedregistry.render() to OpenMetrics
Quick start
Call app.metrics() before registering the routes you want measured. It installs RED instrumentation for later routes and registers the scrape route in one step.
Because the instrumentation is installed as a group hook, it only wraps matched routes registered after the app.metrics() call, the same ordering rule as any app.use(...) middleware. Unmatched 404 paths and synthetic OPTIONS preflights are not counted.
What gets exported
Out of the box, the scrape route exposes:
daloy_http_requests_total{method,route,status}: a request counter (rate; the error rate is the subset with a4xx/5xxstatus).daloy_http_request_duration_seconds{method,route}: a latency histogram with conventional Prometheus buckets.daloy_http_requests_in_flight: a gauge of concurrently-handled requests.- process gauges (
daloy_process_resident_memory_bytes,daloy_process_heap_used_bytes,daloy_process_uptime_seconds) collected at scrape time on Node-like runtimes.
Options reference
All fields are optional. The table below covers the full MetricsRouteOptions surface:
| Option | Type | Default | Description |
|---|---|---|---|
path | string | "/metrics" | Override the scrape endpoint path. |
token | string | - | Require Authorization: Bearer <token>, compared via timingSafeEqual. Required in production unless acknowledgeUnauthenticated is set. |
rateLimit | { limit?, windowMs? } | false` | { limit: 60, windowMs: 60_000 } | Per-IP fixed-window rate limit. Pass false to disable entirely (useful inside private VPC networks). |
registry | MetricsRegistry | fresh registry | Bring your own registry to co-render business metrics alongside the built-in HTTP series. |
route | (ctx) => string | undefined | pathname (capped) | Resolve the low-cardinality route label. Always prefer the route template over the raw pathname. |
maxRouteCardinality | number | 100 | Hard cap on distinct pathname-derived route labels. Overflow collapses to <other>. |
buckets | number[] | conventional Prometheus defaults | Custom latency histogram bucket boundaries in seconds. |
exclude | (path: string) => boolean | - | Skip RED instrumentation for matching paths (e.g. health probes). The scrape path itself is always excluded automatically. |
acknowledgeUnauthenticated | boolean | false | Opt-in bypass for the production refuse-to-boot guard when you intentionally run without a token (e.g. behind a private load balancer). |
The route label
High-cardinality labels are the classic way to melt a Prometheus server. By default the route label uses the request pathname, capped at maxRouteCardinality (100) distinct values before further paths collapse to <other>. For templated routes, supply a resolver that returns the route template:
Custom application metrics
Pass your own MetricsRegistry to register business metrics that render alongside the built-in HTTP series.
Use registry.collect(fn) to refresh point-in-time gauges (queue depth, connection-pool size) only when the endpoint is actually scraped, instead of on a timer.
Manual instrumentation
Prefer to wire the pieces yourself? httpMetrics() returns a Hooks bundle you can app.use(...) without the built-in scrape route, then render the registry from your own handler.
Grafana + Prometheus integration
The repository ships a ready-to-use Docker Compose stack under examples/observability/ that spins up Prometheus and Grafana with a pre-built dashboard, zero extra configuration needed.
1. Start the app
Run any DaloyJS server that calls app.metrics(). The example in the repo uses port 3001:
2. Start the observability stack
This brings up:
- Prometheus at
http://localhost:9090, pre-configured to scrapehost.docker.internal:3001/metricsevery 10 seconds. - Grafana at
http://localhost:3000(admin / admin). Prometheus datasource and the DaloyJS dashboard are auto-provisioned on first start, no manual import required.
3. Open the dashboard
Navigate to http://localhost:3000/d/daloy-http-metrics. The dashboard ships nine panels out of the box:
- Request rate by route
- Error rate (4xx / 5xx)
- Latency percentiles (p50 / p95 / p99)
- In-flight requests
- Request rate by method
- Business metric panel (orders created, from the demo)
- Memory usage (RSS + heap)
- Process uptime
- Request duration heatmap
Pointing at your own app
Edit examples/observability/prometheus.yml and replace the target:
If your app requires a bearer token, add it as a HTTP header:
On Linux you may need to replace host.docker.internal with your host IP address, or add extra_hosts: - "host.docker.internal:host-gateway" to the Prometheus service in examples/observability/docker-compose.yml.
Useful PromQL queries
Security posture
A /metrics endpoint leaks internal route names, latency distributions, request volume, and process memory, so it ships with the same hardened defaults as app.healthcheck():
- Bearer token (
opts.token) compared withtimingSafeEqual. Missing token is a401withWWW-Authenticate; wrong token is a403. - Per-IP rate limit (default
{ limit: 60, windowMs: 60_000 }) returning429withRetry-Afteron overflow. PassrateLimit: falseto disable. - Refuse-to-boot: an unauthenticated scrape endpoint in production throws at registration unless you set a token or explicitly pass
acknowledgeUnauthenticated: true. - Cardinality cap: every metric is bounded by
maxSeries(default 5000); overflowing label combinations are dropped and counted indaloy_metrics_series_dropped_total, a memory-exhaustion defense. - Exposition-injection defense: metric and label names are validated against the Prometheus grammar at definition time, and label values escape
\\,", and newlines so a hostile value cannot forge extra samples.
In most deployments you should also scope the scrape endpoint to your monitoring network at the ingress/firewall layer in addition to the bearer token.