Every inference request that reaches the gateway — served, refused by policy or a guardrail, or failed — becomes one record in a hash-chained audit log. Use it to answer “who sent what to which model, how many tokens did it use, and what did the gateway do about it”, and to prove afterwards that the record has not been edited.

How it works

  1. When a request arrives, the gateway fixes its record id and returns it in X-ManyLayers-Trace-Id.
  2. When the request finishes, the record is handed to a background writer, so logging adds no latency.
  3. The writer appends records in order. Each record’s SHA-256 hash covers its content and the previous record’s hash, so changing or deleting any stored record breaks the chain from that point on.

What each record holds

GroupFields
IdentityOrganization, workspace, team, API key, user; client IP (the connection’s peer address, not X-Forwarded-For) and User-Agent
RequestEndpoint, requested model, served model and provider account, gateway config, request metadata from X-ManyLayers-Metadata
OutcomeHTTP status, latency, input and output tokens, retries, whether a provider was called at all
DecisionsApplied policies, guardrail verdicts, PII and firewall findings (type, offsets and score — never the matched text), the Auto Routing decision
CorrelationOpenTelemetry trace id and root span id
BodiesRequest and response bodies, only when captured (see below), always after PII redaction
ChainPrevious hash and this record’s hash
The record holds token counts, not cost; cost is metered separately and shown in Analytics. When a guardrail flags content it leaves in place (a validated secret, an audit-mode finding), that body is left out of the record rather than storing what was detected.

Configuration

audit:
  retention_days: 90   # records older than this are deleted hourly (default 90)
  log_bodies: false    # deployment default for storing bodies (default false)
These are read from gateway.yaml when the gateway starts. The shipped container config reads log_bodies from MANYLAYERS_AUDIT_LOG_BODIES. A guardrail execution record is kept for the same retention_days.

Deciding whether bodies are stored

For each request, in order:
  1. No logging config matches → audit.log_bodies decides.
  2. Logging configs match → the innermost matching rules decide. Bodies are stored only if every one of them has log_requests: true, and every one’s redaction is applied to the stored copy. A matching rule can turn capture on even when log_bodies is false.
  3. X-ManyLayers-Store-Logs: false on the request → bodies are not stored, whatever the rules say. true changes nothing; it can never turn capture back on.
If a rule cannot be evaluated or its redaction fails or takes longer than 2 seconds, the bodies are dropped rather than stored unredacted.

Logging configs

A logging config is a policy that sets body capture and redaction for a scope: the organization, a workspace, or within it one team, service account, user or API key.
curl -X POST "https://app.manylayers.io/admin/gateway/policies/logging-configs?workspace_id=$WS" \
  -H "Authorization: Bearer ml_pat_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "support: redact before storing",
    "scope": "workspace",
    "log_requests": true,
    "redaction": {
      "pii": true,
      "pii_categories": [],
      "secrets": true,
      "patterns": [{"name": "ticket", "pattern": "TCK-[0-9]{6}"}],
      "replacement": ""
    }
  }'

Key fields

name
string
required
At most 128 characters.
scope
string
default:"workspace"
workspace or organization. Name at most one of team_id, service_account_id, user_id, api_key_id to narrow it.
log_requests
boolean
default:"true"
false keeps bodies out of the log for this scope. Toggle it alone with PATCH /admin/gateway/policies/logging-configs/{id} and {"log_requests": false}.
redaction
object
Applied to the stored copy only — the model still receives the request. secrets runs first, then pii (narrowed by pii_categories, empty = all; categories as in the pii guardrail), then each patterns regex (every pattern needs a name and a pattern). Defaults: ***REDACTED*** for secrets, [REDACTED:{category}] for PII and [REDACTED] for patterns; a non-empty replacement replaces all three. A rule that cannot be compiled is refused with 400 when written.
Routes: GET/POST /admin/gateway/policies/logging-configs, PUT/PATCH/DELETE .../{id}. Reading needs gateway.policies.read; writing needs gateway.policies.manage.

Per-request opt-out

curl https://app.manylayers.io/v1/chat/completions \
  -H "Authorization: Bearer ml-..." \
  -H "X-ManyLayers-Store-Logs: false" \
  -H "Content-Type: application/json" \
  -d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "…"}]}'
Any value other than true or false returns 400. The record itself (tokens, status, latency, findings) is still written.

Query the log

GET /admin/audit on the console host (https://app.manylayers.io) returns the newest records first. A workspace-bound key sees its own workspace; pass ?workspace_id= for another workspace you can reach. Callers who may read analytics across the whole Gateway product, such as organization administrators, can omit it for a cross-workspace view; everyone else must name one.
curl -G "https://app.manylayers.io/admin/audit" \
  -H "Authorization: Bearer ml_pat_..." \
  --data-urlencode "workspace_id=$WS" \
  --data-urlencode "min_status=400" \
  --data-urlencode "from=2026-10-01T00:00:00Z" \
  --data-urlencode 'filter=[{"field":"input_tokens","op":"gte","values":["4000"]}]'
ParameterMeaning
limit, offsetPaging; limit 1–10000, default 100.
idOne record, by X-ManyLayers-Trace-Id.
team_id, model, endpoint, config_idExact matches.
status, min_statusOne HTTP status, or that status and above (100–599).
from, toRFC 3339 time bounds.
has_pii, verdict, retriedtrue for records with PII findings, guardrail verdicts, or router retries.
filterJSON array of up to 20 {field, op, values}. Fields: model, user, team, api_key, endpoint, status, latency_ms, input_tokens, output_tokens, retries, retried, guardrail_flagged, guardrail, guardrail_action, provider, virtual_model, auto_routed, policy. Ops: in, not_in (text); gte, lte, between, in, not_in (numbers); in (booleans).
The response is {"records": […], "bodies_recorded": …}; bodies_recorded is the deployment default (audit.log_bodies), so it tells “body recording is off” apart from “this body was withheld”. GET /admin/audit/filters returns the field catalog and the values seen in a time window. In the console, Request Traces shows the same records with their policy, guardrail and routing detail; its Guardrail executions tab lists guardrail runs. Redacted values appear as [REDACTED] unless you hold gateway.traces.sensitive.read and use Show Original.

Verify the chain

curl https://app.manylayers.io/admin/audit/verify -H "Authorization: Bearer ml-..."
# {"records": 10000, "intact": true, "first_bad_index": null}
This check needs the deployment administrator credential, so it is available to whoever operates the deployment, not to organization administrators. It recomputes the hashes of the most recent 10,000 records across the whole deployment. first_bad_index is the position, oldest first, of the first record whose hash or link does not match.
Retention deletes whole records from the oldest end, which does not break verification: the check starts from the oldest remaining record’s stored previous hash.

Next steps

PII Detection & Redaction

Redaction before storage, and revealing originals.

Tracing & OpenTelemetry Export

Send the same requests to your tracing backend.

Policies

How policy scopes and precedence work.

Metrics

Aggregate views of what the log records one by one.