Grafana Tempo 中的 OpenTelemetry Transformation Language(OTTL):语句、条件、路径与实战过滤
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
OpenTelemetry Transformation Language(OTTL)是 OpenTelemetry Collector 生态中的一种小型领域特定语言(DSL),用于以 OpenTelemetry 原生概念处理遥测数据。本文以 Tempo 仓库内置的 OTTL 包(vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl)为骨架,系统讲解 OTTL 的语句结构、路径体系、语法要素与调试手段,并结合 Tempo 中真实使用 OTTL 的转发过滤链路(modules/distributor/forwarder)给出可复制、可运行的实战配置。读完本文,你将掌握编写 OTTL 语句与条件、理解各信号 Path 上下文、以及利用 OTTL 在 Tempo 中按需过滤 Span 的方法。
OTTL 是什么
OTTL(OpenTelemetry Transformation Language)是一种小型、领域特定的编程语言,其设计目标是用 OpenTelemetry 原生概念与构造来处理遥测数据。它不是一个通用编程语言,而是专门服务于遥测数据的**变更(mutation)与生成(generation)**场景。
当前仓库以 vendor 形式内置了该语言完整实现,位于vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/,其中包括:
parser.go:OTTL 语句与条件的解析入口;grammar.go、LANGUAGE.md:语言文法与完整语法说明;paths.go、contexts/:路径(Path)解析与各信号上下文;ottlfuncs/:预置的函数库(Editors 与 Converters);expression.go、factory.go、functions.go:表达式、工厂与函数注册机制。
Tempo 自己并不重新发明 OTTL,而是在转发器模块中直接引用该包("github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"),将其作为 Span 过滤条件的执行引擎。因此,理解 OTTL 语法,是正确配置 Tempo 转发过滤的前提。
OTTL 语句的两要素
一个 OTTL 语句(Statement)由两部分组成:
- 一个函数(Function):对遥测数据进行变换;
- 一个可选的条件(Condition):决定函数是否执行。
官方 README 给出的经典示例:
set(span.attributes["test"], "pass") where span.attributes["test"] == nil- 函数部分是
set(span.attributes["test"], "pass"):set用第二个参数的值设置第一个参数指向的字段; - 条件部分是
where span.attributes["test"] == nil:仅当该 Span 尚不存在名为"test"的属性时才执行设置。
函数(Editors 与 Converters)
在 OTTL 文法中,函数分为两类:
- Editors(编辑器):直接变换底层遥测数据,通常不返回值。语句中必须有且仅有一个 Editor 调用(见
LANGUAGE.md的 Editors 一节)。编辑器标识符必须以小写字母开头; - Converters(转换器):在把数据作为函数参数或用于布尔表达式之前,将数据转换为新格式,可返回任意类型。转换器标识符必须以大写字母开头,且后面可以追加零个或多个字符串键(
["key"])或整数键([0])进行索引。
例如Int()、IsMatch(field, ".*")、Split(field, ",")[1]都是转换器。当对返回值进行索引时,OTTL 支持pcommon.Map/map[string]any用字符串键索引、pcommon.Slice/[]any用整数键索引;若返回值不支持索引或键类型不匹配,OTTL 会报错。
需要特别强调:OTTL 没有内置的 Editors 或 Converters。调用方必须提供一个"标识符到函数实现"的映射表,OTTL 在执行语句时根据该映射调用对应实现。预置函数库位于vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs/,大多数 Collector 组件中的 OTTL 语句都使用这套函数。
函数参数类型
OTTL 函数单值参数支持Setter、Getter、GetSetter、PMapGetter、FloatGetter、FloatLikeGetter、StringGetter、StringLikeGetter、IntGetter、IntLikeGetter、BoolGetter、BoolLikeGetter、ByteSliceLikeGetter、Enum以及 Go 原生类型string、float64、int64、bool等。切片参数则支持各类*Getter与string、float64、int64、uint8(字节切片字面量按字节切片解析)等。
参数可按需设为可选:使用Optional[T]包装底层类型(如Optional[string]),且所有可选参数必须排在必选参数之后。参数可以按Arguments结构体中定义的顺序传参,也可以使用命名参数(名称是结构体字段名的 snake_case 形式)以任意顺序传参;未命名时,跳过某个可选参数必须把其前面的可选参数一并传完。
通过 Path 访问遥测数据
语句内部通过OTTL Paths访问遥测字段。Path 由小写标识符、点(.)以及方括号中的字符串键或整数键组成,例如:
metric.name span.value_double resource.name resource.attributes["key"] log.attributes["nested"]["values"] datapoint.cache["slice"][1]Path 的语义约定如下:
- 标识符映射到遥测字段;
- 点(
.)用于分隔嵌套字段,首个 Path 段被 OTTL 解释为上下文标识符; - 方括号与键(
["key"]、[0])用于访问 map/slice 中的值。
当访问 map 中不存在的键时返回nil,因此可以用attributes["custom-attr"] != nil这样的布尔表达式检查键是否存在。
Path 的解释不由 OTTL 实现,而是由调用方提供PathExpressionParser完成。这个解析器所在的包通常称为"上下文(Context)"。
各信号上下文与 Path 列表
针对每一种 OpenTelemetry 信号,仓库都提供了对应的 OTTL 上下文实现,位于vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/下:
| 遥测信号 | OTTL 上下文目录 |
|---|---|
| Resource(资源) | contexts/ottlresource |
| Instrumentation Scope(插桩作用域) | contexts/ottlscope |
| Span(跨度) | contexts/ottlspan |
| Span Event(跨度事件) | contexts/ottlspanevent |
| Metric(指标) | contexts/ottlmetric |
| Datapoint(数据点) | contexts/ottldatapoint |
| Log(日志) | contexts/ottllog |
| Profile(性能剖析) | contexts/ottlprofile |
例如contexts/ottlspan/span.go定义了 Span 上下文的PathExpressionParser,使得span.attributes["key"]、span.name、span.status.code等路径可以被正确解析。OTTL 当前不支持跨信号交互,所以不能写出把日志体塞进 Span 属性这样的语句:
set(span.attributes["log body"], log.body)OTTL 语法要素详解
LANGUAGE.md(位于vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/LANGUAGE.md)完整描述了 OTTL 文法。除 Path 外,Value 还可以是以下形式。
字面量(Literals)
- 字符串:双引号包裹,如
"a string"; - 整数:任意数字,可带
+/-前缀,内部统一为int64; - 浮点数:数字加小数点,可带符号,内部统一为
float64,如1.5、-.5; - 布尔:精确字符串
true/false; - 空值:精确字符串
nil; - 字节切片:以
0x开头的十六进制串,如0x0001。
列表与映射
列表(List)由逗号分隔的值序列组成,仅能在函数参数或条件中使用,语法不提供对单个元素的访问器:
[] [1] ["1", "2", "3"] ["a", attributes["key"], Concat(["a", "b"], "-")]映射(Map)是键值对的集合,值可以是嵌套映射或表达式:
{} {"foo": "bar"} {"foo": {"a": 2}} {"foo": {"a": attributes["key"]}}枚举(Enums)
枚举是大写标识符,在解析阶段被EnumParser转换为int64(数值在解析时而非执行时确定),因此枚举符号可以当作整数使用。若 OTTL 函数需要接收枚举参数,参数类型必须声明为Enum而非int64。
数学表达式(Math Expressions)
数学表达式支持+、-、*、/以及括号分组,可作用于int64、float64、time.Time与time.Duration:
time.Time - time.Time得到time.Duration;time.Duration ± time.Time得到time.Time;time.Time - time.Duration得到time.Time;time.Duration ± time.Duration得到time.Duration。
注意事项:*与/优先级高于+与-;时间类型只能使用+/-;int64与float64混用会报错;除零会以错误形式被优雅处理;整数除法遵循 Go 的整数除法规则。因为数学表达式可以引用 Path 与 Converter,所以它们在数据处理期间求值——这也意味着函数若想接受数学表达式作为参数,必须声明为Getter类型。
示例:
1 + 1 end_time_unix_nano - end_time_unix_nano sum([1, 2, 3, 4]) + (10 / 1) - 1布尔表达式(Boolean Expressions)
布尔表达式以字面量where开头,后接一个或多个布尔值,决定 Editor 是否执行;它总是求值为 true 或 false。多个布尔可用and、or连接,用not取反。优先级从高到低为:not>and>or,可用括号覆盖优先级。
布尔值可以是:字面量布尔值、返回布尔值的 Converter,或由左值、运算符、右值组成的比较(Comparison)。
比较运算符包括:
==相等;!=不等;<小于;>大于;<=小于等于;>=大于等于。
not取反示例:
not true not name == "foo" not (IsMatch(name, "http_.*") and kind > 0)比较规则(Comparison Rules)
两个值比较时,若数值类型不同则按float64比较;数值与字符串的比较遵循 Go 的带符号比较规则;对布尔值,false小于true。非基本类型的值只允许==与!=,使用 Go 标准运算符实现。nil字节数组与nil等价;time.Time相等性用time.Equal(),time.Duration用time.Before()/time.After()比较;map、[]any、pcommon.Map、pcommon.Slice等类型则分别通过reflect.DeepEqual或各自的Equal方法比较。
典型条件示例:
name == "a name" 1 < 2 attributes["custom-attr"] != nil IsMatch(resource.attributes["host.name"], "pod-*")Getter / Setter 与函数内日志
遥测数据的读写通过TransformContext(由调用方创建并在求值时传入)以及Getter、Setter、GetSetter接口完成:Getter 用于读取 Path、枚举、字面量与 Converter 解析出的值,Setter 用于更新字段值,GetSetter 同时提供读写能力。
若需在 OTTL 函数内部输出日志,只需在函数签名中加入component.TelemetrySettings类型的参数,OTTL 会把NewParser时传入的 TelemetrySettings 注入函数供其发日志。
在 Tempo 中实战:用 OTTL 过滤转发 Span
OTTL 不仅用于数据变换,也被广泛用于过滤。README 明确给出了 OTTL 的典型使用场景:用 transform processor 修改管道中的数据、用 filter processor 删除管道中的数据、用 tail sampling processor 选择采样目标、用 routing connector 在管道间路由数据。
Tempo 正是把 OTTL 用在了转发(forwarder)的过滤上:在把接收到的 Trace 转发给外部后端(如另一套 OTLP gRPC 端点)之前,先用 OTTL 条件剔除掉不需要的 Span。
转发过滤的配置结构
Tempo 转发器配置在modules/distributor/forwarder/config.go中定义:
name: <转发器名称> backend: otlpgrpc otlpgrpc: # OTLP gRPC 后端连接配置 filter: traces: span: ["<OTTL Span 条件>"] spanevent: ["<OTTL Span Event 条件>"]对应源码中的结构体:
Config:包含Name、Backend、OTLPGRPC、Filter四个字段;FilterConfig.Traces:即TraceFiltersConfig,含SpanConditions(YAML 键span)与SpanEventConditions(YAML 键spanevent)两个字符串数组。
Config.Validate()会校验转发器名称非空、backend 必须为受支持的otlpgrpc。
过滤链路的实现原理
在modules/distributor/forwarder/forwarder.go的New()中,如果配置了任何 Span 或 Span Event 条件,就会返回一个FilterForwarder:
- 通过
filterprocessor.NewFactory()创建 Collector 的 filter processor 工厂; - 用
factory.CreateDefaultConfig()得到"OTTL 函数已正确初始化"的默认配置,并将ErrorMode设为ottl.IgnoreError(出错时忽略而非丢弃数据); - 把配置中的
SpanConditions、SpanEventConditions填入filterprocessor.TraceFilters; - 将下游转发器包装成
consumerToForwarderAdapter,作为 filter processor 的消费端; - 启动 processor 后,
ForwardTraces会先把 Trace 复制一份(避免改动原始数据),再交给 filter processor 过滤,剩余数据才转发出去。
测试modules/distributor/forwarder/forwarder_test.go验证了这一行为:配置SpanConditions: []string{name == "to-filter"}时,名为"to-filter"的 Span 被剔除、"to-keep"的 Span 被保留;而当条件为name == "to-filter-1" or name == "to-filter-2"且两个 Span 都被命中时,下游转发器一次都不会被调用。
条件是如何求值的
Tempo 使用的 filter processor 位于vendor/github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor/。其internal/condition/traces.go展示了 OTTL 条件的执行链路:
- 条件被编译为
ottlspan.NewConditionSequence(...)与ottlspanevent.NewConditionSequence(...),分别处理span与spanevent条件; - 执行时对每个 Span 创建
ottlspan.NewTransformContextPtr(rs, ss, span)上下文,调用spanExpr.Eval(ctx, spanCtx)求值; - 若条件为真,则该 Span 通过
Spans().RemoveIf(...)被移除;Span Event 同理在span.Events().RemoveIf(...)中处理。
也就是说,你在配置里写的每一条span条件,本质上都是一条独立的 OTTL 布尔表达式,最终被编译为可对每个 Span 上下文求值的Condition。
与官方文档的调试方法配合
README 提供了非常实用的调试手段:当 OTTL 语句表现不符合预期时,可在 Collector 中开启 debug 日志,OTTL 会打印出当前语句/条件以及完整的TransformContext,让你准确看到 OTTL 眼中的底层数据:
service: telemetry: logs: level: debug开启后会输出类似如下的日志(parser.go中的initial TransformContext与TransformContext after statement execution):
2024-05-29T16:38:09.600-0600 debug ottl@v0.101.0/parser.go:265 initial TransformContext {"kind": "processor", "name": "transform", "pipeline": "logs", "TransformContext": {"resource": {"attributes": {}, ...}, "scope": {...}, "log_record": {...}, "cache": {}}} 2024-05-29T16:38:09.600-0600 debug ottl@v0.101.0/parser.go:268 TransformContext after statement execution {"kind": "processor", "name": "transform", "pipeline": "logs", "statement": "set(resource.attributes[\"test\"], \"pass\")", "condition matched": true, "TransformContext": {"resource": {"attributes": {"test": "pass"}, ...}}}注意该特性日志量非常大(非常 verbose),但它能提供"OTTL 如何理解底层数据"的准确视图,适合在条件不生效时定位是路径写错还是数据本身不符合预期。在 Tempo 的转发过滤场景中同样适用:把日志级别调到 debug,即可看到每条条件求值时的condition matched结果与TransformContext快照。
进阶学习路径
- 完整语法:阅读
vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/LANGUAGE.md,覆盖文法、比较规则、Getter/Setter 等全部细节; - 预置函数库:
vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/ottlfuncs/中的 Editors 与 Converters 列表(set、IsMatch、Concat、Split等); - 各信号上下文与 Path:
vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/下每个上下文目录的 README 与*.go实现; - Tempo 中的落地用法:
modules/distributor/forwarder/forwarder.go与modules/distributor/forwarder/config.go,以及modules/distributor/forwarder/forwarder_test.go中的过滤行为测试; - filter processor 内部机制:
vendor/github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor/internal/condition/traces.go,了解 Span/Span Event 条件从字符串编译到逐 Span 求值、删除的完整链路。
总结
OTTL 是一套围绕 OpenTelemetry 数据模型设计的轻量 DSL:语句 = 编辑器函数 + 可选条件;值可以是 Path、字面量、列表、映射、枚举、Converter 或数学表达式;条件则基于where引导的布尔表达式与严格定义的比较规则。Tempo 仓库将其内置为 vendor 依赖,并在 distributor 的转发过滤链路中把用户配置的 OTTL 条件编译为逐 Span 求值的 Condition,实现转发前的按需剔除。掌握了 OTTL 的语句、Path 与语法要素,再配合 debug 日志这一利器,你就能在 Tempo 及更广泛的 OpenTelemetry Collector 生态中编写准确、可维护的遥测处理规则。
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考