跳到主要内容
版本:0.5(最新)

Recording Prompts, Responses, and Tool Payloads

By default cubepi's tracing emits structural attributes only — operation names, models, token counts, finish reasons, durations. No prompt content, no model output, no tool arguments or results leave the process. This is deliberate: many agent setups handle PII, customer data, or trade-secret prompts that don't belong in a third-party observability backend.

When you do want content captured — for offline evaluation, debugging a flaky tool call, or building a labelled dataset — opt in explicitly with record_content=True, and combine it with a redact callback to strip the sensitive parts before they leave the process.

Turning content recording on

tracer = Tracer(
service_name="my-bot",
agent_name="assistant",
record_content=True, # ← opt-in
exporters=[JsonlSpanExporter(directory="./cubepi-traces")],
)

With record_content=True, each span layer carries the relevant content attributes per the OTel GenAI semconv:

SpanContent attributes added
invoke_agentgen_ai.system_instructions, gen_ai.input.messages, gen_ai.output.messages
cubepi.turngen_ai.input.messages (per-turn slice), gen_ai.output.messages (per-turn slice)
chat <model>gen_ai.system_instructions, gen_ai.input.messages, gen_ai.tool.definitions, cubepi.llm.raw_request, cubepi.llm.raw_response
execute_tool <tool_name>gen_ai.tool.call.arguments, gen_ai.tool.call.result

The chat span's gen_ai.input.messages contains the full chronological context the provider request actually carried — including prior assistant turns and tool results — not just the new user prompt. This matters for multi-turn tool-using runs: trace consumers can reconstruct exactly what the model saw at each call.

Redacting before export

redact is a (key, value) -> value | None callback invoked at every content-attribute set site. Return:

  • The original value unchanged → keep as-is
  • A modified value of the same shape → substitute
  • None → drop the attribute entirely
def redact(key: str, value):
# Strip secrets from prompts before they leave the process.
if key in ("gen_ai.input.messages", "gen_ai.output.messages"):
return _scrub_messages(value)
# Don't ship raw bodies at all in prod — keep only the normalised shape.
if key in ("cubepi.llm.raw_request", "cubepi.llm.raw_response"):
return None
return value


tracer = Tracer(
service_name="my-bot",
record_content=True,
redact=redact,
exporters=[],
)

redact is the single chokepoint for content — the recorder calls it once per attribute before serializing into the OTel attribute, so anything the function returns is what hits the wire. Exceptions inside redact are swallowed (the attribute is dropped in that case), so a buggy redactor fails closed rather than leaking.

Common patterns

Drop everything but per-message length so dashboards still work without shipping content:

def redact(key, value):
if key in ("gen_ai.input.messages", "gen_ai.output.messages"):
return [{"role": m["role"], "parts": [{"type": "text", "content": "<redacted>",
"length": sum(len(p.get("content", "")) for p in m["parts"])}]}
for m in value]
return value

Tag-based selective recording — strip everything unless a thread is opted in:

import contextvars
RECORD = contextvars.ContextVar("trace.record_content", default=False)

def redact(key, value):
return value if RECORD.get() else None

then RECORD.set(True) for the runs you want captured.

Size budgets

OTel attribute values are JSON-serialized inside the recorder. Most backends truncate or reject attributes over a few hundred KB. If you're recording the raw provider response on every chat span, multi-turn agentic runs can get large fast. Drop or summarise via redact for any field over your budget.

Auditing what's recorded

The recorder always sets service.name, gen_ai.agent.name, and cubepi.run_id on every span — regardless of record_content. Use these to filter the trace backend to a single run and visually confirm what landed.

For deeper audits, JsonlSpanExporter writes one line per span, so you can grep / jq the local files before pointing the same exporter at a remote backend:

jq -r 'select(.attributes["gen_ai.input.messages"]) | .attributes["gen_ai.input.messages"]' \
cubepi-traces/2026-05-19/*.jsonl