Grafana Tempo TraceQL 语言设计解析:Spanset、管道与结构运算符
2026/9/18 11:23:12 网站建设 项目流程

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.statusduration)。这些写法在语言演进中保留为向后兼容的 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

求值语义可以概括为:

  1. 花括号{}从当前 trace 中选出一组 span(spanset);
  2. 管道符|把上一级产生的 spanset 送入下一级表达式(by(.namespace)做分组、count() > 3做聚合过滤);
  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 带有值为200http.status属性,则没有 span 被选中,该 trace 不会出现在结果集中;
  • 如果 trace 中确实存在这样的 span,则只有这些匹配的 span 会被返回,即 trace 被缩减为满足花括号内条件的 span 子集,结果集只包含这个子集。

四、字段类型:Intrinsic 字段与 Attribute 字段

设计文档将 span 上可引用的字段分为两大类,这一划分在当前的 construct-traceql-queries 官方指南中仍然是基础概念。

4.1 Intrinsic 字段(固有字段)

每个 span 都有一些"与生俱来"的固有字段,设计文档给出如下表格:

字段名说明
durationspan 的结束时间减开始时间(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

需要说明的是,设计文档中的durationnamestatus属于 legacy(遗留)写法,在 TraceQL Extensions 设计文档 中被称为 "Legacy Intrinsics",官方承诺"当前没有移除计划",但所有新 intrinsic 只提供带作用域前缀的形式。等价的作用域写法是span:durationspan:namespan: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.httpresource.namespaceevent.exception.messagelink.opentracing.ref_typeinstrumentation.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) > 1s

span 平均持续时间超过 1 秒,且存在带某属性的 span:

avg(duration) > 1s && { .namespace = "prod" }

一条 trace 在任意命名空间内拥有超过 5 个 http.status=200 的 span(分组+管道+聚合的组合):

{ .http.status = 200 } | by(.namespace) | count() > 5

trace 按特定顺序经过两个 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接收SearchRequestSpansetFetcher,编译查询后从存储层拉取 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 承接,该提案为语言补充了四类能力,可作为理解概念文档的延伸:

  1. 属性名转义:允许用双引号包裹属性名以支持空格、数学符号等 Unicode 字符,如{ span."attribute with spaces" = "foo" },并支持\"\\两种转义序列;
  2. 新增作用域trace{ trace:duration > 100ms })、instrumentationscope{ scope:name ~= ".*Java.*" })、event{ event.exception.message =~ ".*Division by zero.*" })、link{ link:traceID = "<hex string>" }),其中 event/link 由于"一个 span 可有多个事件/链接",设计上刻意保守;
  3. 作用域化 intrinsic:统一用:分隔作用域与 intrinsic(如span:name是 intrinsic,span.name是同名属性),并给出完整的 intrinsic 表(trace:durationtrace:rootNamespan:childCountevent:namelink:traceIDparent:id等),设计文档中的 legacy intrinsic(durationnamestatus)继续保留;
  4. 新数据类型:数组(用[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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询