This sample shows how to configure distributed tracing for Temporalio.Extensions.AI, producing
a complete span hierarchy from the external ChatAsync call through the Temporal protocol layers
down to the LLM activity. It also demonstrates the DurableAIPlugin — the plugin-based
registration path — as an alternative to AddDurableAI().
- Full span hierarchy:
durable_chat.send→UpdateWorkflow:Chat→RunActivity:GetResponse→durable_chat.turn conversation.idattribute on both the send and turn spans — filter an entire session in one query- Four
ActivitySourcenames must be registered with the tracer provider TracingInterceptorpropagates the W3Ctraceparentheader across gRPC boundaries- Plugin registration path:
AddWorkerPlugin(new DurableAIPlugin(...))as an alternative toAddDurableAI()
durable_chat.send (conversation.id = <id>)
└─ UpdateWorkflow:Chat ← Temporal SDK protocol span
└─ RunActivity:GetResponse ← Temporal SDK protocol span
└─ durable_chat.turn ← library semantic span
conversation.id, gen_ai.usage.input_tokens,
gen_ai.usage.output_tokens
-
Four sources, not one.
DurableChatTelemetry.ActivitySourceNamecovers library semantic spans; the threeTracingInterceptorsources cover Temporal protocol spans. Omitting any one of them produces gaps in your trace.builder.Services .AddOpenTelemetry() .WithTracing(tracing => tracing .AddSource(DurableChatTelemetry.ActivitySourceName) .AddSource(TracingInterceptor.ClientSource.Name) .AddSource(TracingInterceptor.WorkflowsSource.Name) .AddSource(TracingInterceptor.ActivitiesSource.Name) .AddConsoleExporter());
-
TracingInterceptoris required for connected traces. Without it, Temporal's internal gRPC calls break the distributed trace and the library spans appear disconnected from the protocol spans in your backend. -
conversation.idmakes session filtering practical. Both the client-sidedurable_chat.sendspan and the worker-sidedurable_chat.turnspan carryconversation.id, so a single attribute filter surfaces every span for a session across all service instances. -
DurableAIPluginis the plugin entry point. Gated by[Experimental("TAI001")], it is equivalent toAddDurableAI()and follows the canonical Temporal AI Partner Ecosystem integration pattern. SuppressTAI001with#pragma warning disable TAI001.
- .NET 10 SDK or later
- A local Temporal server:
temporal server start-dev - An OpenAI-compatible API key
dotnet user-secrets set "OPENAI_API_KEY" "sk-..." --project samples/MEAI/OpenTelemetry
dotnet user-secrets set "OPENAI_API_BASE_URL" "https://api.openai.com/v1" --project samples/MEAI/OpenTelemetrydotnet run --project samples/MEAI/OpenTelemetry/DurableOpenTelemetry.csprojSpan data is written to the console by AddConsoleExporter(). Look for entries like:
Activity.DisplayName: durable_chat.send
conversation.id: otel-demo-<guid>
Activity.DisplayName: durable_chat.turn
conversation.id: otel-demo-<guid>
gen_ai.usage.input_tokens: 42
gen_ai.usage.output_tokens: 18
Filter by conversation.id = otel-demo-<guid> to see all spans for the session.