Judgment Labs Logo
Tracing

Reference

Manual attributes, project routing, auto-instrumentation, OpenTelemetry integration, and subagent tracing.

This reference covers the Judgment tracing APIs used for manual attributes, project routing, auto-instrumentation, OpenTelemetry integration, and subagent tracing. The complete Python and TypeScript APIs are in the Tracer SDK Reference.

Manual attributes

Tracer.observe records function arguments and return values automatically. Setting them again inside an observed function overwrites the captured values unless automatic capture is disabled. Python's Tracer.span() and TypeScript's Tracer.span() and Tracer.with() do not capture I/O, so set input and output manually when you use those APIs.

Use the attribute setters below for queryable metadata that is not already part of the function's input or output. The SDK's session and customer setters also propagate those documented IDs to child spans. For custom context that must cross process boundaries, use the lower-level Python baggage API or the baggage APIs in the TypeScript SDK reference. The judgment.* namespace is reserved for documented Judgment attributes.

@Tracer.observe(span_type="agent", span_name="handle")
def handle(user_id: str, question: str) -> str:
    Tracer.set_attribute("user_id", user_id)
    Tracer.set_attributes({"channel": "web", "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("user_id", userId);
    Tracer.setAttributes({ channel: "web", plan: "pro" });
    Tracer.setCustomerUserId(userId);
    return run(question);
}, { spanType: "agent", spanName: "handle" });

In Python, use Tracer.scoped_context() when you need to apply session, customer, or custom attributes before a span starts. Spans created inside the block inherit those values.

LLM metadata

Use recordLLMMetadata() on the active llm span:

Tracer.recordLLMMetadata({
  model: "gpt-5.6-luna",
  provider: "openai",
  output_tokens: 150,
});

The helper accepts model, provider, non_cached_input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens, and total_cost_usd. It stores them as:

  • judgment.llm.model
  • judgment.llm.provider
  • judgment.usage.non_cached_input_tokens
  • judgment.usage.output_tokens
  • judgment.usage.cache_read_input_tokens
  • judgment.usage.cache_creation_input_tokens
  • judgment.usage.total_cost_usd

Use recordLLMMetadata() for these fields when possible. You can also write the documented judgment.* attributes directly. Judgment can calculate cost from the model and token data, so only set total_cost_usd when the provider returns it.

Project routing

Use separate projects for development, tests, staging, and production. Services in one distributed trace must use the same project and different service.name values. Log the project name and environment at startup, and keep the API key out of the logs. To test locally, point the tracer at a separate test project.

For environment-specific projects, derive the project name from the deployment environment and pass it to a single Tracer.init() call.

import os

env = os.environ["APP_ENV"]
Tracer.init(project_name=f"{env} - my_agent")

In TypeScript, pass the computed value as projectName to the single Tracer.init() call.

Auto-instrumentation

Auto-instrumentation records model calls without wrapping each one.

Python uses wrap(). It tracks all LLM API calls, including token usage, cost, and streaming, for sync and async clients. See Model Providers for supported providers.

TypeScript needs all three of the following:

  1. An instrumentation file that the runtime preloads before the app starts.
  2. OpenTelemetry instrumentations registered on Tracer before Tracer.init().
  3. A CommonJS build, because the instrumentations patch modules at require time.
instrumentation.ts
import { Tracer } from "judgeval";
import { OpenAIInstrumentation } from "@opentelemetry/instrumentation-openai";

Tracer.registerOTELInstrumentation(new OpenAIInstrumentation());

await Tracer.init({ projectName });

Import this file first in your entrypoint (import "./instrumentation";), or preload it with your runtime's require/import flag.

Activating spans

In TypeScript, the SDK tracks the active span in its own context rather than OpenTelemetry's global context. Two consequences follow:

  • Tracer.startSpan returns a span but does not make it active. Nothing you run afterward parents under it, and setSessionId finds no current span and silently does nothing.
  • Activating a span with raw OpenTelemetry calls (context.with, trace.setSpan) writes to a context the SDK never reads. Integration spans then start their own traces instead of joining yours.

Create roots with Tracer.observe or Tracer.getOTELTracer().startActiveSpan, which activate the span in the SDK's context. After wiring, send one request and confirm the integration's spans share your root's trace ID.

Python has the same lifecycle distinction between starting and activating a span. Tracer.start_span() returns a span that you must manage yourself. Use Tracer.observe, Tracer.span(), or Tracer.start_as_current_span() when work should run with that span active.

OpenTelemetry integration

Judgment tracing is standard OpenTelemetry. It works with existing OTEL tooling, semantic conventions, collectors, and exporters. To feed Judgment from an existing OTEL setup, add Judgment's span processor to your provider:

tracer_provider.add_span_processor(tracer.get_span_processor())

Set the usual resource attributes (service.name, service.version, and deployment.environment) per the OpenTelemetry Resource specification.

Attribute mappers

Spans that do not come from the judgeval SDK may use different attribute names. OpenTelemetry's GenAI conventions use gen_ai.response.model and the Vercel AI SDK uses ai.response.model, while Judgment reads judgment.llm.model. When the names do not match, fields such as cost, sessions, and inputs/outputs show up empty even though the data is on the span.

An attribute mapper copies those values onto Judgment's keys at ingest, so your instrumentation stays as it is. A mapper is a list of source key to target key pairs:

Source keyTarget key
gen_ai.systemjudgment.llm.provider
gen_ai.response.modeljudgment.llm.model
gen_ai.usage.input_tokensjudgment.usage.non_cached_input_tokens
gen_ai.usage.output_tokensjudgment.usage.output_tokens

To create one, open your project in the Judgment platform and go to Configure → Mappers. It applies to spans ingested from that point on and runs after Judgment's built-in mappers for known providers, so you only need a mapper for attributes those do not already recognize.

By default, the source key is deleted after its value is copied, and the copy is skipped if the target key already holds a value. Turn off Remove source to keep both keys, or turn on Override to write over the existing value.

Target keys must be Judgment attribute keys. See the attribute key reference for the full list.

Mappers can also be managed from your editor or the Judgment agent with the MCP server.

Subagent tracing

Use a forked trace when one agent delegates a meaningful chunk of work to a subagent and the subagent's work is evaluated on its own. The parent trace keeps a lightweight invocation span; the subagent runs in its own linked trace. Otherwise, keep subagent work inside the parent trace.

@Tracer.observe(span_type="agent", fork=True)
def research_subagent(task: str) -> str:
    ...

In TypeScript, pass { spanType: "agent", fork: true }. For lower-level control in Python, see Tracer.start_linked_trace().