Grafana Tempo TraceQL 语言设计解析:Spanset、管道与结构运算符
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
TraceQL 是 Grafana Tempo 为"选择 trace"而设计的专用查询语言。本文以仓库中的设计文档 2022-04 TraceQL Concepts 为核心骨架,结合 2023-11 TraceQL Extensions 与 pkg/traceql 下的引擎源码,系统讲解 TraceQL 的核心概念:spanset 选择、intrinsic 与 attribute 字段、逻辑/结构运算符、聚合器、管道与分组。读完本文,你将理解 TraceQL 查询的求值模型(逐 trace 求值、spanset 缩减),掌握从"选单个 span"到"对 span 集合做聚合与分组"的完整语法,并能在 Tempo 中直接写出可用的 TraceQL 查询。
说明:本文对应的设计文档是 Tempo 2.0 引入 TraceQL 前的概念提案,文档中的示例大量使用当时尚未加作用域前缀的"遗留语法"(如
.http.status、duration)。这些写法在语言演进中保留为向后兼容的 legacy 形式,本文在保留原文档示例的同时,会给出当前仓库文档所用的等价写法,方便你在真实环境中验证。
一、TraceQL 的能力边界:能查什么
设计文档在 Capabilities 一节明确给出 TraceQL 查询可以基于三类信息选择 trace:
- span 属性(attributes)、时间与持续时间(timing and duration);
- span 之间的结构关系(structural relationships);
- 对单个 trace 内 span 集合的聚合数据(aggregated data)。
设计文档同时声明了一个重要原则:TraceQL 在设计上尽可能复用 PromQL 与 LogQL 的语法和语义,因为二者的用户基数大、心智模型成熟;但由于 trace 数据是树状结构(有根、分支、叶子,任意位置的键值对,以及时间戳),TraceQL 的语法与语义必然因"查询 trace"这一专门需求而有所不同。当前官方文档 TraceQL 总览 也确认了这一点:"TraceQL uses similar syntax and semantics as PromQL and LogQL, where possible."
需要强调的是,这份设计文档不是完整的语言规范,而是向社区征求意见的概念框架。后续的语言细化工作由 2023-11 TraceQL Extensions 等提案接力完成,本文末尾会作补充介绍。
二、查询结构:一次只求值一个 trace 的管道表达式
设计文档给出 TraceQL 最核心的结构定义:
A query is an expression that is evaluated on one trace at a time. The query is structured as a set of chained expressions (a pipeline).
即:一条查询是对单个 trace 依次求值的表达式,整体被组织为一系列链式表达式(管道 pipeline)。每个管道表达式都会从结果集中"选择或丢弃 spanset"。文档给出的原型示例为:
{ .http.status = 200 } | by(.namespace) | count() > 3求值语义可以概括为:
- 花括号
{}从当前 trace 中选出一组 span(spanset); - 管道符
|把上一级产生的 spanset 送入下一级表达式(by(.namespace)做分组、count() > 3做聚合过滤); - 如果某个 trace 经过整条管道求值后产出了一个 spanset,那么这个 spanset(连同其所在的 trace)就进入查询的结果集;否则该 trace 被丢弃。
从当前源码看,这一"管道"模型被原样保留并实现。pkg/traceql/ast.go 中定义了PipelineElement接口,任何管道元素都必须实现两个方法:
type PipelineElement interface { Element extractConditions(request *FetchSpansRequest) evaluate([]*Spanset) ([]*Spanset, error) }extractConditions把查询中可下推的条件拍平成存储层可执行的扁平条件(fetch spans request),evaluate则在内存中对 spanset 做真正的求值——这正是 architecture 文档 所描述的引擎职责:"Parses incoming requests and extract flattened conditions the storage layer can work with; Pulls spansets from the storage layer and revalidates that the query matches each span."(注意该文档已被标记为 out of date 并隐藏,仅供理解引擎的分工。)
三、选择 span:花括号与条件
在 TraceQL 中,花括号{}永远表示"从当前 trace 选择一组 span",通常与一个条件配对使用来缩减传入的 span:
{ .http.status = 200 }这条最简单的查询会对每个 trace 的每个 span 逐一求值:
- 如果被求值的 trace 中没有任何 span 带有值为
200的http.status属性,则没有 span 被选中,该 trace 不会出现在结果集中; - 如果 trace 中确实存在这样的 span,则只有这些匹配的 span 会被返回,即 trace 被缩减为满足花括号内条件的 span 子集,结果集只包含这个子集。
四、字段类型:Intrinsic 字段与 Attribute 字段
设计文档将 span 上可引用的字段分为两大类,这一划分在当前的 construct-traceql-queries 官方指南中仍然是基础概念。
4.1 Intrinsic 字段(固有字段)
每个 span 都有一些"与生俱来"的固有字段,设计文档给出如下表格:
| 字段名 | 说明 |
|---|---|
duration | span 的结束时间减开始时间(end - start) |
name | 操作名或 span 名(operation or span name) |
status | 状态值,取值 error、ok 或 unset |
parent | 当前 span 的父 span |
示例:
{ duration > 2s } // 查找包含持续时间超过 2 秒的 span 的 trace { name = "HTTP POST" } // 查找包含名为 "HTTP POST" 的 span 的 trace需要说明的是,设计文档中的duration、name、status属于 legacy(遗留)写法,在 TraceQL Extensions 设计文档 中被称为 "Legacy Intrinsics",官方承诺"当前没有移除计划",但所有新 intrinsic 只提供带作用域前缀的形式。等价的作用域写法是span:duration、span:name、span:status,例如:
{ span:duration > 2s } { span:name = "HTTP POST" }4.2 Attribute 字段(动态属性)
除了 intrinsic,还可以引用 span 或 span 所属 resource 上的动态属性(即通常所说的 tag)。设计文档示例:
{ .http.method = "GET" } // 查找使用 GET HTTP 方法的 trace { .namespace = "prod" } // 查找经过 prod 命名空间的 trace { parent.service.name != .service.name } // 查找跨服务边界的 trace其中第三条示例parent.service.name != .service.name展示了 attribute 与parentintrinsic 的组合用法:当某个 span 的服务名与其父 span 的服务名不同,就说明 trace 在这里跨越了服务边界。
同样,.前缀是遗留写法(默认指 span 属性),当前推荐使用显式作用域(见下一节)。
4.3 作用域属性(Scoped attribute fields)
设计文档特别强调:属性可以被显式限定为span作用域或resource作用域,这样做"可以带来显著的性能收益"(result in significant performance benefits),因为 Tempo 只需要扫描你关心的那部分数据:
{ span.http.status = 200 } { resource.namespace = "prod" }官方文档 construct-traceql-queries 对此给出了更完整的落地方案:attribute 以.分隔作用域与字段名(span.http、resource.namespace、event.exception.message、link.opentracing.ref_type、instrumentation.language),而 intrinsic 用:分隔(span:name)。当前仓库支持的五种 attribute 作用域为:span、resource、event、link、instrumentation scope。
4.4 字段表达式(Field expressions)
字段之间还可以按预期方式组合成表达式。设计文档给出两个示例:
{ parent.duration - duration > 500ms } // 父子 span 持续时间之差超过 500ms { .http.status >= 200 && .http.status < 300 } // "成功"的 HTTP 状态码设计文档对第二条注释了一句关键语义:花括号内的整个表达式必须在单个 span 上求值为真,该 span 才会进入结果集——两个条件必须同时命中同一个 span,而不是分散在不同 span 上。这一"同 span"语义是理解 TraceQL 与后面"组合 spanset"(跨 span 逻辑)之间差异的基石。
五、组合 Spanset:逻辑运算符与结构运算符
5.1 逻辑运算符
设计文档指出,逻辑运算符用于组合多个 span 集合。例如要找到一条同时经过两个特定 region 的 trace:
{ .region = "eu-west-0" } && { .region = "eu-west-1" }注意它与下面这条的本质区别:
{ .region = "eu-west-0" && .region = "eu-west-1" }第二条不会返回任何 trace——因为单个 span 不可能同时把region属性既设为eu-west-0又设为eu-west-1。前者是两个 spanset 之间的"与"(trace 中至少有一个 span 匹配左边、且至少有一个 span 匹配右边),后者是单 span 内的"与"。当前仓库文档对&&与||的表述为:{condA} && {condB}检查两个条件都找到匹配;{condA} || {condB}作为并集(OR)检查任一条件找到匹配。
5.2 结构运算符(Structural Operators)
结构运算符基于 span 树中的 span 关系来评估 trace,这是 TraceQL 区别于 PromQL/LogQL 的核心特色。设计文档定义了三个原型运算符:
| 运算符 | 名称 | 语义 |
|---|---|---|
{ } >> { } | 后代运算符(descendant) | 返回匹配右侧条件的 span,且这些 span 是匹配左侧条件 span 的后代 |
{ } > { } | 子运算符(child) | 返回匹配右侧条件的 span,且这些 span 是匹配左侧条件 span 的直接子节点 |
{ } ~ { } | 兄弟运算符(sibling) | 返回匹配右侧条件的 span,且这些 span 与匹配左侧条件的 span 互为兄弟 |
当前仓库在 pkg/traceql 的运算符枚举与 官方文档 中已把结构运算符扩展到完整矩阵:后代>>、祖先<<、子>、父<、兄弟~,以及对应的否定形式(!>>、!<<、!>、!<、!~,标记为 experimental,可能产生误报)和并集形式(&>>、&<<、&>、&<、&~,同时返回两侧匹配的 span)。官方文档同时给出一个重要补充规则:结构运算符总是返回运算符右侧匹配的 span。
设计文档中关于结构运算符的示例没有给出具体查询,但当前官方文档提供了可直接验证的实例:
{ resource.service.name="frontend" } >> { status = error } // frontend 或其后代服务中出现错误 { resource.service.name = "productcatalogservice" } ~ { resource.service.name="frontend" } // 两服务互为兄弟 { } !< { resource.service.name = "foo" } // foo 服务中的叶子 span六、聚合器(Aggregators)
前面所有表达式都在回答"单个 span"的问题;当需要针对一组 span提问时,就要使用聚合函数。设计文档给出两个核心示例:
count() > 10 // 查找 span 总数大于 10 的 trace avg(duration) > 1s // 查找平均持续时间大于 1 秒的 trace当前仓库的官方文档把聚合器扩充为五类:count(spanset 中的 span 数)、avg(数值型属性或 intrinsic 的平均值)、max(最大值)、min(最小值)、sum(总和)。一个官方文档示例:
count() > 10 avg(span:duration) > 20ms { } | sum(span.bytesProcessed) > 1000000000 // 假设的 bytesProcessed 属性总和超过 1GB七、表达式管道(Expression Pipelining)
管道(|)允许把一个表达式产生的 span 集合"灌入"下一个表达式,这在希望对 trace 的某个子集做聚合时特别有用。设计文档的经典示例:
{ .http.status = 200 } | count() > 3含义:先筛选出所有http.status = 200的 span,再统计这批 span 的数量——查找"拥有超过 3 个 200 状态 span"的 trace。注意如果没有管道,count()是作用在整个 trace 的全部 span 上的;有了管道,聚合就被限制在花括号筛选出的子集内。这是管道与单纯聚合的核心区别。
八、分组(Grouping)
分组允许把一条 trace 拆成若干 span 集合,交给后续管道条目逐一独立求值。设计文档强调:"Each set of spans created by the group isindividuallyevaluated by downstream expressions."
by(.region) | count() > 5含义:按region属性把 trace 内的 span 分组,然后每个分组各自统计数量——查找"在任意一个 region 中都有超过 5 个 span"的 trace。官方文档的同类示例(单服务出现多个错误):
{ status = error } | by(resource.service.name) | count() > 1九、设计文档示例全集
设计文档在 Examples 一节给出了十组经过精心设计、由浅入深的完整示例,覆盖前面所有概念,逐条完整列出如下:
任何 span 匹配某属性:
{ .namespace = "prod" }两个属性出现在同一 span 上:
{ .namespace = "prod" && .http.status = 200 }两个属性出现在 trace 内任意位置(可不同 span):
{ .namespace = "prod" } && { .http.status = 200 }任何 span 持续时间超过 1 秒:
{ duration > 1s }trace 整体持续时间超过 1 秒(利用聚合表达式):
max(end) - min(start) > 1sspan 平均持续时间超过 1 秒,且存在带某属性的 span:
avg(duration) > 1s && { .namespace = "prod" }一条 trace 在任意命名空间内拥有超过 5 个 http.status=200 的 span(分组+管道+聚合的组合):
{ .http.status = 200 } | by(.namespace) | count() > 5trace 按特定顺序经过两个 region(结构运算符):
{ .region = "eu-west-0" } >> { .region = "eu-west-1" }trace 在任意两个服务之间传递时出现超过 1 秒的网络延迟:
{ parent.service.name != .service.name } | max(parent.duration - duration) > 1s这组示例的价值在于:它们从"单 span 过滤"逐步过渡到"聚合、分组、管道、结构关系",恰好构成了 TraceQL 语言能力的完整演示路径。当前官方文档 construct-traceql-queries 还补充了针对具体场景的写法,例如按操作名与服务名过滤:
{resource.service.name = "frontend" && name = "POST /api/orders"}以及使用trace:durationintrinsic(比聚合表达式更快):
{ trace:duration > 5s }十、从设计到实现:引擎源码佐证
设计文档提出的概念在当前仓库的 pkg/traceql 包中有着完整对应实现:
- 编译入口:pkg/traceql/engine.go 的
Compile函数依次完成Parse(词法/语法解析)、expr.validate()(语义校验)、expr.SinglePipeline()(提取单管道)、expr.extractConditions(req)(抽取可下推条件,返回FetchSpansRequest),最终得到求值函数p.evaluate。设计文档中"把查询翻译成存储层可执行的扁平条件"的思想在此落地为FetchSpansRequest。 - 求值执行:pkg/traceql/engine.go 的
ExecuteSearch接收SearchRequest与SpansetFetcher,编译查询后从存储层拉取 spanset 并逐 span 重新校验匹配,与设计文档"Pulls spansets from the storage layer and revalidates"的描述一致。 - 管道元素抽象:pkg/traceql/ast.go 的
PipelineElement接口统一了所有管道元素的"条件抽取 + 求值"两个阶段;pkg/traceql/ast.go 的NeedsFullTrace会识别哪些元素(如SpansetOperation结构运算符、Aggregate聚合器)必须拿到完整 trace 才能求值——这正是结构运算符需要整棵树、而简单属性过滤可以下推存储层的原因。 - 指标类管道:pkg/traceql/ast_metrics.go 实现了
rate() by (...)、topk()等 trace 指标化管道元素,对应设计文档 Summary 中"未来将从 trace 推导指标"的展望——该展望已由 TraceQL metrics 功能落地。
十一、语言演进:TraceQL Extensions 补充
设计文档开篇就声明"这不是完整语言规范",后续细化由 2023-11 TraceQL Extensions 承接,该提案为语言补充了四类能力,可作为理解概念文档的延伸:
- 属性名转义:允许用双引号包裹属性名以支持空格、数学符号等 Unicode 字符,如
{ span."attribute with spaces" = "foo" },并支持\"与\\两种转义序列; - 新增作用域:
trace({ trace:duration > 100ms })、instrumentationscope({ scope:name ~= ".*Java.*" })、event({ event.exception.message =~ ".*Division by zero.*" })、link({ link:traceID = "<hex string>" }),其中 event/link 由于"一个 span 可有多个事件/链接",设计上刻意保守; - 作用域化 intrinsic:统一用
:分隔作用域与 intrinsic(如span:name是 intrinsic,span.name是同名属性),并给出完整的 intrinsic 表(trace:duration、trace:rootName、span:childCount、event:name、link:traceID、parent:id等),设计文档中的 legacy intrinsic(duration、name、status)继续保留; - 新数据类型:数组(用
[0]访问指定元素、用[]匹配任意元素)与 ID 类型(如{ span.id = "8bf5306cb6a28" },只支持=/!=,比较时忽略前导 0)。
十二、在哪里运行 TraceQL
设计文档只定义语言概念,实际使用方式在当前官方文档中有明确说明(TraceQL 总览):
- 命令行:通过 Tempo 的 API 以搜索请求方式执行;
- Grafana:在 Tempo 数据源的 Explore 查询编辑器/查询构建器中使用 TraceQL;
- 前置条件:TraceQL 依赖 Parquet 列式存储格式(Tempo 的默认块格式),相关说明见 Apache Parquet 后端 文档。
结语
TraceQL Concepts 设计文档用极简的概念集合(spanset、花括号选择、intrinsic/attribute 字段、逻辑与结构运算符、聚合、管道、分组)定义了 Tempo 查询语言的全部骨架,而 pkg/traceql 的源码与 TraceQL Extensions 提案把这一骨架细化成了可运行、可扩展的完整语言。理解"对单个 trace 逐条求值、以 spanset 为单位流转、结构运算符永远返回右侧匹配"这三条核心语义,就能举一反三地构造出从简单属性过滤到跨服务结构分析再到 trace 指标化的各种查询。
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考