首页
/ Dify Enterprise 遥测:用 OTel 构建"精简 Span + 富伴生日志"的可观测性体系

Dify Enterprise 遥测:用 OTel 构建"精简 Span + 富伴生日志"的可观测性体系

2026-09-06 17:06:52作者:董斯意

本文基于 Dify 仓库中 api/enterprise/telemetry/ 目录下的官方文档与实现源码,完整讲解 Dify 企业版 OpenTelemetry 导出器的架构设计、OTLP 环境变量配置、跨服务/跨异步任务的确定性 ID 关联模型、内容门控(Content Gating)隐私机制,以及全部遥测信号(Span、Counter、Histogram、结构化日志)的数据字典。读完本文,你可以在 Prometheus / Grafana / Jaeger / Honeycomb 等观测栈中正确配置 Dify 企业版遥测,编写避免 Token 双重计数的 PromQL 查询,并从源码层面理解每条 trace、log、metric 是如何生成与相互关联的。

架构总览:Slim Span + Rich Companion Log

根据 README.md 的定义,Dify 企业版采用 "slim span + rich companion log"(精简 Span + 富伴生日志) 架构,在不压垮 Trace 存储的前提下提供高保真可观测性。三种信号各司其职:

  • Traces(Spans):只捕获高层操作(Workflow 与 Node)的结构、身份与时间信息;
  • Structured Logs(结构化日志):为每个事件提供深度上下文(输入、输出、元数据),并通过 trace_idspan_id 与 Span 关联;
  • Metrics(指标):提供 100% 精确的计数器与直方图,覆盖用量、性能与错误统计。

原文档给出的信号架构图如下:

graph TD
    A[Workflow Run] -->|Span| B(dify.workflow.run)
    A -->|Log| C(dify.workflow.run detail)
    B ---|trace_id| C

    D[Node Execution] -->|Span| E(dify.node.execution)
    D -->|Log| F(dify.node.execution detail)
    E ---|span_id| F

    G[Message/Tool/etc] -->|Log| H(dify.* event)
    G -->|Metric| I(dify.* counter/histogram)

从源码结构可以印证这一策略的分界线:enterprise_trace.py 的模块 docstring 明确写道——只有 workflow run、node execution、draft node execution 三类事件会发射 Span,其余所有事件类型(message、tool、moderation、dataset retrieval 等)只发射"Metrics + 结构化日志"。三类 Span 的名称定义在 entities/__init__.pyEnterpriseTelemetrySpan 枚举中:

class EnterpriseTelemetrySpan(StrEnum):
    WORKFLOW_RUN = "dify.workflow.run"
    NODE_EXECUTION = "dify.node.execution"
    DRAFT_NODE_EXECUTION = "dify.node.execution.draft"

同一文件中还定义了全部事件名(EnterpriseTelemetryEvent,如 dify.message.rundify.tool.execution)、计数器(EnterpriseTelemetryCounter)与直方图(EnterpriseTelemetryHistogram)枚举,是理解全部遥测信号的"总目录"。

配置 OTLP 导出:环境变量一览

企业版 OTLP 导出器完全通过环境变量配置,全部字段定义在 configs/enterprise/__init__.pyEnterpriseTelemetryConfig(Pydantic Settings)中。原文档的配置表与源码默认值一一对应:

