Trace Task Pipeline#
The trace task pipeline is Dify's observability system for shipping LLM run data to external tracing providers (Langfuse, LangSmith, OpenTelemetry, etc.). It converts runtime execution events into structured trace objects, batches them using a background threading timer, and delivers them asynchronously via Celery.
Full pipeline flow:
App invocation
β Queue events (QueueErrorEvent / QueueStopEvent / QueueMessageEndEvent)
β EasyUI / Workflow persistence layer
β TraceTask enqueued in TraceQueueManager
β background threading.Timer fires every N seconds
β TraceTask.execute() builds trace info (MessageTraceInfo / WorkflowTraceInfo)
β TaskData serialized to storage (JSON file)
β process_trace_tasks Celery task
β tracing provider (Langfuse, LangSmith, β¦)
Key source files#
| File | Role |
|---|---|
api/core/ops/ops_trace_manager.py | TraceTask, TraceQueueManager, message_trace(), workflow_trace() |
api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py | Chat/completion pipeline β enqueues MESSAGE_TRACE on success and error events |
api/core/app/workflow/layers/persistence.py | Workflow persistence layer β enqueues WORKFLOW_TRACE on all terminal graph events |
api/tasks/ops_trace_task.py | Celery task that deserializes and dispatches trace data |
api/core/app/task_pipeline/based_generate_task_pipeline.py | Base class; handle_error() marks Message.status = ERROR |
api/core/app/entities/queue_entities.py | QueueErrorEvent, QueueStopEvent, QueueMessageEndEvent definitions |
Event-Based Trigger Points#
The pipeline is driven by events dequeued from AppQueueManager. Three event types determine whether a trace task is created:
QueueErrorEventβ signals an LLM or runtime error. The base pipeline'shandle_error()setsMessage.status = ERRORand returns anErrorStreamResponse. AMESSAGE_TRACEtask is added in the chat pipeline's error handling path.QueueStopEventβ user-initiated stop or moderation halt. Handled alongsideQueueMessageEndEventin the EasyUI pipeline; triggers_save_message()which enqueues aMESSAGE_TRACEtask .QueueMessageEndEventβ normal LLM completion. Also handled by_save_message(), which callstrace_manager.add_trace_task().
The TraceQueueManager.add_trace_task() method is a guarded entry point: tasks are only enqueued when either enterprise telemetry is enabled or a third-party trace instance (e.g., Langfuse) is configured for the app .
Batched Dispatch via Background Timer#
TraceQueueManager owns a process-level threading.Timer and a queue.Queue shared across all manager instances .
- Interval and batch size are controlled by env vars
TRACE_QUEUE_MANAGER_INTERVAL(default: 5 s) andTRACE_QUEUE_MANAGER_BATCH_SIZE(default: 100) . - Each
add_trace_task()call puts a task on the shared queue and ensures the timer is alive; if the timer is dead, a new one is started . - When the timer fires,
TraceQueueManager.run()callscollect_tasks()(drains up tobatch_sizeitems) thensend_to_celery(). send_to_celery()callspersist_trace_task()for each task, thenenqueue_persisted_trace().persist_trace_task()uses theops_trace_payload_path()helper instead of hardcoding the file path format, automatically assignsoperation_idto trace info if not set, and returns a file_info dict withfile_idandapp_id.enqueue_persisted_trace()usesprocess_trace_tasks.apply_async()with a retry policy (max_retries=3, interval_start=0, interval_step=1, interval_max=2) instead of.delay().
The timer is a non-daemon thread (daemon=False), so it will finish its current run on process shutdown before the Python interpreter exits .
Asymmetric Error Behavior: Chat/Completion vs. Workflow Apps#
This is the most important behavioral difference to understand when debugging missing traces.
Chat / Completion apps (EasyUIBasedGenerateTaskPipeline)#
When QueueErrorEvent arrives, the pipeline calls handle_error(), then explicitly adds a MESSAGE_TRACE task via trace_manager.add_trace_task() before yielding an ErrorStreamResponse and breaking out of the event loop . _save_message() is never called, but the error trace task ensures LLM errors are captured alongside successful completions.
Workflow apps (WorkflowPersistenceLayer)#
Trace tasks are not tied to the queue event loop at all. They are emitted by the persistence layer's _enqueue_trace_task(), which is called from every terminal graph event handler:
| Graph event | Status | Trace emitted? |
|---|---|---|
GraphRunSucceededEvent | SUCCEEDED | β |
GraphRunPartialSucceededEvent | PARTIAL_SUCCEEDED | β |
GraphRunFailedEvent | FAILED | β |
GraphRunAbortedEvent | STOPPED | β |
GraphRunPausedEvent | PAUSED | β (no trace, state is saved for resume) |
Practical implication: Both workflow apps and chat/completion apps now emit trace tasks on error events, ensuring observability parity across app types.
Celery Task: process_trace_tasks#
The process_trace_tasks task runs on the "ops_trace" Celery queue.
Execution steps :
- Resolve the JSON file path from
file_info(app_id+file_id) and load it from storage. - Reconstitute typed model objects (
Message,WorkflowRun,Documentfrom raw dicts). - Instantiate the correct
TraceInfoPydantic model viatrace_info_info_map. - Dispatch to enterprise telemetry (if enabled) β only once, guarded by
_enterprise_trace_dispatchedflag. - Dispatch to the configured provider trace instance (Langfuse, LangSmith, etc.) via
trace_instance.trace(trace_info). - Delete the storage file in
finally.
Retry contract : if a provider raises RetryableTraceDispatchError (e.g., Phoenix waiting for a parent span context), the task updates the stored file with the _enterprise_trace_dispatched flag and schedules self.retry(). Retry limits and delay are configured via OPS_TRACE_RETRYABLE_DISPATCH_MAX_RETRIES (default 780) and OPS_TRACE_RETRYABLE_DISPATCH_DELAY_SECONDS in dify_config . Terminal failures increment a Redis counter at {OPS_TRACE_FAILED_KEY}_{app_id} .
Trace Types and TraceTask Preprocessing#
TraceTask is initialized with a trace_type from TraceTaskName. When execute() is called inside send_to_celery(), it dispatches to the appropriate builder method:
TraceTaskName | Builder method | Output type |
|---|---|---|
MESSAGE_TRACE | message_trace() | MessageTraceInfo |
WORKFLOW_TRACE | workflow_trace() | WorkflowTraceInfo |
MODERATION_TRACE | moderation_trace() | ModerationTraceInfo |
TOOL_TRACE | tool_trace() | ToolTraceInfo |
DATASET_RETRIEVAL_TRACE | dataset_retrieval_trace() | DatasetRetrievalTraceInfo |
GENERATE_NAME_TRACE | generate_name_trace() | GenerateNameTraceInfo |
NODE_EXECUTION_TRACE | node_execution_trace() | WorkflowNodeTraceInfo |
Each builder performs DB lookups to hydrate the trace info object. For MESSAGE_TRACE, app_id is read directly from Message.app_id (no join required), and tenant_id is resolved via a single App query . For WORKFLOW_TRACE, token counts are split by summing usage.prompt_tokens / usage.completion_tokens from all node execution outputs columns .
The TraceTask.app_id is None at construction; it must be set on the TraceTask instance before calling persist_trace_task(), preserving the originating application context. The app_id is used as the storage key prefix for the serialized JSON. This ensures that trace tasks retain the correct application context boundary even when processed by different queue managers.
Configuration Variables#
| Variable | Type | Default | Description |
|---|---|---|---|
OPS_TRACE_UNIFIED_ENABLED | bool | false | Enable unified ops tracing for providers registered in the unified registry. |
OPS_TRACE_RETRYABLE_DISPATCH_MAX_RETRIES | int | 780 | Maximum retry attempts for transient ops trace provider dispatch failures. |
OPS_TRACE_RETRYABLE_DISPATCH_DELAY_SECONDS | int | 5 | Delay in seconds between transient ops trace provider dispatch retry attempts. |
OPS_TRACE_PARENT_CONTEXT_TTL_SECONDS | int | 3900 | Retention in seconds for unified tracing parent contexts. Default (3900s) covers the extended retry window (780 retries Γ 5 seconds = 3900s). |
Unified Tracing Mode#
When OPS_TRACE_UNIFIED_ENABLED=true, providers registered in the unified registry use a provider-neutral tracing path that assembles Dify trace semantics once in Core and delegates only provider-specific translation and transport to lightweight adapters.
Registered unified providers:
- Phoenix (
TracingProviderEnum.PHOENIX) - LangSmith (
TracingProviderEnum.LANGSMITH)
Providers not registered in the unified registry continue to use their existing implementations.
Parent context retention: The system validates that OPS_TRACE_PARENT_CONTEXT_TTL_SECONDS must be large enough to cover the retry window, calculated as OPS_TRACE_RETRYABLE_DISPATCH_MAX_RETRIES * OPS_TRACE_RETRYABLE_DISPATCH_DELAY_SECONDS. If the retry window exceeds the TTL, the application will raise a ValueError at startup.