ObservabilityGENERALDOMAIN-SPECIFICCONTESTED

Prediction Logging

The prediction log is the monitor's input, the incident's evidence, the outcome join's left side and the next training set. Log what is necessary, reference what is sensitive, and decide retention before the first row.

The problem, the obvious approach, and why it breaks

Every lesson starts where the work starts: someone has a problem, and the first model that comes to mind looks fine offline.

The question

What does each prediction record need to contain for monitoring, debugging and retraining, and what must it not contain?

The problem

We log every prediction as a JSON blob with the full request — the user's profile, their recent transactions, the score. It is the most useful table we have and the privacy team has just asked why a fraud score log holds two years of everyone's transaction history.

The obvious approach

Log everything. Storage is cheap, the full request is the most complete record, and you cannot know in advance which field the next incident will need. Keep it forever, because old predictions are training data.

Why it breaks

The log is now a copy of the customer database plus every transaction, in a system with weaker access control than either, and a fraud score is not a lawful reason to hold a two-year address history.

How it breaks — usually after the offline metric looked fine
  • The log is now a copy of the customer database plus every transaction, in a system with weaker access control than either, and a fraud score is not a lawful reason to hold a two-year address history.
  • When the privacy team requires deletion, the training sets built from the log contain the deleted data and the models trained on them cannot un-learn it. "Log everything" has turned a retention question into a model-lineage question (ML Privacy).
  • The blob has no schema. The incident query — "show me the null rate of merchant_category by hour" — is a JSON path over a terabyte, and the feature was renamed in the payload eight months ago, so half the rows do not have it.
  • Retraining on the log is straightforward and the log contains predictions made by the model on transactions the model itself declined. Those never got an outcome; the training set built from the log is the approve-set only (Dataset Construction).
ProblemTargetDataRepresentationSplitModelTrainingEvaluationValidationDeploymentInferenceMonitoringDriftRetraining

What is being predicted, and from what data

This domain leads with these two. A target nobody defined precisely is a label nobody can trust, and a dataset nobody can describe is a model nobody can debug.

Target
  • Predict whether a transaction will be charged back; the decision is approve, review or decline. The log's target is to make every prediction explainable, monitorable and joinable to its outcome, and to become the training set for the next model.
  • The label arrives up to 90 days later, so the log has to survive at least that long to be joined, and the join is what makes the log a training set at all.
Data
  • One row per prediction: request id, timestamp, model version, feature values or references, score, decision, threshold policy, fallback rung, and — added later by the join — the outcome and when it was observed.
  • The current log stores raw feature values, including name, address fragments, merchant descriptors and the last thirty transactions, because that was the request payload and logging the payload was easy.
  • The outcome pipeline writes chargebacks keyed on transaction id, which the prediction log also has, so the join is exact — which is why the log is the training set.

How it actually works

Precisely enough to predict its behaviour — not a framework API.

  • A prediction record serves four consumers with different needs. Monitoring needs distributions per hour — features, scores, decisions — and only aggregates. Debugging needs one full record on demand. The outcome join needs a key and a timestamp. Retraining needs the feature vector as served, the label, and enough metadata to reproduce the split (Reproducibility).
  • Raw values and references serve those consumers differently. A feature reference — a snapshot id into a versioned feature log, or a hash of the vector — lets debugging and retraining recover the vector through a system with its own access control and retention, while the prediction log itself holds no personal data. Monitoring can be served by pre-aggregated distributions written at the boundary.
  • Retention is set by the slowest consumer. The outcome join needs the log for the label delay; retraining needs the joined rows for as long as they are in a training window; debugging needs them for the incident window. Each of those is finite and stateable, and "forever" is not a policy (Data Retention in the data domain has the mechanics).

Four consumers, one record

The log is read by four things with different appetites. The monitor wants hourly distributions and never a single row. The on-call engineer wants one full row, rarely. The outcome join wants a key and a timestamp, for every row, for three months. The training-set builder wants the served vector and the label, for every joined row, for a year. The record has to serve all four, and the tension is that the two heaviest consumers — the join and the builder — need the most rows for the longest.

The schema below is the minimum that serves all four. Notice what is absent: the request payload, the user's profile, any raw personal value. The feature snapshot reference is where the vector lives, behind its own access control.

