Request Logger Plugin
The request-logger plugin records every request, response, and failure as structured stdout log lines and, when persist: true plus a configured request-log store, as rows that power the dashboard's Request Logs page. It runs at all three lifecycle stages โ before_request, after_request, and on_error โ so a request is logged whether it succeeds, fails, or is served from cache.
Factsโ
| Stages | before_request and after_request and on_error โ multi-stage, byte-identical config required at all three |
| Reported type | logging (plugin.TypeLogging) |
| Failure policy | Fails open โ a broken log sink costs a warn line, never the request |
| Store write failures | Warned, not returned โ a down/full store never fails the request, but is logged so the gap is visible |
| Agentic loops | Does not re-run per MCP tool-loop turn (unlike guardrails/ratelimit/budget) โ one row per request, not one per turn |
| Timestamps | UTC (time.Now().UTC()) on every logged and persisted entry |
| External dependencies | Optional: REQUEST_LOG_STORE_BACKEND (sqlite or postgres) + REQUEST_LOG_STORE_DSN, set at the process level โ not in plugin config. The store is gateway-owned; the plugin's Close never touches it |
Config keysโ
| Key | Type | Default | Required | Description |
|---|---|---|---|---|
level | string: debug|info|warn|error | info | No | Log level for the before_request/after_request stdout lines. The on_error line always logs at error, regardless of this setting. |
persist | bool | false | No | Write rows to the shared request-log store. Requires REQUEST_LOG_STORE_BACKEND (and REQUEST_LOG_STORE_DSN) to be set at the process level; if it isn't, Init logs a startup warning and the plugin falls back to stdout-only. |
backend / dsn | โ | โ | No | Obsolete. Ignored with a warning at Init โ persistence target moved to the process-level REQUEST_LOG_STORE_BACKEND / REQUEST_LOG_STORE_DSN env vars. Deliberately excluded from GET /admin/plugins/catalog's declared settings (only level and persist are listed). |
Minimal configโ
All three stage entries must carry byte-identical config โ the gateway resolves them to one shared instance by name + JSON-encoded config, and refuses to start if any of the three disagree.
plugins:
- name: request-logger
type: logging
stage: before_request
enabled: true
config:
level: info
persist: false
- name: request-logger
type: logging
stage: after_request
enabled: true
config:
level: info
persist: false
- name: request-logger
type: logging
stage: on_error
enabled: true
config:
level: info
persist: false
To persist rows for the admin API and dashboard, set persist: true on all three entries and configure the store once at the process level:
export REQUEST_LOG_STORE_BACKEND=sqlite
export REQUEST_LOG_STORE_DSN=ferrogw-requests.db
Gotchasโ
- The
on_errorentry is not optional. A failed request never reachesafter_request, so without anon_errorentry a failure produces no terminal row and simply vanishes from the defaultGET /admin/logslisting โ the request happened, but nothing says so. - A completed-then-failed request never gets two terminal rows. If the provider answered, the
after_requestrow was written, and a laterafter_requestplugin then breaks, the failure is annotated onto the row that already exists (recordLateFailure) rather than written as a secondon_errorrow โ otherwise the default listing and everyStatsfigure would double-count that one request. - Cache hits are logged like any other request. Unlike the budget plugin,
request-loggerdeliberately ignoresSkipProviderโ a response served from cache still belongs in the audit trail. Its row carries the real token usage from the cached response paired with the$0cost the gateway hands a cache hit; usage and cost are different facts, and a cache hit is where they diverge. - The
after_requestrow'sProviderandAPIKeyIDanswer different questions.Providernames whoever originally produced the response (for a cache hit, the provider that answered the very first time);APIKeyIDnames whoever consumed it this time. On a cache hit those can be two different credentials. on_errornames the provider from the last routing target attempted, not from a response โ there is no response on this path. On a request that never reached routing (denied by abefore_requestplugin, or no target serves the model), that field is empty.- Store-write failures are warned about, never returned as plugin errors. The framework already treats a plugin error as "the plugin itself broke," and a logging plugin is fail-open by design so a struggling store cannot cost a completed LLM call. If
l.writer.Writefails, the plugin logsrequest log write failed; the persisted request log is incompletenaming the stage, and moves on โ the request is unaffected, but the persisted trail is quietly short a row unless you're watching for that warning. on_errorruns on a context detached from request cancellation, bounded by a 10-second write budget, specifically so a client that disconnected mid-stream still produces a terminal failure row instead of the record vanishing with the connection.- Error text is redacted before it's logged or persisted.
on_errorrunspctx.Error.Error()through the gateway's redactor (emails, JWTs, AWS keys) so an upstream provider error that happened to echo back a credential doesn't land in stdout or the store verbatim. - Does not re-run inside agentic MCP tool loops. Guardrails, the rate limiter, and the budget re-check on every loop turn;
request-loggeris deliberately excluded (along with metrics plugins) so one multi-turn agentic request produces one row, not one row per turn. GET /admin/logsdefaults to one row per request, not one row per stage. The plugin writes a row at every stage it runs โbefore_request,after_request(or the annotated/on_error terminal row), andon_erroron failure. The default listing returns only the terminal stages (after_request+on_error) so each request appears once; pass?stage=allto see the raw per-stage event stream, or?stage=<name>to filter to exactly one stage.backend/dsnin plugin config are a no-op, kept only so an operator running an old config gets a warning explaining where the setting moved, rather than a silently ignored key. SetREQUEST_LOG_STORE_BACKEND/REQUEST_LOG_STORE_DSNinstead.- Unknown config keys are silently ignored โ the plugin reads its own map rather than going through the strict top-level config decoder, so a typo like
persists: trueleavespersistat itsfalsedefault with no error.
Relatedโ
- Request logging โ persisting rows, store backends, and admin log filters
- Dashboard โ the embedded Request Logs page this plugin's persisted rows power
- Plugins overview
- Budget plugin
- Response cache plugin