Windmill 可观测性实战:基于 Tempo、Grafana、Prometheus 与 Loki 的 OpenTelemetry 链路追踪与日志监控
【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill
导读
本文以仓库中 examples/deploy/otel-tracing-grafana 目录下的官方部署示例为主线,完整讲解如何在自托管 Windmill 环境中一键搭建以Grafana Tempo为核心的分布式追踪、以Prometheus为存储的指标监控、以Loki为后端的日志聚合三合一可观测性方案。读完本文你将掌握:观测栈各容器的编排与配置要点、如何通过 Windmill 的 "Instances Settings → OTEL/Prom" 页面把追踪与日志推送到 OpenTelemetry Collector、如何利用 Windmill 注入的job_id、root_job、flow_step_id等标签在 Tempo 中精准检索特定作业或工作流,以及如何借助 Tempo 的 metrics generator 把 span 转化为可长期对比的性能指标。
为什么选择 Tempo:OTLP 兼容与 Windmill 的天然契合
Tempo 是 Grafana 开源的一款分布式追踪系统,专为监控和调试微服务而设计:它能够跨服务追踪请求、分析延迟、定位瓶颈并诊断故障。其典型使用场景包括排查生产问题、监控性能、可视化服务依赖关系以及优化系统可靠性。
Tempo 之所以能与 Windmill 无缝对接,关键在于它原生支持OpenTelemetry Protocol(OTLP)——Windmill 的追踪与日志输出正是基于 OpenTelemetry 生态(这一点在 backend/windmill-worker/src/worker.rs 的tracing::span!宏以及 backend/windmill-common/src/global_settings.rs 维护的一整组OTEL_EXPORTER_OTLP_*环境变量中可以得到印证)。因此,不需要任何自定义 SDK 或协议转换,Windmill 产生的 span 可以直接被 Tempo 接收、存储与查询。
整体架构:一次docker-compose up拉起完整观测栈
该示例的核心是一份完整的 docker-compose.yml。执行以下命令即可启动全部服务:
docker-compose up -d启动后,观测数据在容器间按如下链路流转:
- Windmill Server / Worker 通过 OTLP/gRPC 将traces 与 logs发送到
otel-collector:4317; - OpenTelemetry Collector 按管道拆分:traces 转发给
tempo:4317,logs 以 OTLP/HTTP 形式转发给loki:3100/otlp; - Tempo 的 metrics generator 对 span 进行聚合,把生成的指标通过 remote write 推送给 Prometheus;
- Grafana(
http://localhost:3000)通过预置的数据源接入 Tempo(Trace 查询)、Prometheus(指标)与 Loki(日志); - 整个 Windmill 平台通过 Caddy 反向代理暴露在
http://localhost。
docker-compose 中与观测栈相关的服务定义如下(完整内容见 docker-compose.yml):
# Grafana OpenTelemetry Example init: image: &tempoImage grafana/tempo:latest user: root entrypoint: - "chown" - "10001:10001" - "/var/tempo" volumes: - tempo-data:/var/tempo otel-collector: image: otel/opentelemetry-collector:latest container_name: otel-collector expose: - 4317 volumes: - ./otel-config.yaml:/etc/otel/config.yaml command: ["--config=/etc/otel/config.yaml"] tempo: image: *tempoImage command: [ "-config.file=/etc/tempo.yaml" ] volumes: - ./tempo-config.yaml:/etc/tempo.yaml - tempo-data:/var/tempo expose: - 3200 - 4317 depends_on: - init loki: image: grafana/loki:latest expose: - 3100 command: -config.file=/etc/loki/local-config.yaml volumes: - ./loki-config.yaml:/etc/loki/local-config.yaml prometheus: image: prom/prometheus:latest command: - --config.file=/etc/prometheus.yaml - --web.enable-remote-write-receiver - --enable-feature=exemplar-storage - --enable-feature=native-histograms volumes: - ./prometheus-config.yaml:/etc/prometheus.yaml expose: - 9090 grafana: image: grafana/grafana:11.0.0 volumes: - ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml environment: - GF_AUTH_ANONYMOUS_ENABLED=true - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - GF_AUTH_DISABLE_LOGIN_FORM=true - GF_FEATURE_TOGGLES_ENABLE=traceqlEditor metricsSummary - GF_INSTALL_PLUGINS=https://storage.googleapis.com/integration-artifacts/grafana-exploretraces-app/grafana-exploretraces-app-latest.zip;grafana-traces-app ports: - "3000:3000"几个值得注意的部署细节:
init容器:以 root 身份对 Tempo 数据卷执行chown 10001:10001,解决 Tempo 容器(默认非 root 用户)对数据目录的写权限问题;- Prometheus 启动参数:
--web.enable-remote-write-receiver用于接收 Tempo metrics generator 的 remote write;--enable-feature=exemplar-storage与--enable-feature=native-histograms分别为 trace 示例(exemplar)存储和原生直方图提供支持,与 Tempo 端generate_native_histograms: both的配置呼应; - Grafana 预置插件:通过
GF_INSTALL_PLUGINS安装 grafana-traces-app,增强 Tempo 的 trace 查询体验; - 默认配置即开即用:
GF_AUTH_ANONYMOUS_ENABLED=true且匿名角色为 Admin,方便本地演示,生产环境建议关闭。
示例中的 Windmill 本体由windmill_server(1 副本)、windmill_worker(3 副本,默认 worker 组)、windmill_worker_native(native 组,NUM_WORKERS=8)以及可选的windmill_indexer、windmill_worker_reports(默认注释)组成,数据库使用postgres:16。Worker 通过挂载worker_logs:/tmp/windmill/logs与 Server 共享日志卷,这是日志能够统一汇出的基础。
OpenTelemetry Collector:一条管道,双向分流
otel-config.yaml 是整个观测栈的"路由器",其完整内容如下:
receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 processors: batch: timeout: 5s exporters: otlphttp/loki: endpoint: http://loki:3100/otlp tls: insecure: true otlp/tempo: endpoint: http://tempo:4317 tls: insecure: true service: pipelines: traces: receivers: [otlp] processors: [batch] exporters: [otlp/tempo] logs: receivers: [otlp] processors: [batch] exporters: [otlphttp/loki]配置要点:
- 单一 OTLP 接收端:
otlpreceiver 仅开启 gRPC 协议,监听0.0.0.0:4317,即 Windmill 的 "OTEL/Prom" 设置中需要填写的 endpoint; - batch 处理器:
timeout: 5s,将 span/log 按 5 秒窗口批量导出,降低网络开销与目标端写入压力; - 两条独立管道:
traces管道导出到 Tempo(OTLP/gRPC,tempo:4317);logs管道导出到 Loki(OTLP/HTTP,loki:3100/otlp)。注意两者使用的 exporter 协议不同——Loki 接收的是 OTLP/HTTP,因此端点带/otlp路径; - 内网明文传输:
tls.insecure: true适用于容器间内网通信;若 Collector 暴露到公网或跨网络部署,应补充 TLS 与认证配置。
配置 Windmill:让追踪与日志流向 Collector
在 Windmill UI(http://localhost)完成初始设置后,进入"Instances Settings"(实例设置)→ "OTEL/Prom" 选项卡:
- 填写OpenTelemetry Collector endpoint:
otel-collector:4317(即上文 Collector 的 OTLP/gRPC 监听地址); - 填写Service Name(服务名),用于在 Tempo/Grafana 中标识来自 Windmill 的服务;
- 打开 "Tracing" 与 "Logging" 两个开关,使 Windmill 开始向
otel-collector:4317发送链路追踪与日志。
从源码角度看,这一配置项的作用链路清晰可见:OTEL_TRACING_ENABLED是一个全局原子开关,在 backend/windmill-common/src/lib.rs 中定义(AtomicBool::new(std::env::var("OTEL_TRACING").is_ok())),Worker 在执行每个任务前会判断该开关决定是否注入TRACEPARENT、OTEL_TRACE_ID、OTEL_SPAN_ID等上下文环境变量(见 backend/windmill-worker/src/worker.rs)。同时 backend/windmill-common/src/global_settings.rs 维护了一份允许在 Worker 中透传的 OTEL 环境变量白名单,涵盖OTEL_EXPORTER_OTLP_ENDPOINT、OTEL_EXPORTER_OTLP_TRACES_ENDPOINT、OTEL_EXPORTER_OTLP_LOGS_ENDPOINT、OTEL_EXPORTER_OTLP_PROTOCOL、OTEL_EXPORTER_OTLP_COMPRESSION、OTEL_EXPORTER_OTLP_TIMEOUT、OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE与OTEL_SERVICE_NAME等;其中OTEL_EXPORTER_OTLP_*HEADERS被有意排除在白名单之外,因为其中可能携带导出器的 API Key 等敏感信息。这意味着除了 UI 配置,你也可以直接通过标准 OTLP 环境变量(如OTEL_EXPORTER_OTLP_ENDPOINT、OTEL_SERVICE_NAME)对 Windmill 的遥测导出行为进行细粒度控制。
此外,Windmill API 侧还会为每个 HTTP 请求创建带有method、uri、workspace_id、traceId等字段的 request span(见 backend/windmill-api/src/tracing_init.rs),并通过log_context_middleware把请求上下文(含 trace_id)注入到日志记录中(backend/windmill-api/src/tracing_init.rs),为"日志与追踪关联"提供了基础。
在 Tempo UI 中查看与检索追踪
Tempo 的 UI 以 Grafana 插件形式提供。使用本示例的 docker-compose 部署时,Grafana 位于http://localhost:3000。当你通过 Windmill 运行一个脚本或工作流后,即可在 Tempo UI 中看到对应的 trace 并展开调查。
需要说明的是:虽然 OSS 仓库中add_root_flow_job_to_otlp与set_job_span_parent在非 EE 构建下是空实现(见 backend/windmill-worker/src/otel_oss.rs,其中注释说明完整实现位于otel_ee),但从 backend/windmill-worker/src/result_processor.rs 中add_root_flow_job_to_otlp(&root_job, success)的调用点以及 backend/windmill-worker/src/worker.rs 中set_job_span_parent(&span, arc_job, &rj)的调用可以推断:在完整版本中,Windmill 会把任务 span 挂接到入站分布式追踪上下文(W3Ctraceparent)或由 UUID 派生的上下文之下,从而形成贯穿"请求 → 工作流 → 单步任务"的完整 trace 树。
用 Windmill 注入的标签精准过滤 trace
Tempo UI 的搜索功能支持按标签(tag)过滤。Windmill 会在每个任务的 span 上写入丰富的标签,以下是官方示例文档列出的、用于精准检索的标签清单:
| 标签 | 含义 |
|---|---|
job_id | 作业(job)的 ID |
root_job | 根作业 ID(即整个 flow 的根任务) |
parent_job | 父作业 ID(flow 中的父任务) |
flow_step_id | 工作流内步骤的 ID |
script_path | 脚本路径 |
workspace_id | 工作区名称 |
worker_id | 执行该作业的 Worker ID |
language | 脚本语言 |
tag | 工作流的队列标签(queue tag) |
这些标签并非虚构——它们与 backend/windmill-worker/src/worker.rs 中create_span_with_name函数为 "job" span 注册的字段一一对应。该函数创建的 span 字段包括:job_id、root_job、workspace_id、worker(对应文档中的worker_id)、hostname、tag、language、script_path、flow_step_id、parent_job、job_kind、created_by、trigger_kind、trigger、script_hash,以及 OpenTelemetry 规范字段otel.name、otel.status_code、otel.status_message。具体填充逻辑为:
language取自arc_job.script_lang;- 若存在
flow_step_id,则otel.name会被改写为"{span_name} {step_id}"并记录flow_step_id; parent_job取自arc_job.parent_job;script_path取自arc_job.runnable_path;root_job取自arc_job.flow_innermost_root_job(即 flow 的最内层根任务)。
实际检索示例(Tempo 搜索语法):搜索某个脚本的所有执行记录可过滤script_path;排查某次工作流整体耗时可按root_job过滤出整棵 trace 树;定位流内单个步骤的性能则可结合flow_step_id与parent_job。这种标签体系使得"从一次失败作业反查整条调用链"或"从一条 trace 下钻到具体步骤"都变得非常直接。
失败状态的标记
当任务执行失败时,Windmill 会把otel.status_code置为ERROR,并写入经过截断(上限 512 字符,见 backend/windmill-worker/src/worker.rs 的STATUS_DESCRIPTION_MAX_LEN)的错误信息到otel.status_message,避免单个 verbose 错误撑爆 span 载荷。通过record_job_span_status(backend/windmill-worker/src/worker.rs)可以区分"作业正常运行完成"、"命中缓存"、"执行报错"以及"其他 Worker 已抢先完成"等不同结果,从而在 Tempo 中准确识别真正的失败链路。
用 Prometheus 做指标监控:span → metrics 的自动转化
Tempo 的metrics generator可以把已采集 trace 的时间序列聚合为指标(metrics),进而:
- 对比工作流内单个步骤的性能表现;
- 观察各步骤随时间变化的整体性能与相对贡献;
- 识别并排查问题与异常。
在 tempo-config.yaml 中,metrics generator 的启用逻辑如下:
metrics_generator: registry: external_labels: source: tempo cluster: windmill storage: path: /var/tempo/generator/wal remote_write: - url: http://prometheus:9090/api/v1/write send_exemplars: true traces_storage: path: /var/tempo/generator/traces overrides: defaults: metrics_generator: processors: [service-graphs, span-metrics, local-blocks] # enables metrics generator generate_native_histograms: both要点解读:
processors: [service-graphs, span-metrics, local-blocks]:service-graphs负责生成服务依赖关系图所需的数据;span-metrics将 span 时长等维度聚合为指标(这正是下面指标系列的来源);local-blocks支持基于 trace 的本地查询;remote_write指向 Prometheus:http://prometheus:9090/api/v1/write,并开启send_exemplars: true,使指标样本携带对应的 trace 示例(exemplar),实现"从指标一键跳转到关联 trace";generate_native_histograms: both:同时生成经典与原生直方图,配合 Prometheus 的--enable-feature=native-histograms启动参数使用;- 该示例为演示目的将 trace 保留期(
compactor.compaction.block_retention)设为1h,生产部署时应按实际需求调整。
生成的指标会导出到 Prometheus,可在 Grafana 的Metrics Explorer中查看,具体指标系列如下:
traces_spanmetrics_calls_totaltraces_spanmetrics_latencytraces_spanmetrics_latency_buckettraces_spanmetrics_latency_counttraces_spanmetrics_latency_sumtraces_spanmetrics_size_total
其中traces_spanmetrics_calls_total统计 span 调用次数,traces_spanmetrics_latency及其_bucket/_count/_sum兄弟指标构成延迟分布直方图,traces_spanmetrics_size_total反映 span 数据量。在 Grafana 中通常用traces_spanmetrics_latency_bucket绘制直方图、用rate(traces_spanmetrics_calls_total[5m])观察调用速率、用histogram_quantile()计算 p95/p99 延迟。结合这些指标与标签(如span_name、service及 Windmill 注入的script_path等维度),即可量化评估每个脚本/步骤在整条工作流中的耗时占比。
prometheus-config.yaml中为 Tempo 配置了抓取任务(job_name: 'tempo',targets 为tempo:3200),使 Prometheus 除了接收 remote write 的 span metrics 外,还能直接抓取 Tempo 自身的运行指标。
用 Loki 查看与分析日志
Windmill 的日志会被发送到Loki——一款与 Grafana 无缝集成的日志聚合系统。在 Grafana 中查看日志的步骤:
- 打开 Grafana UI(通常为
http://localhost:3000); - 进入"Explore"区域;
- 选择Loki数据源;
- 使用查询编辑器基于各种标签与字段过滤、检索日志。
本示例的 Loki 通过 OTLP 端点(loki:3100/otlp)接收日志,loki-config.yaml 采用单机本地文件存储(schema: v13、store: tsdb),并开启了allow_structured_metadata: true以保留结构化元数据——这正是 OTLP 日志中携带的 trace/span 上下文、Windmill 标签等结构化信息能够被检索的前提。由于 Collector 在日志管道中保留了 OTLP 语义,来自 Windmill 的日志与 trace 天然共享同一套标签体系。
日志 × 追踪 × 指标:三者的关联闭环
这套方案真正的价值在于关联:
- 日志 ↔ 追踪:Windmill 的日志与 span 共享标签体系(如
job_id、workspace_id),且 backend/windmill-api/src/tracing_init.rs 的中间件会把请求trace_id注入日志上下文;Tempo 的 exemplar 则实现了指标 → trace 的反向跳转。这意味着当你在 Loki 中定位到一条异常日志,可以顺藤摸瓜找到对应的 trace;反之亦然。 - 指标 ↔ 追踪:Prometheus 中的 span metrics 携带 exemplar,可直接下钻到原始 trace,用于复盘"延迟升高期间到底发生了什么"。
对排查与性能分析而言,这构成了一个完整闭环:Grafana 中看指标(整体趋势与异常)→ 经 exemplar 跳到 Tempo 看 trace(定位到具体作业/步骤)→ 再跳到 Loki 看日志(还原执行细节)。
从示例到生产:需要关注的事项
基于仓库内容,部署到生产环境前建议关注以下几点:
- 数据保留期:示例中 Tempo 的
block_retention为1h,属演示配置,生产应按合规与排查需求调大,并考虑将storage.trace.backend从local换为 S3/GCS 等对象存储; - Tempo 存储:当前使用本地文件系统,单机演示足够;大规模场景下分布式后端更合适;
- Loki 存储:示例使用本地文件系统与
tsdbschema,生产可切换为对象存储并部署多副本; - Grafana 安全:示例开启了匿名访问且匿名角色为 Admin,仅适用于本地演示,生产必须关闭并配置认证与权限;
- 权限/镜像选择:docker-compose 通过
${WM_IMAGE}变量指定 Windmill 镜像,并在注释中提示 EE 版本使用ghcr.io/windmill-labs/windmill-ee:main;若需要完整的 OTLP 根作业/traceparent 关联能力,可结合 EE 版本评估(OSS 构建中相关函数为空实现,见 backend/windmill-worker/src/otel_oss.rs); - 敏感环境变量:
OTEL_EXPORTER_OTLP_*HEADERS不会被透传到 Worker,如需携带认证头需自行规划安全通道。
总结
通过 examples/deploy/otel-tracing-grafana 目录下的这份示例,你可以用一条命令得到一套"开箱即用"的 Windmill 可观测性栈:OpenTelemetry Collector 负责接收与分流,Tempo 负责 trace 存储与查询,metrics generator 把 span 转成 Prometheus 指标,Loki 承接日志,Grafana 统一呈现。Windmill 在每个 job span 上注入的丰富标签(job_id、root_job、parent_job、flow_step_id、script_path、workspace_id、worker_id、language、tag)与源码级实现(backend/windmill-worker/src/worker.rs)完全对应,让分布式追踪真正落地到"脚本 → 工作流 → 步骤"的每一层,帮助你在生产环境中快速定位故障、量化瓶颈并持续优化工作流性能。
【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考