One prediction record
1{
2 "request_id": "req_01J8...",
3 "ts": "2026-08-27T10:41:07Z",
4 "model_version": "fraud-v7",
5 "feature_def_version": "fd-v4",
6 "preprocessing_version": "pp-v4",
7 "feature_snapshot_ref": "fs://fraud/2026-08-27/a91c...",
8 "feature_vector_hash": "sha256:5e1f...",
9 "score": 0.42,
10 "threshold_policy": "queue_size_1200",
11 "decision": "review",
12 "fallback_rung": null,
13 "outcome": null,
14 "outcome_observed_at": null
15}

The feature_vector_hash lets a skew check compare what the model saw against a training-side recomputation without either side holding raw values; fallback_rung is null here because the model served this one, and a non-null value is the only way to know a healthy-looking score came from the cache.

The log is the next training set

Once outcomes are joined, the log is the best training set available: serving-time vectors, real traffic mix, real labels. It is also a biased one. Every transaction the model declined has no chargeback outcome, because it was never processed; every transaction sent to review has an outcome shaped by the reviewer. The training set built from the log is drawn from the approve-set, and a model trained on it learns the approve-set's patterns.

This is dataset construction happening by accident, inside the logging system. The fix is not in the log — the log records what happened — but in knowing what is missing: record the decision, so the builder can see which rows have no outcome and why, and treat the declined set as a hole to be filled by exploration or by a delayed manual label rather than as absent data.

leakageoutcomeThe outcome column, joined too early

looks like A chargeback column on the prediction log, populated by a nightly job that joins the outcome table, used by the training-set builder as the label — and, in one version of the pipeline, by a feature that counts "prior chargebacks for this card".

why it leaks The builder cuts a training window by prediction timestamp and reads features as of now, so the prior-chargeback count includes chargebacks that were observed after the prediction was made — sometimes the chargeback on the very transaction being scored.

offline
Validation quality jumps; the prior-chargeback feature becomes the most important one.
production
At serving time the count only includes chargebacks already observed, which for a fresh card is zero; the model has learned to trust a signal that does not exist yet (Temporal Leakage).

fix Store outcome_observed_at alongside outcome, and build every aggregate feature as of the prediction timestamp, not as of the build time — the point-in-time cut that the log's timestamps make possible.

when this feature is fine A prior-chargeback count computed only over chargebacks whose observed_at precedes the prediction timestamp is exactly what serving sees, and it is a strong, legitimate feature.

Necessary, safe, and gone on schedule

The privacy team's question has a precise answer once the consumers are listed: no consumer of the prediction log needs the customer's address, and the transaction history is needed only as the vector the model saw, which a reference can supply. What remains in the log is identifiers and numbers, and what is behind the reference is governed by the feature store's own retention and access policy.

Retention then falls out of the consumers: the log lives for the label delay plus the training window; the feature snapshot lives at least as long; the joined training rows live as long as the models trained on them are in production and traceable. Each is a number, each is defensible, and each is shorter than forever.

must stay trueMinimised, referenced, retained on purpose

The prediction log holds only fields with a named consumer, personal data only as references into access-controlled systems, and every table in the chain has a retention that outlasts its slowest consumer and no longer.

holds when The schema is versioned and audited against its consumer list; the feature snapshot store's retention is at least the log's; the training-set builder records which rows it used; deletion requests can be mapped to affected models.

breaks when A new field is added "for debugging" and holds a raw value; the feature store shortens its retention without telling the log's owners; a training window is extended past the log's retention and the builder silently trains on the rows that survive.

how you would know A schema audit that names a consumer per field; the reference-resolution rate by age; a check that the builder's window lies inside every table's retention; the join rate curve by age.

respond Remove the field or replace it with a reference; align retention across the chain; if a training set was cut across a retention boundary, rebuild it and record the affected model versions.

Building the training set point-in-time from the log
1-- rows whose outcome has had time to arrive; features as of the prediction, not as of now
2select p.request_id, p.feature_snapshot_ref, p.model_version, p.decision,
3 o.chargeback as label
4from predictions p
5join outcomes o using (request_id)
6where p.ts between :window_start and :window_end
7 and p.ts < now() - interval '90 days' -- the label delay: younger rows are not "negative", they are unknown
8 and o.observed_at <= :window_end + interval '90 days'
9 and p.decision <> 'decline'; -- declined rows have no outcome; say so, do not treat them as negatives

The two filters at the end are the lesson. The first keeps unlabelled recent rows from being read as negatives; the second names the hole the model's own decisions cut in the training set, rather than letting the join silently fill it with absence.

How to build it

