h3-prometheus
v0.1.0
Published
Prometheus metrics plugin for H3 v2 — RED, errors, sizes, tracing, and WebSocket signals behind feature flags
Maintainers
Readme
h3-prometheus
h3-prometheus is a Prometheus metrics plugin for H3 v2. It wraps the request lifecycle, records RED and optional lifecycle signals, and exposes a scrape endpoint — with safe default labels and feature flags so you only pay for the metrics you need.
Why h3-prometheus?
H3 already exposes rich lifecycle hooks (matchedRoute, HTTPError.unhandled, Server-Timing, onDispose, h3/tracing, WebSocket). Most apps only scrape a thin RED slice — or worse, put raw pathnames into labels and explode Prometheus cardinality.
h3-prometheus focuses on:
- Safe defaults —
method, matched route pattern,status; unmatched traffic becomes__unmatched__, never the raw pathname - Configurable depth — errors, sizes, Server-Timing, tracing, WebSocket, and Node runtime gauges behind
collectflags - Production scrape auth — open in non-prod; fail-closed in production via token or custom
authorize - Ops-ready assets — Prometheus scrape snippet + Grafana dashboard JSON shipped in the repo
Example Grafana report
The screenshot above matches the included dashboard (grafana/dashboard.json): request rate, error %, p99 latency, in-flight, heap, event-loop lag, traffic by status, latency percentiles, top routes, and handled vs unhandled errors.
Key Features
- 📈 RED metrics — request rate, errors, duration (seconds), in-flight, aborts
- 🛡️ Low-cardinality labels — matched route patterns only; no raw path / query / IP / UA by default
- ⚙️ Feature flags — sizes, Server-Timing allowlist,
h3/tracing, WebSocket hooks, Node runtime gauges - 🔐 Scrape protection —
H3_PROMETHEUS_TOKENor customauthorizefor production - 📊 Grafana included — importable dashboard + Prometheus scrape config under
grafana/
Requirements
- Node.js ≥ 20.19
- Peer dependency: h3 ^2.0.0
Quick Setup
Install the package:
npm install h3-prometheus h3Register the plugin first so ~request wraps the full lifecycle:
import { H3 } from "h3";
import { prometheusPlugin } from "h3-prometheus";
const app = new H3({
plugins: [
prometheusPlugin({
path: "/metrics",
}),
],
});That's it — hit /metrics and point Prometheus at it.
Full configuration example
import { H3 } from "h3";
import { prometheusPlugin } from "h3-prometheus";
const app = new H3({
plugins: [
prometheusPlugin({
path: "/metrics",
// buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
// sizeBuckets: [100, 1_000, 10_000, 100_000, 1_000_000, 10_000_000],
labels: {
status: "code", // or "class" → 2xx / 3xx / …
unmatchedRoute: "__unmatched__", // never use the raw pathname
// custom: (event, ctx) => ({ tenant: "…" }), // you own cardinality
},
collect: {
http: true,
aborts: true,
errors: true,
requestSize: true,
responseSize: true,
durationMode: "headers", // or "body" (end-to-end via onDispose)
serverTiming: false,
// serverTimingNames: ["db", "render"],
tracing: false,
websocket: false,
runtime: { eventLoop: true, memory: true },
},
}),
],
});WebSocket metrics
import { defineWebSocketHandler, H3 } from "h3";
import { instrumentWebSocketHooks, prometheusPlugin } from "h3-prometheus";
const app = new H3({
plugins: [prometheusPlugin({ collect: { websocket: true } })],
});
app.get(
"/ws",
defineWebSocketHandler(
instrumentWebSocketHooks(
{
open() {},
message() {},
},
{ route: "GET /ws" },
),
),
);Options
| Option | Default | Notes |
|--------|---------|--------|
| path | /metrics | Scrape path (always excluded from observation) |
| authorize | env token / non-prod open | Scrape authorization hook |
| buckets | latency defaults (seconds) | Duration histogram buckets |
| sizeBuckets | byte defaults | Size histogram buckets |
| exclude | metrics + /health* + favicon | Skip observation for selected requests |
| labels.status | code | code or class |
| labels.unmatchedRoute | __unmatched__ | Used when there is no matchedRoute |
| labels.custom | — | Extra labels; you own cardinality |
| collect.http | true | RED + in-flight |
| collect.aborts | true | http_aborts_total |
| collect.errors | true | http_errors_total + unhandled |
| collect.requestSize | true | From request Content-Length |
| collect.responseSize | true | From response Content-Length |
| collect.durationMode | headers | body uses onDispose |
| collect.serverTiming | false | Requires serverTimingNames allowlist |
| collect.tracing | false | Enables h3/tracing + handler histogram |
| collect.websocket | false | Use with instrumentWebSocketHooks |
| collect.runtime | on under Node | Heap / RSS / event-loop lag on scrape |
| trustProxy | false | Hint only if labels.custom uses IP/host |
Cardinality warning: default labels are method, route (matched pattern or unmatchedRoute), status, and optionally unhandled / Server-Timing name. Do not put raw path, query, params, IP, User-Agent, cookies, or error messages into labels unless you accept series explosion.
Metrics
| Name | Type | Labels | When |
|------|------|--------|------|
| http_requests_total | counter | method, route, status | collect.http |
| http_request_duration_seconds | histogram | method, route | collect.http |
| http_requests_inflight | gauge | — | collect.http |
| http_aborts_total | counter | — | collect.aborts |
| http_errors_total | counter | method, route, status, unhandled | collect.errors |
| http_request_size_bytes | histogram | method, route | collect.requestSize |
| http_response_size_bytes | histogram | method, route | collect.responseSize |
| http_server_timing_seconds | histogram | method, route, name | collect.serverTiming |
| h3_handler_duration_seconds | histogram | type | collect.tracing |
| websocket_connections_open | gauge | — | collect.websocket |
| websocket_connections_total | counter | route? | collect.websocket |
| websocket_disconnects_total | counter | route? | collect.websocket |
| websocket_messages_total | counter | direction, route? | collect.websocket |
| process_heap_bytes | gauge | — | collect.runtime.memory |
| process_rss_bytes | gauge | — | collect.runtime.memory |
| nodejs_eventloop_lag_seconds | gauge | — | collect.runtime.eventLoop |
Scrape authentication
| Environment | Behavior |
|-------------|----------|
| Non-production | Open by default |
| Production | Fail-closed unless authorize is set, or H3_PROMETHEUS_TOKEN matches Authorization: Bearer … / ?token= |
export H3_PROMETHEUS_TOKEN=secret
# curl -H "Authorization: Bearer secret" http://127.0.0.1:3005/metricsGrafana & Prometheus
Assets in grafana/:
| File | Purpose |
|------|---------|
| grafana/prometheus.scrape.yml | Prometheus scrape_configs snippet |
| grafana/dashboard.json | Grafana dashboard (RED, errors, sizes, runtime) |
| branding/grafana-dashboard.jpg | Example dashboard screenshot |
Prometheus scrape
scrape_configs:
- job_name: h3
metrics_path: /metrics
scrape_interval: 15s
static_configs:
- targets: ["127.0.0.1:3005"]
labels:
service: h3
# Production token:
# authorization:
# type: Bearer
# credentials_file: /etc/prometheus/h3_tokenImport the dashboard
- Grafana → Dashboards → New → Import
- Upload
grafana/dashboard.json(or paste the JSON) - Select your Prometheus datasource → Import
Dashboard variables: Datasource, Job, Route (matched patterns, including __unmatched__).
Useful PromQL
# Request rate
sum(rate(http_requests_total{job="h3"}[5m]))
# Error ratio (numeric status codes)
sum(rate(http_requests_total{job="h3",status=~"[45].."}[5m]))
/ sum(rate(http_requests_total{job="h3"}[5m]))
# Latency p99
histogram_quantile(
0.99,
sum by (le) (rate(http_request_duration_seconds_bucket{job="h3"}[5m]))
)
# Unhandled exceptions
sum(rate(http_errors_total{job="h3",unhandled="true"}[5m]))
# In-flight + runtime
http_requests_inflight{job="h3"}
process_heap_bytes{job="h3"}
nodejs_eventloop_lag_seconds{job="h3"}If you use labels.status: "class", filter errors with status=~"[45]xx" instead.
Public API
| Export | Description |
|--------|-------------|
| prometheusPlugin | H3 plugin (main entry) |
| instrumentWebSocketHooks | Wrap crossws hooks when collect.websocket is on |
| resolveOptions | Resolve plugin options / defaults |
| MetricsRegistry | In-memory registry (advanced / tests) |
| toPrometheusText | Serialize a metrics snapshot |
| DEFAULT_LATENCY_BUCKETS / DEFAULT_SIZE_BUCKETS | Default histogram buckets |
| Types | PrometheusPluginOptions, CollectOptions, LabelsOptions, … |
Playground
npm install
npm run build
npm run dev:playground- App: http://127.0.0.1:3005
- Metrics: http://127.0.0.1:3005/metrics
Demo traffic runs in the background. Disable with H3_PROMETHEUS_TRAFFIC=0. Interval: H3_PROMETHEUS_TRAFFIC_MS=1500.
Scripts
| Script | Description |
|--------|-------------|
| npm run build | Compile TypeScript to dist/ |
| npm test | Run Vitest |
| npm run typecheck | Typecheck without emit |
| npm run dev:playground | Local demo server |
| npm publish | Runs prepublishOnly (typecheck + test + build) |
Star History
Links
License
MIT © Pavel Kuzmin
