请参与 Prometheus 用户调研(2026 年 3 月版) ,帮助社区确定未来开发工作的优先级!

OpenMetrics 2.0 [实验性]

  • 版本:2.0.0-rc0
  • 状态:实验性
  • 日期:2026 年 3 月
  • 作者:Arthur Silva Sens, Bartłomiej Płotka, David Ashpole, György Krajcsovits, Owen Williams, Richard Hartmann
  • 荣休作者:Ben Kochie, Brian Brazil, Rob Skillington

Prometheus 于 2012 年创建,自 2015 年以来已成为云原生可观测性的默认选择。Prometheus 设计的核心部分是其文本指标公开格式,称为 Prometheus 公开格式 0.0.4,自 2014 年起稳定。在这种格式中,我们特别注意使其易于生成、易于接收,并且易于人类理解。截至 2020 年,有超过 700 个公开列出的导出器,数量不详的未列出导出器,以及数千个使用此格式的原生库集成。来自各种项目和公司的数十个接收器支持消费它。

2020 年,OpenMetrics 1.0 发布,旨在清理并完善规范,同时将其引入 IETF。OpenMetrics 1.0 的文本展示格式记录了一个工作标准,并在数十个导出器、集成和采集器中得到了广泛且自然的采用。

2024 年左右,OpenMetrics 项目被并入 CNCF Prometheus 项目旗下。结合在广泛部署 OpenMetrics 1.0 中获得的生产经验,以及 Prometheus 文本格式中缺失的一系列新创新,Prometheus 社区决定开展 OpenMetrics 标准的第二个版本。

OpenMetrics 2.0 的初衷是基于 OpenMetrics 1.0,并对其进行增强,以实现与现代 Prometheus 数据模型更高水平的可靠性、可用性和一致性,同时不牺牲易用性和可读性。OpenMetrics 2.0 还改善了与 OpenTelemetry 数据模型及命名约定的兼容性。

本文档旨在作为独立规范使用。

注意这是 OpenMetrics 2.0 规范的发布候选版 (RC)。这意味着该规范目前处于实验状态——预计不会有重大变更,但我们保留根据早期采用者的反馈在必要时破坏兼容性的权利。潜在的反馈、问题和建议应作为 prometheus/openmetrics 存储库中的 Issue  提交。

概述

指标是一种特定的遥测数据。它们代表一组数据的当前状态快照。它们与日志或事件不同,后者关注的是单个事件的记录或信息。

OpenMetrics 主要是一种线路格式,独立于任何特定的传输方式。该格式预计会被定期消费,并且在连续的公开中具有意义。

实现者应通过响应针对特定进程或设备的记录 URL 的 HTTP GET 请求,以 OpenMetrics 文本格式公开指标。此端点应称为“/metrics”。实现者也可以通过其他方式公开 OpenMetrics 格式的指标,例如,通过定期将指标集推送到操作员配置的 HTTP 端点。

指标和时间序列

本标准将所有系统状态表达为数值;计数、当前值、分布、枚举和布尔状态是常见的例子。与指标相反,单一事件发生在特定时间。指标倾向于在时间上聚合数据,并提供系统状态的采样。虽然这可能会丢失信息,但减少开销是许多现代监控系统中常见的工程权衡。

时间序列是随时间变化的信息的记录。指标时间序列的常见示例包括网络接口计数器、设备温度、BGP 连接状态、延迟分布和告警状态。

规范性语言

本文档中的关键词“必须”(MUST)、“不得”(MUST NOT)、“必要”(REQUIRED)、“应”(SHALL)、“不应”(SHALL NOT)、“应该”(SHOULD)、“不应”(SHOULD NOT)、“建议”(RECOMMENDED)、“不建议”(NOT RECOMMENDED)、“可以”(MAY) 和“可选”(OPTIONAL) 仅当它们全大写出现时(如本文所示),才应按 RFC 2119 RFC 8174  中的描述进行解释。

术语“保留”(RESERVED) 在本文档中用于指定为未来使用或供本标准自身使用而预留的值、名称或字段。除非本标准或其未来版本明确允许,否则不得使用被描述为“保留”的值、名称或字段。

数据模型

本节必须与 ABNF 章节结合阅读。如果两者之间存在分歧,以 ABNF 的限制为准。

数据类型

样本值

OpenMetrics 中的指标值必须是数字 (Number) 或复合值 (CompositeValue)。

数字

数字值必须是浮点数或整数。

请注意,此格式的采集器可能仅支持 float64,例如 Go 的 float64,这是一种 IEEE 754-2008 双精度(binary64)浮点数,具有大约 15-17 位有效十进制数字的精度。必须支持非实数值 NaN、+Inf 和 -Inf。NaN 值不得被视为缺失值,但它可用于表示除以零或任何其他产生未定义或不确定结果的数学运算。

布尔值必须表示为数值,其中 1 为真,0 为假。

复合值 (CompositeValue)

复合值必须包含重新创建 MetricFamily 中指标的样本值所需的所有信息。

以下 MetricFamily 类型必须对指标值使用复合值:

其他 MetricFamily 类型必须使用数字。

时间戳

时间戳必须是以秒为单位的 Unix 时间戳。时间戳应该使用浮点数以表示亚秒级精度,例如毫秒或微秒。可以使用负时间戳。

