从OpenTelemetry到DuckDB:用SQL即时分析遥测数据
可观测性平台通常希望你就在平台内部完成分析,使用它们自己的查询语言和仪表盘。对于你已经知道要问的问题,这很好用;但对于临时性的问题,就不那么顺手了:快速算一个百分位数、按状态码透视,或者看到数据后突然想问“到底是哪个服务真正吃掉了延迟预算?”。
这篇文章介绍了一条用于回答这类问题的小型流水线。一端是 DuckDB,这是一个进程内分析型数据库,可以直接读取 CSV、JSON 和 Parquet;另一端是 Dash0 CLI,它可以从 Dash0 拉取 OpenTelemetry 的 span、日志、指标和 trace,并以 CSV 或 JSON 格式输出。
两者可以在命令行中用管道连接起来:
这样就能对生产环境遥测数据直接执行 SQL,无需中间文件,也不需要导入步骤。
本文其余部分会介绍如何完成设置,以及搭建之后可以做哪些事情。
为什么选择 DuckDB
DuckDB 常被形容为“面向分析的 SQLite”。这个类比是合理的:它只有一个依赖、在进程内运行、不需要服务器(除非你确实想要服务器模式)。不过,这里真正关键的特性是,DuckDB 将文件和流都视为表。你可以让它指向磁盘上的 CSV、S3 中的 Parquet 文件,或者标准输入,然后 直接 查询。
这使得它非常适合承接命令行输出。任何能输出 CSV 或 JSON 的工具——kubectl、aws、gh、ps、日志文件,或者像 dash0 这样的 CLI——都可以用 SQL 查询,而无需自己编写解析器。
说明: 对于正在 Foojay 上阅读本文的 JVM 开发者(Java、Kotlin)来说,好消息是 DuckDB 也提供了 JDBC 驱动。本文中的 SQL 可以原封不动地迁移到 Java 服务中。这条管道适合探索性分析;而当你希望把找到的查询固化下来,并在 Java 代码中定期运行时,JDBC 才是合适的载体。
准备工作
你需要在本机安装两个二进制文件。两者都不需要服务器、容器或配置文件。
1. 安装 DuckDB
在 macOS 和 Linux 上,用 Homebrew 最方便:
DuckDB 安装页面 也提供了各大平台的独立二进制文件。请确认它能正常工作:
2. 安装 Dash0 CLI
Dash0 CLI 以 Homebrew cask 的形式分发:
这种全限定名称会自动指向对应仓库,因此无需单独执行 brew tap。GitHub Releases 二进制、FROM scratch Docker 镜像以及 Nix flake 都记录在安装指南中。请确认:
3. 身份验证
交互式登录使用 OAuth:
对于脚本和 CI,可以改用环境变量设置连接信息。每个配置项的解析顺序为:环境变量、命令行标志、当前 profile。
运行下面的命令,确认凭据和数据集都按预期解析:
从 Dash0 导出 Span
所有 dash0 命令都支持 JSON 输出,并提供与 Dash0 UI 一致的过滤语法。如果它检测到环境中运行的是 AI 编码代理,就会默认输出 JSON、跳过确认提示并关闭颜色。对本流水线来说,关键的标志是 -o csv。
下面是一个查询最近 30 分钟 span 并以 CSV 输出的例子:
输出效果类似这样:
你可以使用与 UI 相同的过滤器来缩小范围——--filter "service.name is checkout"、--filter "otel.span.status.code is ERROR"——不过在探索阶段,通常更简单的做法是拉取一个较宽的时间窗口,然后在 SQL 里做切片。
试试 DuckDB
下面把 span 查询结果通过管道送入 DuckDB,生成每个服务的请求数、错误数和一张小型 ASCII 柱状图:
这里有几个值得注意的地方:
read_csv('/dev/stdin')会把管道传入的流当作表读取,并从表头推断列名和类型。这里不需要CREATE TABLE。- 带点的列名(如
service.name)来自 OpenTelemetry 的语义约定。在 SQL 中必须用双引号括起来("service.name"),否则点号会被解析为 schema 或表限定符。 FILTER (WHERE ...)和bar()都是 DuckDB 的标准功能。
持久化处理
如果要做的不只是一次性查询,更便捷的做法是先导出一次,再把数据加载到 DuckDB 文件中。首先,把 CSV 写下来:
然后创建一个带数值型时长列的表。CLI 会把时长格式化为类似 150ms 或 2.49s 的字符串,因此需要先把它们归一化为毫秒,之后才能进行算术运算:
运行 duckdb /tmp/spans.duckdb,然后把上面的 SQL 粘贴进去。下面所有查询都会作用于生成的 spans 表。
五个实用查询
1. 每个服务的黄金指标
每个服务的延迟百分位数和错误率,一条查询就能搞定:
quantile_cont 返回连续型(插值)百分位数;FILTER (WHERE ...) 则在同一趟扫描中统计错误数。在示例数据中,checkout 的 p99 为 2.46 秒,错误率为 20%。
2. 延迟直方图
把时长分桶,并用 bar() 绘制出来:
这个分布呈双峰形态:大多数请求落在 10 到 500 毫秒之间,另一组则超过 1 秒。无论是平均值还是中位数,都无法展现出这种特征。
3. 按状态进行透视
DuckDB 把 PIVOT 作为关键字支持。下面的查询会把状态值转换为列:
4. 每个服务占用的总延迟比例
这里对聚合结果使用了窗口函数:sum(sum(dur_ms)) OVER () 可以在分组查询内得到总计,因此只需一趟就能算出每个服务的占比:
checkout 用 94 个 span 中的 15 个,就占了总 span 时间的 63.6%。如果要优化一件事,这就是应该开始的地方。
5. 转换为 Parquet
DuckDB 原生支持读写 Parquet:
在这样的小样本上,Parquet 文件大约只有 CSV 的一半大小。在真实数据规模下,压缩比会更好,因为遥测数据有大量重复值,而列式存储配合字典编码能很好地压缩这类数据。生成的 Parquet 文件也可以直接查询:
把一天的 span 数据归档为 Parquet,可以形成一个廉价的冷存储层;任何 DuckDB 客户端——包括通过 JDBC 连接的 JVM 应用——之后都可以查询这些数据。
为什么不把 DuckDB 嵌入 CLI?
理论上可以把 DuckDB 直接构建进 dash0,让 CLI 自行运行这种分析。但这也许并不是个好主意。
Dash0 CLI 以禁用 CGO 的静态二进制构建,以不含 shell 或 libc 的 FROM scratch Docker 镜像发布,并可交叉编译到五种架构。与此同时,DuckDB 是 C++ 库,而且它的 Go 驱动依赖 CGO。如果内嵌 DuckDB,就意味着必须放弃静态链接,镜像体积会大得多,交叉编译也会更复杂。
而且,如上所示,管道本身已经完成了这项工作。CLI 输出 CSV 和 JSON,DuckDB 负责查询;两者彼此无需了解对方。
这个道理同样适用于任何能输出结构化数据的工具。一旦你习惯了把输出接到 DuckDB 中,就很难再回到 awk 了。
总结
从 dash0 login 到每个服务的延迟拆分,只需要几分钟的设置和一条管道。
由于 DuckDB 把标准输入、文件和 Parquet 都当作表来读取,它可以充当命令行上几乎所有内容的 SQL 查询层;Dash0 CLI 输出的 OpenTelemetry 数据正好是它的一个很好的应用场景。
文章《From OpenTelemetry to DuckDB》最初发布于 foojay。