变量 说明 默认值
DEPLOYMENT_EDITION 产品版本;企业版遥测仅在 ENTERPRISE 版本可用 COMMUNITY
ENTERPRISE_TELEMETRY_ENABLED 企业版遥测总开关 false
ENTERPRISE_OTLP_ENDPOINT OTLP 采集端点(例如 http://otel-collector:4318
ENTERPRISE_OTLP_HEADERS OTLP 请求的自定义头(例如 x-scope-orgid=tenant1
ENTERPRISE_OTLP_PROTOCOL OTLP 传输协议(httpgrpc http
ENTERPRISE_OTLP_API_KEY 用于认证的 Bearer Token
ENTERPRISE_INCLUDE_CONTENT 是否在日志中包含敏感内容(输入/输出) false
APPLICATION_NAME 上报给 OTEL 的服务名(与原生 OTel 共享) langgenius/dify
ENTERPRISE_OTEL_SAMPLING_RATE Trace 采样率(0.0 到 1.0)。指标始终 100% 上报 1.0

双重开关与初始化生命周期

"仅企业版可用"并非文档口号,而是硬性门控。ext_enterprise_telemetry.py 中的 is_enabled() 同时检查两个条件:

def is_enabled() -> bool:
    return bool(
        dify_config.DEPLOYMENT_EDITION == DeploymentEdition.ENTERPRISE
        and dify_config.ENTERPRISE_TELEMETRY_ENABLED
    )

该 Flask 扩展在 create_app() 阶段(单线程)初始化 EnterpriseExporter 单例,通过 atexit.register(_exporter.shutdown) 保证进程退出时优雅刷盘,并 import enterprise.telemetry.event_handlers 以触发 blinker 事件处理器的注册;不在企业版或未开启开关时整条链路被完全跳过。exporter.py 中的 is_enterprise_telemetry_enabled() 采用完全一致的双条件判断,两处互为印证。

导出器内部:独立的 Tracer/Meter Provider

阅读 exporter.py 可以看到几个关键实现细节:

  1. 独立的基础设施:企业版导出器使用专用的 TracerProviderMeterProvider 实例,可配置独立采样率,与社区版原生 OTel(ext_otel.py)基础设施完全隔离。Tracer/Meter 名均为 "dify.enterprise"
  2. 协议工厂与端点拼接_ExporterFactory 根据 ENTERPRISE_OTLP_PROTOCOL 选择 HTTP 或 gRPC 导出器;HTTP 模式下自动在端点后追加 /v1/traces/v1/metrics 路径。
  3. TLS 自动探测insecure = not endpoint.startswith("https://"),即以 https:// 开头自动启用安全通道,其余一律按不安全处理(见 exporter.py#L118-L119)。
  4. 头部解析ENTERPRISE_OTLP_HEADERS 通过 W3CBaggagePropagator 按 W3C baggage 语义解析为键值对(key=value,key2=value2)。
  5. API Key 优先级:若设置了 ENTERPRISE_OTLP_API_KEY,它会写入 Authorization: Bearer <key>;若自定义头中已存在 authorization,会记录 warning 并以 API Key 为准。
  6. 采样与批处理:Trace 使用 ParentBasedTraceIdRatio(sampling_rate) 采样器 + BatchSpanProcessor 批量导出;指标使用 PeriodicExportingMetricReader 周期性上报,不受采样率影响,恒为 100% 精度——这正是原文档"Metrics are always 100%"的实现依据。
  7. 10 个计数器 + 6 个直方图在构造期一次性创建,包括 dify.tokens.total/input/outputdify.requests.totaldify.errors.totaldify.feedback.totaldify.dataset.retrievals.totaldify.app.created/updated/deleted.total,以及 dify.workflow.durationdify.node.durationdify.message.durationdify.message.time_to_first_tokendify.tool.durationdify.prompt_generation.duration

关联模型:确定性 ID 生成

跨服务、跨异步任务的信号关联是整套体系的核心难点。Dify 的解法是确定性 ID 生成(原文档 "Correlation Model" 一节):

  • trace_id:由 correlation ID(工作流为 workflow_run_id,草稿节点为 node_execution_id)经 int(UUID(correlation_id)) 派生;
  • span_id:由源 ID(source_id)取 UUID(source_id)低 64 位派生。

这一规则在 id_generator.py 中落地为 CorrelationIdGenerator(继承自 OTEL SDK 的 IdGenerator)。模块 docstring 说明了设计动机:使用 contextvars 保存线程安全的 correlation_id -> trace_id 映射;当设置了 span_id_source 时,span_id 从该值确定性派生,使任意 Span 都能引用另一个 Span 作为父级,而不依赖 Span 的创建顺序——这是跨工作流链接的关键。

def compute_deterministic_span_id(source_id: str) -> int:
    """Derive a deterministic span_id from any UUID string."""
    span_id = uuid.UUID(source_id).int & ((1 << 64) - 1)
    return span_id if span_id != 0 else 1   # OTEL 要求 span_id != 0

generate_trace_id() 则将 uuid.UUID(correlation_id).int 作为 128 位 trace_id 直接返回,解析失败时回退为随机值(见 id_generator.py#L57-L65)。

单元测试 test_id_generator.py 对上述行为逐项验证:低 64 位截取、低 64 位全零时返回 1(满足 OTEL 非零约束)、非法 UUID 抛错、不同 UUID 产生不同 span_id、同一输入确定性输出相同结果。

场景 A:简单工作流

单次工作流运行包含多个节点,所有 Span 与日志共享同一个 trace_id(由 workflow_run_id 派生):

trace_id = UUID(workflow_run_id)
├── [root span] dify.workflow.run (span_id = hash(workflow_run_id))
│   ├── [child] dify.node.execution - "Start" (span_id = hash(node_exec_id_1))
│   ├── [child] dify.node.execution - "LLM" (span_id = hash(node_exec_id_2))
│   └── [child] dify.node.execution - "End" (span_id = hash(node_exec_id_3))

场景 B:嵌套子工作流

一个工作流通过 Tool 或 Sub-workflow 节点调用另一个工作流。子工作流的 Span 通过 parent_span_id_source 参数链接到父工作流,两个工作流共享同一个 trace_id

trace_id = UUID(outer_workflow_run_id)     ← shared across both workflows
├── [root] dify.workflow.run (outer) (span_id = hash(outer_workflow_run_id))
│   ├── dify.node.execution - "Start Node"
│   ├── dify.node.execution - "Tool Node" (triggers sub-workflow)
│   │   └── [child] dify.workflow.run (inner) (span_id = hash(inner_workflow_run_id))
│   │       ├── dify.node.execution - "Inner Start"
│   │       └── dify.node.execution - "Inner End"
│   └── dify.node.execution - "End Node"

嵌套工作流的关键属性(原文档定义,与 enterprise_trace.pydify.parent.* 属性的写入逻辑一致):

  • 内层工作流的 dify.parent.trace_id = 外层 workflow_run_id
  • 内层工作流的 dify.parent.node.execution_id = 触发它的 Tool 节点 execution_id
  • 内层工作流的 dify.parent.workflow.run_id = 外层 workflow_run_id
  • 内层工作流的 dify.parent.app.id = 外层 app_id

在导出层,export_span() 通过 trace_correlation_overrideparent_span_id_source 两个参数实现"跨工作流链接":设置后,trace_id 从外层 correlation 派生,父级 SpanContext 被构造为 is_remote=TrueNonRecordingSpan。源码中还有一处防御性设计值得注意:根 Span 始终使用显式空 Context 作为默认父上下文,避免意外继承 Celery/HTTP 自动埋点中的环境活动 Span,导致一条逻辑 trace 被拆成两条。

场景 C:草稿节点执行(Draft)

在调试器/预览模式下单独运行一个节点。它会创建自己的 trace,节点 Span 即为根:

trace_id = UUID(node_execution_id)   ← own trace, NOT part of any workflow
└── dify.node.execution.draft (span_id = hash(node_execution_id))

关键差异:草稿执行使用 node_execution_id 作为 correlation_id,因此它不是任何工作流 trace 的子节点。对应源码 _draft_node_execution_trace() 中,correlation_id_override=info.node_execution_id,且 parent_span_id_source 被置为 Noneparent_span_id_source = info.workflow_run_id if not correlation_id_override else None),从而保证草稿 Span 成为独立 trace 的根。

内容门控(Content Gating):防止敏感数据泄漏

ENTERPRISE_INCLUDE_CONTENT 设为 false(默认值)时,敏感内容属性(inputs、outputs、query 等)会被替换为引用字符串(reference string),防止真实业务数据流向 OTLP 采集端。configs/enterprise/__init__.py 中该开关的注释直接点明了动机:"Setting the default value to False to avoid accidentally log PII data in traces"(默认设为 False,避免在 trace 中意外记录 PII 数据)。

引用字符串格式

ref:{id_type}={uuid}

示例

ref:workflow_run_id=550e8400-e29b-41d4-a716-446655440000
ref:node_execution_id=660e8400-e29b-41d4-a716-446655440001
ref:message_id=770e8400-e29b-41d4-a716-446655440002

门控开启后,实际内容可通过引用中的 UUID 回查 Dify 数据库获得。实现上,enterprise_trace.py_content_or_ref() 是该机制的唯一出口:

def _content_or_ref(self, value: Any, ref: str) -> Any:
    if self._exporter.include_content:
        return self._maybe_json(value)
    return ref

workflow 事件用 ref:workflow_run_id=...,node 事件用 ref:node_execution_id=...,message/tool 事件用 ref:message_id=...include_content 标志在 EnterpriseExporter.__init__ 中从配置读入,整个进程生命周期内一致。

完整的受门控属性清单见 DATA_DICTIONARY.md 的 "Content-Gated Attributes" 一节,共 20 个属性,例如:dify.workflow.inputs/outputs/querydify.node.inputs/outputs/process_datadify.message.inputs/outputsdify.tool.inputs/outputs/parameters/configdify.moderation.querydify.suggested_question.questionsdify.retrieval.querydify.dataset.documentsdify.generate_name.inputs/outputsdify.prompt_generation.instruction/outputdify.feedback.content 等。

数据字典:全部遥测信号速查

完整的数据字典维护在 DATA_DICTIONARY.md(README 的 "Reference" 一节指向该文件)。以下按原文档脉络浓缩关键内容,便于按图索骥。

资源属性(Resource Attributes)

附加到每个信号(Span、Metric、Log)上,与 exporter.py 构造的 OTel Resource 对应:

属性 类型 示例
service.name string dify
host.name string dify-api-7f8b

Span 属性

dify.workflow.run 的核心属性:dify.trace_id(业务 trace ID,即 Workflow Run ID)、dify.tenant_iddify.app_iddify.workflow.iddify.workflow.run_iddify.workflow.statussucceeded/failed/stopped 等)、dify.workflow.errordify.workflow.elapsed_time(秒)、dify.invoke_fromapi/webapp/debug)、dify.conversation.id(可选)、dify.message.id(可选)、dify.invoked_bygen_ai.usage.total_tokens(可选,全部节点 token 总和)、gen_ai.user.id(可选),以及嵌套场景的 dify.parent.trace_iddify.parent.workflow.run_iddify.parent.node.execution_iddify.parent.app.id(均可选)。

dify.node.execution 在上述身份属性之外还包括:dify.node.execution_iddify.node.iddify.node.type(节点类型见附录)、dify.node.titledify.node.statusdify.node.errordify.node.elapsed_timedify.node.index(执行顺序)、dify.node.predecessor_node_id,以及迭代/循环/并行上下文 dify.node.iteration_iddify.node.loop_iddify.node.parallel_id(可选);LLM 节点额外携带 gen_ai.usage.input/output/total_tokensgen_ai.request.modelgen_ai.provider.namedify.node.execution.draftdify.node.execution 属性相同,用于预览/调试运行。

空值行为(原文档附录明确区分):Span 中值为 null 的属性被省略(对应 export_span()if value is not None 的过滤);日志中 null 值以 JSON null 出现;被内容门控的属性则被替换为引用字符串而非 null

计数器(Counters)

所有计数器均为累加型、100% 精度上报。

Token 计数器dify.tokens.total{token},总消耗)、dify.tokens.input(prompt tokens)、dify.tokens.output(completion tokens)。统一标签集为 tenant_idapp_idoperation_typemodel_providermodel_namenode_type(仅 node_execution 时有值)——该结构由 entities/__init__.py 中的 TokenMetricLabels 模型强制约束(extra="forbid", frozen=True)。

