# Logging and Auditing

The control plane gives operators two evidence trails: request logs for AISIX gateway traffic and an audit log for control-plane state changes. Together, they help teams understand what happened to a request and who changed the resources that affect traffic.

Use request logs to investigate a specific AISIX gateway request. Use the audit log to investigate configuration, access, and other control-plane changes.

## Request Logs[​](#request-logs "Direct link to Request Logs")

Request logs are built from AISIX gateway telemetry. They show individual request outcomes, including request time, status, requested model, caller API key, latency, token counts, and attempt details when available. Latency is reported from two angles — what the caller waited for and what the upstream spent — so a slow request can be attributed without guesswork.

Expanding a row shows the **Request ID** AISIX assigned, and — when the call reached a provider that returned one — a **Provider request ID** beside it. They are different things. The first is the ID AISIX returns to the caller in the `x-aisix-request-id` response header, and the one a caller reporting a problem will normally have. The second is the ID the upstream provider gave the call, such as an OpenAI `chat.completion.id`, an Anthropic message `id`, or a Gemini `responseId`, or the provider's request-ID response header, such as Amazon Bedrock's `x-amzn-requestid`, when the response has no ID of its own. It is what the provider's console and support channel index the call by. Look the request up by the first, read the second from it, and take that to the provider.

The provider request ID is not shown when the call produced none: a response served from cache, a request rejected before AISIX reached the provider, and a provider response that carries neither an ID nor a request-ID header. Failed attempts, token counting, embeddings, audio, and image generation record the header ID. A retried or failed-over request records one per attempt that got a response, so expand the attempt that actually served the caller, or the failed attempt you want to raise with the provider.

A row's token figure is the total the upstream reported for that attempt when it reported one, and otherwise the sum of its prompt, completion, cache-creation, and cache-read counts. Expanding the row shows the reported total as **Total tokens (reported by upstream)**; a row without one has no such line. The counts are recorded as the upstream reported them, so reasoning tokens can exceed completion tokens on some rows. See [Counts Are Recorded as Reported](https://docs.api7.ai/ai-gateway/cloud/usage-reporting.md#counts-are-recorded-as-reported).