本标准中有少数地方使用了时间戳:

  • 样本示例的时间戳 (Exemplar's Timestamp)
  • 样本的时间戳 (Sample's Timestamp)
  • 样本的开始时间戳 (Sample's Start Timestamp)

字符串

字符串必须仅由有效的 UTF-8 字符组成,并且可以是零长度。必须支持 NULL (ASCII 0x0)。

标签

标签是由字符串组成的键值对。

以一个或多个下划线开头的标签名称是保留的,除非本标准另有规定,否则不得使用。在指标族元数据可能冲突的情况下(例如指标联邦情况),此类标签名称可用于代替 TYPE 和 UNIT 元数据。

标签名称应该遵循 ABNF 章节中 label-name 部分的限制。标签名称可以是 ABNF 章节中描述的任何加引号的转义 UTF-8 字符串。请注意,公开 UTF-8 指标可能会降低可用性。

空标签值应被视为该标签不存在。

标签集

标签集必须由标签组成,并且可以为空。在标签集中,标签名称必须是唯一的。

Exemplars(范例)

Exemplars 是对 MetricSet 外部数据的引用。一个常见的用例是程序跟踪的 ID。

样本示例 (Exemplars) 必须由 LabelSet 和数值组成,并且必须包含时间戳。LabelSet 不应包含 Metric 的 LabelSet 中已有的任何标签名称。如果存在,时间戳应早于或等于样本的时间戳。如果存在,时间戳应晚于或等于样本的开始时间戳。同一样本的样本示例应具有相同的标签名称,以保持一致的风格。

样本示例的时间戳应接近被观测的时间点,但不一定要精确。例如,如果获取精确时间戳的成本过高,则可以使用某个外部源或估算值。

当样本示例引用 跟踪上下文 (Trace Context)  时,它应该对 trace-id 字段  使用 trace_id 键,并对 parent-id 字段使用 span_id 键。

虽然没有指定硬性限制,但 Exemplar 的 LabelSet 不应用于传输大型数据,如跟踪跨度细节或其他事件日志。

采集器可以截断 Exemplar 的 LabelSet 或丢弃 Exemplar。截断 Exemplar 的 LabelSet 时,即使在截断后,也应保留 trace_idspan_id

样本 (Sample)

样本是指标内的一个单一数据点。它必须具有一个值,可以具有时间戳。根据 MetricFamily 类型,它可能包含示例 (Exemplars) 并且可能具有开始时间戳。

样本不应具有时间戳。请参阅公开时间戳以了解为何不推荐这样做。如果存在,样本的时间戳指定了观测该值的时间。

如果存在,样本的开始时间戳应指定测量周期开始的时间。这可以帮助采集器区分新指标和以前未见的长时间运行的指标,并即使在采集期间计数器值没有减少的情况下,也能检测计数器重置。

指标

指标通过 MetricFamily 内的唯一 LabelSet 定义。指标必须包含一个或多个样本列表。如果一个指标公开了多个样本,则其样本必须具有单调递增的时间戳。

给定 MetricFamily 中相同名称的指标在 LabelSet 中应具有相同的标签名称集。

指标族

MetricFamily 可能包含零个或多个指标。MetricFamily 中的每个指标必须具有唯一的 LabelSet。MetricFamily 必须具有名称,并应具有帮助 (Help)、类型 (Type) 和单位 (Unit) 元数据。

名称

MetricFamily 名称

  • 必须是字符串。
  • 在 MetricSet 中必须是唯一的。
  • 必须与家族中每个指标的名称相同。
注意OpenMetrics 1.0 要求 MetricName 必须有后缀,并且匹配没有此类后缀的 MetricFamily 名称。为了提高解析器可靠性(即匹配 MetricFamily 元数据)和未来的兼容性,本规范要求指标名称必须严格匹配其 MetricFamily 名称。

名称应使用 snake_case。名称应遵循 ABNF 章节中 metricname 部分的限制。MetricFamily 名称可以是 ABNF 章节中描述的任何加引号并转义的 UTF-8 字符串。请注意,公开 UTF-8 指标可能会降低可用性,尤其是在名称中不包含 _total 或单位后缀时。

指标族名称中的冒号被保留,用于表示该指标族是通用监控系统的计算或聚合结果。

以一个或多个下划线开头的 MetricFamily 名称是保留的,除非本标准另有规定,否则不得使用。

不鼓励的后缀

MetricFamily 名称不应以 _count_sum_gcount_gsum_bucket 结尾。具体而言,当转换为 OpenMetrics 1.0 文本格式时,名称不应造成 MetricName 冲突。采集器可能会拒绝包含此类 MetricFamily 的 MetricSet

一个不符合规范的示例是一个名为 foo_bucket 的 gauge 和一个名为 foo 的直方图。协商旧版 OpenMetrics 或文本格式的导出器,或者仅支持旧版数据模型的采集器,最终可能会以经典表示形式 (foo_bucket, foo_count, foo_sum) 存储 foo 直方图,这会与 gauge 冲突并导致抓取被拒绝或数据丢失。

存在此规则是因为本规范遵循 Prometheus 生态系统向复合值而不是“经典”表示形式的转变。然而,这种转换需要时间。避免此类后缀可以提高与旧版采集器的兼容性,并有助于最终的迁移过程。

类型

Type 指定 MetricFamily 类型。有效值为 "unknown"、"gauge"、"counter"、"stateset"、"info"、"histogram"、"gaugehistogram" 和 "summary"。

单位

单位 (Unit) 指定了 MetricFamily 的单位。如果非空,它应该作为 MetricFamily 名称的后缀,并用下划线分隔。更进一步的特定类型后缀应位于单位后缀之后。直接向最终用户公开单位不是 MetricFamily 名称后缀的指标可能会因为对指标单位的困惑而降低可用性。有关推荐的单位值,请参阅单位与基本单位

帮助

Help 是一个字符串,并且应该非空。它用于为人类消费提供 MetricFamily 的简要描述,并且应该足够短,以便用作工具提示。

指标集

MetricSet 是 OpenMetrics 公开的顶级对象。它必须由 MetricFamilies 组成,并且可以为空。

每个 MetricFamily 名称必须是唯一的。相同的标签名称和值不应该出现在 MetricSet 中的每个 Metric 上。

MetricSet 中不需要对 MetricFamily 进行特定排序。公开者可以让公开内容更易于人类阅读,例如,如果性能权衡合理,可以按字母顺序排序。

如果存在,每个支持在基于推送和基于拉取的系统中支持目标元数据部分的 Info MetricFamily(名为“target_info”)应放在首位。

MetricFamily 类型

Gauge(仪表盘)

Gauges(仪表盘)是当前的测量值,例如当前使用的内存字节数或队列中的项目数。对于 Gauges,绝对值是用户感兴趣的。

Type 为 gauge 的指标中的样本必须具有数字值。

仪表盘可能会随着时间的推移增加、减少或保持不变。即使它们只朝一个方向变化,它们仍然可能是仪表盘而不是计数器。日志文件的大小通常只会增加,资源可能会减少,而队列大小的限制可能是恒定的。

Counter(计数器)

计数器(Counters)测量离散事件。常见的例子是接收到的 HTTP 请求数、花费的 CPU 秒数或发送的字节数。对于计数器,用户感兴趣的是它们随时间增加的速度。

Counter 的 MetricFamily 名称应以 _total 结尾。公开没有 _total 后缀的指标可能会因为对指标类型产生困惑而降低可用性。

Type 为 Counter 的指标中的样本应具有开始时间戳。

Type 为 Counter 的指标中的样本必须具有非 NaN 的数字值。该值必须随时间单调非递减(除非重置为 0),并从 0 开始。该值可以将值重置为 0。如果存在,对应的开始时间戳也必须设置为近似重置时间。

Type 为 Counter 的指标中的样本可以有样本示例。

状态集

StateSets(状态集)表示一系列相关的布尔值,也称为位集。如果需要编码枚举,可以通过 StateSet 来实现。

StateSet 的结构是一组指标,每个状态一个,称为 StateSet MetricGroup。

注意在 OpenMetrics 1.0 中,指标由 MetricPoints 组成(例如,直方图指标具有表示每个具有特殊 "le" 标签的桶的 MetricPoint),这在 OpenMetrics 2.0 中不再适用。OpenMetrics 1.0 的 StateSet 指标等同于 OpenMetrics 2.0 的 StateSet MetricGroup,而 OpenMetrics 1.0 的 StateSet MetricPoint 等同于 OpenMetrics 2.0 的 StateSet 指标。

StateSet MetricGroup 包含一个或多个状态,并且对于每个状态必须包含一个布尔值的指标。状态具有作为字符串的名称。

如果编码为 StateSet,对于单个时间戳,ENUM 必须在 MetricGroup 内正好有一个为 1 (真) 的样本。

这适用于枚举值随时间变化,且状态数量不超过少数的情况。

Type 为 StateSet 的 MetricFamilies 必须具有空的单位字符串。

信息

信息指标用于公开在进程生命周期内不应更改的文本信息。常见示例包括应用程序的版本、修订控制提交以及编译器的版本。

Info 指标的 MetricFamily 名称必须以 _info 结尾。

Type 为 Info 的 MetricFamilies 必须具有空的单位字符串。

Histogram(直方图)

直方图测量离散事件的分布。常见示例包括 HTTP 请求的延迟、函数运行时间或 I/O 请求大小。

直方图样本必须包含 Count 和 Sum。

Count 值必须等于直方图进行的测量次数。Count 在语义上是一个计数器。Count 应该是整数。Count 不得为负。Count 不应为 +Inf 或 NaN。

允许使用浮点 Count,以便能够公开直方图上的算术运算结果,例如可能导致超出整数范围的值的加法。

Sum 值必须等于所有测量事件值的总和。只要直方图测量的事件值没有负数,Sum 在语义上只是一个计数器。

直方图必须测量非 NaN 的值,这些值可以是在经典桶 (Classic Buckets)原生桶 (Native Buckets) 中,或两者兼有。测量 NaN 对于经典桶和原生桶是不同的,请参见它们各自的章节。

每个桶必须具有明确定义的边界和值。桶值通俗地称为桶计数。桶的边界不得为 NaN。桶值在语义上是计数器。桶值应该是整数。桶值不得为负。桶值不应为 +Inf 或 NaN。

允许使用浮点桶值,以便能够公开直方图上的算术运算结果,例如可能导致超出整数范围的值的加法。

直方图不应包含 NaN 测量值,因为将 NaN 包含在 Sum 中会使 Sum 等于 NaN,并会在时间序列的生命周期内掩盖真实测量值的总和。如果直方图包含 NaN 测量值,则 NaN 测量值必须计入 Count,并且 Sum 必须为 NaN。

如果直方图包含 +Inf 或 -Inf 测量值,则 +Inf 或 -Inf 必须计入 Count 并必须加到 Sum 中,这可能会导致 Sum 为 +Inf、-Inf 或 NaN(后者例如在将 +Inf 加到 -Inf 的情况下)。注意,在这种情况下,有限测量值的总和会被掩盖,直到直方图下次重置。

直方图样本应具有开始时间戳。

如果直方图指标具有带有经典桶的样本,则该直方图指标的 LabelSet 不得具有 "le" 标签名称,因为如果样本作为带有 _bucket 后缀的经典直方图序列存储,则直方图中的 "le" 标签将与从桶阈值生成的 "le" 标签冲突。

直方图类型随时间累积,但可以重置。当直方图重置时,Sum、Count、经典桶和原生桶必须重置为它们的零状态;如果存在开始时间戳,则必须将其设置为近似重置时间。直方图重置对于限制直方图使用的原生桶数量非常有用。

直方图样本可以有样本示例。直方图样本中的样本示例值应均匀分布,例如,如果包含经典桶,则为每个经典桶保留一个样本示例。

经典桶 (Classic Buckets)

每个经典桶必须具有一个阈值。样本内的经典桶阈值必须是唯一的。经典桶阈值可以是负数。

经典桶必须计算小于或等于其阈值的测量值数量,包括在较低桶中也被计算的测量值。这允许监控系统为了性能或反拒绝服务原因丢弃除 +Inf 桶之外的任何桶,其方式是丧失粒度但仍然是有效的直方图。

例如,对于一个表示以秒为单位的请求延迟的指标,带有经典桶且阈值为 1、2、3 和 +Inf,则有 value_1 <= value_2 <= value_3 <= value_+Inf。如果十个请求各花费一秒,则 1、2、3 和 +Inf 桶的值都将等于 10。

带有经典桶的直方图样本必须有一个具有 +Inf 阈值的经典桶。+Inf 桶计算所有测量值。Count 值必须等于 +Inf 桶的值。

公开的经典桶阈值应随时间保持不变,并且在旨在聚合指标的目标之间保持不变。阈值的改变可能会阻止受影响的直方图成为同一操作的一部分(例如,不同指标的聚合或随时间的速率计算)。

如果允许 NaN 值,它必须计入 +Inf 桶中,并且不得计入任何其他桶。其逻辑是 NaN 在数学上不属于任何桶,但传统的检测库通常将其放入 +Inf 桶中。

原生桶 (Native Buckets)

带有原生桶的直方图样本必须具有 Schema 值。Schema 必须是 -4 到 8(含)之间的 8 位有符号整数,这些称为标准(指数)Schema。

-4 到 8 范围之外的 Schema 值保留供将来使用,不得使用。

对于任何标准 Schema n,直方图样本可能包含正和/或负原生桶,并且必须包含一个零原生桶。不应存在空的正或负原生桶。

在标准 Schema 的情况下,索引为 i 的正或负原生桶的边界必须按下述方式计算(使用 Python 语法)

正原生桶的上限(包含):(2**2**-n)**i

正原生桶的下限(不包含):(2**2**-n)**(i-1)

负原生桶的下限(包含):-((2**2**-n)**i)

负原生桶的上限(不包含):-((2**2**-n)**(i-1))

i 是一个可以是负数的整数。

对于可以表示为 float64 的最大和最小有限值(称为 MaxFloat64 和 MinFloat64)以及正负无穷大值(+Inf 和 -Inf),上述规则存在例外。

包含 MaxFloat64 的正原生桶(根据上述边界公式)具有 MaxFloat64 的上限(包含)(而不是上述公式计算出的极限,那将导致 float64 溢出)。

下一个正原生桶(相对于前一项的桶,索引为 i+1)具有 MaxFloat64 的下限(不包含)和 +Inf 的上限(包含)。(它可以被称为正原生溢出桶。)

包含 MinFloat64 的负原生桶(根据上述边界公式)具有 MinFloat64 的下限(包含)(而不是上述公式计算出的极限,那将导致 float64 下溢)。

下一个负数 Native Bucket(相对于前一项所在存储桶的索引为 i+1)的上界(不包含)为 MinFloat64,下界(包含)为 -Inf。(它可以被称为负数 Native 溢出 Bucket。)

上述 +Inf 和 -Inf 存储桶之外的 Native Buckets 不得使用。

零 Native Bucket 的边界为 [-threshold, threshold](均包含)。零阈值(Zero threshold)必须是一个非负的 float64 值(threshold >= 0.0)。

如果零阈值大于 0(threshold > 0),则任何落入零 Native Bucket 的测量值必须计入零 Native Bucket,且不得计入任何其他 Native Bucket。零阈值应该等于某个任意 Native Bucket 的下界。

如果不允许 NaN 值,则 Count 值必须等于负数、正数和零 Native Buckets 之和。

如果允许 NaN 值,它不得计入任何 Native Bucket,但必须计入 Count。Count 与负数、正数和零 Native Buckets 之和的差值必须是 NaN 观测值的数量。其逻辑在于 NaN 在数学上不属于任何存储桶。

仪表盘直方图

GaugeHistograms(仪表盘直方图)测量当前的分布。常见的例子是项目在队列中等待了多长时间,或者队列中请求的大小。

GaugeHistogram 样本必须包含 Gcount 和 Gsum 值。

Gcount 值必须等于当前 GaugeHistogram 中的测量值数量。Gcount 在语义上是一个仪表(gauge)。Gcount 应该是一个整数。Gcount 不应该是 -Inf、+Inf、NaN 或负数。

允许浮点数和负数 Gcount,以便能够公开 GaugeHistogram 上算术运算的结果,例如直方图随时间的变化率。

Gsum 值必须等于当前 GaugeHistogram 中所有测量值的总和。Gsum 在语义上是一个仪表(gauge)。

GaugeHistogram 必须在 Classic BucketsNative Buckets(或两者)中测量非 NaN 的值。测量 NaN 的方式在 Classic 和 Native Buckets 中有所不同,请参阅各自的章节。

如果 GaugeHistogram 停止在 Classic 或 Native Buckets 中测量值,但继续在另一种中测量,它必须清除并停止公开已停止测量的存储桶。这避免了同时公开两种不同分布的存储桶。

每个 Bucket 必须具有明确定义的边界和值。Bucket 的边界不得为 NaN。Bucket 值应该是整数。从语义上讲,Bucket 值是仪表(gauges),不应该是 -Inf、+Inf、NaN 或负数。

允许浮点数和负数 Bucket 值,以便能够公开 GaugeHistogram 上算术运算的结果,例如直方图随时间的变化率。

GaugeHistogram 不应该包含 NaN 测量值。如果 GaugeHistogram 包含 NaN 测量值,则 NaN 测量值必须计入 Gcount,且 Gsum 必须为 NaN。

如果 GaugeHistogram 包含 +Inf 或 -Inf 测量值,则 +Inf 或 -Inf 必须计入 Gcount 并加到 Gsum 中,这可能会导致 Gsum 为 +Inf、-Inf 或 NaN(例如在 +Inf 加到 -Inf 时)。

如果 GaugeHistogram Metric 包含带有 Classic Buckets 的样本,则 GaugeHistogram Metric 的 LabelSet 不得具有名称为 "le" 的标签,因为如果样本存储为带有 _bucket 后缀的经典直方图序列,则 GaugeHistogram 中的 "le" 标签将与根据存储桶阈值生成的 "le" 标签冲突。

GaugeHistogram 的 Classic 和 Native buckets 遵循与直方图(Histogram)相同的所有规则,Gcount 和 Gsum 扮演的角色与 Count 和 Sum 相同。

GaugeHistogram 的示例(exemplars)遵循与直方图相同的所有规则。

总结

Summaries 也测量离散事件的分布,并可在直方图成本过高且少量预计算的分位数足够时使用。

不建议使用 Summaries,因为分位数不可聚合,且用户通常无法推断它们涵盖的时间范围。它们可用于向后兼容,因为一些现有的仪表库会公开预计算的分位数且不支持直方图。

Summary 样本必须包含一个 Count、Sum 和一组分位数。

在语义上,Count 和 Sum 的值是计数器,因此不能是 NaN 或负数。Count 必须是整数。

Summary 应该有一个开始时间戳(Start Timestamp)。

开始时间戳不得基于分位数值的收集周期。

分位数是从分位数到数值的映射。例如,名为 myapp_http_request_duration_seconds 的指标中,分位数 0.95 对应的值 0.2,表示在未知时间范围内第 95 百分位的延迟为 200ms。如果相关时间范围内没有事件,则分位数的值必须为 NaN。分位数的指标 LabelSet 不得具有 "quantile" 标签名称。分位数必须在 0 到 1 之间(包含)。分位数值不得为负数。分位数值应该代表最近的值。通常这是指过去 5-10 分钟内的值。

未知

不应使用 Unknown。当无法从第三方系统确定单个指标的类型时,可以使用 Unknown。

类型未知的指标中的样本必须具有 Number 或 CompositeValue 类型的值。

文本格式...

OpenMetrics 格式是常规乔姆斯基文法(Regular Chomsky Grammars),这使得编写快速且小的解析器成为可能。

部分或无效的公开内容必须被视为 整体错误

注意以前版本的 OpenMetrics 曾指定一种 OpenMetric protobuf 格式 ...。OpenMetrics 2.0 不包含 protobuf 表示。有关可用格式(包括官方的 Prometheus protobuf 线上传输格式),请参阅 公开格式文档

协议协商

所有采集器(ingestor)实现必须能够采集使用 TLS 1.2 或更高版本安全保护的数据,并应该支持 TLS 1.3 或更高版本。所有公开器(exposer)应该能够发出使用 TLS 1.3 或更高版本安全保护的数据。采集器实现应该能够从没有 TLS 的 HTTP 中采集数据。所有实现都应该使用 TLS 来传输数据。

协商使用哪个版本的 OpenMetrics 格式是带外的。例如,对于通过 HTTP 的拉取式公开,使用标准的 HTTP 内容类型协商,如果未请求更新版本,则必须默认为标准的最旧版本(即 1.0.0)。

基于推送的协商本质上更复杂,因为通常是公开者发起连接。生产者必须使用标准的最旧版本(即 1.0.0),除非接收者另有要求。

ABNF

ABNF 参考 RFC 7405。

RFC 7405 基于 RFC 5234,但为字符串字面量添加了显式的大小写敏感性标记。字面量 %s"text" 表示 text 是大小写敏感的,而 %i"text" 表示不敏感。

"exposition" 是 ABNF 的顶层令牌。

exposition = metricset HASH SP %s"EOF" [ LF ]

metricset = *metricfamily

metricfamily = *metric-descriptor *sample

metric-descriptor = HASH SP %s"TYPE" SP (metricname / metricname-utf8) SP metric-type LF
metric-descriptor =/ HASH SP %s"HELP" SP (metricname / metricname-utf8) SP escaped-string LF
metric-descriptor =/ HASH SP %s"UNIT" SP (metricname / metricname-utf8) SP *metricname-char LF

metric-type = %s"counter" / %s"gauge" / %s"histogram" / %s"gaugehistogram" / %s"stateset"
metric-type =/ %s"info" / %s"summary" / %s"unknown"

sample = metricname-and-labels SP value [SP timestamp] [SP start-timestamp] *exemplar LF

value = number / "{" composite-value "}"

timestamp = realnumber

; Lowercase st @ timestamp
start-timestamp = %s"st" "@" timestamp

exemplar = SP HASH SP labels-in-braces SP number SP timestamp

metricname-and-labels = metricname [labels-in-braces] / name-and-labels-in-braces
labels-in-braces = "{" [label *(COMMA label)] "}"
name-and-labels-in-braces = "{" metricname-utf8 *(COMMA label) "}"

label = label-key "=" DQUOTE escaped-string DQUOTE

; Number value
number = realnumber
; Case insensitive
number =/ [SIGN] (%i"inf" / %i"infinity")
number =/ %i"nan"

; Real floats
; Leading 0s explicitly okay
realnumber = [SIGN] 1*DIGIT ["." *DIGIT] [ "e" [SIGN] 1*DIGIT ]
realnumber =/ [SIGN] *DIGIT "." 1*DIGIT [ "e" [SIGN] 1*DIGIT ]

; Integers
; Leading 0s explicitly okay
integer = [SIGN] 1*"0" / [SIGN] positive-integer
non-negative-integer = ["+"] 1*"0" / ["+"] positive-integer
positive-integer = *"0" positive-digit *DIGIT
positive-digit = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"

BS = "\"
COMMA = ","
HASH = "#"
SIGN = "-" / "+"

metricname = metricname-initial-char 0*metricname-char

metricname-char = metricname-initial-char / DIGIT
metricname-initial-char = ALPHA / "_" / ":"
metricname-utf8 = DQUOTE escaped-string-non-empty DQUOTE

label-key = label-name / DQUOTE escaped-string-non-empty DQUOTE
label-name = label-name-initial-char *label-name-char

label-name-char = label-name-initial-char / DIGIT
label-name-initial-char = ALPHA / "_"

escaped-string = *escaped-char
escaped-string-non-empty = 1*escaped-char

escaped-char = normal-char
escaped-char =/ BS ("n" / DQUOTE / BS)
escaped-char =/ BS normal-char

; Any unicode character, except newline, double quote, and backslash
normal-char = %x00-09 / %x0B-21 / %x23-5B / %x5D-D7FF / %xE000-10FFFF

; Composite values
composite-value = histogram-value / gauge-histogram-value / summary-value

; Histograms
histogram-value = h-count "," h-sum "," histogram-buckets
gauge-histogram-value = %s"g" h-count "," %s"g" h-sum "," histogram-buckets

; count:x
h-count = %s"count" ":" number
; sum:f allows real numbers and +-Inf and NaN
h-sum = %s"sum" ":" number

histogram-buckets = classic-buckets / native-buckets [ "," classic-buckets ]

; bucket:[...,+Inf:v] The +Inf bucket is required.
classic-buckets = %s"bucket" ":" "[" [ ch-le-counts "," ] ch-pos-inf-bucket "]"
ch-le-counts = (ch-neg-inf-bucket / ch-le-bucket) *("," ch-le-bucket)
ch-pos-inf-bucket = "+" %s"Inf" ":" number
ch-neg-inf-bucket = "-" %s"Inf" ":" number
ch-le-bucket = realnumber ":" number

; schema:3,zero_threshold:1e-128,zero_count:2,negative_spans:[1:1],negative_buckets:[2],positive_spanes:[-3:1,2:2],positive_buckets:[3,1,0]
native-buckets = nh-schema "," nh-zero-threshold "," nh-zero-count [ "," nh-negative-spans "," nh-negative-buckets ] [ "," nh-positive-spans "," nh-positive-buckets ]

; schema:i
nh-schema = %s"schema" ":" integer
; zero_threshold:f
nh-zero-threshold = %s"zero_threshold" ":" realnumber
; zero_count:x
nh-zero-count = %s"zero_count" ":" number
; negative_spans:[1:2,3:4] and positive_spans:[-3:1,2:2]
nh-negative-spans = %s"negative_spans" ":" "[" [nh-spans] "]"
nh-positive-spans = %s"positive_spans" ":" "[" [nh-spans] "]"
; Spans hold offset and length. The offset can start from any index, even
; negative, however subsequent spans can only advance the index, not decrease it.
nh-spans = nh-start-span *("," nh-span)
nh-start-span = integer ":" non-negative-integer
nh-span = non-negative-integer ":" non-negative-integer

; negative_buckets:[1,2,3] and positive_buckets:[1,2,3]
nh-negative-buckets = %s"negative_buckets" ":" "[" [nh-buckets] "]"
nh-positive-buckets = %s"positive_buckets" ":" "[" [nh-buckets] "]"

nh-buckets = number *("," number)

; Summary

; count:12.0,sum:100.0,quantile:[0.9:2.0,0.95:3.0,0.99:20.0]
summary-value = cs-count "," cs-sum "," cs-quantile

; count:x where x is a number
cs-count = %s"count" ":" number
; sum:x where x is a real number or +-Inf or NaN
cs-sum = %s"sum" ":" number
; quantile:[...]
cs-quantile = %s"quantile" ":" "[" [ cs-q-counts ] "]"
cs-q-counts = cs-q-count *("," cs-q-count)
cs-q-count = realnumber ":" number

总体结构

必须使用 UTF-8。不得使用字节顺序标记(BOM)。注意 NULL(字节 0x00)是有效的 UTF-8 字节,而例如字节 0xFF 则不是。

内容类型必须是

application/openmetrics-text; version=2.0.0; charset=utf-8

行尾必须使用换行符 (\n) 表示,且不得包含回车符 (\r)。公开内容必须以 EOF 结尾,并应该以 EOF\n 结尾。

一个完整的 exposition 示例

# TYPE acme_http_router_request_seconds summary
# UNIT acme_http_router_request_seconds seconds
# HELP acme_http_router_request_seconds Latency though all of ACME's HTTP request router.
acme_http_router_request_seconds{path="/api/v1",method="GET"} {count:807283,sum:9036.32,quantile:[0.95:2,0.99:20]} [email protected]
acme_http_router_request_seconds{path="/api/v2",method="GET"} {count:34,sum:479.3,quantile:[0.95:2.5,0.99:2.9]} [email protected]
# TYPE go_goroutines gauge
# HELP go_goroutines Number of goroutines that currently exist.
go_goroutines 69
# TYPE process_cpu_seconds_total counter
# UNIT process_cpu_seconds_total seconds
# HELP process_cpu_seconds_total Total user and system CPU time spent in seconds.
process_cpu_seconds_total 4.20072246e+06
# TYPE acme_http_request_seconds histogram
# UNIT acme_http_request_seconds seconds
# HELP acme_http_request_seconds Latency histogram of all of ACME's HTTP requests.
acme_http_request_seconds{path="/api/v1",method="GET"} {count:2,sum:1.2e2,schema:0,zero_threshold:1e-4,zero_count:0,positive_spans:[1:2],positive_buckets:[1,1],bucket:[0.5:1,1:2,+Inf:2]} [email protected]
# TYPE acme_http_request_seconds:rate5m gaugehistogram
acme_http_request_seconds:rate5m{path="/api/v1",method="GET"} {gcount:0.01,gsum:2.0,schema:0,zero_threshold:1e-4,zero_count:0.0,positive_spans:[1:2],positive_buckets:[0.005,0.005]}
# TYPE "foodb.read.errors" counter
# HELP "foodb.read.errors" The number of errors in the read path for fooDb.
{"foodb.read.errors","service.name"="my_service"} 3482
# EOF

UTF-8 引号...

不符合 metricname ABNF 定义的指标名称必须用双引号括起来,并且必须使用替代的 UTF-8 语法。在这些指标中,根据 ABNF,带引号的指标名称必须移动到括号内作为第一项,且不带标签名称和等号。在 TYPE、UNIT 和 HELP 行中,指标名称必须用双引号括起来。引号和替代指标语法可用于任何指标名称,无论该名称是否需要引号。

不符合 label-name ABNF 定义的标签名称必须用双引号括起来。任何标签名称都可以用双引号括起来。

以正则表达式表示,不需要用引号括起来的指标名称匹配: ^[a-zA-Z_:][a-zA-Z0-9_:]*$。对于标签名称,字符串匹配: ^[a-zA-Z_][a-zA-Z0-9_]*$

完整示例

# TYPE "process.cpu.seconds" counter
# UNIT "process.cpu.seconds" seconds
# HELP "process.cpu.seconds" Total user and system CPU time spent in seconds.
{"process.cpu.seconds","node.name"="my_node"} 4.20072246e+06
# TYPE "quoting_example" gauge
# HELP "quoting_example" Number of goroutines that currently exist.
{"quoting_example","foo"="bar"} 4.5
# EOF

转义

凡是 ABNF 注明转义的地方,必须应用以下转义规则

  • 换行符, \n (0x0A) -> 字面量 \n (字节码 0x5c 0x6e)
  • 双引号 -> \" (字节码 0x5c 0x22)
  • 反斜杠 -> \\ (字节码 0x5c 0x5c)

应该使用双反斜杠来表示反斜杠字符。不应该将单个反斜杠用于未定义的转义序列。例如,\\a 是等效且更可取的写法,优于 \a

转义也必须应用于带引号的 UTF-8 字符串。

数字

整数不得包含小数点。例如 2300421341298465647914

浮点数必须用小数点或科学记数法表示。例如 8903.1234211.89e-7。浮点数必须在 IEEE 754 定义的 64 位浮点值范围内,但可能会因为尾数位数过多而导致精度损失。这可以用于编码纳秒级分辨率的时间戳。

复合值 (CompositeValues)...

CompositeValue 表示为带有字段的结构化数据。字段周围不得有任何空格。有关格式和可能值的确切详细信息,请参阅 ABNF。

时间戳

如果需要纳秒级精度,时间戳不应该使用浮点数的指数表示法,因为 float64 的表示没有足够的精度,例如 1604676851.123456789

Exemplar

没有标签的 Exemplar 必须用 {} 表示一个空的 LabelSet。

MetricFamily

MetricFamilies 之间不得有显式分隔符。下一个 MetricFamily 必须通过元数据或用于新 MetricFamily 的新指标名称来标记。

MetricFamily 不得交错出现。

同一个 MetricFamily 的名称和 Metric 的名称应该具有相同的引号规则。

一个违反此规则的示例

# TYPE "read_errors" counter
# HELP read_errors The number of errors in the read path for fooDb.
{"read_errors","service.name"="my_service"} 3482
read_errors{"service.name"="my_service2"} 123

MetricFamily 元数据

有四种元数据:MetricFamily 名称、TYPE、UNIT 和 HELP。一个名为 foo_total 的计数器指标的元数据示例如下

# TYPE foo_total counter

如果没有公开 TYPE,则 MetricFamily 必须解释为“类型未知”(Type Unknown)。

如果指定了单位,则必须在 UNIT 元数据行中提供。此外,下划线和单位应该是 MetricFamily 名称的后缀(或者计数器的 _total 之前的中缀)。

请注意,如果单位不是 MetricFamily 名称的后缀(或中缀),直接向终端用户公开指标可能会因为对指标单位的混淆而降低可用性。

一个单位为“秒”的 foo_seconds_total 指标的有效示例

# TYPE foo_seconds_total counter
# UNIT foo_seconds_total seconds

一个有效但不推荐的示例,其中单位既不是名称的后缀也不是中缀

# TYPE foo_total counter
# UNIT foo_total seconds

以下也是有效的:

# TYPE foo_seconds_total counter

如果单位已知,则应该提供。

UNIT 或 HELP 元数据行在换行符之前可以有一个空值字符串。这种情况必须被视为该元数据行不存在。

完整示例

# TYPE foo_seconds_total counter
# UNIT foo_seconds_total seconds
# HELP foo_seconds_total Some text and \n some \" escaping

有关必须用双引号括起指标名称的情况,请参阅 UTF-8 引号 章节。

对于一个 MetricFamily,每种类型的元数据行不得超过一个。顺序应该是 TYPE、UNIT、HELP。

除了这些元数据和消息末尾的 EOF 行之外,不得暴露以 # 开头的行。

未知元数据...

采集器必须支持没有元数据行的指标系列(Metric Families)。

一个 foo_seconds_total 计数器和一组不相关的、没有元数据行的未知类型指标的有效但不推荐的示例

# TYPE foo_seconds_total counter
foo_seconds_total 1
foo_milliseconds_total 2
foo_count 3

指标

指标不得交错。请参阅下方的 StateSet 示例。

一个没有标签、没有时间戳且值为 0 的样本必须渲染为

bar_seconds_count 0

bar_seconds_count{} 0

标签值可以是任何有效的 UTF-8 值,因此必须按照 ABNF 的规定进行转义。一个带有两个标签的有效示例:

bar_seconds_count{a="x",b="escaping\" example \n "} 0

指标名称和标签名称也可以是任何有效的 UTF-8 值,并且在某些情况下,必须根据 ABNF 进行引号转义。有关详细信息,请参阅 UTF-8 引号 章节。

{"\"bar\".seconds.count","b\\"="escaping\" example \n "} 0

指标类型...

Gauge

样本的值必须是 Number。

对于类型为 Gauge 的 MetricFamily,MetricFamily 名称没有推荐的后缀。

一个带有无标签指标且样本无时间戳的 MetricFamily 示例

# TYPE foo gauge
foo 17.0

一个带有两个标签指标且样本无时间戳的 MetricFamily 示例

# TYPE foo gauge
foo{a="bb"} 17.0
foo{a="ccc"} 17.0

一个 MetricFamily 示例,不含任何 Metric:

# TYPE foo gauge

一个带有标签指标且样本带有时间戳的示例

# TYPE foo gauge
foo{a="b"} 17.0 1520879607.789

一个带有无标签指标且样本带有时间戳的示例

# TYPE foo gauge
foo 17.0 1520879607.789

一个带有无标签指标且样本带有两个时间戳的示例

# TYPE foo gauge
foo 17.0 123
foo 18.0 456

Counter

样本的值必须是 Number。

如果存在,样本的开始时间戳必须与带有 st@ 前缀的样本内联。如果存在值的相关时间戳,开始时间戳必须紧随其后。如果存在 exemplar,开始时间戳必须在其之前添加。

一个带有无标签指标,且样本没有时间戳和没有开始时间戳的示例

# TYPE foo_total counter
foo_total 17.0

一个带有无标签指标,且样本有时间戳但没有开始时间戳的示例

# TYPE foo_total counter
foo_total 17.0 1520879607.789

一个带有无标签指标,且样本没有时间戳但有开始时间戳的示例

# TYPE foo_total counter
foo_total 17.0 [email protected]

一个带有无标签指标,且样本有时间戳和开始时间戳的示例

# TYPE foo_total counter
foo_total 17.0 1520879607.789 [email protected]

一个带有无标签指标,没有 _total 后缀,且样本有时间戳和开始时间戳的示例

# TYPE foo counter
foo 17.0 1520879607.789 [email protected]

请注意,如果 _total 不是 MetricFamily 名称的后缀,直接向终端用户公开指标可能会因为对指标类型的混淆而降低可用性。

样本可以有 Exemplars。

一个带有无标签指标,且样本有时间戳、开始时间戳和 exemplar 的示例

# TYPE foo_total counter
foo_total 17.0 1520879607.789 [email protected] # {trace_id="KOO5S4vxi0o"} 0.67 1520879606.1

StateSet

对于类型为 StateSet 的 MetricFamily,MetricFamily 名称没有推荐的后缀。

StateSets 必须在 StateSet MetricGroup 中为每个状态拥有一个指标。每个状态的指标必须有一个标签,标签名称为 MetricFamily 名称,标签值为状态名称。如果状态为真,指标样本的值必须为 1,如果状态为假,则必须为 0。

一个示例,包含 "a"、"bb" 和 "ccc" 三个状态,其中只有 bb 值被启用,指标名称为 foo:

# TYPE foo stateset
foo{foo="a"} 0
foo{foo="bb"} 1
foo{foo="ccc"} 0

一个在 Metric 上带有 "entity" 标签的示例:

# TYPE foo stateset
foo{entity="controller",foo="a"} 1.0
foo{entity="controller",foo="bb"} 0.0
foo{entity="controller",foo="ccc"} 0.0
foo{entity="replica",foo="a"} 1.0
foo{entity="replica",foo="bb"} 0.0
foo{entity="replica",foo="ccc"} 1.0

StateSet MetricGroups 不得交错。

一个正确示例,其中一个 MetricFamily 内有多个 MetricGroups,每个 MetricGroup 内有多个指标,每个指标内有多个样本

# TYPE foo stateset
foo{entity="controller",foo="a"} 1.0 1000000000.000
foo{entity="controller",foo="a"} 0.0 1000000001.000
foo{entity="controller",foo="bb"} 0.0 1000000000.000
foo{entity="controller",foo="bb"} 1.0 1000000001.000
foo{entity="controller",foo="ccc"} 0.0 1000000000.000
foo{entity="controller",foo="ccc"} 0.0 1000000001.000
foo{entity="replica",foo="a"} 1.0 1000000000.000
foo{entity="replica",foo="a"} 1.0 1000000001.000
foo{entity="replica",foo="bb"} 0.0 1000000000.000
foo{entity="replica",foo="bb"} 1.0 1000000001.000
foo{entity="replica",foo="ccc"} 0.0 1000000000.000
foo{entity="replica",foo="ccc"} 0.0 1000000001.000

一个不正确示例,其中 MetricGroups 被交错排列

# TYPE foo stateset
foo{entity="controller",env="dev",foo="a"} 1.0
foo{entity="controller",env="dev",foo="bb"} 0.0
foo{entity="controller",env="dev",foo="ccc"} 0.0
foo{entity="replica",env="dev",foo="a"} 1.0
foo{entity="replica",env="dev",foo="bb"} 0.0
foo{entity="replica",env="dev",foo="ccc"} 1.0
foo{entity="controller",env="prod",foo="a"} 1.0
foo{entity="controller",env="prod",foo="bb"} 0.0
foo{entity="controller",env="prod",foo="ccc"} 0.0

一个不正确的示例,其中 Metrics 交错出现:

# TYPE foo_seconds summary
# UNIT foo_seconds seconds
# TYPE foo stateset
foo{entity="controller",env="dev",foo="a"} 1.0
foo{entity="controller",env="dev",foo="bb"} 0.0
foo{entity="controller",env="prod",foo="a"} 1.0
foo{entity="controller",env="dev",foo="ccc"} 0.0
foo{entity="controller",env="prod",foo="bb"} 0.0
foo{entity="controller",env="prod",foo="ccc"} 0.0

Info

样本值必须始终为 1。

一个带有无标签指标,且有一个带有 "name" 和 "version" 标签的样本值的示例

# TYPE foo_info info
foo_info{name="pretty name",version="8.2.7"} 1

一个带有标签 "entity" 的指标,且有一个带有 “name” 和 “version” 标签的样本值的示例

# TYPE foo_info info
foo_info{entity="controller",name="pretty name",version="8.2.7"} 1.0
foo_info{entity="replica",name="prettier name",version="8.1.9"} 1.0

指标标签和样本值标签可以按任何顺序排列。

总结

样本的值必须是 CompositeValue。

CompositeValue 必须按照 countsumquantile 的顺序包含 Count、Sum 和分位数值作为字段。

如果存在,样本的开始时间戳必须与带有 st@ 前缀的样本内联。如果存在值的相关时间戳,开始时间戳必须紧随其后。如果存在 exemplars,开始时间戳必须在其之前添加。

分位数必须按分数值递增的顺序排序。

一个带有无标签指标,且样本有 Sum、Count 和开始时间戳的示例

# TYPE foo summary
foo {count:17,sum:324789.3,quantile:[]} [email protected]

一个带有无标签指标,且样本有两个分位数和开始时间戳的示例

# TYPE foo summary
foo {count:0,sum:0.0,quantile:[0.95:123.7,0.99:150]} [email protected]

带有 Classic Buckets 的直方图...

样本的值必须是 CompositeValue。

CompositeValue 必须按照 countsumbucket 的顺序包含 Count、Sum 和 Classic Bucket 值作为字段。

如果存在,样本的开始时间戳必须与带有 st@ 前缀的样本内联。如果存在值的相关时间戳,开始时间戳必须紧随其后。如果存在 exemplars,开始时间戳必须在其之前添加。

Classic Buckets 必须按照其阈值的数值递增顺序排序。

所有 Classic Buckets 必须存在,即使是值为 0 的桶。

一个带有无标签指标的示例,样本具有 Sum、Count 和开始时间戳值,以及 12 个 Classic Buckets。为了演示目的,展示了一种广泛且非典型但有效的存储桶阈值。

# TYPE foo histogram
foo {count:17,sum:324789.3,bucket:[0.0:0,1e-05:0,0.0001:5,0.1:8,1.0:10,10.0:11,100000.0:11,1e+06:15,1e+23:16,1.1e+23:17,+Inf:17]} [email protected]

带有 Native Buckets 的直方图...

样本的值必须是 CompositeValue。

CompositeValue 必须按照 countsumschemazero_thresholdzero_count 的顺序包含 Count、Sum、Schema、零阈值和零 Native Bucket 值作为字段。

如果没有负数 Native Buckets,则应该省略 negative_spansnegative_buckets 字段。如果没有正数 Native Buckets,则应该省略 positive_spanspositive_buckets 字段。

如果存在负数(和/或正数)Native Buckets,则 negative_spansnegative_buckets(和/或 positive_spanspositive_buckets)字段必须在 zero_count 字段之后按照此顺序出现。

Native Bucket 值必须按其索引排序,并且其值必须放置在 negative_buckets(和/或 positive_buckets)字段中。

注意Bucket 值是绝对计数值,与某些将 Bucket 值存储为相对于前一个桶的增量(delta)的实现不同。

值为 0 的 Native Buckets 不应该存在。

为了将 negative_buckets(和/或 positive_buckets)映射回其索引,必须按以下方式构造 negative_spans(和/或 positive_spans)字段:每个 span 由一对数字组成,一个是名为偏移量(offset)的整数,另一个是名为长度(length)的非负整数。只有列表中的第一个 span 可以有负偏移量。它定义了其对应 negative_buckets(和/或 positive_buckets)中第一个桶的索引。长度定义了桶列表开始处的连续桶数量。后续 span 的偏移量定义了排除(未填充)桶的数量。长度定义了排除桶之后列表中的连续桶数量。

保留空的正数或负数 Native Buckets 的一个示例是为了减少表示两个 span 之间偏移量仅为 1 的情况所需的 span 数量,这意味着通过包含一个空桶,span 的数量可以减少一个。

每个 span 列表中所有长度值的总和必须等于相应 bucket 列表的长度。

包含所有字段的示例

# TYPE acme_http_request_seconds histogram
acme_http_request_seconds{path="/api/v1",method="GET"} {count:59,sum:1.2e2,schema:7,zero_threshold:1e-4,zero_count:0,negative_spans:[1:2],negative_buckets:[5,7],positive_spans:[-1:2,3:4],positive_buckets:[5,7,10,9,8,8]} [email protected]

没有任何存储桶使用的示例

# TYPE acme_http_request_seconds histogram
acme_http_request_seconds{path="/api/v1",method="GET"} {count:0,sum:0,schema:3,zero_threshold:1e-4,zero_count:0} [email protected]

同时带有 Classic 和 Native Buckets 的直方图...

直方图样本的值必须是 CompositeValue。

CompositeValue 必须按照 countsum 的顺序包含 Count 和 Sum。

countsum 之后,必须包含 Native Buckets 的其余字段,然后必须包含 Classic Buckets 的其余字段(即 bucket 字段)。

这种顺序确保了如果偏好 Native Buckets,实现可以轻松跳过 Classic Buckets。

# TYPE acme_http_request_seconds histogram
# UNIT acme_http_request_seconds seconds
# HELP acme_http_request_seconds Latency histogram of all of ACME's HTTP requests.
acme_http_request_seconds{path="/api/v1",method="GET"} {count:2,sum:1.2e2,schema:0,zero_threshold:1e-4,zero_count:0,positive_spans:[1:2],positive_buckets:[1,1],bucket:[0.5:1,1:2,+Inf:2]}
Exemplars 和开始时间戳...

Exemplars 可以附加到直方图样本上。

如果公开器为 Classic 和 Native Buckets 保留单独的 exemplars 集,则为了性能和向后兼容性原因,公开器可以只附加其中一组,该组应该是与 Classic Buckets 关联的 exemplars。

如果存在,样本的开始时间戳必须与带有 st@ 前缀的样本内联。如果存在值的相关时间戳,开始时间戳必须紧随其后。如果存在 exemplars,开始时间戳必须在其之前添加。

一个带有 Native Buckets 和开始时间戳,且具有多个 Exemplars 的直方图示例

# TYPE foo histogram
foo {count:17,sum:324789.3,schema:0,zero_threshold:1e-4,zero_count:0,positive_spans:[0:2],positive_buckets:[5,12]} [email protected] # {trace_id="shaZ8oxi"} 0.67 1520879607.789 # {trace_id="ookahn0M"} 1.2 1520879608.589

一个带有 Classic Buckets 和开始时间戳的直方图示例,其中没有 exemplar 落入 "0.01" 桶和 "+Inf" 桶。一个没有标签的 exemplar 落入 "0.1" 桶。一个带有一个标签的 exemplar 落入 "1" 桶,另一个落入 "10" 桶。

# TYPE foo histogram
foo {count:17,sum:324789.3,bucket:[0.01:0,0.1:8,1.0:11,10.0:17,+Inf:17]} [email protected] # {} 0.054 1520879607.7 # {trace_id="KOO5S4vxi0o"} 1.67 1520879602.890 # {trace_id="oHg5SJYRHA0"} 9.8 1520879607.789

一个同时带有 Classic 和 Native Buckets 以及开始时间戳的直方图示例。

# TYPE foo histogram
foo {count:17,sum:324789.3,schema:0,zero_threshold:1e-4,zero_count:0,positive_spans:[0:2],positive_buckets:[5,12],bucket:[0.01:0,0.1:8,1.0:11,10.0:17,+Inf:17]} [email protected] # {} 0.054 1520879607.7 # {trace_id="KOO5S4vxi0o"} 1.67 1520879602.890 # {trace_id="oHg5SJYRHA0"} 9.8 1520879607.789

带有 Classic Buckets 的 GaugeHistogram...

带有 Classic Buckets 的 GaugeHistogram 样本遵循与带有 Classic Buckets 的直方图样本相同的语法,不同之处在于 Count 和 Sum 公开为字段 gcountgsum,且 GaugeHistograms 没有开始时间戳。

一个带有无标签指标,一个无时间戳的样本值,且没有 Exemplars 的示例

# TYPE foo gaugehistogram
foo {gcount:42,gsum:3289.3,bucket:[0.01:20,0.1:25,1:34,+Inf:42]}

带有 Native Buckets 的 GaugeHistogram...

带有 Native Buckets 的 GaugeHistogram 样本遵循与带有 Native Buckets 的直方图样本相同的语法,不同之处在于 Count 和 Sum 公开为字段 gcountgsum,且 GaugeHistograms 没有开始时间戳。

一个带有无标签指标,一个无时间戳的样本值,且没有 Exemplars 的示例

# TYPE acme_http_request_seconds gaugehistogram
acme_http_request_seconds{path="/api/v1",method="GET"} {gcount:59,gsum:1.2e2,schema:7,zero_threshold:1e-4,zero_count:0,negative_spans:[1:2],negative_buckets:[5,7],positive_spans:[-1:2,3:4],positive_buckets:[5,7,10,9,8,8]}

同时带有 Classic 和 Native Buckets 的 GaugeHistogram...

带有 Classic 和 Native Buckets 的 GaugeHistogram 样本遵循与带有 Classic 和 Native Buckets 的直方图样本相同的语法,不同之处在于 Count 和 Sum 公开为字段 gcountgsum,且 GaugeHistograms 没有开始时间戳。

Unknown

样本的值必须是 Number 或 CompositeValue。

对于类型为 Unknown 的 MetricFamily,MetricFamily 名称没有推荐的后缀。

一个带有无标签指标且样本无时间戳的示例

# TYPE foo unknown
foo 42.23

一个没有 MetricFamily 元数据的指标,且样本无时间戳的示例

foo 42.23

设计考虑

范围

OpenMetrics 旨在为在线系统提供遥测数据。它运行在不提供硬实时或软实时保证的协议之上,因此它自身也无法做出任何实时保证。OpenMetrics 的延迟和抖动属性与底层网络、操作系统、CPU 等一样不精确。它足够精确,可以用于聚合以作为决策依据,但不能反映单个事件。

应支持各种规模的系统,从每小时接收几次请求的应用程序到监控 400Gb 网络端口的带宽使用情况。应能对传输的遥测数据进行任意时间段的聚合和分析。

它旨在以固定的节奏传输数据传输时刻的状态快照。

范围之外

摄取方如何发现哪些暴露方存在,反之亦然,这超出了本标准的范围,因此未在本标准中定义。

失败模式...

本规范提倡事务处理:任何编码、解码或验证错误都必须拒绝整个 MetricSet 的采集。失败的抓取优于不准确的抓取或破坏事务性的部分指标视图(例如,抓取 StateSet MetricGroup 的一部分,或在单个警报表达式中聚合的两个计数器中仅抓取一个)。

此规则有一个例外:Exemplar 特有的故障不应导致整个公开操作失败。如果 Exemplar 格式错误或无效,它应该被丢弃或忽略,从而允许采集有效的指标数据。

扩展与改进

此 OpenMetrics 的第二个版本基于久经考验的事实标准 Prometheus 公开格式,例如 Prometheus 文本格式 0.0.4、Prometheus Protobuf 格式和 OpenMetrics 1.0。

此版本在第一个版本的基础上进行了重大更改,以提高可靠性、性能、与 Prometheus Protobuf 格式的兼容性,以及 OpenTelemetry 数据模型和命名约定。同时,该格式保留了以简单方式公开遥测数据且易于阅读的能力。这种格式与之前版本、Prometheus 查询语言和数据模型足够接近,以简化转换。

它还确保有一个易于实现的基础标准。这可以在标准的未来版本中构建。我们的意图是,标准的未来小版本将始终在语法和语义上要求对 2.0 版本的支持。

我们希望允许监控系统从 OpenMetrics 公开中获取可用信息,而无需过多负担。如果剥离所有元数据和结构,仅将 OpenMetrics 公开视为无序样本集,它应该能够独立使用。

此原则在整个标准中得到一致应用。例如,鼓励 MetricFamily 的单位在名称中重复,以便不理解单位元数据的系统也可以获取单位。然而,与之前版本不同的是,为了促进与 OpenTelemetry 的兼容性,不再强制要求复制单位名称并添加 _total 后缀。

与之前版本的另一个变化是,现在指标名称必须严格匹配其 MetricFamily 名称。在 OpenMetrics 1.0 中,MetricFamily 名称是去除了类型特定后缀(如 _total_bucket)的指标名称。这一变化通过使指标与其 MetricFamily 之间的关联明确化,提高了解析器的可靠性。

通过这种格式公开的每一行都是自包含的,即从中获取的信息是完整的,并且可以有意义地存储。这是通过引入复合类型和移动开始时间戳(以前的 Created 值)内联来实现的。这些是相对于第一个版本的重大变化,是 Prometheus 引入原生直方图以及解析之前版本 _created 行的性能需求所必需的。

OpenMetrics 1.0 的重大变更摘要

  • 开始时间戳改为使用 st@ 符号内联,取代了单独的 _created 样本。
  • 指标名称必须与其 MetricFamily 名称完全匹配;隐式后缀剥离已移除。
  • 计数器的 _total 后缀和单位后缀从“必须”(MUST)变为“应该”(SHOULD)。
  • 直方图、GaugeHistogram 和 Summary 值整合到单个 CompositeValue 行中;Count 和 Sum 现在是必须的。
  • 引入了带有指数桶方案的原生直方图。
  • 允许带引号的 UTF-8 指标名称和标签名称。
  • Exemplar 时间戳现在是强制性的;每个样本允许多个 Exemplars。
  • Protobuf 格式规范已移除。

单位和基本单位

为了在系统间保持一致性并避免混淆,单位主要基于国际单位制(SI)基本单位。基本单位包括秒、字节、焦耳、克、米、比率、伏特、安培和摄氏度。在适用的情况下应提供单位。

例如,将所有持续时间指标都以秒为单位,就不会有猜测某个指标是纳秒、微秒、毫秒、秒、分钟、小时、天还是周的风险,也不必处理混合单位。通过选择无前缀的单位,我们避免了像在复杂系统的涌现行为中出现“千毫秒”这样的情况。

由于值可以是浮点数,标准内置了亚基本单位的精度。

同样,混合使用比特和字节会引起混淆,所以选择字节作为基本单位。虽然开尔文在理论上是更好的基本单位,但实际上大多数现有硬件都暴露摄氏度。千克是 SI 基本单位,但“千”这个前缀有问题,所以选择克作为基本单位。

虽然在所有可能的情况下都应该使用基本单位,但开尔文是一个公认的单位,可以在某些用例中替代摄氏度,例如颜色或黑体温度,因为在这些情况下不太可能比较摄氏度和开尔文指标。

比率是基本单位,而不是百分比。在可能的情况下,应以 gauge 或 counter 的形式暴露给定分子和分母的原始数据。这在摄取方的分析和聚合中具有更好的数学特性。

分贝不是基本单位,首先,“deci”是 SI 前缀;其次,贝尔是对数单位。要暴露信号/能量/功率比,直接暴露比率会更好,如果可能的话,暴露原始功率/能量则更佳。浮点数指数足以覆盖甚至极端的科学用途。一个电子伏特(~1e-19 J)到一颗超新星释放的能量(~1e44 J)有 63 个数量级,而一个 64 位浮点数可以覆盖超过 2000 个数量级。

如果无法避免非基本单位且转换不可行,实际单位仍应包含在指标名称中以求清晰。例如,焦耳是能量和功率的基本单位,因为瓦特可以表示为单位为焦耳的计数器。在实践中,某个第三方系统可能只暴露瓦特,因此在这种情况下,以瓦特表示的 gauge 将是唯一现实的选择。

并非所有的 MetricFamily 都有单位。例如,HTTP 请求的计数就没有单位。技术上讲,单位是“HTTP 请求”,但从这个意义上说,整个 MetricFamily 名称就是单位。做到如此极端是没有用的。应始终牢记在下游系统中为人类消费提供良好图表轴的可能性。

无状态性

OpenMetrics 定义的传输格式在不同的 exposition 之间是无状态的。之前暴露过的信息不得对未来的 exposition 产生任何影响。每个 exposition 都是暴露方当前状态的一个独立快照。

必须向现有和新的摄取方提供相同的独立 exposition。

一个核心设计选择是,暴露方不得仅仅因为某个指标最近没有变化或观测而将其排除。暴露方不得对摄取方消费 exposition 的频率做任何假设。

跨时间的数据暴露与指标演变

指标在能够分析其随时间演变时最为有用,因此,exposition 必须在时间维度上有意义。因此,仅仅一个单独的 exposition 有用且有效是不够的。对指标语义的某些更改也可能破坏下游用户。

解析器通常通过缓存之前的结果来进行优化。因此,即使在技术上不构成破坏性变更,也应避免在不同的 exposition 之间更改标签的暴露顺序。这通常也使得编写 exposition 的单元测试更容易。

指标和样本不应该在不同的 exposition 之间出现又消失,例如,一个计数器只有在有历史记录时才有用。原则上,一个给定的指标应该从进程启动时就存在于 exposition 中,直到进程终止。通常无法预先知道一个 MetricFamily 在给定进程的生命周期内将有哪些指标(例如,延迟直方图的标签值是 HTTP 路径,由最终用户在运行时提供),但一旦一个类似计数器的指标被暴露,它就应该一直被暴露直到进程终止。一个计数器没有增加并不意味着它不再具有当前值。在某些情况下,停止暴露某个特定指标可能是合理的;请参阅“缺失数据”一节。

通常,更改 MetricFamily 的类型,或在其指标中添加或删除标签,对采集器来说都是破坏性的。

一个值得注意的例外是,向 Info 指标添加标签不是破坏性的。这样您可以在有意义时向现有的 Info MetricFamily 添加额外信息,而不必被迫创建带有额外标签值的全新 info 指标。采集系统应确保对此类添加具有弹性。

更改 MetricFamily 的 Help 不是破坏性变更。对于可能的值,在浮点数和整数之间切换不是破坏性变更。向状态集添加新状态不是破坏性变更。在不更改指标名称的情况下添加单位元数据不是破坏性变更。

直方图的桶不应该在不同的 exposition 之间发生变化,因为这很可能导致性能问题并破坏摄取方。同样,来自任何一致的应用程序二进制文件和环境的所有 exposition 都应该对给定的直方图 MetricFamily 使用相同的桶,以便所有摄取方都可以对它们进行聚合,而无需摄取方实现异构桶的直方图合并逻辑。一个例外可能是偶尔手动更改桶,这被认为是破坏性变更,但当性能特征因新软件发布而改变时,这可能是一个有效的权衡。

即使更改在技术上不是破坏性的,它们仍然会带来成本。例如,频繁的更改可能会给摄取方带来性能问题。一个在不同 exposition 之间变化的 Help 字符串可能会导致每个 Help 值都被存储。频繁地在整数和浮点数值之间切换可能会妨碍高效压缩。

NaN

在 OpenMetrics 中,NaN 和其他任何数字一样,通常是除以零的结果,例如,如果最近没有观测数据,摘要分位数就会出现这种情况。NaN 在 OpenMetrics 中没有特殊含义,尤其不得用作缺失或其他坏数据的标记。

缺失数据

在某些有效的情况下,数据会停止存在。例如,一个文件系统可以被卸载,因此其表示可用磁盘空间的 Gauge 指标就不再存在了。对于这种情况,没有特殊的标记或信号。后续的 exposition 只是不再包含这个指标。

数据暴露性能

指标只有在合理的时间范围内能够被收集时才有用。需要数分钟才能暴露的指标被认为是没有用的。

根据经验,数据暴露不应超过一秒。

通过 OpenMetrics 序列化的旧系统指标可能需要更长时间。因此,不能做硬性的性能假设。

Exposition 应该是最新状态的。例如,处理 exposition 请求的线程不应该依赖于缓存的值,应尽可能绕过任何此类缓存。

并发性

为了实现高可用性和即时访问,一种常见的方法是使用多个摄取方。为了支持这一点,必须支持并发 exposition。所有并发系统的最佳实践(BCP)都应该被遵循,常见的陷阱包括死锁、竞争条件以及过于粗粒度的锁定,这些都会妨碍 exposition 的并发进行。

指标命名与命名空间

我们旨在平衡指标和标签名称命名的可理解性、避免冲突和简洁性。名称通过下划线分隔,因此指标名称最终采用“snake_case”。虽然我们强烈推荐本文档中的做法,但其他指标系统在命名约定方面有不同的理念。OpenMetrics 允许公开这些指标,但如果不遵循这里推荐的约定和后缀,指标系统内各服务链条出现冲突和不兼容的风险会增加。希望使用替代约定的用户需要特别小心,并付出额外努力确保整个系统的一致性。

举个例子,“http_request_seconds”虽然简洁,但在大量应用程序之间会发生冲突,而且这个指标具体测量的是什么也不清楚。例如,在复杂的系统中,它可能是在认证中间件之前或之后测量的。

指标名称应指明它们来自哪部分代码。因此,一家名为“万能制造公司”(A Company Manufacturing Everything)的公司可能会给其代码中的所有指标加上“acme_”前缀,如果他们有一个测量延迟的 HTTP 路由器库,它可能会有一个类似“acme_http_router_request_seconds”的指标,并附有帮助字符串,说明这是总体延迟。

我们的目的不是要防止所有应用程序之间所有潜在的冲突,因为那需要像全球指标命名空间注册表或基于 DNS 的长命名空间这样的繁琐解决方案。相反,我们的目标是保持一种轻量级的非正式方法,以便对于一个给定的应用程序,其组成库之间发生冲突的可能性非常小。

在一个监控系统的整个部署中,我们的目标是,相同指标名称代表不同含义的冲突情况不常见。例如,`acme_http_router_request_seconds` 可能会出现在“万能制造公司”开发的数百个不同应用程序中,这是正常的。如果“另一家实体制造公司”也在其 HTTP 路由器中使用了 `acme_http_router_request_seconds` 这个指标名称,那也没关系。如果两家公司的应用程序都由同一个监控系统监控,这种冲突是不希望看到的,但可以接受,因为没有哪个应用程序试图同时暴露这两个名称,也没有哪个目标试图(错误地)两次暴露同一个指标名称。如果一个应用程序希望同时包含“我的示例公司”和“超级刺激公司”的 HTTP 路由器库,那就会有问题,其中一个指标名称需要以某种方式更改。

由此推论,一个库越是公开,其指标名称的命名空间就应该越好,以减少此类情况发生的风险。`acme_` 对于公司内部使用来说是个不错的选择,但这些公司可能会为在其公司外部共享的代码选择 `acmeverything_` 或 `acorpme_` 这样的前缀。

在按公司或组织进行命名空间划分之后,命名空间和命名应继续按库/子系统/应用程序分形进行,例如上面的 `http_router` 库。目标是,如果您熟悉代码库的整体结构,您可以根据指标名称很好地猜测出给定指标的检测代码在哪里。

对于一个常见且非常知名的现有软件,软件本身的名称可能就足以区分了。例如,`bind_` 对于 DNS 软件来说可能就足够了,尽管更常规的命名方式是 `isc_bind_`。

以 `scrape_` 为前缀的指标由摄取方用于附加与单个 exposition 相关的信息,因此不应由应用程序直接暴露。已经由通用监控系统消费和处理过的指标,在随后的 exposition 中可能会包含此类指标名称。如果一个暴露方希望提供关于单个 exposition 的信息,可以使用类似 `myexposer_scrape_` 的指标前缀。一个常见的例子是 gauge `myexposer_scrape_duration_seconds`,用于表示从暴露方角度看,该 exposition 花费了多长时间。

在 Prometheus 生态系统中,出现了一组在所有实现中都一致的、以 `process_` 为前缀的进程级指标。例如,对于打开文件的 ulimit,MetricFamilies `process_open_fds` 和 `process_max_fds` 这两个 gauge 分别提供了当前值和最大值。(这些名称是遗留的,如果今天定义这样的指标,它们很可能被称为 `process_fds_open` 和 `process_fds_limit`)。总的来说,要获得具有完全相同语义的名称是非常具有挑战性的,这就是为什么不同的检测工具应该使用不同的名称。

避免在指标名称中出现冗余。避免使用像“metric”、“timer”、“stats”、“counter”、“total”、“float64”等子字符串——作为一个通过 OpenMetrics 暴露的具有给定类型(可能还有单位)的指标,这类信息已经隐含其中,不应明确包含。出于同样的原因,您不应将指标的标签名称包含在指标名称中,此外,监控系统对指标的后续聚合可能会使此类信息不正确。

避免在您的检测代码的指标名称中包含来自监控系统其他层的实现细节。例如,MetricFamily 名称不应仅仅因为它碰巧目前在某处通过 OpenMetrics 暴露而包含字符串“openmetrics”,或者仅仅因为您当前的监控系统是 Prometheus 而包含“prometheus”。

标签命名空间

对于标签名称,不建议按公司或库进行显式命名空间划分,考虑到标签名称长度的增加,来自指标名称的命名空间就足够了。但是,建议采取一些最基本的措施来避免常见的冲突。

有些标签名称,如 region、zone、cluster、availability_zone、az、datacenter、dc、owner、customer、stage、service、team、job、instance、environment 和 env,很可能与通用监控系统可能添加的用于识别目标的标签发生冲突。尽量避免使用它们,在这些情况下,添加最少的命名空间可能是合适的。

标签名“type”非常通用,应避免使用。例如,对于与 HTTP 相关的指标,如果要区分 GET、POST 和 PUT 请求,“method”会是更好的标签名。

虽然有关于指标名称的元数据,如 HELP、TYPE 和 UNIT,但没有关于标签名称的元数据。这是因为这样做会使格式膨胀而收效甚微。带外文档是暴露方可以向其摄取方呈现这些信息的一种方式。

指标名称与标签

在某些情况下,在 MetricFamily 中使用多个指标或使用多个 MetricFamilies 似乎是有道理的。对 MetricFamily 求和或求平均值应该是有意义的,即使不总是很有用。例如,混合电压和风扇转速是没有意义的。

提醒一下,OpenMetrics 的构建假设是摄取方可以处理和执行数据聚合。

将总和与其他指标一起暴露是错误的,因为这会导致下游摄取方在聚合时重复计算。

wrong_metric{label="a"} 1
wrong_metric{label="b"} 6
wrong_metric{label="total"} 7

指标的标签应保持在确保唯一性所需的最小数量,因为每增加一个标签,用户在确定下游要使用的标签时就需要多考虑一个。可以应用于许多 MetricFamily 的标签可以考虑移入类似于数据库{{normalization}}的_info 指标。如果几乎所有指标的用户都期望有额外的标签,那么将其添加到所有 MetricFamily 可能是一个更好的权衡。例如,如果您有一个与不同 SQL 语句相关的 MetricFamily,其唯一性由包含完整 SQL 语句哈希的标签提供,那么为了人类可读性,再有一个包含 SQL 语句前 500 个字符的标签也是可以的。

经验表明,下游摄取方发现处理单独的总数和失败 MetricFamily 比在一个 MetricFamily 中使用 {result="success"} 和 {result="failure"} 标签更容易。此外,通常最好暴露单独的读写和收发 MetricFamily,因为全双工系统很常见,下游摄取方更关心这些值的单独情况而不是聚合情况。

所有这些并不像听起来那么容易。这是一个需要领域专家在数据暴露和被暴露系统方面凭借经验和工程权衡来找到良好平衡的领域。指标和标签名称字符

OpenMetrics 建立在现有广泛采用的 Prometheus 文本暴露格式及其周围形成的生态系统之上。向后兼容性是一个核心设计目标。扩展或收缩 Prometheus 文本格式支持的字符集将违背这一目标。破坏向后兼容性将产生比仅仅是传输格式更广泛的影响。特别是,为处理 Prometheus 生态系统内传输的数据而创建或采用的查询语言依赖于这些精确的字符集。标签值支持完整的 UTF-8,因此该格式可以表示多语言指标。

元数据类型

元数据可以来自不同的来源。多年来,出现了两个主要来源。虽然它们在功能上通常相同,但讨论它们的概念差异有助于理解。

“目标元数据”通常是暴露方外部的元数据。常见的例子是来自服务发现、CMDB 或类似系统的数据,例如关于数据中心区域的信息,服务是否属于特定部署,或者生产或测试环境。这可以通过暴露方或摄取方向所有捕获此元数据的指标添加标签来实现。通过摄取方来做这件事是首选,因为它更灵活,开销更小。在灵活性方面,硬件维护团队可能关心机器位于哪个服务器机架中,而使用同一台机器的数据库团队可能关心它包含生产数据库的第 2 个副本。在开销方面,硬编码或配置此信息需要额外的分发路径。

“暴露方元数据”来自暴露方内部。常见的例子是软件版本、编译器版本或 Git 提交的 SHA。

在基于推送和基于拉取的系统中支持目标元数据

在基于推送的消费中,通常由暴露方向摄取方提供相关的目标元数据。在基于拉取的消费中,可以采用基于推送的方法,但更典型的是,摄取方已经先验地知道目标的元数据,例如从机器数据库或服务发现系统中获取,并在消费 exposition 时将其与指标关联起来。

OpenMetrics 是无状态的,并向所有摄取方提供相同的 exposition,这与推送式方法相冲突。此外,推送式方法会破坏拉取式摄取方,因为会暴露不需要的元数据。

一种方法是让推送式摄取方根据操作员配置,通过带外方式提供目标元数据,例如作为 HTTP 标头。虽然这可以为推送式摄取方传输目标元数据,并且本标准也不禁止这样做,但它的缺点是,即使拉取式摄取方应该使用自己的目标元数据,能够访问暴露方自身知道的元数据通常仍然很有用。

首选的解决方案是提供这些目标元数据作为公开的一部分,但以不影响整体公开的方式进行。Info MetricFamilies 就是为此设计的。公开器可以包含一个名为 "target_info" 的 Info MetricFamily,其中包含一个带有元数据且没有标签的指标。文本格式的一个示例可能是

# TYPE target_info info
# HELP target_info Target metadata
target_info{env="prod",hostname="myhost",datacenter="sdc",region="europe",owner="frontend"} 1

当一个暴露方为此目的提供此指标时,它应该位于 exposition 的最前面。这是为了效率,以便依赖它获取目标元数据的摄取方不必在应用基于其内容的业务逻辑之前缓冲其余的 exposition。

暴露方不得向 exposition 中的所有指标添加目标元数据标签,除非为特定摄取方明确配置。暴露方不得为 MetricFamily 名称添加前缀或以其他方式根据目标元数据改变 MetricFamily 名称。通常,同一个标签不应出现在 exposition 的每个指标上,但在极少数情况下,这可能是涌现行为的结果。同样,在非常小的 exposition 中,来自一个暴露方的所有 MetricFamily 名称可能碰巧共享一个前缀。例如,由“万能制造公司”用 Go 语言编写的应用程序很可能包含带有 acme_、go_、process_ 前缀的指标,以及来自任何使用的第三方库的指标前缀。

暴露方可以作为 Info MetricFamilies 暴露暴露方元数据。

上述讨论是在单个公开器的背景下进行的。来自通用监控系统的公开可能包含来自许多单独目标的指标,因此可能会公开多个 target_info 指标。指标可能在采集过程中已经添加了目标元数据作为标签。指标名称不得根据目标元数据而改变。例如,如果所有指标都源自 staging 环境中的目标,则所有指标都以 staging_ 为前缀是不正确的。

客户端计算与派生指标

暴露方应将任何数学或计算留给摄取方处理。一个显著的例外是 Summary 分位数,不幸的是,为了向后兼容性,这是必需的。Exposition 应该是对任意时间段都有用的原始值。

举个例子,您不应暴露一个表示过去 5 分钟内计数器平均增长率的 gauge。让摄取方根据他们跨 exposition 消费的数据点计算增量,具有更好的数学特性,并且对抓取失败更具弹性。

另一个例子是直方图/摘要的平均事件大小。暴露自应用程序启动或自指标创建以来的计数器平均增长率,既有前面例子的问​​题,也妨碍了聚合。

标准差也属于这一类。将平方和作为计数器暴露是正确的方法。它没有被包含在本标准的直方图值中,因为 64 位浮点精度不足以在实践中实现这一点。由于平方,53 位尾数中只有一半可用于精度。例如,一个每秒观察 1 万个事件的直方图会在 2 小时内失去精度。使用 64 位整数也好不到哪里去,因为失去了浮点小数点,因为一个通常跟踪秒级事件长度的纳秒级分辨率整数在 19 次观察后就会溢出。当 128 位浮点数变得普遍时,可以重新审视这个设计决策。

另一个例子是避免暴露请求失败率,而是暴露单独的失败请求和总请求计数器。

数字类型

对于一个每秒递增一百万次的计数器,使用 float64(它有 53 位尾数)需要超过一个世纪才会开始失去精度。然而,一个 100 Gbps 网络接口的字节吞吐量精度可能会在大约 20 小时内开始因 float64 而丢失。虽然对于一个 100 Gbps 网络接口来说,在数年内丢失 1KB 的精度在实践中不太可能成为问题,但对于具有如此高吞吐量的整数数据,int64 是一个选择。

摘要分位数必须是 float64,因为它们是估计值,因此从根本上说是不准确的。

暴露时间戳

OpenMetrics 的一个核心假设是,暴露方暴露的是他们所暴露内容的最新的快照。

虽然向公开数据附加时间戳的用例有限,但它们非常少见。以前附加了时间戳的数据,特别是已经采集到通用监控系统中的数据,可能会携带时间戳。实时或原始数据不应携带时间戳。在不同的公开中公开相同的指标样本值和相同的时间戳是有效的,但是如果底层指标现在丢失了,这样做则是无效的。

时间同步是一个难题,每个系统内部的数据应保持一致。因此,摄取方应能根据自己的视角为数据附加当前时间戳,而不是基于暴露方设备的系统时间。

对于带时间戳的指标,通常无法检测到指标在不同 exposition 之间何时消失。然而,对于不带时间戳的指标,摄取方可以在指标不再出现的 exposition 中使用自己的时间戳。

综上所述,通常不应该公开样本时间戳,因为应该由采集器将自己的时间戳应用到它们采集的样本上。

跟踪指标最后变更时间

假设你有一个计数器 my_counter,它被初始化,然后在时间 123 增加了 1。以文本格式暴露它的正确方式是:

# HELP my_counter Good increment example
# TYPE my_counter counter
my_counter_total 1

根据父章节的规定,摄取方应该可以自由地附加自己的时间戳,所以以下方式是不正确的:

# HELP my_counter Bad increment example
# TYPE my_counter counter
my_counter_total 1 123

如果计数器最后一次更改的具体时间很重要,那么正确的方式是:

# HELP my_counter Good increment example
# TYPE my_counter counter
my_counter_total 1
# HELP my_counter_last_increment_timestamp_seconds When my_counter was last incremented
# TYPE my_counter_last_increment_timestamp_seconds gauge
# UNIT my_counter_last_increment_timestamp_seconds seconds
my_counter_last_increment_timestamp_seconds 123

通过将最后更改的时间戳放入其自己的 Gauge 作为值,摄取方可以自由地为这两个指标附加自己的时间戳。

经验表明,暴露绝对时间戳(这里纪元时间被认为是绝对的)比暴露经过的时间、自……以来的秒数或类似的时间戳更稳健。无论哪种情况,它们都会是 gauge。例如:

# TYPE my_boot_time_seconds gauge
# HELP my_boot_time_seconds Boot time of the machine
# UNIT my_boot_time_seconds seconds
my_boot_time_seconds 1256060124

比以下方式更好:

# TYPE my_time_since_boot_seconds gauge
# HELP my_time_since_boot_seconds Time elapsed since machine booted
# UNIT my_time_since_boot_seconds seconds
my_time_since_boot_seconds 123

相反,对于 exemplar 时间戳没有最佳实践限制。请记住,由于竞争条件或设备间时间不同步,exemplar 时间戳相对于采集器的系统时钟或其他来自同一公开的指标,看起来可能稍晚。同样,同一个样本的 st@ 看起来可能出现在 exemplar 或样本时间戳之后。

请记住,常用的监控系统支持从纳秒到秒的分辨率,因此当截断为秒分辨率时,具有相同时间戳的两个样本可能会在采集器中导致明显的重复。在这种情况下,必须使用时间戳最早的样本。

阈值

暴露系统的期望边界可能是有意义的,但需要谨慎处理。对于普遍适用的值,为这些阈值发出 Gauge 指标是有意义的。例如,数据中心的 HVAC 系统知道当前的测量值、设定点和警报设定点。它对期望的系统状态有一个全局有效且正确的视图。作为反例,一些阈值可能会随着规模、部署模型或时间的推移而改变。一定量的 CPU 使用率在一种设置下可能是可接受的,而在另一种设置下则是不希望的。值的聚合可以进一步改变可接受的值。在这样的系统中,暴露边界可能会适得其反。

例如,队列的最大大小可以与队列中当前的项目数量一起暴露,如下所示:

# HELP acme_notifications_queue_capacity The capacity of the notifications queue.
# TYPE acme_notifications_queue_capacity gauge
acme_notifications_queue_capacity 10000
# HELP acme_notifications_queue_length The number of notifications in the queue.
# TYPE acme_notifications_queue_length gauge
acme_notifications_queue_length 42

大小限制

本标准不对单次公开的样本数量、可能存在的标签数量、状态集可能具有的状态数量、信息值中的标签数量或指标名称/标签名称/标签值/帮助信息的字符限制规定任何特定限制。

具体的限制可能会妨碍合理的使用场景,例如,虽然一个给定的公开数据在经过通用监控系统后可能具有适当数量的标签,但可能会添加一些目标标签,从而使其超过限制。对这些数字的具体限制也无法反映通用监控系统的真实成本所在。因此,这些指导方针旨在帮助公开方和摄取方理解什么是合理的。

另一方面,如果公开数据在某个维度上过大,与所公开指标带来的好处相比,可能会导致严重的性能问题。因此,关于任何单次公开数据大小的一些指导方针将是有用的。

采集器可以选择自行施加限制,特别是为了防止攻击或中断。尽管如此,采集器需要考虑合理的用例,并尽量不对其产生不成比例的影响。如果任何单个值/指标/公开超出了此类限制,则整个公开必须被 拒绝

总的来说,有三件事会影响通用监控系统摄取时间序列数据的性能:唯一时间序列的数量、这些序列中随时间变化的样本数量,以及唯一字符串(如指标名称、标签名称、标签值和帮助信息)的数量。摄取方可以控制其摄取频率,因此这方面无需进一步考虑。

唯一时间序列的数量大致等于文本格式中非注释行的数量。截至2020年,总共1000万个时间序列被认为是一个很大的数量,通常是任何单实例摄取方上限的数量级。任何单次公开数据都不应在未经尽职调查的情况下超过1万个时间序列。一个常见的考虑因素是水平扩展:如果将实例数量扩展1-2个数量级会发生什么?30年前,在单个部署中拥有一千台架顶式交换机是难以想象的。如果一个目标是单例的(例如,公开与整个集群相关的指标),那么几十万个时间序列可能是合理的。重要的不是唯一MetricFamilies的数量或单个标签/桶/状态集的基数,而是时间序列的总数量级。1000个各有一个Metric的gauge与一个有1000个Metric的gauge成本相同。

如果特定类型的所有目标都公开相同的时间序列集,那么每个额外目标的字符串对大多数合理的现代监控系统都不会产生增量成本。然而,如果每个目标都有唯一的字符串,则会产生这样的成本。举一个极端的例子,一个由许多目标使用的1万字符的指标名称本身在实践中不太可能成为问题。相反,一千个目标各自公开一个唯一的36个字符的UUID,在需要存储的字符串方面,其成本是那个1万字符指标名称的三倍以上(假设采用现代方法)。此外,如果这些字符串随时间变化,旧的字符串仍需要存储至少一段时间,从而产生额外成本。假设上一段提到的1000万个时间序列,每小时100MB的唯一字符串可能表明该使用场景更像是事件日志记录,而不是指标时间序列。

安全性

实现者可以选择(MAY)提供身份验证、授权和计费;如果选择这样做,这应该(SHOULD)在OpenMetrics之外处理。

所有公开器实现应该能够使用 TLS 1.3 或更高版本保护其 HTTP 流量。如果公开器实现不支持加密,操作员应该在可行的情况下使用反向代理、防火墙和/或 ACL。

指标公开应独立于向最终用户公开的生产服务;因此,通常不鼓励在TCP/80、TCP/443、TCP/8080和TCP/8443等端口上为使用OpenMetrics的公共服务设置/metrics端点。

IANA

虽然目前大多数Prometheus公开格式的实现都使用来自{{PrometheusPorts}}非正式注册表的非IANA注册端口,但OpenMetrics可以在一个明确定义的端口上找到。

IANA为公开数据的客户端分配的端口是<为历史一致性请求的9099>。

如果需要在同一个IP地址和端口上访问多个指标端点,操作员可以考虑使用反向代理,该代理通过localhost地址与公开方通信。为简化多路复用,端点应该(SHOULD)在其路径中包含自己的名称,即/node_exporter/metrics。不应该(SHOULD NOT)将多个公开数据合并为一个,原因在“在推和拉系统中支持目标元数据”一节中已说明,并且也是为了允许独立的摄取,避免单点故障。

OpenMetrics 希望注册 MIME 类型 application/openmetrics-text

本页内容