Evaluation and reliability

What to log in an AI system so you can debug it later

A log is complete when you can replay a run without the original process. Here is the record to write for every model call, the fields everyone forgets, and what must never be written down.

On this page
  1. What to log in an AI system, in one paragraph
  2. The Replay Record
  3. The record, as JSON you can copy
  4. Log the prompt by reference, not by value
  5. Retrieval provenance is the field everyone skips
  6. What must never be written down
  7. What this costs, and where to sample
  8. Cardinality, or the log field that takes down the dashboard
  9. Design the schema backwards from the questions you will ask
  10. Definitions, and the logging checklist

What to log in an AI system, in one paragraph

The short answer

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

Framework

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.

01
Identity: what run is this

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.

02
Inputs by reference: what did it read

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.

03
Retrieval provenance: where did the evidence come from

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.

04
Decisions: what did it do

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.

05
Output and verdict: what came back

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.

06
Outcome: what happened next

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

llm_call recordjsonc
{
  "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.

Full prompt text on every callTemplate id, version and content hash
StorageGrows with prompt length times call volume, and system prompts only get longerA fixed small field per call, plus one copy of each distinct prompt
QueryingFull text search over megabytes to find one versionAn exact match on a hash, or a join to the registry
ReproducibilityYou have the text but not the identity, so you cannot count how many runs used itGroup by hash and every affected run is one query away
RedactionPersonal data appears inline in every record and has to be scrubbed everywhereVariables are redacted once, and the template itself carries no personal data
Failure modeRetention rules delete the log and the prompt text with itThe registry entry is deleted and the hash resolves to nothing
The registry needs its own retention rule

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 classRuleWhere it is enforced
API keys, bearer tokens, signed URLsNever, at any log level, including inside a captured request or exception objectA deny list in the logging wrapper, applied before serialisation
Payment data and government identifiersNever in raw form. Store a format preserving token or a hash with a per tenant saltThe redactor at the SDK boundary, before the record object exists
Names, emails, phone numbers, addressesReplace with a stable pseudonymous reference. Keep the mapping in a separate store with its own access controlThe redactor, plus a detector running on free text fields
Free text a person typedStore only with a lawful basis and a stated retention period. Sample rather than keeping everythingThe sampling policy, agreed with whoever owns privacy, written into the config
Whole source documents or chunk textStore the id and version, not the bytes. A log is not a document storeThe record schema itself, which has no field for it
Model output containing personal dataSame rule as user free text. A model repeating a name does not make the name safe to keepThe redactor, applied on the way out as well as on the way in
Six classes, six rules, and the exact place each is enforced.
Redact at the edge, never at the sink

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.

Log volume and storage cost, per month

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.

0GB written per month
0GB held in hot retention
0Storage cost per month at your price
0GB per month if you kept every payload

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.

Sample the run, not the call

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.

  1. Show me the full trace for this complaintneeds: trace id surfaced on the customer facing artifact

    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.

  2. Show me every run that used prompt version 7needs: template id and prompt hash on every call

    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.

  3. Show me every failure grouped by failing schema pathneeds: structured validation results, not message strings

    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.

  4. Show me every run that retrieved document Xneeds: chunk ids in an indexed array field

    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.

  5. Show me cost by tenant last week, split by templateneeds: tokens, price snapshot, tenant id and template id together

    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.

  6. Show me every result a human edited, with the diffneeds: the outcome write back

    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

Definitions
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.
AI logging readiness
0 of 14 done

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.

Cite this

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?
Log the output by reference with its hash and keep the bytes for a sampled slice plus every failure, override and flagged run. Log the prompt by template id, version and content hash rather than by value, with the variable values redacted. This keeps the query surface small, keeps personal data out of the pipeline, and still lets you resolve any historical run to the exact text that produced it.
How long should AI logs be kept?
Tier it. Skeleton records carrying ids, versions, token counts, timings and verdicts are small and worth keeping for a year or more because they answer aggregate questions cheaply. Payloads containing prompts, outputs and variable values are large and sensitive, and a few weeks usually covers the debugging need. Reference tables such as the prompt registry and price snapshots are tiny and should be kept for the life of the system.
Is it safe to send AI logs to a third party observability vendor?
Only if redaction happens in your client wrapper before the record is built, not in a rule configured inside the vendor. Anything the vendor filters has already crossed the boundary. Check what the contract says about retention, subprocessors and region, confirm that support staff access is logged, and treat raw model outputs as personal data by default because users routinely paste personal information into free text.
What is the one field most teams forget?
The outcome write back, meaning what a human actually did with the result. Accepted, edited with a diff, overridden, escalated or reversed, joined to the run by the correlation id. It is missing more often than any other field because it lives in the product interface rather than in the AI code, and it is the field that turns a pile of logs into an evaluation dataset and a quality metric that needs no ground truth.
Do we need distributed tracing, or are structured logs enough?
Structured logs with a consistent correlation id will carry you a long way and are simpler to run. Tracing earns its cost once a run fans out across several services or many parallel tool calls, because reconstructing timing and parent relationships from log timestamps then becomes guesswork. If you already run tracing, emit spans per model call and per tool call and put the same ids on your log records so the two views join.
How do we log without storing personal data at all?
Replace identifiers with stable pseudonymous references generated by a keyed hash, keep the mapping in a separate store with its own access control and its own retention rule, and run a detector over free text fields to catch personal data that arrives inside a user message. Store document ids rather than document text. Accept that some investigations will then need a deliberate, logged lookup through the mapping store, which is the correct trade.
Cite this

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/

Rather have it built than read about it?

Send the process you want automated. You get a scoped plan back, with the build shape, the stack and a realistic timeline.

Start a project