Jaeger Metrics Query Service 数据模型解析为何内置 OpenMetrics Protobuf 而非 OpenTelemetry【免费下载链接】jaegerCNCF Jaeger, a Distributed Tracing Platform项目地址: https://gitcode.com/GitHub_Trending/ja/jaeger本篇文章围绕 Jaeger 仓库中internal/proto/metrics目录的设计决策展开深入解析 Jaeger 的 Metrics Query Service指标查询服务为何选择以选择性复制的方式内置 OpenMetrics 的 Protobuf 数据模型而不是直接引入 OpenTelemetry 或通过 submodule 引用上游仓库。读完本文你将理解 Jaeger 指标数据在协议层的建模方式、gogoproto 自定义序列化Marshal/Unmarshal的启用动机与代价权衡以及这套数据模型在 Jaeger 的 metricstore 存储抽象层中如何被实际消费。1. 背景Metrics Query Service 与数据模型internal/proto/metrics/README.md 开宗明义地指出该目录定义了MetricsQueryService 的 API 集合及其所需的数据模型data models。整个目录只包含两个 Protobuf 定义文件openmetrics.proto来自 OpenMetrics 的数据模型基于 OpenObservability/OpenMetrics 仓库 v1.0.0 的proto/openmetrics_data_model.proto复制并修改otelspankind.proto来自 OpenTelemetry 的SpanKind枚举基于 opentelemetry-proto v0.8.0 的opentelemetry/proto/trace/v1/trace.proto。在 Jaeger 中这套数据模型被 Metrics Query Service 的存储抽象层直接使用。以 internal/storage/v1/api/metricstore/interface.go 中的Reader接口为例三个核心查询方法都直接返回*metrics.MetricFamily// Reader can load aggregated trace metrics from storage. type Reader interface { // GetLatencies gets the latency metrics for a specific quantile (e.g. 0.99) and list of services // grouped by service and optionally grouped by operation. GetLatencies(ctx context.Context, params *LatenciesQueryParameters) (*metrics.MetricFamily, error) // GetCallRates gets the call rate metrics for a given list of services grouped by service // and optionally grouped by operation. GetCallRates(ctx context.Context, params *CallRateQueryParameters) (*metrics.MetricFamily, error) // GetErrorRates gets the error rate metrics for a given list of services grouped by service // and optionally grouped by operation. GetErrorRates(ctx context.Context, params *ErrorRateQueryParameters) (*metrics.MetricFamily, error) }其中metrics即github.com/jaegertracing/jaeger/internal/proto-gen/api_v2/metrics也就是由本目录的.proto文件生成的 Go 代码包。可以看到数据模型MetricFamily是整个指标查询链路中的公共返回类型无论是 Jaeger v2 中 jaegerquery 扩展暴露的/api/metrics/latencies、/api/metrics/calls、/api/metrics/errors等 HTTP 端点见 cmd/jaeger/internal/extension/jaegerquery/internal/http_handler.go还是底层各类存储后端实现都围绕这一模型展开。2. 为何采用 OpenMetrics 数据模型而非 OpenTelemetry 的README 明确列出了选择 OpenMetrics 数据模型、放弃 OpenTelemetry 数据模型的五个理由这些理由至今仍是理解 Jaeger 指标协议选型的核心依据OpenTelemetry 仍在快速演进README 提到当时最近一次复制下来的数据模型副本在几周后就过时了a recent copy of the data model being taken which became outdated after a few weeks直接引入意味着需要持续追着上游变更兼容性预期当时 OpenTelemetry 被预期在年底前完全兼容 OpenMetricssupposedly fully compatible with OpenMetrics by the end of the yearOpenMetrics 是事实标准OpenMetrics 是许多后端已经在使用的 Prometheus 事实格式the de-facto Prometheus format used by many backends alreadyOpenMetrics 足够稳定稳定性stable是协议选型的关键考量未来可转换OpenTelemetry 未来将支持 OpenTelemetry ↔ OpenMetrics 之间的转换因此即便现在选择 OpenMetrics将来仍然可以在此基础上实现面向 OpenTelemetry 原生后端的支持。这五点理由共同指向一个结论在协议/数据模型层面采用稳定、被广泛采纳的 OpenMetrics同时保留未来对接 OpenTelemetry 生态的通道是当时更稳妥的工程决策。3. 为什么不直接用 submodule 引入 OpenMetrics 上游仓库一种直觉做法是既然 OpenMetrics 是公开仓库为什么不直接以 git submodule 方式引入其.proto定义README 记录了实际探索后遇到的两类硬性问题。3.1 缺少自定义序列化支持导致的运行时 panicJaeger 使用 gogoproto 生成代码而要通过 gRPC 等场景在线上传输over the wire外部消息类型必须为这些类型启用自定义的 Marshal/Unmarshal 方法。直接 import 上游 proto 而不启用自定义序列化时会出现如下运行时错误panic: invalid Go type v1.Metric for field jaeger.api_v2.GetMetricsResponse.Metrics即生成的代码无法识别来自外部包的消息类型无法完成序列化直接 panic。3.2 强行启用 gogoproto 扩展导致编译错误为了修复上述问题README 提到曾尝试在上游 proto 上启用 gogoproto 的自定义 Marshal 与 Unmarshal 方法如gogoproto.marshaler_all、gogoproto.unmarshaler_all但这会导致生成代码的编译错误——因为被引用的上游 Protobuf 定义本身并没有声明这些 gogoproto 选项生成的代码与选项声明不一致。3.3 传递依赖带来的构建成本退一步说即便直接 import 可行也会把上游仓库的全部传递依赖一并引入并为其生成代码——其中很多并非 Jaeger 所需后果是构建时间变长longer build times容器镜像体积可能变大potentially larger container image sizes。4. 选择性复制消息与枚举带来的三项收益在以上限制下README 明确了选择性复制selectively copying必要消息与枚举这一最终方案并列出三项收益可以对外部定义的自定义数据模型如 OpenMetrics进行序列化与反序列化Marshaling and unmarshaling of externally defined custom data models such as those from OpenMetrics——即解决 3.1 的 panic 问题可以继续使用 gogoproto 的自定义 un/marshalers而 gogoproto 文档宣称其自定义序列化在性能上更快reportedly faster marshaling and unmarshaling避免不必要的依赖从而得到更简单的 proto 定义、更快的构建时间和更小的镜像体积。这三项收益正是internal/proto/metrics目录存在意义的最精炼概括复制少量消息、启用 gogo 扩展、去掉一切多余依赖。5. openmetrics.proto 数据模型详解openmetrics.proto 是整个数据模型的核心。文件头部的注释清楚地说明了它与上游的关系——它是OpenObservability/OpenMetricsv1.0.0 的proto/openmetrics_data_model.proto的副本并做了三处追加修改增加gogoproto/gogo.proto的 import增加指定各语言生成代码 package 的 optionGo 为option go_package metricsJava 为option java_package io.jaegertracing.api_v2.metrics增加启用 gogoproto 自定义序列化的 option。其中关键的 gogoproto 选项文件 internal/proto/metrics/openmetrics.proto 第 36–43 行如下// Enable gogoprotobuf extensions (https://github.com/gogo/protobuf/blob/master/extensions.md). // Enable custom Marshal method. option (gogoproto.marshaler_all) true; // Enable custom Unmarshal method. option (gogoproto.unmarshaler_all) true; // Enable custom Size method (Required by Marshal and Unmarshal). option (gogoproto.sizer_all) true;marshaler_all启用自定义Marshal方法unmarshaler_all启用自定义Unmarshal方法而sizer_all生成自定义Size方法——注释明确指出Size是 Marshal 和 Unmarshal 所必需的。这三个选项以_all后缀作用于文件内所有消息类型因此MetricSet、MetricFamily、Metric、Label、MetricPoint以及各类 value 消息全部获得自定义序列化能力。5.1 顶层消息结构整个数据模型遵循 OpenMetrics 规范的三层结构// The top-level container type that is encoded and sent over the wire. message MetricSet { // Each MetricFamily has one or more MetricPoints for a single Metric. repeated MetricFamily metric_families 1; }MetricSet线上传输的顶层容器包含多个MetricFamilyMetricFamily单一指标族的名称name必填、类型type、单位unit、帮助文本help与若干MetricMetric在同一指标族内具有一组唯一标签labels的单个指标包含若干MetricPoint。5.2 MetricType 枚举MetricType枚举覆盖了 OpenMetrics 定义的全部八种指标类型且每种类型都严格对应一种MetricPointvalue 用法枚举值数值必须使用的 MetricPoint value 类型UNKNOWN0unknown_valueGAUGE1gauge_valueCOUNTER2counter_valueSTATE_SET3state_set_valueINFO4info_valueHISTOGRAM5histogram_valueGAUGE_HISTOGRAM6histogram_valueSUMMARY7summary_value在生成的 Go 代码 internal/proto-gen/api_v2/metrics/openmetrics.pb.go 中这一枚举被转换为MetricType类型及其MetricType_name/MetricType_value映射表例如MetricType_COUNTER 2、MetricType_HISTOGRAM 5等。5.3 MetricPoint 与其 value 的 oneof 设计MetricPoint是承载实际数值的核心消息它通过oneof value将七种指标值类型并列起来并附带可选的时间戳timestampmessage MetricPoint { // Required. oneof value { UnknownValue unknown_value 1; GaugeValue gauge_value 2; CounterValue counter_value 3; HistogramValue histogram_value 4; StateSetValue state_set_value 5; InfoValue info_value 6; SummaryValue summary_value 7; } // Optional. google.protobuf.Timestamp timestamp 8; }各 value 消息的要点如下UnknownValue / GaugeValue均通过oneof value提供double_value或int_value两种数值表示CounterValuetotaldouble_value或uint64 int_value、可选的created计数开始收集的时间以及可选的exemplarHistogramValue可选的sumdouble_value或int_value、count、created以及repeated Bucket buckets其中Bucket由必填的count、可选的upper_bound和可选的exemplar组成Exemplar包含必填的value、可选的timestamp与可选的label列表——注释特别指出 labels 用于携带附加信息例如 trace id这正是把追踪与指标关联起来的关键字段StateSetValuerepeated State states每个State由必填的enabledbool与name组成InfoValuerepeated Label infoSummaryValue可选的sum、count、created与repeated Quantile quantileQuantile由必填的quantile如 0.99与value组成。其中Label消息name、value均必填被多处复用标识时间序列、作为 INFO 指标的值、以及作为 Histogram 中 exemplar 的附加标签。6. otelspankind.proto与 OpenTelemetry 唯一的耦合点otelspankind.proto 基于 OpenTelemetryopentelemetry-protov0.8.0 的 trace proto 复制而来仅保留SpanKind枚举并同样启用了 gogoproto 的三个_all选项。README 明确写道Jaeger 与 OpenTelemetry 在查询指标这一语境下不存在直接依赖唯一例外就是SpanKindwith exception toSpanKind。SpanKind的五个取值及其语义如下枚举值数值语义SPAN_KIND_UNSPECIFIED0未指定。实现可假定为 INTERNALSPAN_KIND_INTERNAL1应用内部的运算操作默认值SPAN_KIND_SERVER2服务端处理 RPC 或其他远程网络请求SPAN_KIND_CLIENT3描述对某个远程服务的请求SPAN_KIND_PRODUCER4生产者向 broker 发送消息与消费端通常无直接关键路径延迟关系SPAN_KIND_CONSUMER5消费者从 broker 接收消息这个枚举在实际查询链路中扮演指标聚合过滤维度的角色。例如 jaegerquery 扩展的默认参数 cmd/jaeger/internal/extension/jaegerquery/internal/default_params.go 将其默认值设为SPAN_KIND_SERVERdefaultMetricsSpanKinds []string{metrics.SpanKind_SPAN_KIND_SERVER.String()}而 Prometheus 后端在构造查询时会把SpanKinds拼接为正则过滤条件见 internal/storage/metricstore/prometheus/metricstore/reader.goif len(metricsParams.SpanKinds) 0 { spanKindFilter fmt.Sprintf(span_kind ~ %q, strings.Join(metricsParams.SpanKinds, |)) }7. 生成流程与构建集成这两个.proto文件通过 scripts/makefiles/Protobuf.mk 中定义的proto-openmetricstarget 参与代码生成OPENMETRICS_PROTO_FILES$(wildcard internal/proto/metrics/*.proto) ... .PHONY: proto-openmetrics proto-openmetrics: $(call print_caption, Processing OpenMetrics Protos) $(foreach file,$(OPENMETRICS_PROTO_FILES),$(call proto_compile, $(PROTO_GEN)/api_v2/metrics, $(file)))从 Makefile 的注释和全局选项可以看到生成过程统一使用--gogo_out插件# --gogo_out generates GoGo Protobuf output with gRPC plugin enabled并在编译时通过M前缀将google/protobuf/timestamp.proto等标准类型重映射到github.com/gogo/protobuf/types这正是MetricPoint.timestamp、CounterValue.created等字段能使用 gogo 自定义序列化的前提。生成产物输出到internal/proto-gen/api_v2/metrics/即internal/proto-gen/api_v2/metrics/openmetrics.pb.go由openmetrics.proto生成约五千余行包含全部消息类型、MetricType枚举以及每个类型自带的Marshal/Unmarshal/Size方法internal/proto-gen/api_v2/metrics/otelspankind.pb.go由otelspankind.proto生成包含SpanKind枚举。也就是说选择性复制 本地化生成不是一次性的手工拷贝而是被纳入了 Jaeger 正式的 protobuf 构建流水线internal/proto/metrics/是唯一的事实源single source of truth。8. 关键权衡与上游同步的维护成本为何可控README 也坦诚地记录了这种方案的主要代价需要与原始源 proto 定义保持同步Synchronizing with the original source proto definition。但它随即给出维护成本可预期的三点判断耦合面极小Jaeger 与 OpenTelemetry 在查询指标的语境下没有直接依赖唯一的例外是SpanKind现有模型已满足需求现有的数据模型已经足以满足当前的指标查询需求the existing data model more than satisfies existing metrics querying requirements角色定位清晰OpenTelemetry 的指标数据模型本质上只是指标数据的载体carrier of metrics data而不是 Jaeger 与 OpenTelemetry 组件之间的通信协议rather than a protocol of communication between Jaeger and OpenTelemetry components。因此尽管存在同步成本但它被限制在一个极小的、可控的范围内——只需要在 OpenMetrics/OpenTelemetry 上游协议有实质变化时更新internal/proto/metrics/下的这两个文件并重新生成代码即可。9. 数据模型在存储后端的实际落地最后回到数据模型的消费端可以直观地看到MetricFamily这一抽象在各类存储后端中是如何被填充的。Prometheus 后端MetricsReader的三个方法GetLatencies/GetCallRates/GetErrorRates最终都汇入executeQuery见 internal/storage/metricstore/prometheus/metricstore/reader.go将 PromQL 查询结果转换为*metrics.MetricFamily返回。Elasticsearch 后端internal/storage/metricstore/elasticsearch/to_domain.go 中的Translator.ToDomainMetricsFamily把 ES 聚合结果翻译成 Jaeger 的指标域模型return metrics.MetricFamily{ Name: m.metricName, Type: metrics.MetricType_GAUGE, Help: m.metricDesc, Metrics: domainMetrics, }, nil当GroupByOperation为 true 时指标名会从...service变为...service_operation帮助文本追加 operation并生成service_name与operation两组标签——这些标签正是第 5 节Label消息的实际应用。ClickHouse 后端internal/storage/v2/clickhouse/metricstore/reader.go 同样实现了这一接口。可见无论底层存储是什么internal/proto/metrics定义的数据模型都是 Jaeger 指标查询的统一出口格式——这正是该目录在架构上的核心价值所在。结语回顾整个设计internal/proto/metrics目录体现了 Jaeger 在协议选型上的一个典型权衡以少量复制 本地生成换取稳定性、序列化能力与构建效率以明确同步边界控制维护成本。它选用稳定的 OpenMetrics 作为指标数据的事实标准模型只保留 OpenTelemetry 的SpanKind作为唯一耦合点并通过 gogoproto 扩展获得高性能的自定义序列化。理解这套数据模型的来龙去脉对于阅读 Jaeger 指标查询源码、接入新的指标存储后端或评估追踪/指标/日志统一协议选型都很有参考价值。【免费下载链接】jaegerCNCF Jaeger, a Distributed Tracing Platform项目地址: https://gitcode.com/GitHub_Trending/ja/jaeger创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考