Skip to content

Optional execution telemetry

Install durable-workflow[telemetry] for the optional OpenTelemetry API. Configure your application's tracer provider, sampler and exporter before constructing the interceptor. The ordinary SDK install does not require OpenTelemetry, a collector, a logging integration or an exporter.

import logging
from durable_workflow import Worker
from durable_workflow.telemetry import ExecutionTelemetry, LogContextFilter

# Configure your application-owned OpenTelemetry provider/exporter first.
telemetry = ExecutionTelemetry(namespace=client.namespace)
worker = Worker(client, task_queue="orders", workflows=[OrderWorkflow],
                activities=[charge], interceptors=[telemetry])
handler = logging.StreamHandler()
handler.addFilter(LogContextFilter())
logging.getLogger().addHandler(handler)

The interceptor scopes workflow/query execution and actual activity attempts. Existing task metadata supplies bounded workflow, task and attempt identifiers. current_log_context() and the standard logging filter expose the current application child span's trace/span IDs. Application request IDs and messages are preserved. Arguments, results, headers, exception text and baggage are not recorded by this integration. These identifiers are not metric labels.

Servers without the exact optional observability capability use independent task traces. When available, a valid run origin becomes the task parent, while task-bound dispatch and committed outcome contexts become links. Workers read them after the actual poll identity and existing history/authority fences. Insufficient claim identity omits propagation without changing business gates. Retries have separate attempt spans, and replay emits no new attempt spans for recorded activity results. Use the configured Server/SDK tuple qualified in Workflow #773.

For admission or commands outside worker execution, pass telemetry=telemetry to Client and use its existing await client.get_cluster_info() discovery during setup. Worker commands use the active interceptor. Only actual discovery of the exact supported worker_protocol.server_capabilities.observability_context object enables propagation. Unknown or malformed discovery disables it. No extra business request, mandatory protocol floor or version-string inference is added. Caller capture occurs before HTTP instrumentation starts; application arguments and request keys retain their existing meaning.

Official W3C propagators validate headers. Baggage defaults empty; opt in with baggage_allowlist=("tenant",). Secondary links never carry baggage. Context and link envelopes are limited to 4096 bytes, with at most 32 links. Invalid metadata is ignored, with only fixed discarded or filtered span diagnostics.

Async tasks and worker-managed callback threads retain isolated execution contexts; completion, cancellation and failures restore the previous context. Application-created background work follows Python's normal context-copying rules and needs an explicit empty context when it should be detached.

Supervised callbacks run in spawned processes. Pass an importable provider_factory="my_application.telemetry:make_provider" to construct a fresh provider there. Providers, exporter sockets and thread pools are omitted from serialized invocations. A factory can also configure the child's global provider for application HTTP/database instrumentation. A supplied parent provider is used only in that process. A forked process without a factory uses a no-op tracer instead of reusing inherited exporter sockets.

Use a synchronous processor and bounded exporter I/O when child spans must be exported before callback process exit. This integration never flushes or shuts down a shared provider per task; the application owns shutdown and bounded drain. Export failures preserve business results, while abrupt termination may lose telemetry. Spans do not provide exactly-once delivery guarantees.