Trace providers
Each directory here is an optional uv workspace package that sends Dify traces (messages, workflow runs, tool calls, retrieval, moderation, and so on) to one external observability backend. trace-sdk/ is the contract every provider implements; it is not a provider itself.
The contract (trace-sdk, imported as dify_trace_sdk)
A provider is a subclass of dify_trace_sdk.provider.TraceProvider[ConfigT]:
| Member | Purpose |
|---|---|
id |
The stable provider ID stored in app tracing settings, such as "langfuse". |
config_model |
A ProviderConfig subclass. Mark credentials with secret() and values that verification resolves with derived(). |
capabilities |
A ProviderCapabilities value: whether the backend prices usage natively and links to traces and spans. |
verify(config) |
Checks a new or edited configuration against the backend and returns it with derived fields filled. Raises VerificationError(code) with a stable code that contains no secret. |
project_url(config) |
The console link for a configuration. It must not perform I/O. |
send(config, trace) |
Delivers one trace unit and classifies every failure (see below). |
What send receives (dify_trace_sdk.model):
- A
Traceis a parent-first tree of completedSpans that uses OpenTelemetry ID formats.- Each span carries timing, inputs and outputs, status, and typed LLM fields (
model_name,model_provider,usage,cost,time_to_first_token). - All values are strict JSON.
- Each span carries timing, inputs and outputs, status, and typed LLM fields (
- A message trace has a
messageroot and anllmchild. - A workflow run has a
workflowspan with node spans and loop or iteration wrapper spans. For chatflows these hang under themessageroot. - Auxiliary operations join their message's trace as separate units.
trace.parentnames the message root span, and the first span of the unit points at it. - A workflow nested in another one is its own trace. Its root carries
parent_workflow_run_idandparent_node_execution_id. - A unit can be sent again after a retryable failure. Derive vendor IDs deterministically from the trace and span IDs (
dify_trace_sdk.ids), so a resend updates the same records instead of duplicating them.
Failure classes (dify_trace_sdk.errors):
| Raise | When | Pipeline behaviour |
|---|---|---|
ExportRetryableError |
Nothing was accepted and the cause is transient (unreachable, 408/425/429/5xx). | Sends the same unit again later, up to OPS_TRACE_EXPORT_MAX_ATTEMPTS attempts in total. |
ExportRejectedError |
The backend refused the unit, or the configuration cannot work. | Drops the unit and logs a warning. |
ExportOutcomeUnknownError |
Part or all of the unit may have been accepted. | Drops the unit; sending again could duplicate it. |
Error messages are logged and must never contain credentials, request bodies, or vendor response bodies. Raise them from None.
Network access
A provider never opens sockets, starts threads, or reads environment variables or files. All traffic goes through self.egress:
egress.request(...)sends one HTTP request through the SSRF proxy, with no retries or redirects, a bounded response, and no ambient cookies.egress.grpc_channel(endpoint)opens a channel pinned to a vetted address.- The helpers in
dify_trace_sdk.exportinganddify_trace_sdk.otlpturn egress failures and HTTP or gRPC statuses into the failure classes above.
If you must use a vendor SDK, route its HTTP through EgressHttpxTransport or EgressRequestsSession (dify_trace_sdk.transports) and disable its batching, background threads, and retries. Importing the SDK must not change process-wide state; some SDKs install a Sentry client or patch other libraries on import.
Configuration and secrets
Stored configurations keep the provider's fields at the top level, with secret() fields encrypted per workspace. Secrets are write-only:
- The console never receives them back; it gets the names of the secrets that are set.
- When an edit leaves a secret empty or omits it, the stored value is kept.
- Clients cannot set
derived()fields.
ProviderConfig hides inputs from validation errors; keep validator messages fixed texts too.
Adding a provider
-
Create
api/providers/trace/trace-<name>/with:- a
pyproject.tomlfor the projectdify-trace-<name>, depending ondify-trace-sdk(plus any vendor SDK that is safe to import); src/dify_trace_<name>/{config.py,provider.py,py.typed};- tests that mirror the sources, e.g.
src/dify_trace_<name>/provider.pymaps totests/unit_tests/src/dify_trace_<name>/test_provider.py.
List
py.typedunder[tool.setuptools.package-data]. - a
-
Register the provider class as an entry point:
[project.entry-points."dify.trace_providers"] <id> = "dify_trace_<name>.provider:<Name>Provider" -
Add the ID to
SUPPORTED_PROVIDERSinapi/services/tracing/providers.pyand the provider's form fields to the web console's tracing field registry. -
In
api/pyproject.toml, adddify-trace-<name> = { workspace = true }under[tool.uv.sources]and add the package to thetrace-<name>andtrace-alldependency groups. Then runuv lockanduv syncfromapi/. -
Test with
dify_trace_sdk.testing.RecordingEgressand the sample traces there. Tests must not reach the network.
trace-langfuse/ is the smallest reference implementation; trace-arize-phoenix/ and trace-tencent/ show OTLP over HTTP and gRPC.
How traces reach a provider
-
Producers emit typed events (
dify_trace_sdk.events) through aTraceScopeafter their records are committed:- generators, task pipelines and the workflow persistence layer;
- moderation, tools, retrieval, conversation naming and suggested questions.
A scope exists only when the app has tracing enabled or enterprise telemetry is on. The producer snapshots the event and hands it to a background publisher, so storage and broker I/O do not delay the execution. Each process buffers at most 256 pending events; a full buffer drops new events with a warning. Graceful shutdown drains accepted events, but events not yet published can be lost if the process crashes.
-
tasks.trace_delivery_taskhands each event onOPS_TRACE_QUEUE(defaultops_trace_v2) toservices.tracing.delivery.TraceDeliveryService:repositories.trace_record_repositoryreloads the records through the event's workspace.- Enterprise telemetry receives every record.
- The Agent trace collector (
services.tracing.service) takes the executions it manages. services.tracing.canonicalbuilds the trace for the app's current provider configuration, and the provider'ssendruns.
-
A retryable failure stores only the built trace and schedules that send again. Payloads that an interrupted delivery leaves in storage are removed daily by
sweep_trace_payloads.
The tracing services depend only on the ports they declare: records, destinations, the provider catalog, secret encryption, the retry queue and the Agent trace ledger. extensions/application_services/tracing.py is the one place that chooses their adapters (services/tracing/adapters.py, services/tracing/egress.py, services/tracing/registry.py and the tracing repositories) and reads tracing configuration.
Rolling upgrade from the previous pipeline
The new task names and payloads are incompatible with old workers. Use separate queues while
both versions run. OPS_TRACE_QUEUE defaults to ops_trace_v2 for event delivery, export retries,
Agent trace exports/reconciliation, payload sweeping, and the sandbox metering task that previously
shared ops_trace. Celery routing is configured in extensions/ext_celery.py.
This isolates tracing tasks; other queues and database migrations still need their normal
compatibility checks when running both versions.
- Apply the database migrations and start new workers with
OPS_TRACE_QUEUE=ops_trace_v2. The Docker and development worker launchers include this queue by default and excludeops_trace. ExplicitCELERY_QUEUES,CELERY_WORKER_QUEUES, or--queueslists must selectops_trace_v2for the new pool. Keep the old tracing worker pool subscribed only toops_trace. - Roll out the new API and all other producers with the same setting. Replace the old Beat scheduler with the new one; do not run duplicate schedules. Allow old in-flight requests, workflow jobs, and their buffered trace producers to finish before stopping them.
- Let the old workers finish exporting their tasks. Check broker messages and worker
active, reserved/prefetched, scheduled/ETA, and retry deliveries. An empty broker queue alone
does not prove it has drained. Then stop the old workers gracefully and confirm no old
producers or workers remain. The new sweeper never deletes legacy
ops_trace/payloads. - Keeping
ops_trace_v2is supported. To restore the old queue name, first have new-version workers consume bothops_trace_v2,ops_trace. SetOPS_TRACE_QUEUE=ops_traceon every new API, worker, and Beat process and roll them out. Keep consumers for both queues until the new queue, active tasks, reservations, ETA tasks, and retries drain; a redelivery can retain its original queue. Then removeops_trace_v2from worker subscriptions.
Do not use the new version's retired process_trace_tasks handler to drain old messages: it can
only discard them, not export the legacy format. Do not purge queues or legacy payloads during
the transition. Returning to old binaries after publishing new-format tasks also requires
keeping new-version consumers available until those tasks finish.
Saved provider configuration and credentials need no changes. Queue migration is deployment configuration, shared by the API, workers, and Beat; it does not change app tracing settings.