GioJSdocs
On this page

Observability

Health checks, Prometheus metrics, request IDs and JSON logs, and the dev dashboard.

Two endpoints are served directly by Rust:

  • /_gio/health - liveness probe, on unless [health] enabled = false ([health] details = false keeps only status and readiness)
  • /_gio/metrics - Prometheus metrics, opt-in via [metrics] in gio.toml; with neither a token nor an ip_allowlist it answers this machine only
toml
[metrics]
enabled = true
token = "a-long-random-secret"   # secure it for production

Metrics

Request series carry a route label: the matched route pattern (/posts/:id), never the raw path, so one series covers every post and the number of series is bounded by your routes. Cache hits keep the pattern of the render they replay, and a request a plugin's onRequest hook rewrote is labeled with the route it rendered.

MetricTypeLabels
gio_requests_totalcountermethod, status, cache, route
gio_request_duration_secondshistogramroute
gio_node_ipc_latency_secondshistogram (Rust to Node round trip)route
gio_cache_entries, gio_cache_size_bytesgauge-
gio_prefetch_rejected_totalcounter-
gio_image_processed_totalcounterformat
gio_ratelimit_checked_total, gio_ratelimit_rejected_totalcounterpath (and rule)
gio_memory_bytesgauge (Rust server process only)type="rss"
gio_workersgauge (Node render workers configured)-
gio_worker_readygauge (1 while the worker is connected)worker
gio_worker_in_flightgauge (requests, streaming renders and SSE streams)worker
gio_worker_restarts_totalcounterworker

The gio_worker_* series carry one value per Node worker, labeled by its index in the pool (worker="0" to worker="N-1", see Render workers): a worker whose gio_worker_in_flight stays high while the others idle is busy with long renders or streams, and a climbing gio_worker_restarts_total is a worker that keeps crashing. gio_memory_bytes measures the Rust server alone - each Node worker is a separate process, so watch worker memory with your process or container tools.

Besides patterns - and the fixed paths of app/sitemap.ts, app/robots.ts and app/manifest.ts (/sitemap.xml, ...) - route takes three reserved values: static (public/ files, hashed chunks, app CSS), internal (the server's own /_gio/* endpoints) and unmatched (no app route: a 404 for an unknown path, or a render the worker never answered). cache is the tier that answered: hit, stale, miss, stream, error, static or bypass. Each label is bounded on its own, so request data can never grow the exposition without bound: a method other than the nine standard ones (GET, POST, ...) is reported as _other, and so is any route pattern past the first 1024 distinct ones (the reserved values always keep their name). The number of series therefore grows with your routes, not with traffic; a backstop of 16384 gio_requests_total series, far above what an app's routes produce, counts anything beyond it under _other in every label. The rate-limit counters key by request path and cap at 512 values each. Slowest routes at the 95th percentile:

bash
histogram_quantile(0.95,
  sum by (route, le) (rate(gio_request_duration_seconds_bucket[5m])))

Request IDs

Every request gets an id, returned as the X-Request-Id response header on every response - pages, cache hits, static files, redirects and errors. The same id ties together both processes' logs. The Rust server (filtered by RUST_LOG) emits each line for the request inside a request span, which no level filter drops - at RUST_LOG=warn or error the warnings and errors still carry the id:

bash
INFO request{request_id=0b8e3c52-7a1d-4f0e-9c3b-5d2a6e8f1a47}: giojs_server: request completed method=GET path=/posts/7 status=500 cache="miss"
ERROR giojs_server::ipc: Node render error [RENDER_ERROR]: Internal Server Error digest=3f9a1c0b7e2d request_id=0b8e3c52-7a1d-4f0e-9c3b-5d2a6e8f1a47

and every JSON line the Node worker writes while handling the request carries it as requestId, alongside the error digest a production error page shows:

bash
{"level":"error","msg":"ssr render failed","requestId":"0b8e3c52-7a1d-4f0e-9c3b-5d2a6e8f1a47","path":"/posts/7","digest":"3f9a1c0b7e2d","error":"connect ECONNREFUSED ..."}

So a user's error reference leads to the worker line with the real error, and its requestId to everything else that request did. Route handlers and getServerSideProps can read the id as req.requestId / ctx.requestId to pass it to downstream services. Behind a trusted proxy ([server] trusted_proxies), a valid incoming X-Request-Id - nginx's $request_id, say - is kept, so one id spans the proxy's access log too; from anyone else it is replaced. Many proxies pass a client's own header through rather than setting one; behind those, set accept_request_id = false and GioJS generates every id. See Configuration.

Work that outlives its response keeps the id of the request that started it: a stale-while-revalidate refresh, the Suspense holes of a PPR cache hit and the tail of a streamed body log under the triggering request's id in both processes.

JSON logs

The Node worker always writes JSON lines; the Rust server writes human-readable text by default. For a log shipper (Loki, Datadog, CloudWatch, Elastic), switch the server to JSON too, in gio.toml or - overriding it - the environment:

toml
[logging]
format = "json"   # or GIO_LOG_FORMAT=json; "text" is the default

Both processes then emit one JSON object per line on the same stream, with the same core keys - ts (RFC 3339, UTC), level (lowercase), msg - plus the event's own fields. Server lines also carry target (the Rust module), and the fields of the spans they were logged in are flattened into the line, so every line of a request has a top-level request_id; worker lines carry the same id as requestId:

bash
{"ts":"2026-10-06T12:00:01.204518Z","level":"info","msg":"request completed","target":"giojs_server","cache":"miss","encoding":"br","method":"GET","path":"/posts/7","prefetch":"n/a","request_id":"0b8e3c52-7a1d-4f0e-9c3b-5d2a6e8f1a47","status":"200"}
{"ts":"2026-10-06T12:00:01.198Z","level":"info","msg":"cart loaded","requestId":"0b8e3c52-7a1d-4f0e-9c3b-5d2a6e8f1a47","items":3}

Parse ts as the timestamp and level as the severity, and map request_id and requestId to one field (a Loki | json | line_format, a Datadog remapper, a CloudWatch Logs Insights coalesce(request_id, requestId)) to follow a request across both processes. RUST_LOG and GIO_LOG_LEVEL filter the two sides as before. An unknown format in gio.toml is a startup error; an unknown GIO_LOG_FORMAT is ignored with a warning.

Dev dashboard

In development, /_gio/devtools shows live request logs, route manifest, cache stats, a memory sparkline, and IPC latency - generated entirely in Rust.

The dashboard and its endpoints only answer to localhost hosts (localhost, *.localhost, 127.0.0.1, [::1]) on connections from this machine, so other websites cannot read them through DNS rebinding and other machines cannot reach them with a forged Host, and its state and stream endpoints also refuse cross-site requests. To open it through another hostname or LAN IP, list that host under [dev] allowed_hosts - see Configuration.

toml
[dev]
allowed_hosts = ["192.168.1.20", "myvm.local"]