Most important first.

  • Define the record as a schema, versioned: request id, timestamp, model version, feature-definition version, feature snapshot reference, score, decision, threshold policy, fallback rung. Raw personal values are not fields; identifiers that resolve through access-controlled systems are.
  • Log feature values as references where possible — an id into the feature store's versioned snapshot, or a hash for equality checks — and raw values only for features that are not personal and are needed at monitoring granularity. Minimise: a field nobody has a consumer for is a liability with no benefit.
  • Write the outcome join as a separate table keyed on request id, with the observation time, so that the label delay is visible and a training set can be cut point-in-time (Point-in-Time Correctness, Ground-Truth Delay).
  • Set retention per table from its consumers, and make the training-set builder record which log rows it used, so a deletion request can name the models affected (Model Lineage).

What to measure

Which number actually maps to the decision — and which numbers look relevant and are not.

  • Join rate between the prediction log and the outcome table, per day, as a function of age. This is the number that says whether the log is becoming a training set, and its shape is the label-delay curve.
  • The fraction of rows whose feature reference resolves. A reference into a snapshot that was expired is a row that cannot be debugged or trained on.
  • Log volume and cost are worth watching and are not a quality signal; a smaller log with a schema outperforms a larger one without.

What must stay true after deployment

The field this whole domain exists for. A model is a set of assumptions with weights attached; these are the ones a monitor or a test should be checking.

Assumptions
  • Every prediction, including those served by a fallback rung, writes a record keyed on the request id; the record contains the vector as the model saw it, or a reference that resolves for as long as any consumer needs it.
  • Retention on the prediction log, the feature snapshot and the outcome table each outlast the label delay plus the training window, and no consumer is silently reading past a retention boundary.
  • Personal data in the log is limited to identifiers that resolve through access-controlled systems, and the training-set builder records which rows it consumed so that deletion can be honoured.
How to verify — offline, online, and over time
  • Offline: rebuild last month's training set from the log and confirm the feature vectors match the ones the model actually saw — a hash comparison against the feature snapshot.
  • Online: a daily check that every decision event has a prediction record, that references resolve, and that the join rate by age follows the expected label-delay curve.
  • Over time: an audit that lists the fields in the log, names a consumer for each, and confirms no raw personal value is present that a reference could replace — repeated when the schema changes.

What can go wrong

Failure modes in production
  • The feature snapshot is retained for 30 days and the label arrives at 90; the outcome joins but the features are gone, and the training set has labels for vectors it cannot recover.
  • The log records the request payload before preprocessing rather than the vector the model saw, so a serving-side preprocessing change is invisible in the log and the training set built from it inherits skew (Train / Serve Skew).
  • Sampling is introduced to cut cost — one in ten predictions — and the sample is uniform, so rare decisions and rare slices vanish from both monitoring and the training set.
What the recommended approach costs
  • References instead of raw values make debugging a two-step lookup and make the log useless if the feature store's retention is shorter than the log's.
  • A schema with versioning is work every time a feature is added, and the work is what makes the log queryable eight months later.
  • Retention chosen from consumers means old predictions are deleted, and someone will one day want a training window longer than the retention allowed.
Misreads
  • "Storage is cheap, so log everything." Storage is cheap; holding personal data without a purpose is not, and a training set built from an over-retained log inherits every deletion obligation.
  • "The prediction log is just for debugging." It is the outcome join's left side and the next training set. What is in it, and what is missing from it — the declined transactions with no outcome — shapes the next model.
  • "We log the request, so we log the features." The request is what arrived; the features are what the model saw after fetching and preprocessing. Log the vector, or a reference to it, or the log cannot detect skew.

Where this applies

ML advice is stated as universal far more often than it is. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.

  • GENERALThat the prediction log serves monitoring, debugging, the outcome join and retraining, and that those consumers set its schema and retention, holds for every deployed model; the privacy constraints depend on what the features are.
  • DOMAIN-SPECIFICA fraud or credit model's features are personal and regulated, so references and minimisation are mandatory; a model over telemetry or product catalogues has few personal features and can log raw values with far less ceremony — the retention question remains.
  • CONTESTEDA defensible position holds that logging references rather than values trades a solved storage problem for an unsolved consistency problem: the feature snapshot store now has to exist, be versioned, and outlive the log, and when it does not, the log is a table of dangling pointers that helps nobody. Teams who hold this log the served vector in full, restrict access to the log instead, and accept the retention obligation. That is reasonable when the vector holds little personal data; it is hard to defend when the vector is a customer profile.

Where the depth lives

This domain teaches the model and hands the rest off by name.