⚠️ 防双重计数警告dify.tokens.total 在 workflow 层级已包含全部节点 token,必须用 operation_type 过滤后再聚合:

App-level total
├── workflow          ← sum of all node_execution tokens (DO NOT add both)
│   └── node_execution ← per-node breakdown
├── message           ← independent (non-workflow chat apps only)
├── rule_generate     ← independent helper LLM call
├── code_generate     ← independent helper LLM call
├── structured_output ← independent helper LLM call
└── instruction_modify← independent helper LLM call

核心规则:workflow 的 token 已包含所有 node_execution token,永远不要两者相加。 另注意:应用名称只存在于 Span 属性 dify.app.name 中,不存在于指标标签——指标查询请一律使用 app_id

原文档给出的常用 PromQL 查询(完整保留):

# ── Totals ──────────────────────────────────────────────────
# App-level total (exclude node_execution to avoid double-counting)
sum by (app_id) (dify_tokens_total{operation_type!="node_execution"})

# Single app total
sum (dify_tokens_total{app_id="<app_id>", operation_type!="node_execution"})

# Per-tenant totals
sum by (tenant_id) (dify_tokens_total{operation_type!="node_execution"})

# ── Drill-down ──────────────────────────────────────────────
# Workflow-level tokens for an app
sum (dify_tokens_total{app_id="<app_id>", operation_type="workflow"})

