Analytics and tracing
DIAL Core writes an analytics log entry for each request, which usage analytics reads, and emits OpenTelemetry spans for every incoming request. These static settings choose what extra data the analytics log collects, how Core protects its memory under load, and how spans are enriched and correlated. This page is for platform engineers who run DIAL observability; the observability pipeline itself is in Observability.
Analytics log
| Setting | Default | Description |
|---|---|---|
analytics.collectClaims | false | Adds a claims object with user_id, roles, and user_display_name to each entry. |
analytics.claimsAllowlist | [] | A JSON array of claims to add to claims, taken as is from the identity provider's token or user info response. Each entry is a dot-separated path, such as email or resource_access.roles, and is written under that exact path; missing claims are left out. ["*"] collects every top-level claim. |
analytics.collectHeaders | false | Adds a headers object with the request headers to each entry, filtered by the two lists below. |
analytics.headersBlacklist | ["authorization", "api-key", "cookie", "proxy-authorization"] | Case-insensitive, full-match regular expressions. Headers whose name matches any of them are left out. Use a pattern such as x-stainless-.* to drop a family of headers. |
analytics.headersAllowlist | Not set | Case-insensitive, full-match regular expressions. When set, only headers that match one of them are collected; the block list still wins. Leave it out to collect every header that is not blocked. |
analytics.maxPendingBodyHeapPercent | -1 | A limit on the request and response bodies held by log entries waiting to be written, as a percent of the JVM's maximum heap, for example 10 or 2.5. 0 or below means no limit. See Body memory limit. |
deploymentStats.ttlMs | 3600000 | How long the per-trace usage record is kept in Redis, in milliseconds; each write refreshes it. A trace whose root is a background Responses job keeps it for at least backgroundJob.jobTtlMs plus backgroundJob.scanIntervalMs. |
Claims and headers are written unhashed, so collectClaims, claimsAllowlist, and collectHeaders can put personal or sensitive data in the log. Turn them on only where that is acceptable, and curate the header lists for your setup.
Collecting claims
collectClaims and claimsAllowlist work independently: either one alone emits the claims object, with only its own members. While collectClaims is on, an allowlist entry named user_id, roles, or user_display_name is skipped if the fixed set already wrote it.
- One list can cover several providers that name the same claim differently, for example
["email", "upn", "preferred_username"]. - A path that is not a dot-separated claim name, such as
"."or"a..b", is rejected at startup with a warning. - A claim value larger than 4 KB is cut and ends in
>>.
Prefer the hashed user ID where you can. When identityProviders.<name>.loggingKey points at the email claim and obfuscateUserEmail stays on, user.id already identifies the user as a salted hash, without the raw address in the log.
Body memory limit
Log entries for API keys without secured carry request and response bodies. Under high load, or with server.instances above 1, the writer can fall behind and fill the heap. With a positive analytics.maxPendingBodyHeapPercent, while waiting entries hold that share of the heap or more, Core writes new entries without request.body, response.body, and assembled_response until the writer catches up. Every other field is still written. 10 is a starting point.
| Metric | Type | What it shows |
|---|---|---|
dial_analytics_log_pending_body_bytes | Gauge | The body bytes held by waiting entries now. |
dial_analytics_log_bodies_skipped_total | Counter | Entries written without bodies because of this limit. Entries of secured keys are not counted. |
Tracing
Core always emits OpenTelemetry spans for incoming requests. The tracing settings control the enrichment on top of them, and none of them publish prompts, completions, tool payloads, API keys, arbitrary headers, or upstream provider names.
| Setting | Default | Description |
|---|---|---|
tracing.genAiSpanAttributes | true | Adds OpenTelemetry GenAI semantic-convention gen_ai.* attributes and dial.* attributes to the request span and to OpenTelemetry log records. |
tracing.responseTraceHeaders | true | Adds the W3C traceparent of Core's root span, X-DIAL-TRACE-ID, and X-DIAL-SPAN-ID to every response, all exposed through CORS. Nothing is added when no OpenTelemetry SDK is attached. |
tracing.conversationIdHeaders | ["x-claude-code-session-id", "thread-id", "x-session-id", "x-dial-client-channel-id", "X-CONVERSATION-ID"] | Request headers, in priority order, that can carry a conversation or session ID. See Conversation ID. |
The tracing section used to live in the dynamic configuration. A tracing block left in a config file is ignored, so its values are replaced by the defaults; move it to the static settings.
Conversation ID
Core takes the first usable header from tracing.conversationIdHeaders:
- It publishes the value as
gen_ai.conversation.idwhengenAiSpanAttributesis on. - It always writes the value to each analytics log entry as
session.id, also forsecuredkeys. The member is left out when no header matches. - Matching is case-insensitive and values are trimmed. A value longer than 256 characters is skipped, and the next header gets its turn.
- Blank entries and known credential headers, such as
authorization,cookie, andx-api-key, are rejected with a warning. - Your array replaces the defaults; it does not add to them. An empty array turns correlation off.
The defaults cover Claude Code (x-claude-code-session-id), OpenAI Codex CLI (thread-id), OpenCode (x-session-id), and DIAL Chat (x-dial-client-channel-id, with X-CONVERSATION-ID as the legacy fallback). Any other client needs its own header listed.
To drop individual attributes, use an attributes or transform processor in the OpenTelemetry Collector rather than Core. For the attributes Core publishes and the spans it emits, see Tracing.
Example
{
"analytics": {
"collectClaims": false,
"claimsAllowlist": ["email", "upn"],
"collectHeaders": true,
"headersAllowlist": ["traceparent", "tracestate", "user-agent", "x-forwarded-.*"],
"maxPendingBodyHeapPercent": 10
},
"tracing": {
"genAiSpanAttributes": true,
"responseTraceHeaders": true
}
}
Further reading
- Tracing — collect and export Core's spans
- DIAL Analytics — ingest the analytics log into queryable usage data
- gflog.xml — where Core writes its logs
- Usage limits and cost control — token, request, and cost limits and how DIAL enforces them