Redacting PII from agent traces.
Turning on content capture in your tracing SDK creates a second, ungoverned copy of your most sensitive data inside a system your observability vendor operates — and unlike the database it came from, nobody wrote a retention rule for it. The reflex fix, redact everything globally, quietly destroys the only artefact that can explain a hard failure. Redaction and debuggability are one dial, and the way out is not a better global setting but two tiers: redact in-process before the span leaves, and buy reproducibility back with a short-lived break-glass tier sized against your actual detection time.
Content capture is off by default, and turning it on is a data-processing decision.
The OpenTelemetry GenAI semantic conventions make every attribute that can hold prompt or completion content opt-in, precisely so that content never appears because someone forgot a flag. That default is the last cheap moment in this whole process.
- Metadata alone is safe and nearly useless for agents. Model name, token counts, latency and status tell you a run was slow. They cannot tell you why the agent called the refund tool with the wrong account, which is the class of question agent tracing exists to answer.
- Agent traces hold categories your database never did. The user's raw phrasing, the retrieved documents, tool arguments, tool results and the model's reasoning content. The last one is the sharpest: it may restate sensitive input in paraphrase, which no field-based rule will catch.
- The blast radius is a vendor, not a table. Once captured, that content sits in a SaaS backend with its own access model, its own support engineers and its own sub-processors — a scope your DPA for the observability vendor was probably not written against.
- Flip the burden of proof. Enabling content capture is not a debugging convenience; it is a decision to process personal data in a new system. Treat it as one — name the lawful basis, the retention period and the access list before the flag goes to
true, which is the discipline data governance already asks for elsewhere. - Decide it per environment, not per organisation. Full capture in staging against synthetic data costs nothing and answers most questions. Production is where the dial has to be earned.
Redact in the SDK, because the collector is already too late.
The common architecture puts redaction in the collector or as a processing rule in the backend, and it is wrong for a reason that is easy to say and hard to walk back: by then the data has left your process and, in most deployments, your network.
- Egress has already happened at the collector. If the span travelled over the wire with the raw prompt inside it, you have exported personal data and then deleted it — which is a different event, with different obligations, from never having exported it.
- Backend-side masking is display-layer redaction. The raw value is in the store, in backups, in the search index and in whatever the vendor's own logs captured on ingest. A masked view does not make it absent.
- In-process means the span attribute is built already redacted. One span processor in the SDK, running before the exporter, so the unredacted string exists only as a local variable for the lifetime of a function call.
- Fail closed on the redactor. If the redactor throws, drop the content attribute and keep the span — never ship the raw value because the sanitiser errored. This is the single most common way a careful design leaks in production.
- Cover every sink, not just the tracer. The same minute also lands in your eval dataset, your prompt-logging table, your feedback store and your LLM-judge's inputs. One redactor, one version, in front of all of them — the failure mode exfiltration risk keeps finding is the sink nobody inventoried.
Say the rule in one line so it survives a design review: the unredacted string must never cross a process boundary. Every architecture that puts redaction downstream is arguing about how quickly to delete something it has already sent.
Tokenise deterministically — a mask throws away the analysis you traced for.
Replacing everything with [REDACTED] is the cheapest implementation and it destroys most of the value of having a trace at all. The choice of replacement is where redaction is either a tax or a design.
- Deterministic tokens preserve joins. A keyed hash means the same email becomes the same
usr_7f2ain every span, so "did this user hit the failure twice", "how many distinct users are affected" and "is this one customer or a systemic bug" all remain answerable without anyone learning who they are. - Keep the type in the token.
<EMAIL_7f2a>rather than<REDACTED>preserves the structure a debugger needs: you can still see that the tool was called with an email where an account ID belonged. - Tokens make erasure tractable. A deletion request maps to a token you can search for across traces, evals and feedback stores. With an irreversible mask you cannot even confirm you complied — which is why erasure against agent memory keeps ending up as a manual archaeology exercise.
- Store the mapping only if you need re-identification, and store it elsewhere. A separate keyed store with its own access control and its own shorter retention. If nobody needs to reverse it, do not keep it — the token still does the join.
- Rotate the key deliberately, and know what it costs. Rotation is a privacy improvement and it breaks longitudinal joins across the boundary. Rotate on a schedule that matches how far back your analyses actually look.
Recall is a number you must measure, because free text has no schema.
Structured PII is the easy half. The hard half is the same fact written by a person in a sentence, and a detector that has never been measured is a compliance claim with no evidence behind it.
- Build a labelled corpus from real spans. A few hundred production spans, hand-annotated per entity type, held in a controlled location. Without it you have opinions about your redactor, not a recall figure.
- Report recall per entity type, never in aggregate. "97% overall" typically hides 99% on emails and 60% on free-text addresses and account numbers. The aggregate is dominated by the easy classes and tells you nothing about the ones that matter.
- Run it as a regression test in CI. The redactor is code with a version. A change that drops recall on any entity type fails the build, exactly as a broken unit test would.
- New tools are new leaks. The day someone adds a tool returning a payload shape the redactor has not seen, recall on that path is untested. Make "run the redaction eval against a sample of its results" part of the tool-onboarding checklist alongside the tool catalogue steps.
- Watch precision too, in a specific way. Over-redaction that eats identifiers, error codes and stack frames turns a trace into a page of tokens. Track how often a redacted span was insufficient for triage — your engineers already know; ask them.
The break-glass tier, and the number that makes it real.
Redaction removes exactly the detail that explains the hardest failures. Recovering it needs a second tier, and that tier is either sized against a measured number or it is theatre.
- Size the TTL off your detection time, not off comfort. If unredacted spans live 24 hours and your median time to notice a quality regression is nine days, the tier will be empty every single time you need it. Measure MTTD from your own incident history and set the TTL above it.
- Access is a request with a reason, an approver and an expiry. Named individuals, a linked incident, a session that expires, and an entry in the access log the subject could in principle be shown. Standing access to unredacted traces is the same authorisation smell as standing production database access.
- Scope the grant to a trace, not to a tier. "This investigator, these 40 spans, for four hours" beats "the SRE group can read raw traces". The narrower grant is barely harder to implement and vastly easier to defend.
- Log the access as loudly as the incident. A break-glass read is an event worth alerting on — it is one of the few signals that distinguishes an investigation from the sanctioned-and-inbound misuse pattern, where everything is individually permitted.
- Keep the tier out of the vendor by default. The redacted stream goes to your observability SaaS; the short-lived raw stream stays in your own account. That split is what makes the whole design explainable to a regulator, an auditor or a works council in one sentence.
Redaction is not retention, and neither one covers the other.
The two controls are routinely substituted for each other, and each substitution leaves a real gap that shows up at the worst moment.
- Redacted data is still personal data, often. A trace of one user's session, tokenised but complete, is frequently re-identifiable from its content. Redaction lowers the risk class; it rarely takes you out of scope, so the retention policy still applies — as sampling and retention sets out.
- Sampling is not a privacy control either. Keeping 5% of traces means the 5% you kept are whole. It reduces volume, not sensitivity.
- Legal hold beats your TTL and you should know where. When a hold lands, automated expiry must stop for the covered scope without stopping everywhere — the coordination retention and legal hold describes, now applied to a store your vendor operates.
- Eval datasets outlive traces and inherit nothing. A failure promoted into a regression suite in March is still there in December, long after the trace expired, carrying whatever was in it. Redact on promotion, and re-run the redactor when its version changes.
- Rehearse the deletion, do not assert it. Once a quarter, take one subject and time how long it actually takes to remove them from traces, evals, feedback and the judge's inputs. The gap between the policy and the stopwatch is the finding.
Concretely, this quarter: put one versioned redactor in the SDK in front of every sink, emitting deterministic typed tokens rather than masks; build a few-hundred-span labelled corpus and run per-entity recall as a CI gate; then add a break-glass raw tier in your own cloud account with a TTL set above your measured MTTD and per-trace, time-boxed, alerted access. You are not choosing between privacy and debuggability — you are choosing whether that trade is made once, globally, by whoever set a flag, or deliberately, per investigation, by someone who has to sign for it.
Related: tracing and observability for what the spans should contain, the OTel GenAI conventions for the attribute names this gates, failure taxonomies and triage for the work redaction must not block, and agent observability for the concept underneath.