# Node-level breakdown within an app
sum by (node_type) (dify_tokens_total{app_id="<app_id>", operation_type="node_execution"})

# Model breakdown for an app
sum by (model_provider, model_name) (dify_tokens_total{app_id="<app_id>"})

# Input vs output per model
sum by (model_name) (dify_tokens_input_total{app_id="<app_id>"})
sum by (model_name) (dify_tokens_output_total{app_id="<app_id>"})

# ── Rates ───────────────────────────────────────────────────
# Token consumption rate (per hour)
sum(rate(dify_tokens_total{operation_type!="node_execution"}[1h]))

# Per-app consumption rate
sum by (app_id) (rate(dify_tokens_total{operation_type!="node_execution"}[1h]))

在 Tempo / Jaeger 中通过应用名反查 app_id 的 trace 查询:

{ resource.dify.app.name = "My Chatbot" } | select(resource.dify.app.id)

请求计数器 dify.requests.total{request})按 type 区分附加标签:

type 附加标签
workflow tenant_idapp_idstatusinvoke_from
node tenant_idapp_idnode_typemodel_providermodel_namestatus
draft_node tenant_idapp_idnode_typemodel_providermodel_namestatus
message tenant_idapp_idmodel_providermodel_namestatusinvoke_from
tool tenant_idapp_idtool_name
moderation tenant_idapp_id
suggested_question tenant_idapp_idmodel_providermodel_name
dataset_retrieval tenant_idapp_id
generate_name tenant_idapp_id
prompt_generation tenant_idapp_idoperation_typemodel_providermodel_namestatus

