Temporal Tracing
Trace Temporal workflow and activity spans with Judgment.
Trace a Temporal app by making each activity execution its own trace, grouped by the workflow ID as the session. Temporal's tracing interceptor is optional; the final section explains when it is useful.
Read Durable work for the full pattern: app roots, sessions, activity boundaries, waits, errors, and flushes.
uv add judgeval temporaliopip install judgeval temporalioActivity roots
Initialize the tracer at import time in every process (the producer service and each worker) before any activity, client, or app object is built. Then run each activity through an observed helper. Flush after that helper returns, when its root has ended:
import os
from judgeval import Tracer
Tracer.init(project_name=os.environ["JUDGMENT_PROJECT_NAME"])from judgeval import Tracer
from temporalio import activity
import app.tracing # initializes tracing before the activity is defined
@Tracer.observe(span_type="agent", span_name="app.execute_step")
async def _execute_traced_step(req: StepRequest) -> StepResult:
info = activity.info()
Tracer.set_session_id(info.workflow_id)
Tracer.set_attributes({"app.activity.attempt": info.attempt})
return await run_step(req) # model and tool calls happen here
@activity.defn
async def execute_step(req: StepRequest) -> StepResult:
try:
return await _execute_traced_step(req)
finally:
Tracer.force_flush()Wrap the model client once (Tracer.wrap(...)) so model calls appear as
children inside whichever activity root is active. Trace submit and approval
endpoints only when they do app work, such as starting or resuming the run in
their own process. Leave them untraced if they only enqueue or acknowledge a
request. Status polls do not represent agent work, and
workflow code should not create spans because Temporal replays it.
This produces one trace per activity attempt, connected model and tool children, and a session that groups the whole job across queue and approval waits. Retries appear as separate traces with distinct attempt numbers.
Temporal's tracing interceptor (optional)
Temporal's tracing interceptor carries the caller's trace context into later work. An activity that runs after a queue or approval wait can then attach under a submit trace that already ended, and its spans can duplicate the activity roots above. Use the activity roots above as the default Judgment unit of work. Add the interceptor only when you need its transport spans, then verify that it does not duplicate activity roots or parent delayed work under a submit trace that has already ended.
from temporalio.client import Client
from temporalio.contrib.opentelemetry import TracingInterceptor
from judgeval import JudgmentTracerProvider, Tracer
Tracer.init(project_name="temporal_app")
JudgmentTracerProvider.install_as_global_tracer_provider()
client = await Client.connect("localhost:7233", interceptors=[TracingInterceptor()])TracingInterceptor comes from temporalio[opentelemetry]. If Temporal spans
appear as their own roots for work an activity root already covers, or later
activities attach under an ended submit root, remove the interceptor.