On this page
- What to log in an AI system, in one paragraph
- The Replay Record
- The record, as JSON you can copy
- Log the prompt by reference, not by value
- Retrieval provenance is the field everyone skips
- What must never be written down
- What this costs, and where to sample
- Cardinality, or the log field that takes down the dashboard
- Design the schema backwards from the questions you will ask
- Definitions, and the logging checklist
What to log in an AI system, in one paragraph
Write one record per run and one record per model call, joined by a single correlation id, and make the pair complete enough to replay the run without the original process. That means the prompt identified by template id, version and content hash rather than pasted in full, the model identity the provider returned rather than the alias you requested, every sampling parameter, the retrieval set with chunk ids, versions and scores, every tool call with the arguments the model produced and the outcome status, the raw output stored by reference with its hash, the validation verdict with the specific failing path, token counts in and out, the finish reason, the retry lineage, and the outcome once a human touches the result. Redact personal data at the point the record is created, never at the sink.
- 1 idflows from the trigger event through every model call and tool call to the outcome record written days later
- 2 recordsper run: a summary at run level to query, a detail record per call to drill into
- hashthe prompt rather than storing it, so a six month old failure still resolves to the exact text that produced it
- 0raw secrets, tokens or unredacted personal data in a log line, at any sampling rate, in any environment
The test for whether your logging is adequate has nothing to do with volume. It is whether a colleague, handed only a customer complaint and a trace id, can reconstruct what the system saw and why it answered the way it did, six weeks later, without asking anyone. Most AI logging fails that test not because it is thin but because it is unjoinable: the model call is in one place, the retrieval in another, the human's correction in a product database, and no field connects them.
The Replay Record
The Replay Record
Six field groups. A record is complete when a run can be replayed from it, and each group exists because a specific investigation is impossible without it.
trace_id, run_id, call_id, parent_call_id, tenant_id, a pseudonymous user_ref, environment, release hash and the wall clock in UTC with milliseconds. Everything else is worthless without these. The single most common logging failure is a beautifully detailed record that cannot be joined to the thing a customer complained about, because the trace id never reached the customer facing artifact.
template_id, version, prompt sha256, the variable names with redacted or hashed values, requested and returned model identity, temperature, top_p, max_tokens, seed if you set one, tool schema hash, and the assembled token count. Store prompt text once in a registry keyed by hash. Logging the full text on every call multiplies your bill by the length of your system prompt and buries the fields you actually query.
The query or its embedding hash, index name and index version, k requested and k returned, the ordered chunk ids with scores, the reranker identity and the post rerank order, and the newest document timestamp in the returned set. Without this you cannot tell a retrieval failure from a generation failure, which is the first fork in every investigation of a wrong answer in a RAG system.
Every tool call in order with the tool name, the argument object exactly as the model produced it, an outcome status, latency, and a hash or truncated preview of the result. Record the arguments before your validation layer repaired them, because the difference between what the model asked for and what your code executed is frequently the bug you are hunting.
The raw completion stored by reference with its hash, the parsed object, the schema validation result with the failing path rather than a message string, the finish reason, tokens in and out, any guard rail that fired, and the retry lineage if this was attempt two or three. A finish reason of length rather than stop is a truncated answer, and it is invisible unless you record it.
What a human or a downstream system did with the result: accepted, edited with the diff, overridden, escalated or reversed, written back against the same trace id, possibly days later. This is the field that turns a log into a training and evaluation dataset, it costs almost nothing to add at the moment somebody clicks, and almost nobody builds it.
The two record levels matter. Run level is what you query in aggregate: one row per unit of work with the totals, the terminal state and the outcome. Call level is what you read during an investigation: the full detail of each step. Putting everything at call level makes aggregate queries expensive, and putting everything at run level makes debugging impossible, which is why systems that logged only one of the two end up rebuilding both anyway.
The record, as JSON you can copy
{
"schema": "llm_call/v1",
"ts": "2026-08-26T09:41:12.884Z",
"env": "prod",
"release": "9f3c1ab",
"trace_id": "01J9Z7QK4M2X", // one id from trigger to outcome
"run_id": "run_8f21",
"call_id": "call_3",
"parent_call_id": "call_2",
"tenant_id": "t_1042",
"user_ref": "u_9c1f8b", // pseudonymous, never an email
"surface": "support_reply_draft",
"prompt": {
"template_id": "support_reply",
"version": 7,
"sha256": "b41d0f9c...", // resolves in the prompt registry
"vars": {
"ticket_id": "T-88213",
"customer_name": "[REDACTED:name]",
"locale": "en-GB"
},
"assembled_tokens": 3411 // catches silent context truncation
},
"model": {
"requested": "vendor/model-alias",
"returned": "vendor/model-alias-2026-07-11", // log what came back
"temperature": 0.2,
"top_p": 1,
"max_tokens": 800,
"seed": null
},
"retrieval": {
"index": "kb_support",
"index_version": "2026-08-25T02:10Z",
"k_requested": 8,
"k_returned": 8,
"reranker": "rr_v3",
"newest_doc_ts": "2026-08-24T17:02Z",
"chunks": [
{"id": "kb:3391#c07", "doc_version": 14, "score": 0.81, "rank_final": 1},
{"id": "kb:1180#c02", "doc_version": 9, "score": 0.74, "rank_final": 2}
]
},
"tools": [
{"name": "lookup_order", "args": {"order_id": "A-4417"}, "args_source": "model",
"status": "ok", "latency_ms": 212, "result_sha256": "77a1c2d0..."},
{"name": "issue_refund", "args": {"order_id": "A-4417", "amount": 41.5},
"args_source": "model", "status": "blocked_by_policy", "latency_ms": 3,
"policy": "refund_ceiling"}
],
"output": {
"raw_sha256": "0ac9e14b...",
"raw_ref": "blob://outputs/2026/08/26/run_8f21_call_3",
"parsed_ok": true,
"validation": {"ok": false, "failing_paths": ["$.next_action"]},
"guardrails_fired": ["pii_in_output"],
"finish_reason": "stop"
},
"usage": {
"input_tokens": 3411,
"cached_input_tokens": 2800,
"output_tokens": 240,
"price_snapshot": "prices/2026-08-01.json", // never hardcode a price here
"cost": null // filled by a job that reads the snapshot
},
"retry": {"attempt": 1, "of": 3, "previous_call_id": null, "reason": null},
"latency_ms": 1840,
"outcome": { // written back later, joined on trace_id
"state": "edited",
"by": "u_31aa",
"at": "2026-08-26T09:47:02Z",
"edit_distance": 34,
"reason_code": "tone"
}
}
Three details in that record are worth naming because they are the ones teams leave out and then wish they had. The returned model identity, because providers move aliases and a quality shift with no deploy in your repository is almost always this. The assembled token count, because it is the only cheap detector for context truncation silently dropping the front of your prompt. And args_source on every tool call, distinguishing the arguments the model generated from the arguments your code substituted, because the delta between those two is where a surprising number of agent bugs live.
Log the prompt by reference, not by value
Store prompt text once in a registry keyed by content hash, and put the hash on the call record. The instinct to paste the full assembled prompt into every log line is understandable and it is wrong on cost, on privacy and, counterintuitively, on reproducibility.
The one real weakness of hashing is that the hash is useless if the registry entry is gone. Prompt registry entries are tiny and there is no reason to expire them on the same schedule as logs, so keep them for the life of the system. The same applies to price snapshots and tool schema versions: keep the small reference tables forever, expire the large per call records aggressively. Prompts belonging in version control at all is the wider argument in prompts as code.
Retrieval provenance is the field everyone skips
In a retrieval system, the first question about any wrong answer is whether the right evidence was retrieved and ignored, or never retrieved at all. Those two failures have completely different fixes, and you cannot distinguish them after the fact unless the chunk ids were logged at the time. Adding this field later does not help, because it cannot be backfilled.
- Chunk ids with scores and final rank tell you whether the correct passage was in the candidate set. If it was retrieved at rank nine and your k is eight, the fix is retrieval configuration. If it was retrieved at rank one and the answer ignored it, the fix is in the prompt or the model.
- Document version per chunk tells you which revision of the policy the answer was based on. Without it, a customer disputing an answer becomes an argument, because the document has since been edited and the current text supports neither party.
- Index version and newest document timestamp turn a stale answer from a mystery into a one line query. This is also the field that powers the freshness alert described in monitoring an AI system in production.
- Reranker identity and the order before and after reranking catch the case where retrieval was correct and the reranker demoted the right passage. This is a real and frequent failure, and it is completely invisible if you log only the final order.
- k requested against k returned catches partial index failures, filter conditions that excluded everything, and permission filters that silently removed the only relevant document for that user.
Log ids and scores rather than chunk text. The text is reconstructible from the id plus the document version, it is often personal or confidential, and it is by a wide margin the largest thing you could put in the record. If you cannot reconstruct a chunk from its id and version, that is a gap in your document store, not a reason to duplicate the corpus into your log pipeline. That is a gap worth closing in the document store itself.
What must never be written down
Logging is the most common way personal data escapes an AI system, because logs get shipped to third party observability vendors, replicated to analytics warehouses, read by contractors during incidents, and retained far past the point anyone remembers what is in them. Decide the rules before the first record is written, and enforce them in code rather than in a policy document.
| Data class | Rule | Where it is enforced |
|---|---|---|
| API keys, bearer tokens, signed URLs | Never, at any log level, including inside a captured request or exception object | A deny list in the logging wrapper, applied before serialisation |
| Payment data and government identifiers | Never in raw form. Store a format preserving token or a hash with a per tenant salt | The redactor at the SDK boundary, before the record object exists |
| Names, emails, phone numbers, addresses | Replace with a stable pseudonymous reference. Keep the mapping in a separate store with its own access control | The redactor, plus a detector running on free text fields |
| Free text a person typed | Store only with a lawful basis and a stated retention period. Sample rather than keeping everything | The sampling policy, agreed with whoever owns privacy, written into the config |
| Whole source documents or chunk text | Store the id and version, not the bytes. A log is not a document store | The record schema itself, which has no field for it |
| Model output containing personal data | Same rule as user free text. A model repeating a name does not make the name safe to keep | The redactor, applied on the way out as well as on the way in |
A redactor that lives in the log pipeline is a redactor that has already failed, because the unredacted data existed in memory, in a buffer, and in any stack trace thrown between the call site and the sink. Put redaction in the client wrapper that builds the record, so the object never holds raw personal data at any point. The test is simple: kill the process between the model call and the log write, then check the crash dump. If the personal data is in there, the redaction is in the wrong place.
Retention should be tiered rather than uniform. Skeleton records with ids, metrics and verdicts are small, and keeping them for a year costs little and answers most questions. Full payloads, meaning raw outputs and variable values, are large and sensitive, and a much shorter window covers the debugging need. The legal and consent side of this is the subject of personal data in AI pipelines, and the decision record side, where you must be able to justify an automated decision later, is in making agent decisions auditable.
What this costs, and where to sample
Log volume in an AI system is dominated by payloads, not by metadata. A skeleton record is a few kilobytes. A record carrying the assembled prompt, the retrieved chunk text and the full completion can be two orders of magnitude larger, and it is the same record. That is why the sampling decision is not about whether to log but about which parts of the record to keep for everything and which parts to keep for a slice.
Rough arithmetic on your own numbers. The storage price field is the one to check against your provider today, because ingest pricing and retention pricing usually differ and the ingest side is normally the larger figure. Every default here is a round number for illustration, not a measurement of anything.
Compare the last two outputs and the sampling policy writes itself. The rule that works in practice is to keep the skeleton record for every single call with no sampling at all, and keep the full payload for a random slice plus every run in four categories: anything where validation failed, anything where a guard rail fired, anything a human overrode or escalated, and anything flagged by a customer. Those four categories are exactly the runs you will want to read, they are a small share of traffic, and a random slice on top of them stops the kept set from being a biased view of only the failures.
Sampling decisions must be made once at the start of a run and inherited by every call inside it, using the trace id as the sampling key. A per call coin flip gives you the second and fifth steps of an eight step agent run, which is worse than useless during an investigation because it looks complete and is not. Decide at the head, propagate the decision, and record the sampling rate on the run so you can weight aggregates correctly later.
Cardinality, or the log field that takes down the dashboard
Logs, traces and metrics have different rules and the difference is cardinality. A log line can carry any number of unique values. A metric label cannot, because most time series systems create one series per unique combination of labels, and a single unbounded label turns a cheap counter into millions of series and a very expensive Tuesday.
- Never put trace_id, run_id, user_ref, prompt hash, document id or any free text into a metric label. Those belong in logs and traces, which are built for them.
- Bounded label sets that are safe: model identity, template id, tool name, outcome class, guard rail name, tenant tier, environment, and validation pass or fail.
- tenant_id is the borderline case. It is fine with tens of tenants and dangerous with tens of thousands. Use tenant tier as the label and keep the exact tenant in the log record, then query logs when you need per tenant detail.
- Validation failure paths are safe as labels only if the schema is fixed and small. If the path can contain an array index, normalise it before it becomes a label or you have unbounded cardinality by another route.
- Error strings from providers are never safe as labels, because they frequently contain request ids. Map them to a small enumerated class at the boundary and label with the class.
The practical rule is to decide, for every field in the replay record, whether it is a log field, a trace attribute, a metric label, or all three, and write that decision into the schema definition rather than leaving it to whoever adds the next instrumentation line. This is a five minute exercise that prevents a category of incident which is tedious to diagnose and embarrassing to explain.
Design the schema backwards from the questions you will ask
The schema is correct when it answers the questions that actually get asked under pressure, and those questions are remarkably consistent across systems. Write them down first, then check that each one is a query rather than an investigation. Any question that cannot be answered by a query is a missing field, and it is far cheaper to add now than after the incident that needed it.
- Show me the full trace for this complaint
The customer has a reference number and you need every step behind it. This fails when the trace id lives only in your backend and never reaches the ticket, the email, the document or the interface, which means the support agent has to guess at timestamps. Put the trace id somewhere visible, even if it is an HTML comment or a small grey string in a footer.
- Show me every run that used prompt version 7
You changed a prompt and want the blast radius, or you found a defect and want to know who received it. Without the version and hash on the call record this becomes a reconstruction from deploy timestamps, which is approximate at exactly the moment approximate is not acceptable.
- Show me every failure grouped by failing schema path
Validation errors stored as prose cannot be grouped, so a single dominant failure mode hides inside forty slightly different strings. Store the failing path as an array of strings and the grouping is a query. This one field changes debugging a structured output system more than any other.
- Show me every run that retrieved document X
A document turns out to be wrong and you need every answer that relied on it, for correction or for notification. This is a legal requirement in some settings and it is impossible without chunk ids at call time. Index the array, because a full scan across months of logs is the difference between a query and a project.
- Show me cost by tenant last week, split by template
Cost attribution is asked for by finance and by whoever is deciding what to optimise. Storing the price snapshot reference rather than a computed figure means a price change does not rewrite history, and it also means you never hardcode a number that goes stale.
- Show me every result a human edited, with the diff
This is the query that builds your evaluation set, sizes the real quality problem, and tells you which parts of the output people never trust. It is the smallest amount of instrumentation on this list and it is the one most often missing, because it lives in the product interface rather than in the AI code.
If a question on that list would take you more than a minute to answer today, the fix is a field, not a dashboard. Building this properly across an agent that fans out across many tools is the substance of agent observability.
Definitions, and the logging checklist
- Correlation id
- A single identifier generated at the triggering event and carried through every model call, tool call, retry and downstream write for that unit of work, including outcome records written days later. It is what makes logs from separate systems joinable into one story.
- Replayable run
- A logged run containing enough detail to reconstruct what the system saw and produced without access to the original process: prompt by hash, model identity, sampling parameters, retrieval set, tool calls with arguments, raw output and verdict.
- Retrieval provenance
- The record of which chunks were retrieved for a run, with their document ids, document versions, similarity scores and final rank after reranking. It is what separates a retrieval failure from a generation failure, and it cannot be backfilled.
- Redaction at the edge
- Removing or tokenising sensitive values in the client wrapper that constructs the log record, so raw personal data never exists in the record object, in a buffer, or in a stack trace. Redaction applied later in the pipeline has already failed.
- Cardinality
- The number of distinct values a field can take. High cardinality fields such as identifiers and free text belong in logs and traces. Metric labels need bounded, low cardinality values, because most time series databases create one series per unique label combination.
- Skeleton record
- The small always-logged part of a call record holding ids, versions, counts, timings and verdicts but no payload text. It is cheap enough to keep for every call with no sampling, and it answers most aggregate questions on its own.
One closing warning about sequencing. Every field in the replay record is cheap to add on day one and expensive to add on day two hundred, because the value of a log is entirely historical and you cannot backfill history. The teams that regret their logging are never the ones who logged a slightly larger record than they needed. They are the ones who discovered in an incident that the one field which would have answered the question was the field nobody thought to write.
ChatGPTalker, "What to Log So Future You Can Debug It" (2026). A log is complete when a run can be replayed from it: identity, inputs by hash, retrieval provenance, tool decisions, output and verdict, and the human outcome, all joined by one correlation id, with redaction applied in the client wrapper rather than in the log pipeline.
Questions readers ask next
Should we log the full prompt and the full model output?
How long should AI logs be kept?
Is it safe to send AI logs to a third party observability vendor?
What is the one field most teams forget?
Do we need distributed tracing, or are structured logs enough?
How do we log without storing personal data at all?
ChatGPTalker. "What to Log in an AI System So You Can Debug It Later." chatgptalker.com, 2026-08-26. https://chatgptalker.com/guides/what-to-log-in-ai-systems/