Request logging
The request-logger plugin records every request the gateway handles โ as structured stdout log lines always, and as rows in a shared request-log store when persistence is configured. The admin API reads that same store, powering GET /admin/logs, GET /admin/logs/stats, and the dashboard's Request Logs page.
Enable the request-logger pluginโ
request-logger is a multi-stage plugin: it needs one plugins[] entry per lifecycle stage it participates in, each carrying byte-identical config. Give it all three โ before_request, after_request, and on_error โ or logging is incomplete:
- Missing
after_request: the row written atbefore_requestnever gets its completion data. Noduration_ms,ttft_ms,cost_usd, token counts, or provider โ half the job. - Missing
on_error: a failed request never reachesafter_request, so it produces no terminal row at all and vanishes from the default/admin/logslisting.
Listing the same multi-stage plugin at only one stage isn't a smaller version of logging โ it silently drops the rows the other stages were responsible for.
plugins:
- name: request-logger
type: logging
stage: before_request
enabled: true
config:
level: info
persist: true
- name: request-logger
type: logging
stage: after_request
enabled: true
config:
level: info
persist: true
- name: request-logger
type: logging
stage: on_error
enabled: true
config:
level: info
persist: true
The gateway compares each stage's config (name + JSON encoding) and refuses to start if any of the three disagree โ that's what keeps the instances in sync rather than silently splitting state.
request-logger is a logging-type plugin, so it fails open: if the log store is down, full, or unreachable, the request still completes. The write failure is logged as a warning rather than silently dropped, so an operator can tell the persisted trail went incomplete instead of trusting a log that quietly lost rows.
| Key | Type | Default | Description |
|---|---|---|---|
level | string | info | stdout log level (debug|info|warn|error) for the request/response lines. on_error always logs at error regardless of this setting. |
persist | bool | false | Write rows to the shared request-log store. Requires REQUEST_LOG_STORE_BACKEND / REQUEST_LOG_STORE_DSN to be set at the process level โ without them, persist: true logs a startup warning and the plugin stays stdout-only. |
backend and dsn keys inside the plugin's own config block are obsolete โ they're ignored with a warning. Persistence targets and credentials are process-level configuration, set once via the environment variables below, not per plugin instance.
A cache-served response is logged like any other request: it carries the real token usage but a real, measured $0 cost (not null โ see nullable columns below).
Configure the request-log storeโ
export REQUEST_LOG_STORE_BACKEND=sqlite
export REQUEST_LOG_STORE_DSN=ferrogw-requests.db
Supported backends are sqlite and postgres. This is the same store the plugin writes to and the admin API reads from โ one shared store, not a per-plugin database.
When the store isn't configured, admin log endpoints return 501 not implemented:
GET /admin/logsGET /admin/logs/statsDELETE /admin/logs
What each row containsโ
| Column | Type | Notes |
|---|---|---|
duration_ms | float, nullable | End-to-end time the gateway spent on the request, excluding the after_request plugin stage. |
ttft_ms | float, nullable | Time to first token. Only measured for streaming requests โ non-streaming rows carry null. |
cost_usd | float, nullable | Estimated cost from the model catalog. null means "unpriced" โ the catalog has no rate for that model โ which is different from a real $0.0 (an unlisted/free model, or a cache hit). Treating null as zero silently understates spend. |
api_key_id | string | Opaque credential ID the request was served under (never the secret). Empty when the request carried no credential. |
All timestamps are stored in UTC.
Query the request logโ
GET /admin/logs supports:
| Filter | Description |
|---|---|
limit | Default 50, max 200. |
offset | Default 0. |
stage | before_request, after_request, on_error, or all for the raw per-stage stream. See below. |
model, provider | Exact match. |
api_key_id | Exact match against the credential's opaque ID. api_key_id=none selects rows with no credential โ both unauthenticated requests and rows written before the column existed. |
since | RFC3339 timestamp. |
By default (no stage param), the endpoint returns one row per request โ only terminal-stage rows (after_request for a completion, on_error for a failure). The plugin writes one row per stage it ran at, so without this default every request would appear listed twice: once from before_request with no completion data, once from its terminal stage. Pass stage=all to get the raw per-stage event stream instead, or stage=<name> to see one specific stage.
DELETE /admin/logs requires before (RFC3339), with optional stage, model, and provider filters (no api_key_id โ a purge is scoped by time and dimension, not credential).
Request log statisticsโ
GET /admin/logs/stats supports:
| Filter | Description |
|---|---|
limit | Top-N entries per dimension breakdown, max 100. |
buckets | Number of points in the time series, max 120. Omit or 0 for no series. |
stage, model, provider | Exact match. |
since | RFC3339 timestamp. |
The response includes:
- Summary: total/error entry counts,
prompt_tokensandcompletion_tokenssplit out separately,cost_usdtotal, andunpriced_requestsโ the count of requests the catalog couldn't price, so acost_usdtotal isn't misread as the whole bill. - Percentiles:
latency_msandttft_msdistributions (p50, p95, p99, max, mean, count), computed from the storedduration_ms/ttft_msvalues.nullwhen nothing was measured in the window, rather than a misleading zero. - Dimension breakdowns:
by_stage,by_provider,by_modelโ each with count, errors, tokens,cost_usd, andunpricedper group, so you can see which model consumed the tokens, not just which was called most. top_errors: the most frequent distinct failure messages (fixed at 8).series: a time series withbucketspoints when requested, each carryingrequests,errors,prompt_tokens, andcompletion_tokens; the response markstruncated: trueif the series stopped short of the requested window.
Relatedโ
- request-logger plugin reference
- Dashboard โ the Request Logs page built on this API
- Server settings โ
REQUEST_LOG_STORE_BACKEND/REQUEST_LOG_STORE_DSNand other process configuration - Monitoring