错误计数器 dify.errors.total{error}):workflowtenant_idapp_id)、node/draft_node(加 node_typemodel_providermodel_name)、message(加 model_providermodel_name)、tool(加 tool_name)、prompt_generation(加 operation_typemodel_providermodel_name)。

其他计数器dify.feedback.total{feedback},标签 tenant_idapp_idrating);dify.dataset.retrievals.total{retrieval},标签 tenant_idapp_iddataset_idembedding_model_providerembedding_modelrerank_model_providerrerank_model);dify.app.created/updated/deleted.total{app},created 另有 mode 标签)。

直方图(Histograms)

指标 单位 标签
dify.workflow.duration s tenant_idapp_idstatus
dify.node.duration s tenant_idapp_idnode_typemodel_providermodel_nameplugin_name
dify.message.duration s tenant_idapp_idmodel_providermodel_name
dify.message.time_to_first_token s tenant_idapp_idmodel_providermodel_name
dify.tool.duration s tenant_idapp_idtool_name
dify.prompt_generation.duration s tenant_idapp_idoperation_typemodel_providermodel_name

其中 workflow 时长的计算在源码中有明确取舍(enterprise_trace.py#L264-L272):优先用 workflow_runcreated_at/finished_at 墙钟时间差,因为数据库中的 elapsed_time 默认写 0,且 Celery 写库与 trace 任务可能存在竞态。

结构化日志

Span 伴生日志(signal 类型 span_detaildify.workflow.rundify.node.execution/dify.node.execution.draft 的 Span 各伴随一条日志,包含全部 Span 属性加详情属性(如 dify.app.namedify.workspace.namedify.workflow.versiondify.workflow.inputs/outputs/query,节点的 dify.node.total_pricedify.node.currencydify.plugin.namedify.credential.name/iddify.dataset.ids/names 等)。事件属性为 dify.event.namedify.event.signal="span_detail",以及 trace_idspan_idtenant_iduser_id

独立日志(signal 类型 metric_only:没有对应 Span 的事件,包括 dify.message.run(含 dify.message.durationdify.message.time_to_first_token 等)、dify.tool.executiondify.moderation.checkdify.moderation.action 取值 pass/block/flag)、dify.suggested_question.generationdify.dataset.retrieval(含 dify.retrieval.document_count 与 embedding/rerank 模型信息)、dify.generate_name.executiondify.prompt_generation.executiondify.app.created/updated/deleteddify.feedback.created,以及遥测系统自身健康诊断事件 dify.telemetry.rehydration_failed(携带 dify.telemetry.errordify.telemetry.payload_typedify.telemetry.correlation_id)。

这些日志行由 telemetry_log.pyemit_telemetry_log() 统一发射:compute_trace_id_hex() 将业务 UUID 转换为 32 位十六进制 OTEL trace_id,compute_span_id_hex() 转换为 16 位十六进制 span_id(两者均带 lru_cache(4096)),最终经 StructuredJSONFormatter 输出到 stdout/Loki/Elastic。

附录枚举值

  • Operation Typesworkflownode_executionmessagerule_generatecode_generatestructured_outputinstruction_modify
  • Node Typesstartendanswerllmknowledge-retrievalknowledge-indexif-elsecodetemplate-transformquestion-classifierhttp-requesttooldatasourcevariable-aggregatorloopiterationparameter-extractorassignerdocument-extractorlist-operatoragenttrigger-webhooktrigger-scheduletrigger-pluginhuman-input
  • Workflow Statusesrunningsucceededfailedstoppedpartial-succeededpaused
  • Payload Typesworkflownodemessagetoolmoderationsuggested_questiondataset_retrievalgenerate_nameprompt_generationappfeedback

延伸阅读:实现与测试入口

模块 路径 说明
官方架构文档 api/enterprise/telemetry/README.md 本文主体依据
数据字典 api/enterprise/telemetry/DATA_DICTIONARY.md 全部信号/属性/枚举速查
确定性 ID 生成器 api/enterprise/telemetry/id_generator.py CorrelationIdGenerator、span_id 低 64 位派生
OTLP 导出器 api/enterprise/telemetry/exporter.py Tracer/Meter Provider、协议工厂、export_span 父子链接
信号处理器 api/enterprise/telemetry/enterprise_trace.py 各事件到 Span/日志/指标的映射
结构化日志发射 api/enterprise/telemetry/telemetry_log.py trace_id/span_id 十六进制化与日志输出
信号枚举 api/enterprise/telemetry/entities/init.py Span/Event/Counter/Histogram 名称与 TokenMetricLabels
Flask 扩展 api/extensions/ext_enterprise_telemetry.py 开关门控、单例初始化、atexit 关闭
配置定义 api/configs/enterprise/init.py 各环境变量的 Pydantic 定义与默认值
单元测试 api/tests/unit_tests/enterprise/telemetry/ test_id_generator.pytest_exporter.pytest_enterprise_trace.py

需要注意的适用前提:整套企业版遥测仅对 DEPLOYMENT_EDITION=ENTERPRISE 的部署生效,且默认关闭(ENTERPRISE_TELEMETRY_ENABLED=false);Trace 采样率只影响 Span,不影响指标;内容门控默认开启(不输出内容),如需采集输入/输出细节须显式设置 ENTERPRISE_INCLUDE_CONTENT=true 并自行承担数据合规责任。

登录后查看全文
热门项目推荐
相关项目推荐