
Loki 中的 OTLP 与 Prometheus 命名转换otlptranslator 库实战指南【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki导读Grafana Loki 在接收 OpenTelemetry ProtocolOTLP格式的日志与指标数据时会遇到一个关键问题OTLP 的指标名、属性名attribute key和单位unit遵循 OpenTelemetry 语义约定如http.server.request.duration、By、requests/s而 Prometheus 兼容的命名体系只接受[a-zA-Z0-9:_]指标名与[a-zA-Z0-9_]标签名。本篇文章围绕 Loki 仓库中 vendor 的github.com/prometheus/otlptranslator库README.md系统讲解如何用 Go 将 OTLP 指标名、属性名、单位转换为 Prometheus 兼容格式。读完本文你将掌握MetricNamer、LabelNamer、UnitNamer三个核心 API 的完整用法四种翻译策略Translation Strategy的取舍以及 Loki 内部如何在 OTLP 日志摄入路径中实际使用该库做标签归一化。otlptranslator 是什么otlptranslator是 Prometheus 与 OpenTelemetry 两个生态共同维护的内部 Go 库用于将 OTLP 的指标与属性名称转换为 Prometheus 兼容格式。它遵循官方 OpenTelemetry to Prometheus compatibility specification同时服务 Prometheus 与 OpenTelemetry 两侧。需要注意README 明确声明这是两个项目的内部库对外部使用不作任何稳定性承诺without any stability guarantees for external usage因此在生产代码中直接依赖它之前应评估 API 变动风险。在 Loki 中它同样以 vendor 形式固定版本引入属于内部实现细节。该库由以下核心文件组成vendor 目录文件职责metric_namer.go指标名翻译、单位后缀拼接、namespace 前缀label_namer.go标签名属性名翻译与保留名处理unit_namer.goOTLP 单位字符串 → Prometheus 单位约定strategy.go四种翻译策略常量与判定方法metric_type.goOTLP 指标类型枚举constants.goExemplar、Scope、target_info 相关约定键名strconv.go标签名清理、保留名识别、下划线折叠等底层工具安装与快速上手安装命令go get github.com/prometheus/otlptranslator最小可用示例README 快速开始package main import ( fmt github.com/prometheus/otlptranslator ) func main() { // 使用传统 Prometheus 命名翻译 后缀追加 禁止 UTF-8 strategy : otlptranslator.UnderscoreEscapingWithSuffixes namer : otlptranslator.NewMetricNamer(myapp, strategy) // 将 OTLP 指标翻译为 Prometheus 格式 metric : otlptranslator.Metric{ Name: http.server.request.duration, Unit: s, Type: otlptranslator.MetricTypeHistogram, } fmt.Println(namer.Build(metric)) // 输出: myapp_http_server_request_duration_seconds // 翻译标签名 labelNamer : otlptranslator.LabelNamer{UTF8Allowed: false} fmt.Println(labelNamer.Build(http.method)) // 输出: http_method }从源码可以看到NewMetricNamer只是策略到两个布尔开关的映射metric_namer.go#L107-L113func NewMetricNamer(namespace string, strategy TranslationStrategyOption) MetricNamer { return MetricNamer{ Namespace: namespace, WithMetricSuffixes: strategy.ShouldAddSuffixes(), UTF8Allowed: !strategy.ShouldEscape(), } }也就是说直接构造MetricNamer{Namespace, WithMetricSuffixes, UTF8Allowed}三个字段与传入某个策略是等价的二者可互换使用。指标名翻译MetricNamer 详解MetricNamer是构建指标名的核心结构体metric_namer.go#L99-L103type MetricNamer struct { Namespace string // 可选指标名前缀 WithMetricSuffixes bool // 是否根据类型/单位追加 _total、_ratio、单位后缀 UTF8Allowed bool // true: 原样保留NoTranslationfalse: 转义为 legacy 合规名 }Build(metric Metric) (string, error)依据配置走两条路径metric_namer.go#L151-L156UTF8Allowed true时调用buildMetricName不做字符转义但仍会加后缀与 namespace否则调用buildCompliantMetricName执行完整 Prometheus 命名规范化。输入结构体Metricmetric_namer.go#L125-L129仅含三个字段type Metric struct { Name string Unit string Type MetricType }MetricType枚举定义在 metric_type.go覆盖 OTLP 数据模型中的全部类型MetricTypeUnknown、MetricTypeNonMonotonicCounterdelta 计数器、MetricTypeMonotonicCounter累计计数器、MetricTypeGauge、MetricTypeHistogram、MetricTypeExponentialHistogram、MetricTypeSummary。完整规范化路径compliant 模式当UTF8Allowedfalse且WithMetricSuffixestrue时走normalizeName函数metric_namer.go#L225-L265处理顺序为分词用FieldsFunc把名字按非法字符切分为 token非法字符被丢弃token 之间以_连接。注意这同时会把多个连续下划线折叠为单个下划线——这是 OTel→Prometheus 规范的强制行为源码注释引用了规范原文。拼接单位通过buildUnitSuffixes解析主单位与 per 单位后缀用addUnitTokens追加若 token 中已存在同名 token 则跳过避免重复。Counter 追加_total当metricType MetricTypeMonotonicCounter时先移除已存在的totaltoken 再追加total。Gauge 无量纲单位追加_ratio仅当unit 1且类型为 Gauge 时追加。源码注释解释了原因部分 OTel receiver 不规范地用单位1表示对象计数器在相关 issue 修复前只对 Gauge 追加_ratio理论上计数器也可以表达比值但从数学角度不成立。namespace 前缀若配置了 namespace插入到 token 列表最前面。数字开头保护若最终名以数字开头前缀_。addUnitTokens还有去重与去尾下划线的细节metric_namer.go#L267-L298per 单位若恰为per_则整体丢弃追加 per 单位前先裁掉主单位后缀末尾的_避免出现_per_双下划线。非法结果防护buildCompliantMetricName的defer兜底逻辑metric_namer.go#L158-L177会拒绝两类结果规范化后为空串 → 报错normalization for metric %q resulted in empty name规范化后全为下划线不含任何非_字符→ 报错resulted in invalid name。这意味着诸如!!!这类纯非法字符的指标名会被显式拒绝而不是悄悄变成一个无意义的下划线串。无后缀路径当WithMetricSuffixesfalse时metric_namer.go#L185-L206只做最简单的字符替换非法字符替换为_、拼接 namespace、数字开头加_前缀不做单位与类型处理。UTF-8 直通路径buildMetricNamemetric_namer.go#L311-L352保留原始名称但仍执行namespace 前缀拼接、_ratioGauge单位1、_totalCounter、per 单位与主单位后缀追加。trimSuffixAndDelimiter确保追加后缀前先去掉同名旧后缀及其分隔符避免requests_total_total这类重复。单位映射表OTLP 单位遵循 UCUMUnified Code for Units of Measure的 c/s 记法unitMapmetric_namer.go#L34-L68将其映射为 Prometheus 基础单位全名类别OTLP 单位 → Prometheus 单位时间d→days、h→hours、min→minutes、s→seconds、ms→milliseconds、us→microseconds、ns→nanoseconds字节By→bytes、KiBy→kibibytes、MiBy→mebibytes、GiBy→gibibytes、TiBy→tibibytes、KBy→kilobytes、MBy→megabytes、GBy→gigabytes、TBy→terabytesSIm→meters、V→volts、A→amperes、J→joules、W→watts、g→grams其他Cel→celsius、Hz→hertz、1→无量纲、%→percentperUnitMapmetric_namer.go#L72-L80处理/右侧的 per 单位单数形式s→second、m→minute、h→hour、d→day、w→week、mo→month、y→year拼接时前缀per_如per_second。指标名翻译示例namer : otlptranslator.MetricNamer{WithMetricSuffixes: true, UTF8Allowed: false} // Counter 追加 _total counter : otlptranslator.Metric{ Name: requests.count, Unit: 1, Type: otlptranslator.MetricTypeMonotonicCounter, } fmt.Println(namer.Build(counter)) // requests_count_total // Gauge 带单位换算 gauge : otlptranslator.Metric{ Name: memory.usage, Unit: By, Type: otlptranslator.MetricTypeGauge, } fmt.Println(namer.Build(gauge)) // memory_usage_bytes // 无量纲 Gauge 追加 _ratio ratio : otlptranslator.Metric{ Name: cpu.utilization, Unit: 1, Type: otlptranslator.MetricTypeGauge, } fmt.Println(namer.Build(ratio)) // cpu_utilization_ratio标签名翻译LabelNamer 详解LabelNamerlabel_namer.go#L36-L49把 OTLP 属性名attribute key转换为 Prometheus 标签名type LabelNamer struct { UTF8Allowed bool // Deprecated: 未来版本会移除。为 true 时给以 _ 开头的标签前缀 key // 以 __ 开头的保留标签不受影响。 UnderscoreLabelSanitization bool // 允许在 UTF8Allowedfalse 时保留连续多个下划线。 // 该选项违反 OTel→Prometheus 规范仅用于兼容依赖旧行为的遗留系统。 PreserveMultipleUnderscores bool }Build的翻译规则label_namer.go#L65-L91空字符串直接报错label name is emptyUTF8Allowedtrue原样返回但若全为下划线则报错默认路径sanitizeLabelName把非法字符替换为_并折叠连续下划线数字开头 → 前缀key_如123invalid→key_123invalid启用UnderscoreLabelSanitization已废弃时单下划线开头但非__保留名 → 前缀key结果全为下划线 → 报错。sanitizeLabelNamestrconv.go#L30-L71的底层逻辑值得关注合法标签字符集为[a-zA-Z0-9]isValidCompliantLabelChar注意不含冒号这与指标名字符集不同保留标签reserved labelisReservedLabel识别以__开头并以__结尾、长度 ≥ 4 的标签如__name__此时先剥离双下划线处理中间内容再重新包回__...__从而保护保留名不被折叠破坏默认折叠连续下划线PreserveMultipleUnderscorestrue时只替换非法字符、保留连续下划线。示例README 标签翻译labelNamer : otlptranslator.LabelNamer{UTF8Allowed: false} labelNamer.Build(http.method) // http_method labelNamer.Build(123invalid) // key_123invalid labelNamer.Build(_private) // _private labelNamer.Build(__reserved__) // __reserved__ (保留) labelNamer.Build(labelwith$symbols) // label_with_symbols注意 README 中_private的输出是_private默认未启用已废弃的UnderscoreLabelSanitization情况下单下划线开头不会被加key前缀。若启用该废弃选项则会变为key_private。单位翻译UnitNamer 详解UnitNamerunit_namer.go#L26-L28仅有一个UTF8Allowed字段专门把 OTLP 单位字符串转为 Prometheus 单位名unitNamer : otlptranslator.UnitNamer{UTF8Allowed: false} unitNamer.Build(s) // seconds unitNamer.Build(By) // bytes unitNamer.Build(requests/s) // requests_per_second unitNamer.Build(1) // (无量纲)内部流程unit_namer.go#L46-L71buildUnitSuffixes先用SplitN(unit, /, 2)切分主单位与 per 单位两侧分别走unitMap/perUnitMap映射未命中则原样返回per 单位加per_前缀随后cleanUpUnit将非法字符替换为_并折叠连续下划线最后按主单位_per单位、主单位、per单位的优先级组装并清理首尾下划线。两个细节对准确理解很重要含{}的单位如{requests}不会被当作主单位处理strings.ContainsAny(mainUnitOTel, {})时跳过从而避免产生非法后缀Build的Build返回值不是 error 类型即使单位无法翻译也会返回清理后的原始字符串不会失败。四种翻译策略strategy.gostrategy.gostrategy.go#L23-L60用一组标准字符串常量定义策略推荐在UnderscoreEscapingWithSuffixes完整 Prometheus 风格兼容与NoTranslation保留 OTel 原生名之间二选一策略常量是否转义是否加后缀说明UnderscoreEscapingWithSuffixes是是默认选项指标名非法字符非字母数字/下划线/冒号转_标签名非法字符非字母数字/下划线转_按规则追加单位与类型后缀UnderscoreEscapingWithoutSuffixes是否同上转义但不追加任何后缀NoUTF8EscapingWithSuffixes否是名称原样接受可按规则追加单位与类型后缀NoTranslation实验性否否完全不做任何翻译保留原生指标/标签名ShouldEscape与ShouldAddSuffixesstrategy.go#L64-L86是NewMetricNamer展开策略的依据。NoTranslation 的已知风险务必阅读源码注释对NoTranslation给出了明确警告strategy.go#L41-L58PromQL 体验受损在 YAML 编写的告警规则、仪表盘、自动扩缩容配置中直接书写含点号/连字符的原生名需频繁转义时间序列碰撞最坏情况会导致 OOOout-of-order错误更糟的是悄悄生成畸形的时间序列。例如单位seconds的foo.bar序列与单位milliseconds的另一个foo.bar序列会被当成同一个序列摄入因此该策略目前是实验性的不应在生产环境使用。配置选项组合示例除策略常量外也可以直接组合结构体字段达到同样效果README 配置选项// Prometheus 合规模式 - 支持 [a-zA-Z0-9:_] compliantNamer : otlptranslator.MetricNamer{UTF8Allowed: false, WithMetricSuffixes: true} // 透明直通模式即 NoTranslation utf8Namer : otlptranslator.MetricNamer{UTF8Allowed: true, WithMetricSuffixes: false} utf8Namer otlptranslator.NewMetricNamer(, otlptranslator.NoTranslation) // 带 namespace 与后缀的生产配置 productionNamer : otlptranslator.MetricNamer{ Namespace: myservice, WithMetricSuffixes: true, UTF8Allowed: false, }在 Loki 中的实际应用OTLP 日志摄入的标签归一化otlptranslator 并非只在指标领域使用——Loki 的 OTLP 日志摄入路径直接复用了它的LabelNamer做属性名归一化这是理解该库价值的最佳现场1. OTLP 属性 → 流标签 / 结构化元数据pkg/loghttp/push/otlplabels/labels.go 的AttributeToLabels是核心入口labelNamer : otlptranslator.LabelNamer{} keyWithPrefix, err : labelNamer.Build(keyWithPrefix) if err ! nil { return nil, fmt.Errorf(symbolizer lookup: %w, err) }它使用零值LabelNamer{}即UTF8Allowedfalse走默认的 legacy 合规翻译。Map 类型的属性值会被递归展开父键以下划线前缀拼接prefix _ k例如app下的env会展开为app_env随后整个键名交给LabelNamer.Build归一化。ResourceAttrsToStreamLabels、LogAttrsToLabels、ScopeAttrsToStructuredMetadata分别把资源属性、日志记录属性、Scope 属性按 OTLPConfig 分流为流标签IndexLabel或结构化元数据StructuredMetadata并处理service.name→service_name的服务名发现、severity_number、trace_id、span_id等一等字段。2. Distributor 摄入路径的防御性归一化pkg/distributor/distributor.go#L818 在写入路径中同样创建otlptranslator.LabelNamer{}对每条 entry 的结构化元数据structured metadata标签名做归一化labelNamer : otlptranslator.LabelNamer{} for _, entry : range stream.Entries { ... normalized, err labelNamer.Build(lbl.Name) if err ! nil { return err } ... }在 pkg/chunkenc/symbols.go#L190 也有同样的用法。也就是说Loki 会在多处统一保证任何进入索引/块编码的标签名都符合 Prometheus 标签命名规范从而规避非法字符导致的查询与存储问题。这印证了 README 中 internal library 的定位——在 Loki 里它正是作为内部基础设施被静默复用。错误处理与边界情况使用本库时建议养成检查error的习惯以下是Build方法可能返回的错误及其触发条件场景调用错误空标签名LabelNamer{}.Build()label name is empty规范化后全下划线LabelNamer{}.Build(!!!)normalization for label name !!! resulted in invalid name ___UTF-8 模式下全下划线LabelNamer{UTF8Allowed:true}.Build(___)label name ___ contains only underscores指标名规范化后为空MetricNamer{UTF8Allowed:false}.Build(Metric{Name:!!!})normalization for metric !!! resulted in empty name指标名规范化后全下划线同上变体resulted in invalid name此外LabelNamer.Build返回(string, error)二元组而UnitNamer.Build只返回string——单位翻译永不失败无法翻译时返回清理后的原串。适用前提与版本说明本文示例基于当前仓库 vendor 的 otlptranslator 版本源码版权声明为 2025 The Prometheus Authors / OpenTelemetry Authors代码源自 Prometheus 的storage/remote/otlptranslator与 opentelemetry-collector-contrib 的pkg/translator/prometheus该库被 Prometheus 与 OpenTelemetry 标记为内部库、无外部稳定性承诺引入时应锁定版本翻译规则严格遵循 OpenTelemetry 官方 Prometheus and OpenMetrics 兼容性规范包括连续下划线折叠、_total/_ratio后缀、单位映射、保留标签等行为规范升级可能导致行为变化UnderscoreLabelSanitization已被标记 Deprecated未来版本会移除请勿在新代码中依赖。小结otlptranslator以极小的 API 面三个 Namer 一个策略枚举完整覆盖了 OTLP→Prometheus 的命名翻译需求MetricNamer处理指标名、单位后缀与 namespaceLabelNamer处理属性名与保留名UnitNamer处理单位字符串TranslationStrategyOption提供开箱即用的四档配置。在 Loki 中它作为内部库被 OTLP 日志摄入路径otlplabels与 distributor 写入路径distributor.go静默复用确保所有进入索引的标签名始终符合 Prometheus 规范——这既是它在指标侧的价值也是日志侧保证数据可查询、可聚合的关键一环。【免费下载链接】lokiLike Prometheus, but for logs.项目地址: https://gitcode.com/GitHub_Trending/lok/loki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考