---
title: "Request logs and observability"
---

# Request logs and observability

Turn on model gateway request logs in Neural Inverse Govern, read and search them, open sealed bodies, and attribute traffic with metadata, tags and client identity.

Every gateway request is in the audit log as `models.gateway.request`, without prompt content. Request logs add a searchable row per request and, if you choose, the bodies, sealed under your organisation's key.

## Turn request logs on

Request logs are off by default. Open **Models > Request log settings**, or:

```bash
curl -sS -X PUT "$KEYSTONE_URL/api/v1/models/logging" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H 'Content-Type: application/json' \
  -d '{"mode": "metadata", "retention_days": 30, "max_classification": "internal"}'
```

| Field | Values |
|---|---|
| `mode` | `off`, `metadata` (one row per request) or `full` (metadata plus bodies) |
| `retention_days` | 1 to 365 |
| `max_classification` | The strictest classification whose bodies `full` keeps (default `internal`) |

Every field is required. Reading the settings needs `models:view`; changing them needs `models:admin` and is audited as `models.logging.put`. `full` needs the deployment's key management service (`503 models.request_logs.unavailable` otherwise).

## What a row holds

One row per request, rewritten by each upstream attempt and ending with the outcome the caller got. Refusals are logged too, including those refused before the caller was resolved.

Fields include `request_id`, `api`, `requested_model`, `route`, `model_key`, `status` (`completed`, `upstream_error`, `client_closed` or `refused`), `http_status`, `refusal_code`, `attempts`, `input_tokens`, `output_tokens`, `cost_micros`, `latency_ms`, `cache`, `stream`, `user_id` or `api_key_id`, `project_id`, `classification`, `prompt`, `context_request_id`, `client_id`, `client_app`, `has_bodies` and `body_note`.

## Bodies

In `full` mode the row also keeps the request as sent upstream after DLP (the redacted body, never the original) and the answer as the caller received it, each at most 2 MiB. Each body is sealed with AES-256-GCM under a data key wrapped by the organisation's key `models-gateway-logs`. Nothing is stored unsealed.

Bodies are never kept for a request in a regulated project, whatever `max_classification` says. When a body is not kept, `body_note` says why: `classification`, `too_large`, `unavailable`, `seal_failed` or `not_scanned`.

## Read logs

Open **Models > Request logs** (under Monitor) or **AI Gateway > Gateway requests**.

| Route | Permission | Audit |
|---|---|---|
| `GET /api/v1/models/logs` | `models:view` | `models.request_log.list` |
| `GET /api/v1/models/logs/{request_id}` | `models:view` | `models.request_log.get` |
| `GET /api/v1/models/logs/{request_id}?include=bodies` | `models:admin` | `models.request_log.read_bodies`, committed with the read |

`GET /logs` filters by `from`, `to`, `user_id`, `model` (answering model, requested model or route), `status` and `context_request_id`, pages with `limit` (1 to 200) and `cursor`, and returns newest first.

Rows older than the retention are deleted hourly by `models.request_log_purge` (schedule `NI_AUTH_MODELS_REQUEST_LOG_PURGE_SCHEDULE`, `off` disables it).

## Attribute traffic

| Request header | Rule |
|---|---|
| `X-Keystone-Metadata` | One JSON object, at most 8192 bytes and 16 keys. Keys match `^[a-z][a-z0-9_.-]{0,63}$`; values are strings of at most 256 bytes. Keys starting with `keystone.` are reserved. |
| `X-Keystone-Tags` | Up to 10 comma-separated tags matching `^[A-Za-z0-9][A-Za-z0-9_.:/@-]{0,63}$` |
| `X-Keystone-Client` | `name/version` of the calling app, recorded as advisory |

A header that breaks a rule is refused with `400 invalid_request` before anything is reserved. Metadata and tags are DLP-scanned at the request's classification, carried into usage events, the audit record and route conditions, and never sent to the provider. A gateway key's own metadata wins over the header's.

The OAuth client of the caller's token (`client_id`) is recorded as verified. `X-Keystone-Client` is recorded as advisory, because a client can send anything.

## Token counting

`POST /api/v1/models/gateway/v1/messages/count_tokens` counts a request's input tokens. It is decided like a real request (access, DLP, guardrails, rate limits) and audited with `operation: count_tokens`. Models served through the Anthropic wire format are counted by their provider with the organisation's key; any other model, or a failed provider count, is answered with the gateway's estimate and `"estimated": true`. Nothing is generated and no budget is reserved.

## Other views

| Console page | Shows |
|---|---|
| Models > Provider health | Probe results and circuit state per connection |
| Models > Model usage | Usage and spend from the gateway's usage events |
| AI Gateway > Gateway overview, Gateway costs | Traffic and FinOps views |
| AI Gateway > Gateway keys > key > Usage | Per-key analytics |

The organisation's model usage also appears in Insights. Request logs and usage events are separate from the audit log, which is described on [Audit log](https://neuralinverse.com/docs/govern/audit/).
