Add attributes and context
Add queryable metadata, session and customer context, LLM usage, and handled errors to traced work.
Add context after the trace structure is healthy. Keep input, output, model usage, session identity, customer identity, and error state on the span that owns them so Judgment can display, query, and evaluate the run correctly.
Add queryable attributes
Tracer.observe captures function arguments and return values automatically.
Setting them again inside an observed function overwrites the captured values
unless automatic capture is disabled. Lower-level span APIs do not capture I/O,
so set input and output explicitly when you use them.
Use custom attributes for metadata that is not already part of the function’s input or output:
@Tracer.observe(span_type="agent", span_name="handle")
def handle(user_id: str, question: str) -> str:
Tracer.set_attribute("app.channel", "web")
Tracer.set_attributes({"app.plan": "pro"})
Tracer.set_customer_user_id(user_id)
return run(question)const handle = Tracer.observe(async function handle(
userId: string,
question: string,
): Promise<string> {
Tracer.setAttribute("app.channel", "web");
Tracer.setAttributes({ "app.plan": "pro" });
Tracer.setCustomerUserId(userId);
return run(question);
}, { spanType: "agent", spanName: "handle" });Use your own namespace for custom keys. The judgment.* namespace is reserved
for the public keys in Attribute keys.
In Python, use
Tracer.scoped_context()
when session, customer, or custom attributes must apply before a span starts.
For context that must cross process boundaries, use the
Python baggage API or the TypeScript
baggage APIs in the SDK reference.
Name the work consistently
Use spans for the model calls, tool calls, retrievals, and decisions that the selected integration does not capture. Name every span for the work it performs and keep its kind consistent:
| Span kind | Use it for |
|---|---|
agent |
The app root or a step where an agent decides or delegates. |
llm |
One model call and its model, token, and cost metadata. |
tool |
A tool, function, or retrieval call. |
Record handled and returned failures
An uncaught exception marks an observed span as an error. A returned failure does not, so mark it when the application recognizes a failure signal:
- A non-zero exit code or HTTP status of 400 or higher
- An error payload
- An exhausted retry or iteration limit
- A cancellation or timeout
- A non-empty denial list
Decide failure from the application’s status field, result type, or explicit success rule. A child can fail while the root succeeds when the agent still reaches a valid result. If the final result fails, mark the root as an error even when the function returns normally.
from opentelemetry.trace import Status, StatusCode
@Tracer.observe(span_type="tool", span_name="fetch_record")
def fetch_record(record_id: str) -> dict:
result = client.fetch(record_id)
if result.get("error"):
Tracer.get_current_span().set_status(
Status(StatusCode.ERROR, result["error"])
)
return resultconst fetchRecord = Tracer.observe(async function fetchRecord(
recordId: string,
) {
const result = await client.fetch(recordId);
if (result.error) {
Tracer.setError(new Error(result.error));
}
return result;
}, { spanType: "tool", spanName: "fetch_record" });Record each LLM call once
Prefer a supported framework or model-provider integration. When none exists,
create one llm span and pass the available fields to recordLLMMetadata():
modelprovidernon_cached_input_tokensoutput_tokenscache_read_input_tokenscache_creation_input_tokenstotal_cost_usd
Tracer.recordLLMMetadata({
model: "gpt-5.6-luna",
provider: "openai",
output_tokens: 150,
});
Judgment can calculate cost from the provider, model, and token counts. Set
total_cost_usd only when the provider or subprocess reports a real cost; an
invented zero makes paid work look free.
Keep model metadata on the model span. Do not copy it to a parent span, which
would count the same usage twice. If a tool calls a model, keep both spans: the
tool span represents the action and its llm child owns model usage.
See Attribute keys for the exact
judgment.* keys written by this helper.
Group traces into sessions
Set the session ID on each trace’s root while it is active. Child spans inherit it.
- Use a stable application identity such as a chat, workflow, or job ID.
- Keep the same ID across retries, replays, and worker replacement.
- If a CLI, subprocess, or upstream service owns the conversation, use its conversation ID and keep the application’s request ID as a custom attribute.
- Record values you will query as attributes. Values buried only inside input or output payloads are not directly queryable.
@Tracer.observe(span_type="agent", span_name="chat_turn")
def chat_turn(chat_id: str, user_message: str) -> str:
Tracer.set_session_id(chat_id)
return run(user_message)const chatTurn = Tracer.observe(async function chatTurn(
chatId: string,
userMessage: string,
) {
Tracer.setSessionId(chatId);
return run(userMessage);
}, { spanType: "agent", spanName: "chat_turn" });Use set_customer_id or setCustomerId for a tenant or account, and
set_customer_user_id or setCustomerUserId for an individual end user. The
Sessions view then groups traces by the stable session while customer fields
remain available for filters and breakdowns.
Verify
Run one normal request and one handled failure. In Judgment, confirm:
- The root has the expected session and customer identities.
- Custom attributes are searchable and do not use the reserved
judgment.*namespace. - Each model call appears once with its provider, model, usage, and cost.
- The handled failure span is marked as an error while a successfully recovered root remains successful.
Troubleshooting
| Symptom | Check |
|---|---|
| Session or customer ID is missing | Call its setter while the intended root is active. |
| Values exist but are not searchable as Judgment fields | Use a public Judgment key directly or configure an attribute mapper. |
| Model usage or cost is doubled | Remove the duplicate manual wrapper, provider integration, or copied parent metadata. |
| Returned failures remain successful | Mark the active span from the application’s explicit failure signal. |
| Input or output changed unexpectedly | Do not overwrite automatic Tracer.observe capture unless that is intentional. |