Grafana Tempo 中的 OTTL Profile Context:面向 OTLP Profile 数据的转换与过滤编程指南
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
导读
本文围绕 OTTL Profile Context 展开,它是 OpenTelemetry Collector Contrib 中 OTTL(OpenTelemetry Transformation Language)为pdata Profiles(OTLP profile 数据的内部表示)提供的专用上下文实现。在 Grafana Tempo 这类以 OTLP 为数据面的分布式追踪后端中,OTTL Profile Context 是编写 Profile 数据过滤与变换语句(如 filter processor 的profile_conditions)时的核心编程入口。读完本文,你将掌握 Profile Context 支持的完整路径清单、各字段的读写语义、其与资源/作用域上下文的层级关系,以及如何在 filter processor 中落地实战。
[!NOTE] 本文涉及的文档与源码均来自当前仓库的 vendor 依赖目录,文档声明该能力仅适用于
0.124.0及更高版本。
一、背景:OTTL 与 Profile 数据模型
OTTL 是 OpenTelemetry Collector Contrib 提供的一套面向遥测数据的变换/过滤语言,它通过"上下文(Context)"把语言表达式与具体的遥测数据结构绑定。目前仓库 vendor 中提供了一系列上下文实现:
- ottldatapoint
- ottllog
- ottlmetric
- ottlprofile(本文主题)
- ottlresource
- ottlscope
- ottlspan
- ottlspanevent
Profile Context 是其中的"Profile 专用"实现,它面向 pdata Profiles,即 collector 内部对 OTLP profile 数据的表示。凡是需要与 OTLP profiles 交互的场景(例如按 Profile 字段过滤采样、改写 profile 属性、读取/写入 profile 时间与周期),都应使用 Profile Context 作为语句求值的载体。
二、Profile Context 的核心机制
2.1 上下文层级
从源码结构看,Profile 数据在 OTLP 中遵循ResourceProfiles -> ScopeProfiles -> Profile的三级嵌套关系。TransformContext正是对这一层级的封装。见 ottlprofile/profile.go:
type TransformContext struct { profile pprofile.Profile dictionary pprofile.ProfilesDictionary instrumentationScope pcommon.InstrumentationScope resource pcommon.Resource cache pcommon.Map scopeProfiles pprofile.ScopeProfiles resourceProfiles pprofile.ResourceProfiles }它同时实现了ctxresource.Context、ctxscope.Context、ctxprofile.Context三个内部接口(见同文件 L37-L42),这意味着在一条 OTTL 语句中你可以无缝访问 Resource 层与 InstrumentationScope 层的字段——这正是下文路径表中resource.*、instrumentation_scope.*得以存在的底层原因。
2.2 实例生命周期:对象池复用
为了支撑高吞吐的 profile 处理,TransformContext通过sync.Pool复用实例(见 profile.go):
var tcPool = sync.Pool{ New: func() any { return &TransformContext{cache: pcommon.NewMap()} }, }NewTransformContextPtr(...)从池中取出实例并填充字段;- 使用完毕后必须调用
Close(),该方法会清空各字段并归还池中(见 profile.go); NewTransformContext(非指针版本)已在v0.145.0被标记为 Deprecated,新代码应使用指针版本。
2.3 枚举与 Context 名
ContextName = ctxprofile.Name,其值为字符串"profile"(见 ctxprofile/context.go)。Profile Context 不支持枚举符号(parseEnum直接返回enum symbol not found错误,见 profile.go)。
三、支持的路径(Paths)完整清单
Profile Context 整体遵循 profiles proto 中的字段命名。所有整数类型均以int64读写,所有双精度浮点类型均以float64读写。下表是文档声明支持的全部路径,字段类型引用自 pdata 数据类型:
| path | field accessed | type |
|---|---|---|
| cache | 当前变换上下文临时缓存的值。cache可在复杂变换中作为数据临时占位符 | pcommon.Map |
| cache[""] | cache 中某个条目的值。支持多重索引以访问嵌套字段。 | string, bool, int64, float64, pcommon.Map, pcommon.Slice, []byte 或 nil |
| resource | 被处理 profile 的 resource | pcommon.Resource |
| resource.attributes | 被处理 profile 的 resource 属性 | pcommon.Map |
| resource.attributes[""] | 被处理 profile 的 resource 属性值。支持多重索引以访问嵌套字段。 | string, bool, int64, float64, pcommon.Map, pcommon.Slice, []byte 或 nil |
| instrumentation_scope | 被处理 profile 的 instrumentation scope | pcommon.InstrumentationScope |
| instrumentation_scope.name | 被处理 profile 的 instrumentation scope 名称 | string |
| instrumentation_scope.version | 被处理 profile 的 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 |
| profile.attributes | 被处理 profile 的属性 | pcommon.Map |
| profile.attributes[""] | 被处理 profile 的属性值。支持多重索引以访问嵌套字段。 | string, bool, int64, float64, pcommon.Map, pcommon.Slice, []byte 或 nil |
| profile.sample_type | 被处理 profile 的 sample type | pprofile.ValueType |
| profile.sample_type.type | profile.sample_type关联的类型 | string |
| profile.sample_type.unit | profile.sample_type关联的单位 | string |
| profile.sample | 被处理 profile 的 samples | pprofile.SampleSlice |
| profile.time_unix_nano | 被处理 profile 的 unix nano 时间 | int64 |
| profile.time | 被处理 profile 的time.Time时间 | time.Time |
| profile.duration_unix_nano | 被处理 profile 的 unix nano 时长 | int64 |
| profile.duration | 被处理 profile 的纳秒时长 | int64 |
| profile.period_type | 被处理 profile 的 period type | pprofile.ValueType |
| profile.period_type.type | profile.period_type关联的类型 | string |
| profile.period_type.unit | profile.period_type关联的单位 | string |
| profile.period | 被处理 profile 的 period | int64 |
| profile.profile_id | 被处理 profile 的 profile id | pprofile.ProfileID |
| profile.profile_id.string | profile id 的字符串表示 | string |
| profile.attribute_indices | 被处理 profile 的 attribute indices | []int64 |
| profile.dropped_attributes_count | 被处理 profile 的 dropped attributes count | int64 |
| profile.original_payload_format | 被处理 profile 的原始载荷格式 | string |
| profile.original_payload | 被处理 profile 的原始载荷 | []byte |
| otelcol.* | ottlotelcol 上下文暴露的所有路径 | varies |
注:上表为原文档的完整表格,未做删减;其中
cache[""]、resource.attributes[""]等索引写法中的""表示具体键名/索引。
四、路径的源码级实现:每个字段的 Get/Set 语义
路径的解析入口在 ctxprofile/profile.go 的PathGetSetter:它对path.Name()做 switch 分发,把sample_type、sample、time_unix_nano、time、duration_unix_nano、duration、period_type、period、profile_id、attribute_indices、dropped_attributes_count、original_payload_format、original_payload、attributes等字段分别映射到对应的 accessor。理解这些 accessor 的实现,能帮你预判 OTTL 语句在读写时会遇到的行为与边界条件。
4.1 时间与时长:int64 与非负校验
profile.time_unix_nano的 Getter 返回Time().AsTime().UnixNano(),Setter 接受int64并通过pcommon.NewTimestampFromTime(time.Unix(0, i))写回(profile.go L89-L103)。profile.time的 Getter 直接返回time.Time,Setter 同样接受time.Time(L105-L119)。profile.duration_unix_nano与profile.duration在 Setter 中对负值直接报错duration_unix_nano must be non-negative,因为底层DurationNano是uint64(L121-L157)。这意味着负时长无法通过 OTTL 写入。
4.2 Profile ID:二进制与十六进制字符串
profile.profile_id读写pprofile.ProfileID,且 Setter 拒绝空 ID(profile ids must not be empty,L189-L206)。profile.profile_id.string将 ID 以hex.EncodeToString编码为小写十六进制字符串返回;Setter 则通过ctxcommon.ParseProfileID解析字符串并校验非空(L208-L230)。在条件表达式中比较 profile ID 时,使用.string形式更便于与日志中常见的十六进制表示对齐。
4.3 ValueType 与字符串字典(strindex)
profile.sample_type与profile.period_type都是pprofile.ValueType,其底层存储并非直接字符串,而是指向 profile 数据字典字符串表的索引(TypeStrindex/UnitStrindex)。见 ctxprofile/value_type.go:
- 读取
profile.sample_type.type时,先检查currIndex是否越界(strindex %d is out of range),再从ProfilesDictionary.StringTable()中取回字符串; - 写入时,若当前索引对应的字符串与新值相同则复用索引;否则调用
pprofile.SetString向字符串表追加新值并返回新索引,从而在保持 profile 数据"字典化"存储的同时提供字符串级读写语义。
对使用方而言,这意味着你可以像操作普通字符串一样读写sample_type.type、sample_type.unit、period_type.type、period_type.unit,而无需关心字典索引细节。
4.4 属性与原始载荷
profile.attributes支持两种形态:无索引时返回整个pcommon.Map(通过ctxprofilecommon.AccessAttributes),带索引(如profile.attributes["key"])时返回具体值(AccessAttributesKey,L60-L67),与resource.attributes[""]、instrumentation_scope.attributes[""]行为一致。profile.original_payload以[]byte形式读写OriginalPayload().FromRaw(...),可用于携带 pprof 等原始格式(L275-L288)。profile.attribute_indices以[]int64形式读写底层int32切片(L232-L241)。
五、实战:在 Filter Processor 中使用 Profile Context
Profile Context 最典型的生产场景是 filter processor,它允许通过profile_conditions按 OTTL 布尔表达式丢弃匹配的 profile。在 filter processor 的 profiles.go 中可以看到其调用方式:
dic := pd.Dictionary() pd.ResourceProfiles().RemoveIf(func(rp pprofile.ResourceProfiles) bool { resource := rp.Resource() // 先按 resource 级条件过滤 ... rp.ScopeProfiles().RemoveIf(func(sp pprofile.ScopeProfiles) bool { sp.Profiles().RemoveIf(func(profile pprofile.Profile) bool { tCtx := ottlprofile.NewTransformContextPtr(rp, sp, profile, dic) defer tCtx.Close() skip, err := fpp.skipProfileExpr.Eval(ctx, tCtx) ... }) return sp.Profiles().Len() == 0 }) return rp.ScopeProfiles().Len() == 0 })对应到配置文件中,filter processor 的 config.go 定义了ProfileFilters结构,其中ProfileConditions的注释明确说明:"如果任一条件求值为 true,该 profile 将被丢弃"。
filter processor 支持两种等价写法(二者不可混用,混用会报cannot use context inferred profile conditions ...错误,见 config.go):
写法一:统一的profile_conditions(推荐,支持resource.、scope.、profile.前缀混合)
processors: filter/profiles: error_mode: ignore profile_conditions: - resource.attributes["host.name"] == "test" - profile.duration_unix_nano > 3000 - instrumentation_scope.name == "pyroscope"说明:
profile_conditions通过condition.NewProfileParserCollection构建,配置了WithProfileCommonParsers与WithProfileParser,因此同一列表中既可出现resource./scope.前缀的表达式(resource/scope 上下文),也可出现profile.前缀的表达式(profile 上下文),前缀决定了语句归属的求值层级。
写法二:分层的profiles.resource与profiles.profile(旧式,已标记 Deprecated)
processors: filter/profiles: error_mode: ignore profiles: resource: - resource.attributes["host.name"] == "test" profile: - profile.duration_unix_nano > 3000两者的求值顺序与丢弃语义在源码中体现为:
- 先对 ResourceProfiles 求值
skipResourceExpr,命中则整组 ResourceProfiles 被移除(profiles.go L110-L123); - 再逐层下钻到 Profile,对每个 profile 求值
skipProfileExpr,命中则移除该 profile(L127-L142); - 处理完后,如果
ResourceProfiles().Len() == 0,processor 返回ErrSkipProcessingData跳过下游(L101-L103),并通过pd.SampleCount()前后差值记录被过滤的样本数(L84-L94)。
六、上下文扩展:otelcol.*路径与路径上下文名
6.1otelcol.*委托路径
路径表中最后一行的otelcol.*表示 Profile Context 继承了 ottlotelcol 上下文暴露的全部路径(典型如otelcol.resource.attributes等与 collector 元信息相关的字段)。在pathExpressionParser(见 profile.go)中,ctxotelcol.PathGetSetter与ctxresource、ctxscope、ctxprofile并列注册,实现了这一委托关系。
6.2 显式路径前缀(EnablePathContextNames)
对于存在歧义的语句(例如同一路径名在不同上下文中的含义不同),可以启用EnablePathContextNames()选项(见 profile.go L161-L171),它注册了以下合法上下文前缀:
profile(ctxprofile.Name)scope与scope(legacy 名)resourceotelcol
启用后,所有语句路径必须带合法上下文前缀,否则报错。该选项被标记为 Experimental,未来可能变更或移除。
6.3 错误模式与语句序列
NewParser支持注入自定义函数(functions map[string]ottl.Factory[*TransformContext])与 OTTL 选项;NewStatementSequence/NewConditionSequence分别支持通过WithStatementSequenceErrorMode/WithConditionSequenceErrorMode设置ErrorMode(如ignore、propagate、silent),这是 filter processor 中error_mode配置的底层实现入口(见 profile.go L146-L209)。
七、调试与可观测性
TransformContext实现了zapcore.ObjectMarshaler(见 profile.go L56-L62),序列化输出包含resource、scope、profile、cache四部分。这在开启 OTTL 调试日志时非常有用——filter processor 的 README 示例展示了形如condition evaluation result {"match": true, "TransformContext": {"resource": ..., "scope": ..., "profile": ..., "cache": {}}}的日志输出,可直接查看条件求值时上下文中的实际字段值,用于排查条件未命中或误命中问题。
八、小结
OTTL Profile Context 将 OTLP profile 的 pdata 模型以路径形式完整暴露给 OTTL 语言,是编写 profile 过滤、变换、校验逻辑的统一入口。其要点可归纳为:
- 层级完整:一个 TransformContext 同时承载
resource、instrumentation_scope、profile三层数据,语句可跨层访问; - 类型统一:整数以
int64、双精度浮点以float64读写,复合类型(Map/Slice/ValueType/ProfileID)遵循 pdata 类型; - 字典透明:
sample_type/period_type的 type/unit 虽底层存储为字符串表索引,但对 OTTL 用户呈现字符串语义; - 实战落地:在 filter processor 中通过
profile_conditions(或已弃用的profiles.profile)即可基于上述路径完成 profile 级过滤,配合error_mode控制错误行为。
如需进一步深入,可继续阅读仓库中的相关实现:Profile Context 源码、路径 accessor 实现、filter processor profiles 处理 以及 filter processor 配置定义。
【免费下载链接】tempoGrafana Tempo is a high volume, minimal dependency distributed tracing backend.项目地址: https://gitcode.com/GitHub_Trending/tempo1/tempo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考