Rows with an `estimated` badge contain one or more token counts calculated locally because the upstream response omitted those values or reported zero. This can occur with OpenAI-compatible relays, client disconnects mid-stream, and upstream errors after a partial response. Estimated counts are included in spend and budget calculations alongside provider-reported counts. The badge distinguishes estimated values when investigating usage or spend. See [Usage Reporting](https://docs.api7.ai/ai-gateway/cloud/usage-reporting.md#how-usage-is-reported) for the supported endpoints and estimation behavior.

![Dashboard Logs view showing request filters, status, token usage, latency, and expanded request details](https://static.api7.ai/uploads/2026/09/23/lptVjeGa_request-logs-dev.png)

Expanding a row also shows the **Team** bound to the caller API key and the request's [cost attribution tags](https://docs.api7.ai/ai-gateway/cloud/cost-attribution-tags.md). Request Logs has no tag filter. Use [Cost Analysis](https://docs.api7.ai/ai-gateway/cloud/cost-analysis.md) to group usage by tag; selecting an API key row there opens this page filtered to that key.

When the upstream reported the service tier it served the request at, the expanded row shows it as **Service tier**, in the provider's own vocabulary. The name can differ from the requested tier and from model to model. When OpenAI serves a request with priority processing, GPT-6 models report `fast` and earlier models report `priority`; a request downgraded to Standard reports `default`. [Service-tier rates](https://docs.api7.ai/ai-gateway/cloud/model-pricing.md#price-by-service-tier) are matched against this value. See [Read the Served Service Tier](https://docs.api7.ai/ai-gateway/observability/metrics-and-usage-events.md#read-the-served-service-tier).

The expanded row also shows **Platform cost**, the attempt's usage at the target model's own price, and **Price tier** when a tier's rates determined the cost. See [Read Platform Cost and the Price Tier](https://docs.api7.ai/ai-gateway/cloud/model-pricing.md#read-platform-cost-and-the-price-tier).

**Cost breakdown** lists what each billed item cost at its unit price, together with any off-peak discount. See [Cost Breakdown of a Single Request](https://docs.api7.ai/ai-gateway/cloud/cost-analysis.md#cost-breakdown-of-a-single-request).

Freshly completed requests can take a few seconds to appear because data planes flush telemetry in batches.

Each row is timestamped with its local date and time, so a window that spans several days stays readable. Hover over a timestamp to see the full date with its time zone.

Start with request logs when you need to verify a live request, inspect an upstream error, confirm a policy rejection, or check how routing and failover resolved a request.

### Read the Latency Figures[​](#read-the-latency-figures "Direct link to Read the Latency Figures")

A request log row shows what the caller waited for. Expand the row to see the measurement broken out, because a single number cannot answer whether a slow request was the provider's fault or the gateway's:

| Field                | Measures                                                                                                                                                                             | Scope                                                          |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- |
| **Caller latency**   | Gateway received the request until it completed a non-streaming response or started sending a streamed one.                                                                          | The whole request, including every retry and failover attempt. |
| **Upstream latency** | How long the attempt that served the request spent talking to the provider.                                                                                                          | One attempt.                                                   |
| **Upstream TTFT**    | How long that attempt waited for the provider's first streamed frame, whatever it carries — metadata openers such as `response.created` or `message_start` included. Streaming only. | One attempt.                                                   |

Caller latency is the figure to quote in an SLO discussion, and it is what the **Latency p50 / p99** card on the Overview page reports.

Upstream TTFT stops at the first frame of the stream, not the first visible token. This is the convention proxies and gateways in front of AISIX use, so the figure compares directly with what they log. A reasoning model that streams nothing visible while it thinks — for example, a `/v1/responses` upstream that opens the stream immediately but emits no reasoning summaries — therefore shows a small TTFT even though the answer text starts much later; that thinking wait is part of upstream latency, not TTFT.

For a streamed response, caller latency deliberately stops when the stream begins rather than when it ends. A stream's total duration grows with however many tokens the model generated, which says little about the experience; the wait before output starts appearing is what a user notices.

Comparing the fields locates the delay. When caller latency far exceeds upstream TTFT on a request that did not retry, the time went to gateway-side work. The usual cause is an output guardrail that masks responses: it must buffer the whole stream before releasing any of it. When the two figures are close, the provider was the slow part. On a request that retried or failed over, the gap also contains the earlier attempts, so check the attempt rows first.

The **Latency p50 / p99** card counts successful requests only. A rejected request is usually fast precisely because little happened, and including those would pull the percentiles down and mask real slowness. A streamed response that failed after its `200` headers is recorded with the failure's status and is therefore not a success; see [Streams That Fail After the Response Head](https://docs.api7.ai/ai-gateway/observability/metrics-and-usage-events.md#streams-that-fail-after-the-response-head).

The card's percentiles are approximate. The control plane computes them from an hourly usage rollup that sorts each latency into logarithmic buckets, each about 10% wider than the one below, so a reported p50 or p99 is within about 5% of the exact percentile of the same requests. The Overview page's other figures, such as request counts and tokens, are exact sums from the same rollup.

### Read Guardrail Hits[​](#read-guardrail-hits "Direct link to Read Guardrail Hits")

Expanding a request shows the guardrails that ran and what each one did. **Enforced hits** lists what a guardrail in `block` mode did, and **Monitor hits** lists what a guardrail in `monitor` mode would have done:

| Section       | Label                        | `action`                  | Meaning                                                                                                                                                  |
| ------------- | ---------------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Enforced hits | **rewritten**                | `masked`                  | Content was masked and the request continued.                                                                                                            |
| Enforced hits | **blocked**                  | `blocked`                 | The guardrail's policy refused the content.                                                                                                              |
| Enforced hits | **check unavailable**        | `blocked_unavailable`     | The guardrail could not evaluate the content, and its failure policy refuses what it cannot check.                                                       |
| Enforced hits | **blocked: buffer cap**      | `blocked_buffer_exceeded` | A held-back streamed response outgrew the output buffer cap and was refused unscanned. The hit names the guardrail whose `max_buffer_bytes` was reached. |
| Enforced hits | **matched, not masked**      | `mask_unsupported`        | A mask rule matched on a route that cannot rewrite content, such as a passthrough route, so the content was served unmodified.                           |
| Monitor hits  | **would block**              | `would_block`             | The guardrail would have refused the content.                                                                                                            |
| Monitor hits  | **would mask**               | `would_mask`              | The guardrail would have masked the content.                                                                                                             |
| Monitor hits  | **would match, cannot mask** | `would_mask_unsupported`  | A mask rule matched on a route that cannot rewrite content, so enforcing it would not mask anything either.                                              |

**Bypass reason** shows the raw failure tag when content was served without a guardrail's decision. Besides a guardrail kind's own failure tags, it can be `unscannable_body`, for content AISIX could not read, or `output_buffer_exceeded`, for a held-back stream released unscanned under `on_buffer_exceeded: fail_open`. See [Guardrail Behavior](https://docs.api7.ai/ai-gateway/traffic-controls/guardrails/behavior.md#the-bypass-reason).

### Tell Policy Blocks from Fail-Closed Blocks[​](#tell-policy-blocks-from-fail-closed-blocks "Direct link to Tell Policy Blocks from Fail-Closed Blocks")

The **Guardrail blocks** tab lists every request a guardrail refused. Its **Block type** filter separates two kinds of refusal: **Policy**, where a guardrail refused the content, and **Fail-closed**, where no guardrail reached a verdict, for example because its provider was unavailable, and the guardrail was configured to refuse rather than let the request through. The filter defaults to **All blocks**, and each blocked row carries a **Policy** or **Fail-closed** badge. An export started from the tab applies the same filter.

A refusal is classified as fail-closed only on positive evidence: at least one enforced hit has an action that starts with `blocked_`, such as `blocked_unavailable` or `blocked_buffer_exceeded`, and no hit carries the policy verdict `blocked`. Every other refusal is listed under **Policy**. That includes refusals whose type cannot be told apart, such as a refusal recorded with no enforced hits or with hits too large to read, and every refusal recorded before the control plane was upgraded to a version with this filter.

### See What a Semantic Guardrail Measured[​](#see-what-a-semantic-guardrail-measured "Direct link to See What a Semantic Guardrail Measured")

Expanding a request also shows **Semantic guardrail scores** when an embedding-similarity guardrail screened it. Each entry names the guardrail and the hook that ran, which example list it scored against, the similarity it measured against the threshold it was compared to, the embedding model that produced the score, and the line number of the closest example in that list.

These are recorded on requests the guardrail allowed as well as on ones it refused, and in `monitor` mode as well as `block`. That is what makes them useful for tuning: a monitor hit appears only when the guardrail would have blocked, so a row tuned just short of firing — a deny threshold slightly too high, or an allow threshold slightly too low — produces no monitor hit at all and looks identical to a guardrail that is not running. The scores are recorded either way.

A2A and rerank requests can report these scores, but both run only the input hook. A2A resolves no model or MCP server, so only an environment, caller API key, or team attachment reaches it. Every rerank request emits a usage event, at zero tokens when the upstream reports no token count, so a screened rerank request always has a row to carry its scores.

If the expanded request has no scores, check the rest of the row before deciding that the guardrail did not run:

| What to check               | Why scores can be absent                                                                                           |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Gateway and guardrail scope | An older gateway recorded the request, or no semantic guardrail applied.                                           |
| Earlier guardrail hits      | The chain stopped when a higher-priority guardrail blocked the request.                                            |
| Attempt details             | Retried, failed-over, and ensemble requests keep scores only on the terminal row, which is not always listed last. |
| Endpoint and hook point     | An output-only guardrail cannot run on an input-only endpoint.                                                     |
| Request content             | Under the default `text_source: user_messages`, an image-only request has no text to embed.                        |
| Streaming result            | The output exceeded `max_buffer_bytes` and was refused before semantic screening ran.                              |
| Failure evidence            | The embedding call failed before AISIX could calculate a score.                                                    |

An embedding failure leaves one of these signals on the same row:

| Configuration                     | Request outcome                                                          | Evidence                                                               |
| --------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| Block mode, fail closed (default) | Refused                                                                  | **Enforced hits** shows **check unavailable** (`blocked_unavailable`). |
| Monitor mode, fail closed         | Served                                                                   | **Monitor hits** shows **would block**.                                |
| Either mode, fail open            | Served without this guardrail's decision; the remaining chain still runs | **Bypass reason** identifies the failure.                              |

Check enforced hits, monitor hits, and the bypass reason before treating an empty score section as proof that nothing happened.

Scores from different embedding models are not comparable, which is why the model is shown beside every score. Neither the screened text nor the example text is recorded — the example is identified only by `top_example_index`, a zero-based index into that direction's list, which the dashboard renders as a line number counting from one. See [Calibrate Semantic Screening Guardrails](https://docs.api7.ai/ai-gateway/traffic-controls/guardrails/semantic-screening-calibration.md) for how to use these values to choose a threshold.

### Filter by Request Kind[​](#filter-by-request-kind "Direct link to Filter by Request Kind")

Rows carry the kind of work the request asked for — a chat completion, an image generation, a video submission, a tool call — and the **Operation** filter narrows the feed to one of them. Nothing else on a row answers that question: every OpenAI-compatible endpoint reports the same inbound protocol, so a text conversation and an image generation look alike, and a model name is no substitute because one model serves several endpoints and callers address models through aliases and model groups.

Rows are marked with the operation except for the four conversational ones — `chat`, `messages`, `responses` and `completions` — which are the common case; expand a row to see the value itself. Requests recorded before the gateway was upgraded carry no operation: they are neither marked nor returned by the filter. The filter applies on every tab, since MCP, A2A and passthrough traffic are operations too, and it stays applied when you switch tabs. It narrows within the tab rather than overriding it, so a combination with nothing in it — `mcp` on the LLM tab, say — returns an empty list rather than the MCP traffic.

The values and what each one covers are listed in [Tell Request Kinds Apart](https://docs.api7.ai/ai-gateway/observability/metrics-and-usage-events.md#tell-request-kinds-apart), which also describes how the same field reaches an external exporter.

Two things are worth knowing before reading a count. The operation describes the **request**, so a failed or policy-rejected row carries the same value a successful one would — which is what lets you see which endpoint a rejection came from. And a request that retries or fails over contributes one row per attempt, so counting rows counts attempts rather than requests. To see one row per request instead, see [Show the Last Attempt of Each Request](#show-the-last-attempt-of-each-request).

### Show the Last Attempt of Each Request[​](#show-the-last-attempt-of-each-request "Direct link to Show the Last Attempt of Each Request")

Select **Last attempt per request** in the filter bar to keep the marked last row of each request. It is off by default, because the failed attempts are often what you are looking for. It applies to both the list and **Export**, stays applied when you switch tabs, and **Reset** clears it.

In the Admin API, pass `final_attempt_only=true` to `GET /environments/{env_id}/usage_events` or `GET /environments/{env_id}/usage_events/export`. The parameter is a boolean, defaults to `false`, and answers `400` to any other value. The list's `total`, the export's `X-Export-Total` header, and the `total` and `exported` figures in the JSON export's trailer all count the rows returned, within the limits described in [Totals and Paging](#totals-and-paging) and [Export Requests](#export-requests).

Keep these points in mind when reading the result:

* The row that stays is the request's last record. It can be the fallback that answered, the last failure, a guardrail refusal, or an ensemble's judge.
* Tokens and cost on the rows left out are not added to the row that stays. Do not total a filtered result to calculate spend.
* Rows from a gateway at 1.8.0 or earlier, and rows recorded before the upgrade, carry no marker and are always returned. A fallback request recorded that way still shows every attempt with the filter on.
* If the gateway dropped a request's last row, that request does not appear in the filtered result at all.
* This narrows a query and is not access control. Anyone who can read the feed can still read every attempt by leaving the parameter out, and the rows that stay still carry `attempt_kind`, `attempt_index`, and `attempt_model`. If you show this data to someone who must not see the attempts, control that in your own service.

### Search Requests[​](#search-requests "Direct link to Search Requests")

The search box above the filters looks for the text you type anywhere in a request's text fields, ignoring case. It covers the error message and error class, the requested and resolved model names, and the provider and provider key labels. It also covers the client user agent and source IP, the finish reason, the request ID, and the provider request ID — so an ID quoted by a provider's support channel finds its request here. Use it when all you have is part of an error a client reported and you do not know which field carries it.

Search combines with the filters rather than replacing them. For example, a search for `rate limit` together with the **5xx** status filter returns only the server errors whose text mentions a rate limit. The dedicated filters remain the precise way to narrow by a known request ID, model, provider key, or caller API key.

In the **Requested model / group** filter, typing searches aliases by case-insensitive substring, while choosing a suggested model or group matches the complete alias exactly, including case. Exports preserve whether the active filter in the request list was typed or selected.

### Totals and Paging[​](#totals-and-paging "Direct link to Totals and Paging")

The list loads the rows first and shows the total when its count finishes. The count stops at 10,000 matching rows, so a larger result shows as `10,000+`. If the count does not finish in time, the footer reports the total as unknown, and **Next** stays available as long as the current page is full.

Only the first 10,000 matching rows can be paged through, and a hint appears when you reach that depth. Narrow the time range to reach older rows. If loading the rows takes too long, the page shows an error asking you to narrow the time range or add filters.

The **Status** filter suggests **Failed (4xx/5xx)**, which sets the value `400-599`. It filters by status code only, so it does not find an MCP or A2A failure recorded under status `200`, which the list labels **tool error** or **agent error**. To find those rows, [search](#search-requests) for the `error_class` value, such as `tool_error`. See [Failed Tool Calls](https://docs.api7.ai/ai-gateway/mcp-gateway/observability.md#failed-tool-calls) and [JSON-RPC Errors From an Agent](https://docs.api7.ai/ai-gateway/agent-gateway/observability.md#json-rpc-errors-from-an-agent) for the values. The field still accepts other values as free text: a family such as `4xx`, an exact code such as `429`, or a range such as `500-599`.

`GET /environments/{env_id}/usage_events` applies the same limits:

* `total` counts up to 10,000 rows and comes with `total_is_capped`, which is `true` when more rows match.
* The count has 5 seconds. If it does not finish, the rows are still returned, and `total` and `total_is_capped` are both absent.
* An offset, `(page - 1) * limit`, of 10,000 or more returns `400` `INVALID_REQUEST`. Narrow the time range instead.
* The row query has 30 seconds. Past that, the call returns `503` `QUERY_TIMEOUT`. Narrow the time range or add filters.

AISIX Enterprise compatibility paging

An existing AISIX Enterprise integration that must page beyond the first 10,000 rows can send `legacy_paging=true`. AISIX Cloud rejects this parameter with `400` `INVALID_REQUEST`, and the dashboard does not use it.

Compatibility paging removes the offset boundary and the application-level time budgets. It returns an exact, uncapped `total` with `total_is_capped: false`, unless `include_total=false` omits the count. Every page that includes the total repeats the full count, and a deep offset can hold a database connection while it reads past the earlier rows. A row that arrives during the walk shifts the later pages. If the query fails, including a timeout enforced by the database, the call returns `500` `INTERNAL`. Keep the default bounded behavior for new automation and narrow the time range whenever possible.

`total` counts attempts rather than requests and stops at its cap, so do not use it as a traffic figure. For request counts over a window, read `GET /environments/{env_id}/usage_summary`.

### Export Requests[​](#export-requests "Direct link to Export Requests")

**Export** downloads every request matching the current filters and search, not just the page on screen. Choose **CSV** for spreadsheet review or **JSON** for a downstream pipeline; JSON returns the same fields as the control-plane API, wrapped in a `data` array with a `total` count.

Both formats include the following data:

| Data                  | Export behavior                                                                                                                                                                                                                                                                                                          |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Request and outcome   | Request time, request ID, attempt details, status, error text, operation, requested and resolved model names, and caller API key name.                                                                                                                                                                                   |
| Usage and latency     | Token counts plus separate upstream and caller-facing latency fields. `total_tokens` is the upstream-reported total; it is `null` in JSON and empty in CSV when absent.                                                                                                                                                  |
| Cache-write retention | `cache_write_30m_tokens` and `cache_write_12h_tokens` contain the [cache-write split by retention](https://docs.api7.ai/ai-gateway/observability/metrics-and-usage-events.md#cache-writes-by-retention). Their CSV columns are immediately before `currency` and are empty when the gateway did not report them.         |
| Platform cost         | `upstream_cost`, `price_tier_index`, and `price_tier_max_input_tokens`. A `null` value is empty in CSV. See [Read Platform Cost and the Price Tier](https://docs.api7.ai/ai-gateway/cloud/model-pricing.md#read-platform-cost-and-the-price-tier).                                                                       |
| Guardrails            | `guardrail_scores` is a CSV column and a JSON array. `guardrail_blocked` marks every refusal, while `guardrail_blocked_fail_closed` marks only fail-closed refusals. Rows recorded before the field existed read `false`; see [Tell Policy Blocks from Fail-Closed Blocks](#tell-policy-blocks-from-fail-closed-blocks). |
| Attribution           | CSV includes `team_id`, `tags`, and `service_tier`. `tags` is a JSON object and is empty without tags; `service_tier` is empty when the upstream reported none. JSON omits these fields when they have no value.                                                                                                         |
| Cost breakdown        | `cost_components` is a JSON array and `off_peak_discount` is the amount removed. Both are empty in CSV and `null` in JSON for rows recorded before breakdown data existed. See [Cost Breakdown of a Single Request](https://docs.api7.ai/ai-gateway/cloud/cost-analysis.md#cost-breakdown-of-a-single-request).          |

CSV files include a UTF-8 byte-order mark so spreadsheet applications read non-ASCII error messages correctly.

Timestamps are written in UTC by default, RFC 3339 with a `Z` suffix — for example `2026-09-12T04:03:01Z`. Exporting from the dashboard instead renders `occurred_at` and `received_at` in the browser's own time zone, so the downloaded file shows the same wall-clock time the page does. A caller using the control-plane API chooses with the optional `timezone` query parameter, an IANA zone name such as `Asia/Seoul`: every timestamp then carries that zone's numeric offset (`2026-09-12T13:03:01+09:00`), while `timezone=UTC` keeps the `Z` form and an empty, `Local`, or unrecognized name is rejected with a `400`. Both formats behave the same way, and omitting the parameter leaves the file byte-for-byte as it was before, so existing automation is unaffected.

Cost is in the [organization's currency](https://docs.api7.ai/ai-gateway/cloud/model-pricing.md#choose-the-organization-currency). The CSV names its column `cost`, and its last column, `currency`, names that currency on every row. A JSON row carries the cost as `cost` and again under the deprecated name `cost_usd`, and the trailer after the rows carries `currency`. Either way it is the organization's current currency, so costs recorded before a currency switch are reported in it unconverted. A control plane at 1.9.0 or earlier names the CSV column `cost_usd` and exports no `currency`. The column keeps its position, so a script that reads columns by position is unaffected.

One export returns at most 50,000 requests, newest first. When the filters match more than that, the control plane exports the newest 50,000 and sets `X-Export-Truncated` to `true`. Narrow the time range or the filters to export the remainder.

The control plane counts matches only up to 50,001, which is enough to know the file is cut short. For a larger match, `X-Export-Total` and the JSON `total` report 50,001 rather than the full count.

The count before the file starts can take about 35 seconds, and each batch of rows read has 30 seconds. If the count runs past that, the export returns `503` `QUERY_TIMEOUT` with no file, so narrow the time range or add filters. A failure after the file has started cuts the connection, so the download fails rather than leaving a short file that looks complete.

## Audit Log[​](#audit-log "Direct link to Audit Log")

The audit log records control-plane state changes for compliance review and operational investigation. It shows who created, updated, or deleted resources such as environments, models, API keys, provider keys, budgets, policies, and admin tokens.

Use the audit log when traffic behavior changes after a configuration update. Request logs can show the request outcome, while the audit log can show whether a control-plane resource changed before that outcome.

Audit log access is restricted to organization owners and admins.

Entries are listed newest first, and the footer below the trail reports how many entries the current filters match in total. Page numbers refer to that whole filtered set rather than to what has been loaded so far, so a review can be resumed at a known position.

### Filter and Search the Trail[​](#filter-and-search-the-trail "Direct link to Filter and Search the Trail")

The filters above the trail narrow it by resource type, actor, and time. The resource-type list offers only the types this organization has actually recorded. The time range offers presets and a **Custom range**, which takes an explicit start and end time so a review can be pinned to the exact window an incident covers.

The search box looks for the text you type anywhere in an entry's readable fields, ignoring case. It covers the recorded before and after state, which is where a resource's display name lives. It also covers the resource type and identifier, the action, the actor identifier, the client IP address, and the user agent. Use it when a ticket names the resource but not which change touched it.

Search combines with the filters rather than replacing them. For example, a search for a model name together with an actor returns only that person's changes mentioning the model. The dedicated filters remain the precise way to narrow by a known resource type or actor.

### Export the Trail[​](#export-the-trail "Direct link to Export the Trail")

**Export** downloads every entry matching the current filters and search, not just the page on screen. Choose **CSV** for spreadsheet review, or **JSON** for a downstream pipeline or an evidence archive. JSON returns the same fields as the control-plane API, wrapped in a `data` array with a `total` count.

Both formats carry the entry time and identifier, the action, the resource type and identifier, the client IP address and user agent, and the complete before and after state. They also carry the actor's email address alongside the actor identifier, so an exported file stays readable for a reviewer who does not have dashboard access. CSV files are UTF-8 with a byte-order mark, so spreadsheet applications read non-ASCII resource names correctly.

In AISIX Enterprise behind an ingress or load balancer, the client IP address is the proxy's address until you list that proxy in `api.trustedProxies`, or in `AISIX_TRUSTED_PROXIES` in Docker Compose. See [Reverse Proxies](https://docs.api7.ai/ai-gateway/on-premises/deployment.md#reverse-proxies).

The `created_at` column is rendered in the time zone the browser reports, RFC 3339 with that zone's numeric offset — for example `2026-09-12T13:03:01+09:00` — so the exported trail reads the same wall-clock time the page showed. When the browser names no zone, the column stays UTC with a `Z` suffix, as it always did.

One export returns at most 50,000 entries, newest first. When the filters match more than that, the control plane exports the newest 50,000 and reports the full match count. Narrow the time range or the filters to export the remainder.

A failure after the file has started cuts the connection, so the download fails rather than leaving a short file that looks complete.

The audit trail and its exports use the authenticated dashboard audit route. That route is not part of the admin-token automation workflow documented in the AISIX Cloud Admin API reference.

## Investigate a Request Outcome[​](#investigate-a-request-outcome "Direct link to Investigate a Request Outcome")

Send the request through the AISIX gateway endpoint with a caller API key and model alias that belong to the same environment as the gateway.

After the request completes, check request logs for a matching request time, status, requested model, and caller API key. If the request used routing or failover, inspect the resolved model or attempt details when they are available.

An upstream authentication, quota, or provider-side error can still prove that the AISIX gateway path is working. In this case, the request reached AISIX. AISIX selected the configured model and provider key, then the upstream provider returned an error.

Do not treat a provider error as a resource-projection failure unless the log shows the wrong model, provider key, or environment.

## Investigate Policy Rejections[​](#investigate-policy-rejections "Direct link to Investigate Policy Rejections")

Policies configured through the control plane can reject traffic at the gateway before it calls the upstream provider. For budgets, the control plane counts spend and marks a budget exhausted, and the gateway enforces the budgets it has received, including while the control plane is unreachable. Budget hard stops return a budget-related error. Rate-limit policies are enforced by the gateway and return a rate-limit error. Guardrails are also enforced by the gateway and can reject unsafe content before or after the provider call, depending on the guardrail hook.

When a request is rejected, first identify whether the response came from AISIX or from the upstream provider. Then use request logs to check the status and request identity.

For budget rejections, compare the returned budget scope with the Budgets view. For rate-limit rejections, check the caller API key, model, team, or member policy that matches the request. For guardrail rejections, check the guardrail scope and the model or caller identity that triggered it.

## If Traffic Does Not Appear in Request Logs[​](#if-traffic-does-not-appear-in-request-logs "Direct link to If Traffic Does Not Appear in Request Logs")

Not every request produces a row. Requests rejected before AISIX could attribute them — an unauthenticated call, a failed authentication, a body that cannot be parsed, a body-size rejection — reach the gateway's metrics, and its access log where one is written at all, but never Request Logs. The health and discovery routes produce no Request Logs row at any outcome; `/livez` and `/readyz` write no access-log line either. For the complete list of outcomes and the status and `error_class` each one carries, see [What the Usage Log Records](https://docs.api7.ai/ai-gateway/observability/metrics-and-usage-events.md#what-the-usage-log-records).

If request logs do not show the expected record for a request that should have one, check the request path first. Request logs are scoped to AISIX gateway traffic for the selected environment, so requests sent to another gateway endpoint or another environment appear elsewhere.

If the request path is correct, check that the AISIX gateway has a recent heartbeat and can reach the control-plane telemetry endpoint.

Other control-plane signals can help narrow the cause:

| Signal                        | What it shows                                                                                                                |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Data planes heartbeat         | Whether the AISIX gateway is connected to the control plane and reporting from the expected environment.                     |
| Usage                         | Whether the control plane received gateway telemetry for aggregate usage and budget workflows.                               |
| Observability exporter health | Whether the gateway has applied exporter configuration and is reporting delivery status for external telemetry destinations. |

## External Exporters[​](#external-exporters "Direct link to External Exporters")

Request logs show the control plane's view of gateway telemetry. Observability exporters are configured from the environment's **Observability** view and send usage events from the AISIX gateway to destinations you control.

Use exporters when you need to send request telemetry to an external tracing, logging, storage, or accounting system. Exporter delivery happens from the AISIX gateway directly to the destination. A request log can exist even when an external destination has a credential, network, or receiver-path issue.

## Data Retention[​](#data-retention "Direct link to Data Retention")

The control plane keeps usage data for a per-organization retention window. This includes the data behind both the **Request Logs** and **Usage** views. By default, records are kept for 30 days, and the control plane removes older records each day.

Organization owners and admins set the window in **Settings**, on the **Data** tab, under **Usage log retention**, to any value from 1 to 365 days. A longer window preserves more history for investigation and reporting, and a shorter one reduces how much data is stored. A change applies going forward and takes effect on the next daily cleanup. The time range you can pick in **Request Logs** follows the window.

Retention is why older traffic eventually stops appearing in Request Logs and Usage. Removing those records does not reduce spend that [budgets](https://docs.api7.ai/ai-gateway/traffic-controls/budgets.md#how-spend-is-counted) have already counted. For longer history, send usage events to external storage with an [observability exporter](https://docs.api7.ai/ai-gateway/observability/exporters.md) before the records are removed.

Audit events are kept for 730 days by default, then removed by a daily cleanup. The period is a deployment setting, not an organization setting: an AISIX Enterprise operator sets it with `audit.retention_days`, and `0` keeps audit events forever. See [AISIX Enterprise Configuration](https://docs.api7.ai/ai-gateway/reference/on-premises-configuration.md#audit-log-retention). To keep audit records beyond the retention period, [export the trail](#export-the-trail) before they are removed.

## Next Steps[​](#next-steps "Direct link to Next Steps")

Use the [AISIX Enterprise / Cloud Admin API](https://docs.api7.ai/ai-gateway/reference/cloud-admin-api.md) for supported automation operations. To deliver gateway telemetry to systems you operate, continue with [Observability Exporters](https://docs.api7.ai/ai-gateway/observability/exporters.md).
