OTTL Metric Context 完整解析:在 OpenTelemetry Collector 中以 OTTL 读写 pdata 指标数据
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
导读
本文围绕 OpenTelemetry Collector Contrib 中的Metric Context(ottlmetric)展开,讲解如何在 OTTL(OpenTelemetry Transformation Language)语句与条件中访问、校验和改写单条 OTLP 指标(Metric)及其关联的 Resource、Instrumentation Scope 与缓存。文章覆盖 Metric Context 的全部受支持 Path、枚举符号,并结合本仓库 vendor 目录下的pkg/ottl/contexts/ottlmetric源码与 filter/transform 处理器的真实调用链,帮助读者在配置 filter processor、transform processor 等组件时,准确编写可用的指标级 OTTL 表达式,并理解其底层实现。
一、Metric Context 是什么
Metric Context 是 OTTL 针对pdata Metric的一种 Context 实现,pdata(pmetric)是 OpenTelemetry Collector 内部对 OTLP 指标(opentelemetry/proto/metrics/v1/metrics.proto)的内存表示。它适用于“与单条 OTLP 指标交互”的场景,例如:
- 在filter processor的
metric_conditions中按指标名、指标类型、Resource 属性等条件丢弃指标; - 在transform processor中改写指标描述、单位、类型等字段;
- 在自定义 Collector 组件中,基于该 Context 构造布尔表达式,判断指标是否应被处理。
在 OTTL 体系里,每个信号(Signal)都有自己的 Context:Resource、Instrumentation Scope、Span、Span Event、Metric、DataPoint、Log、Profile。Metric Context 处于指标信号的中间层级,其层级关系为resource → scope → metric → datapoint(该层级也体现在 filter processor 的文档中:vendor/github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor/README.md)。
版本说明:Metric Context 文档仅适用于
0.120.0及以后的 opentelemetry-collector-contrib 版本,早期版本行为请查阅对应的 release 分支文档。
二、源码定位与上下文结构
Metric Context 的实现位于仓库 vendor 目录:
- 文档:vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/ottlmetric/README.md
- 实现:vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/ottlmetric/metrics.go
- 内部指标上下文:
vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl/contexts/internal/ctxmetric/context.go
从metrics.go的源码可以看到,TransformContext携带一条指标及其完整层级:
type TransformContext struct { resourceMetrics pmetric.ResourceMetrics scopeMetrics pmetric.ScopeMetrics metric pmetric.Metric cache pcommon.Map }它同时实现了ctxresource.Context、ctxscope.Context、ctxmetric.Context三个内部接口,因此同一个 TransformContext 既能暴露resource.*、instrumentation_scope.*路径,也能暴露metric.*路径。ctxmetric.Context接口只要求一个方法GetMetric() pmetric.Metric,对应metric.*路径的字段访问都由此展开。
2.1 上下文的生命周期管理
值得注意的源码细节是,Metric Context 使用sync.Pool复用 TransformContext 对象:
NewTransformContextPtr(resourceMetrics, scopeMetrics, metric, options...)从对象池取出实例并填充数据;Close()清空字段并放回池中(tcPool.Put(tCtx))。
这意味着在 filter/transform 处理器内部,每个指标都会创建一个上下文、求值后立即归还,避免高频指标处理场景下的频繁分配。调用方(例如filterprocessor/metrics.go中的processSkipExpression)会严格按照NewTransformContextPtr(...)→Eval(...)→Close()的顺序使用:
tCtx := ottlmetric.NewTransformContextPtr(rm, smetrics, metric) skip, err := fmp.skipMetricExpr.Eval(ctx, tCtx) tCtx.Close()2.2 解析器的组装
NewParser(functions, telemetrySettings, options...)通过ctxcommon.NewParser组装路径解析器,将metric、resource、scope(含instrumentation_scope旧名)、otelcol等多个子上下文注册进统一路径表:
map[string]ottl.PathExpressionParser[*TransformContext]{ ctxresource.Name: ctxresource.PathGetSetter[*TransformContext], ctxscope.Name: ctxscope.PathGetSetter[*TransformContext], ctxscope.LegacyName: ctxscope.PathGetSetter[*TransformContext], ctxmetric.Name: ctxmetric.PathGetSetter[*TransformContext], ctxotelcol.Name: ctxotelcol.PathGetSetter[*TransformContext], }因此,在 metric 上下文中,你可以同时使用metric.*、resource.*、instrumentation_scope.*(以及旧写法scope.*)和otelcol.*路径,而解析器会依据路径前缀把它们分发到对应的 GetSetter 实现上。
三、完整 Path 参考表
Metric Context 支持通过 metrics proto 中的字段名来访问 pdata 数据。所有整数值以int64读取/写入,所有浮点值以float64读取/写入。下表为当前文档声明的全部受支持路径:
| path | 访问的字段 | 类型 |
|---|---|---|
metric.cache | 当前转换上下文临时缓存的值,可在复杂转换中作为数据占位 | pcommon.Map |
metric.cache[""] | cache 中某个条目的值,支持多重索引访问嵌套字段 | string,bool,int64,float64,pcommon.Map,pcommon.Slice,[]byte或nil |
resource | 被处理指标所属的 resource | pcommon.Resource |
resource.attributes | 被处理指标的 resource attributes | pcommon.Map |
resource.attributes[""] | resource 属性的值,支持多重索引访问嵌套字段 | string,bool,int64,float64,pcommon.Map,pcommon.Slice,[]byte或nil |
instrumentation_scope | 被处理指标的 instrumentation scope | pcommon.InstrumentationScope |
instrumentation_scope.name | instrumentation scope 的名称 | string |
instrumentation_scope.version | instrumentation scope 的版本 | string |
instrumentation_scope.attributes | instrumentation scope 的属性 | pcommon.Map |
instrumentation_scope.attributes[""] | instrumentation scope 属性的值,支持多重索引访问嵌套字段 | string,bool,int64,float64,pcommon.Map,pcommon.Slice,[]byte或nil |
metric.name | 指标名称 | string |
metric.description | 指标描述 | string |
metric.unit | 指标单位 | string |
metric.type | 指标数据类型 | int64 |
metric.metadata | 与指标关联的元数据 | pcommon.Map |
metric.aggregation_temporality | 指标的聚合时态(temporality) | int64 |
metric.is_monotonic | 指标的单调性 | bool |
metric.data_points | 指标的数据点 | pmetric.NumberDataPointSlice、pmetric.HistogramDataPointSlice、pmetric.ExponentialHistogramDataPointSlice或pmetric.SummaryDataPointSlice |
otelcol.* | ottlotelcol 上下文暴露的全部路径 | 视具体字段而定 |
3.1 使用要点
metric.cache与metric.cache["key"]是本上下文独有的临时缓存能力:缓存从 OTTL 上下文创建时初始化(sync.Pool的New函数中cache: pcommon.NewMap()),每次Close()时通过cache.Clear()清空。它适合在多个语句之间暂存中间计算结果。- 属性/缓存的多重索引语法形如
resource.attributes["a"]["b"],可用于访问嵌套的pcommon.Map或pcommon.Slice。 metric.data_points的类型是切片(slice),具体是 Number、Histogram、ExponentialHistogram 还是 Summary 取决于metric.type的取值,因此在条件中通常需要先判断metric.type再使用对应类型的数据点。otelcol.*将 otelcol 上下文(pkg/ottl/contexts/ottlotlcol)暴露的全部路径注入本上下文,可用于访问 Collector 自身的运行时信息(例如组件 ID、配置信息等,具体路径以 ottlotelcol 文档为准)。
3.2 底层实现:枚举解析与 SymbolTable
metrics.go中的parseEnum负责将 OTTL 语句里的枚举符号解析为数值,它直接查询ctxmetric.SymbolTable:
func parseEnum(val *ottl.EnumSymbol) (*ottl.Enum, error) { if val != nil { if enum, ok := ctxmetric.SymbolTable[*val]; ok { return &enum, nil } return nil, fmt.Errorf("enum symbol, %s, not found", *val) } return nil, errors.New("enum symbol not provided") }也就是说,Metric Context 中可用的枚举名称并非魔法字符串,而是由内部ctxmetric包维护的符号表集中定义,语句中写错枚举名会在解析阶段直接报enum symbol, X, not found。
四、枚举(Enums)参考表
Metric Context 支持来自 metrics proto 的枚举名,同时也支持 pdata 定义的指标数据类型枚举(其数值定义见go.opentelemetry.io/collector/pdata/pmetric/metrics.go)。完整枚举清单如下:
| 枚举符号 | 值 |
|---|---|
AGGREGATION_TEMPORALITY_UNSPECIFIED | 0 |
AGGREGATION_TEMPORALITY_DELTA | 1 |
AGGREGATION_TEMPORALITY_CUMULATIVE | 2 |
METRIC_DATA_TYPE_NONE | 0 |
METRIC_DATA_TYPE_GAUGE | 1 |
METRIC_DATA_TYPE_SUM | 2 |
METRIC_DATA_TYPE_HISTOGRAM | 3 |
METRIC_DATA_TYPE_EXPONENTIAL_HISTOGRAM | 4 |
METRIC_DATA_TYPE_SUMMARY | 5 |
4.1 实践建议
- 聚合时态(Aggregation Temporality)枚举用于与
metric.aggregation_temporality(int64)比较,例如判断 Sum/Histogram 是 Delta 还是 Cumulative:metric.aggregation_temporality == AGGREGATION_TEMPORALITY_CUMULATIVE - 数据类型枚举用于与
metric.type(int64)比较,例如只处理 Sum:metric.type == METRIC_DATA_TYPE_SUM - 这两个枚举类别不可混用:
METRIC_DATA_TYPE_*只对metric.type有意义,AGGREGATION_TEMPORALITY_*只对metric.aggregation_temporality有意义。
五、Metric Context 在 Collector 处理器中的真实应用
5.1 filter processor 中的 metric_conditions
filter processor 使用 OTTL 条件决定丢弃哪些遥测数据,条件之间是 OR 关系,任一条件满足即丢弃。在metric_conditions列表中可以使用的上下文包括resource、scope、metric和datapoint(见 filterprocessor/README.md)。
典型配置(来源于该 README 的 Basic Config 示例):
processors: filter: error_mode: propagate metric_conditions: - metric.name == "my.metric" and resource.attributes["my_label"] == "abc123" - metric.type == METRIC_DATA_TYPE_HISTOGRAM - resource.attributes["service.name"] == "my_service_name"底层求值调用链
在 filter processor 的实现中(vendor/github.com/open-telemetry/opentelemetry-collector-contrib/processor/filterprocessor/metrics.go),filterMetricProcessor同时持有三个表达式:
skipResourceExpr expr.BoolExpr[*ottlresource.TransformContext] skipMetricExpr expr.BoolExpr[*ottlmetric.TransformContext] skipDataPointExpr expr.BoolExpr[*ottldatapoint.TransformContext]处理指标时按层级自顶向下执行:先对ResourceMetrics求值skipResourceExpr,再对每个Metric求值skipMetricExpr(即本篇文章的 Metric Context),最后对 DataPoint 求值skipDataPointExpr。每层丢弃后,上层会检查剩余数量,若全部被丢弃则连带移除上层对象——这与 README 中“如果某个 metric 的所有 datapoint 都被丢弃,则该 metric 也会被丢弃”的行为完全一致。processMetrics中还通过metricDataPointCountBeforeFilters - metricDataPointCountAfterFilters统计被过滤的数据点数量并记录到处理器遥测中。
条件序列的构建
filterottl.NewBoolExprForMetric(见vendor/github.com/open-telemetry/opentelemetry-collector-contrib/internal/filter/filterottl/filter.go)负责把配置中的条件字符串解析为可求值的条件序列:
func NewBoolExprForMetric(conditions []string, functions map[string]ottl.Factory[*ottlmetric.TransformContext], errorMode ottl.ErrorMode, set component.TelemetrySettings) (*ottl.ConditionSequence[*ottlmetric.TransformContext], error) { parser, err := ottlmetric.NewParser(functions, set, parserOptions...) statements, err := parser.ParseConditions(conditions) c := ottlmetric.NewConditionSequence(statements, set, ottlmetric.WithConditionSequenceErrorMode(errorMode)) return &c, nil }即:ottlmetric.NewParser(解析路径/枚举)→ParseConditions(语法解析)→NewConditionSequence(组合为条件序列)。解析、构造失败会在配置加载阶段直接报错。
5.2 error_mode 的三种行为
filter processor 的error_mode决定条件求值出错时的处理策略:
| error_mode | 说明 |
|---|---|
ignore | 忽略条件返回的错误、记录日志并继续执行下一条条件(推荐,且在多数发行版为默认) |
silent | 忽略错误且不记录日志,继续执行下一条条件 |
propagate | 将错误向上传递,导致该批次数据被丢弃 |
对应到代码,WithConditionSequenceErrorMode(errorMode)把该模式注入ConditionSequence,条件求值时按此策略处理错误。
5.3 transform processor 与语句序列
在 transform processor 中,Metric Context 同样被用作语句求值上下文。与 filter 不同,transform 以Statement(函数 + 可选条件)形式改写指标。metrics.go提供了配套的语句序列 API:
NewStatementSequence(statements, telemetrySettings, options...):创建语句序列;NewConditionSequence(conditions, telemetrySettings, options...):创建条件序列;WithStatementSequenceErrorMode/WithConditionSequenceErrorMode:分别设置语句/条件求值的错误模式。
一个典型的 transform 语句形如:
set(metric.description, "new description") where metric.name == "http.server.duration"5.4 调试技巧
当 OTTL 语句或条件行为不符合预期时,可在 Collector 配置中把日志级别调为 debug(参考 pkg/ottl/README.md):
service: telemetry: logs: level: debug此时 OTTL 会输出初始 TransformContext、每条语句执行后的 TransformContext(包含 resource、scope、metric 与 cache 的快照,序列化逻辑见metrics.go的MarshalLogObject),帮助你准确判断 OTTL 视角下底层数据的真实形态。
六、Metric Context 相关的 OTTL 背景知识
6.1 OTTL 语句的两部分结构
OTTL 语句由两部分组成:一个转换函数 + 一个可选的执行条件。例如:
set(span.attributes["test"], "pass") where span.attributes["test"] == nil在 metric 场景下,对应地:
set(metric.unit, "s") where metric.name == "http.server.duration"其中set是函数,where之后是条件,条件中引用的metric.*、resource.*等路径即由 Metric Context 提供。OTTL 目前不支持跨信号引用,因此不能在 metric 上下文中访问 span 或 log 的字段。
6.2 条件的层级推断与执行顺序
filter processor 会基于条件中出现的路径前缀自动推断上下文(Context Inference)。例如metric.name == "x"推断为 metric 上下文,resource.attributes["host"] == "localhost"推断为 resource 上下文。同一条信号下的多个条件按层级顺序执行:resource → scope → metric → datapoint;高层对象被丢弃后,低层条件不会再被检查。若单条条件混用了不同层级的路径(如metric.name == "a" or resource.attributes["b"] == "c"),处理器会在较低层级(metric)上求值——因为高层信息在低层上下文中总是可见的。
6.3 可用的 OTTL 函数
filter processor 与 transform processor 可访问全部 OTTL 转换函数(Converter),并额外提供少量处理器自有函数。与 metric 相关的有两个:
HasAttrKeyOnDatapoint(key):若指标的任一 datapoint 的属性 Map 中存在给定 key,返回true;HasAttrOnDatapoint(key, value):若指标的任一 datapoint 的属性 Map 中存在给定 key 且值为给定 value(非字符串属性值按""比较),返回true。
例如:
HasAttrOnDatapoint("http.method", "GET")需要说明的是,这两个函数在实现中要求使用 metric 上下文,并且依赖对指标 datapoint 的遍历——从源码结构看,这类“指标级但涉及 datapoint 内容”的函数正是 Metric Context 的典型使用场景:过滤粒度是整条指标,判断依据来自其数据点。
七、在 Grafana Tempo 中的关联位置
本仓库(Grafana Tempo)将 opentelemetry-collector-contrib 作为 vendor 依赖引入,pkg/ottl及其上下文实现位于 vendor/github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl 下。从仓库源码结构看,Tempo 的modules/distributor/forwarder(forwarder.go)引用了pkg/ottl与processor/filterprocessor,用于在 trace 转发链路中基于 OTTL 表达式进行过滤;同目录的测试 ottl_ismatch_test.go 也印证了 OTTL 表达式在 Tempo 分发链路中的实际运用。因此,本仓库中的 OTTL 相关文档与实现,对于在 Tempo 及其依赖的 Collector 组件中编写指标级过滤/转换规则具有直接的参考价值。
八、小结
Metric Context(ottlmetric)是 OTTL 访问 pdata Metric 的标准入口,本文覆盖了它的全部受支持路径与枚举、上下文对象结构、解析器组装方式,以及它在 filter/transform 处理器中的真实调用链与配置示例。实践时可遵循以下要点:
- 字段路径以 metrics proto 字段名为准,整数用
int64、浮点用float64; - 判断指标类型用
metric.type == METRIC_DATA_TYPE_*,判断聚合时态用metric.aggregation_temporality == AGGREGATION_TEMPORALITY_*; - 可同时使用
metric.*、resource.*、instrumentation_scope.*与otelcol.*路径; - 临时数据放
metric.cache,跨语句传递; - 出错处理优先使用
error_mode: ignore,配合 debug 日志排查条件行为。
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考