Observability for Cortex AI Gateway¶
Cortex AI Gateway records traces and spans for all requests traversing the gateway. Traces give you full execution transparency into agent behavior: which models were invoked, step timing, token consumption, and failure reasons.
Query traces with the AGENT_TRACE_TABLE table function:
Querying AGENT_TRACE_TABLE requires the MONITOR privilege on the gateway object. See
Access control.
Note
Traces the gateway records, and traces client applications export to it, land directly in customer-owned event tables inside your Snowflake security boundary, where access is governed by native Snowflake RBAC. Credit and usage metering is collected separately and surfaced in AI_GATEWAY_USAGE_HISTORY view.
By default the gateway records metadata about each request, not its content. Prompt and response payloads are captured only when payload capture is explicitly enabled. See Change the specification.
How traces are structured¶
The gateway models AI activity using standard OpenTelemetry execution hierarchies:
- Conversation thread (
conversation_id): an optional identifier grouping related multi-turn interactions over time. - Trace (
trace_id): a single end-to-end execution, or agent turn, which can encompass multiple model calls. - Span (
span_id): a single atomic unit of work within a trace, such as one inference call. Each span records timing, status, the model requested, token counts, and outcome metadata.
Each span is one row. Group rows by trace_id to assemble a trace, and a conversation_id groups
related traces into one thread:
Every inference span has gen_ai.operation.name set to chat.
To inspect traces, spans, and conversations without assembling any of this yourself, use the AI Gateway Observability page in Snowsight.
Note
Both identifiers come from the request itself:
conversation_idis populated from a specific request header, so it’s only present when the client sends it.trace_idcomes from the request’straceparentheader, following the W3C Trace Context convention.
Send client traces to the gateway¶
The spans the gateway records are what it sees from the outside: one span per request it serves. A client that uses OpenTelemetry can also export its own traces to the gateway, which adds the client’s view of the same work, including the steps it took between requests.
Client telemetry is off until you turn it on in the gateway specification:
FROM SPECIFICATION replaces the whole specification, so start from what DESCRIBE AI GATEWAY returns.
See Change the specification.
The gateway then accepts OTLP traces over http/protobuf or http/json at
<gateway-endpoint>/telemetry/v1/traces, authenticated with a token, the same as for
inference. See
Setting up authentication. Get the endpoint from DESCRIBE AI GATEWAY SNOWFLAKE, as described in
Gateway endpoint.
Client-exported spans land in AGENT_TRACE_TABLE alongside the spans the gateway records, so a single
query covers both.
The attributes a client exports are the client’s own, so they vary by client and aren’t described here.
When the client propagates its trace context on the requests it sends, its spans share a trace_id with
the gateway’s, so grouping on it reassembles a turn from both sides. See
Trace context propagation.
What the exporter has to be set to¶
Whatever client you’re configuring, its OTLP trace exporter needs these three values:
| Setting | Value |
|---|---|
| Protocol | http/protobuf or http/json. |
| Endpoint | <gateway-endpoint>/telemetry/v1/traces. |
| Authorization header | Bearer <token>, the same as for inference. |
A client that reads the standard OpenTelemetry environment variables takes these as
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, and
OTEL_EXPORTER_OTLP_TRACES_HEADERS, with OTEL_TRACES_EXPORTER set to otlp. Clients that keep
telemetry behind a switch of their own need that turned on as well.
Trace context propagation¶
For client spans and gateway spans to join into one turn, the client has to send a traceparent header
on its requests. Without it both sides are still recorded, they just don’t share a trace_id.
For how to turn propagation on when setting up a coding agent, see Instrument a coding agent.
Example: OpenCode¶
OpenCode exports through an OpenTelemetry plugin, configured in
~/.config/opencode/opencode.json:
endpoint takes the base, and the exporter appends the OTLP path, so requests arrive at
<gateway-endpoint>/telemetry/v1/traces. {env:SNOWFLAKE_TOKEN} reads the token from an environment
variable, so it isn’t stored in the file.
Trace Claude requests¶
You can send Claude traces to the gateway from your application, through the Claude SDKs or Claude Code, and from the Claude API. Set up either one, or both.
Claude Code and SDK¶
The Claude SDKs and Claude Code emit an OpenTelemetry span for each request they send. Configure the
exporter to send spans to the gateway, and route the requests through the gateway. Each client span then
shares a trace_id with the gateway span for the same request.
Register a tracer provider before the first call. The SDK uses whichever provider your application registers:
Register a tracer provider before the first call. The SDK uses whichever provider your application registers:
Claude Code reads its telemetry settings from the env block of ~/.claude/settings.json:
Tracing requires both CLAUDE_CODE_ENABLE_TELEMETRY and CLAUDE_CODE_ENHANCED_TELEMETRY_BETA. By
default, Claude Code sends the traceparent header only to the Anthropic API.
CLAUDE_CODE_PROPAGATE_TRACEPARENT sends it to the gateway as well, which is required for trace
correlation between client and server spans. For more information, see
Claude Code monitoring.
By default, the SDKs record metadata, not prompts or responses. To record prompts and responses, or to change the content size limit, set a conversation ID, or turn off tracing for a client, see the SDK options in Set up OpenTelemetry with the Claude SDK.
Claude API¶
The Claude API exports spans for the requests it serves, including each model run and each server tool it runs. To enable this, add the gateway as a telemetry destination in the Claude Console. Your application code doesn’t change.
- In the Claude Console, open the OpenTelemetry settings page and click Add destination.
- For Provider, select Snowflake.
- Enter the gateway’s base URL, derived by running
DESCRIBE AI GATEWAY SNOWFLAKE. - Enter an authorization token: a programmatic access token, or another form of identification accepted by the gateway for inference. See Setting up authentication.
- Click Add destination, then use Test in the destination’s More actions menu to send a test span.
Note
By default, a programmatic access token works only for a user who is subject to a network policy. If you
use one here, the user’s network policy must allow traffic from the Claude API, which sends spans from
Anthropic’s IP addresses (such as
160.79.104.0/21). Claude Platform on AWS sends traffic from different addresses. For more information,
see Network policy requirements.
The other fields are optional. Content mode defaults to metadata only, and Workspaces sets which Claude Console workspaces send spans.
Claude API spans have service.name set to claude-platform. If the request came from a Claude SDK
client with tracing set up, the API span appears under the SDK span in the same trace. For more
information about destination settings, delivery, and limits, see
Set up OpenTelemetry with the Claude API.
What the gateway records¶
Traces are stored in a Snowflake event table,
so they use the standard event table schema: identifiers and timing are top-level columns, while
semantic detail lives in the scope, record, record_attributes, and resource_attributes objects,
which you extract with the colon operator and cast. For the full schema, see
Event table columns. Because every gateway row is a span,
record_type is always SPAN. For the fields a span carries, see
the SPAN record type.
Span identity and outcome¶
| Column or attribute | Description |
|---|---|
start_timestamp, timestamp | When the span started and ended. Subtract them for duration. |
trace:"trace_id" | Which execution the span belongs to. Group on this to reassemble a turn. |
trace:"span_id" | The span’s own identifier. |
record:"parent_span_id" | The parent span. NULL marks the root span of a trace. |
record:"name" | The operation and model, such as chat claude-sonnet-5. |
record:"kind" | The span kind. The gateway’s own spans are SPAN_KIND_SERVER. |
scope:"name" | What emitted the span. The gateway’s own spans are aigateway/tracing. See Send client traces to the gateway. |
record:"status":"code" | The outcome: STATUS_CODE_OK, STATUS_CODE_ERROR, or STATUS_CODE_UNSET. |
record:"status":"message" | Either client_error or server_error. Set only when the span failed. |
Gateway and user¶
These live in resource_attributes, because they describe the process that emitted the span rather than
the individual request.
| Attribute | Description |
|---|---|
resource_attributes:"ai_gateway_id" | The identifier of the gateway that served the request. |
resource_attributes:"gateway_name" | The name of the gateway that served the request. |
resource_attributes:"user" | The user who sent the request. |
Request and response¶
| Attribute | Description |
|---|---|
record_attributes:"gen_ai.operation.name" | The operation. Inference calls are chat. |
record_attributes:"gen_ai.provider.name" | The provider that served the request, anthropic or openai. |
record_attributes:"gen_ai.request.model" | The model requested. unknown when the model couldn’t be resolved. |
record_attributes:"gen_ai.response.model" | The model that served the response. Set on responses only. |
record_attributes:"gen_ai.conversation.id" | The conversation thread the turn belongs to, taken from the x-snowflake-ai-gateway-conversation-id header, or x-claude-code-session-id when that isn’t present. |
record_attributes:"gen_ai.usage.input_tokens", record_attributes:"gen_ai.usage.output_tokens" | Total input tokens, including cached input tokens, and total output tokens for the span. |
record_attributes:"gen_ai.usage.cache_creation.input_tokens", record_attributes:"gen_ai.usage.cache_read.input_tokens" | Prompt cache write and read tokens. These counts are subsets of gen_ai.usage.input_tokens, not additional tokens. Populated intermittently. |
record_attributes:"gen_ai.request.max_tokens" | The max_tokens value sent with the request. |
record_attributes:"gen_ai.request.temperature", record_attributes:"gen_ai.request.seed" | Sampling parameters, set only when the caller sends them. seed applies to OpenAI Chat Completions only. |
record_attributes:"gen_ai.output.type" | The output type, text. Unset for reasoning-only responses. |
record_attributes:"gen_ai.client.operation.duration" | Client-observed duration of the inference call. |
record_attributes:"openai.api.type" | The OpenAI API surface used, chat_completions. |
record_attributes:"gen_ai.input.messages", record_attributes:"gen_ai.output.messages", record_attributes:"gen_ai.system_instructions" | Prompt, response, and system prompt content. Populated only when request and response capture is enabled. |
Don’t add the cache counts to gen_ai.usage.input_tokens, because doing so counts those tokens twice.
Calculate non-cached input as input tokens - cache read input tokens - cache write input tokens.
For credits consumed, see AI_GATEWAY_USAGE_HISTORY view.
HTTP¶
| Attribute | Description |
|---|---|
record_attributes:"http.request_id" | The request identifier. Set on every span. |
record_attributes:"http.status_code" | The HTTP status code of the response. A value of 400 or greater marks a failed span. |
record_attributes:"http.request.header.*" | Selected request headers, including :method, user-agent, content-length, anthropic-beta, and x-claude-code-session-id. Snowflake-internal headers are excluded. |
Query traces¶
Two queries cover most of what you need: one that lists turns, and one that opens a turn up into its spans.
Neither filters on scope, so both cover client-exported spans as well as the gateway’s own. Add
scope:"name"::string = 'aigateway/tracing' to either to see only what the gateway recorded.
List turns¶
Each span is one row, so this rolls the rows of a trace into a single row per turn:
Inspect one turn¶
Take a trace_id from the previous query. One row per span means no rollup is needed:
To read the prompts and responses, add the content attributes. They’re large, so select them only when you need them:
Note
Capturing message content is opt in, so these attributes are populated only when the gateway
specification has both logging.enabled and logging.capture_payload.request_response set to true.
Payload capture records nothing on its own while logging is off. To turn them on, see
Change the specification.
They are also not captured for users excluded through logging.capture_payload.exclude_tags; see
Exclude users from payload capture.
Once it’s on, the captured prompts and responses are retained in the event table in your own account. Treat that data as sensitive: it contains everything your users sent through the gateway.
Cost considerations¶
Traces are stored in an event table, so gateway observability is billed the way other telemetry data is: Snowflake charges for the serverless compute used to ingest the traces, and for the storage they occupy in the event table. Refer to the Snowflake Service Consumption Table for the applicable rates. For more detail on how telemetry ingestion is billed and how to keep it down, see Costs of telemetry data collection.
Turning on payload capture records the full prompts and responses, so it increases the volume stored per request.
Usage notes¶
- Individual records are capped at 1 MB. Records larger than that are truncated. Truncation behavior preserves the beginning and the end of the record, and truncates the middle sections.