Skip to content
Judgment Labs
Esc
navigateopen⌘Jpreview
On this page

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 result
const 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():

  • model
  • provider
  • non_cached_input_tokens
  • output_tokens
  • cache_read_input_tokens
  • cache_creation_input_tokens
  • total_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:

  1. The root has the expected session and customer identities.
  2. Custom attributes are searchable and do not use the reserved judgment.* namespace.
  3. Each model call appears once with its provider, model, usage, and cost.
  4. 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.

Was this page helpful?