OpenMetrics 1.0

  • 版本:1.0
  • 状态:已发布
  • 日期:2020 年 11 月
  • 作者:Richard Hartmann、Ben Kochie、Brian Brazil、Rob Skillington

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

通过 OpenMetrics,我们正在清理和收紧规范,明确目的是将其引入 IETF。我们正在记录一个具有广泛和有机采用的工作标准,同时引入最小的、很大程度上向后兼容的、经过深思熟虑的更改。截至 2020 年,已有数十个导出器、集成和接收器使用并优先协商 OpenMetrics。

鉴于生态系统中广泛的采用和显著的协调需求,对 Prometheus 公开格式 0.0.4 或 OpenMetrics 1.0 进行彻底的更改被认为超出了范围。

注意OpenMetrics 2.0 的开发正在进行中。请阅读此处了解如何加入 Prometheus OM 2.0 工作组。

概述

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

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

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

指标和时间序列

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

时间序列是随时间变化的信息记录。虽然时间序列可以支持任意字符串或二进制数据,但本 RFC 的范围仅限于数值数据。

指标时间序列的常见示例包括网络接口计数器、设备温度、BGP 连接状态和告警状态。

数据模型

本节必须与 ABNF 节一起阅读。如果两者之间存在分歧,则必须优先考虑 ABNF 的限制。这减少了重复,因为必须支持文本线路格式。

数据类型

价值观

OpenMetrics 中的指标值必须是浮点数或整数。请注意,格式的接收者可能只支持 float64。非实数值 NaN、+Inf 和 -Inf 必须被支持。NaN 不能被视为缺失值,但可以用来表示除以零的情况。

布尔值

布尔值必须遵循 `1==true`,`0==false`。

时间戳

时间戳必须是 Unix 纪元秒。可以使用负时间戳。

字符串

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

标签

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

以下划线开头的标签名称是保留的,除非本标准指定,否则不得使用。标签名称必须遵循 ABNF 部分中的限制。

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

标签集

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

指标点

每个指标点由一组值组成,具体取决于指标族类型。

Exemplars(范例)

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

Exemplars 必须由一个 LabelSet 和一个值组成,并且可以有一个时间戳。它们各自可以与 MetricPoints 的 LabelSet 和时间戳不同。

Exemplar 的 LabelSet 的标签名称和值的总长度不得超过 128 个 UTF-8 字符代码点。为简化实现并保持文本和 proto 格式之间的一致性,范例的文本表示中的其他字符(如 `",=`)不计入此限制。

接收器可以丢弃范例。

指标

指标由 MetricFamily 中的唯一 LabelSet 定义。指标必须包含一个或多个 MetricPoint 的列表。对于给定的 MetricFamily,具有相同名称的指标应在其 LabelSet 中具有相同的标签名称集。

指标点不应有明确的时间戳。

如果一个指标公开了多个 MetricPoint,那么它的 MetricPoint 必须具有单调递增的时间戳。

指标族

一个指标族可以有零个或多个指标。指标族必须有一个名称、HELP、TYPE 和 UNIT 元数据。指标族中的每个指标都必须有一个唯一的 LabelSet。

名称

指标族名称是一个字符串,并且在 MetricSet 中必须是唯一的。名称应使用蛇形命名法(snake_case)。指标名称必须遵循 ABNF 部分中的限制。

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

以下划线开头的 MetricFamily 名称是保留的,除非本标准指定,否则不得使用。

后缀

MetricFamily 的名称不能与 MetricSet 中另一个 MetricFamily 在文本格式中的样本指标名称产生潜在冲突。例如,一个名为“foo_created”的 Gauge,因为一个名为“foo”的 Counter 可能会在文本格式中创建一个“foo_created”。

公开者应避免使用可能与文本格式样本指标名称所用后缀混淆的名称。

  • 各种类型的后缀如下:
  • 计数器 (Counter):`_total`、`_created`
  • 摘要 (Summary):`_count`、`_sum`、`_created`、``(空)
  • 直方图 (Histogram):`_count`、`_sum`、`_bucket`、`_created`
  • 仪表盘直方图 (GaugeHistogram):`_gcount`、`_gsum`、`_bucket`
  • 信息 (Info):`_info`
  • 仪表盘 (Gauge):``(空)
  • 状态集 (StateSet):``(空)
  • 未知 (Unknown):``(空)
类型

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

单位

单位指定指标族单位。如果非空,它必须是指标族名称的后缀,并用下划线分隔。请注意,进一步的生成规则可能会使其在文本格式中成为中缀。

帮助

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

指标集

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

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

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

如果存在,根据下文“在基于推送和基于拉取的系统中支持目标元数据”一节,名为“target”的信息指标族应放在首位。

指标类型

Gauge(仪表盘)

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

类型为 gauge 的指标中的 MetricPoint 必须有单个值。

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

仪表盘可用于编码枚举,其中枚举具有许多状态并随时间变化,这是最高效但用户友好度最低的方式。

Counter(计数器)

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

类型为 Counter 的指标中的 MetricPoint 必须有一个名为 Total 的值。Total 是一个非 NaN 值,并且必须随时间单调非递减,从 0 开始。

类型为 Counter 的指标中的 MetricPoint 应该有一个名为 Created 的时间戳值。这可以帮助接收者区分新指标和它之前未见过的长期运行的指标。

指标计数器的 Total 值可以重置为 0。如果存在,相应的 Created 时间也必须设置为重置的时间戳。

一个指标的计数器的 Total 值可以有一个范例。

状态集

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

StateSet 指标的一个点可以包含多个状态,并且必须为每个状态包含一个布尔值。状态有一个名称,是字符串。

StateSet 指标的 LabelSet 不得具有与其 MetricFamily 名称相同的标签名称。

如果编码为状态集,枚举在 MetricPoint 内必须只有一个布尔值为真。

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

类型为 StateSets 的 MetricFamilies 必须有一个空的 Unit 字符串。

信息

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

Info 指标的 MetricPoint 包含一个 LabelSet。Info MetricPoint 的 LabelSet 不得具有与其 Metric 的 LabelSet 的标签名称相同的标签名称。

Info 可用于对值不随时间变化的枚举进行编码,例如网络接口的类型。

类型为 Info 的 MetricFamilies 必须有一个空的 Unit 字符串。

Histogram(直方图)

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

直方图指标点必须至少包含一个桶,并且应包含 Sum 和 Created 值。每个桶必须有一个阈值和一个值。

直方图 MetricPoints 必须有一个 +Inf 阈值的桶。桶必须是累积的。例如,对于一个表示请求延迟(单位为秒)的指标,其阈值为 1、2、3 和 +Inf 的桶的值必须遵循 value_1 <= value_2 <= value_3 <= value_+Inf。如果十个请求各耗时 1 秒,则 1、2、3 和 +Inf 桶的值必须等于 10。

+Inf 桶计算所有请求。如果存在,Sum 值必须等于所有测量事件值的总和。MetricPoint 内的桶阈值必须是唯一的。

在语义上,Sum 和 bucket 的值是计数器,因此不能是 NaN 或负数。可以使用负阈值桶,但这样 Histogram MetricPoint 就不能包含 sum 值,因为它在语义上不再是计数器。桶阈值不能是 NaN。Count 和 bucket 的值必须是整数。

直方图 MetricPoint 应该有一个名为 Created 的时间戳值。这可以帮助接收者区分新指标和之前未见的长期运行的指标。

直方图指标的 LabelSet 不得有“le”标签名。

桶值可以有范例。桶是累积的,允许监控系统出于性能/反拒绝服务的原因丢弃任何非+Inf的桶,这种方式会损失粒度,但仍然是一个有效的直方图。

每个桶覆盖小于或等于其值的值,并且范例的值必须在此范围内。范例应放入值最高的桶中。一个桶不得有多个范例。

仪表盘直方图

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

GaugeHistogram MetricPoint 必须有一个 +Inf 阈值的桶,并且应该包含一个 Gsum 值。每个桶必须有一个阈值和一个值。

GaugeHistogram 的桶遵循与 Histogram 相同的所有规则。

GaugeHistogram 的 bucket 和 Gsum 在概念上是 gauges,但是 bucket 值不能为负数或 NaN。如果存在负阈值 bucket,那么 sum 可以为负数。Gsum 不能为 NaN。Bucket 值必须是整数。

GaugeHistogram 的 Metric 的 LabelSet 不得有 "le" 标签名。

桶值可以有范例。

每个桶覆盖小于或等于其值的值,并且范例的值必须在此范围内。范例应放入值最高的桶中。一个桶不得有多个范例。

总结

摘要(Summaries)也测量离散事件的分布,当直方图成本过高和/或平均事件大小足够时,可以使用摘要。

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

Summary MetricPoint 可以由 Count、Sum、Created 和一组分位数组成。

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

类型为 Summary 且包含 Count 或 Sum 值的指标中的 MetricPoint 应该有一个名为 Created 的时间戳值。这可以帮助接收者区分新指标和之前未见的长期运行指标。Created 不能与分位数值的收集周期相关。

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

未知

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

未知类型指标中的一个点必须有单个值。

数据传输与线路格式

必须支持文本线路格式,并且是默认格式。可以支持 protobuf 线路格式,但必须在协商后才能使用。

OpenMetrics 格式是正则乔姆斯基文法,使得编写快速小巧的解析器成为可能。文本格式压缩效果好,而 protobuf 已经是二进制且高效编码的。

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

协议协商

所有接收器实现必须能够接收使用 TLS 1.2 或更高版本加密的数据。所有公开器应能够发出使用 TLS 1.2 或更高版本加密的数据。接收器实现应能够接收来自不使用 TLS 的 HTTP 的数据。所有实现都应使用 TLS 传输数据。

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

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

文本格式

ABNF

ABNF 遵循 RFC 5234 规范。

"exposition" 是 ABNF 的顶层令牌。

exposition = metricset HASH SP eof [ LF ]

metricset = *metricfamily

metricfamily = *metric-descriptor *metric

metric-descriptor = HASH SP type SP metricname SP metric-type LF
metric-descriptor =/ HASH SP help SP metricname SP escaped-string LF
metric-descriptor =/ HASH SP unit SP metricname SP *metricname-char LF

metric = *sample

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

sample = metricname [labels] SP number [SP timestamp] [exemplar] LF

exemplar = SP HASH SP labels SP number [SP timestamp]

labels = "{" [label *(COMMA label)] "}"

label = label-name EQ DQUOTE escaped-string DQUOTE

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

timestamp = realnumber

; Not 100% sure this captures all float corner cases.
; Leading 0s explicitly okay
realnumber = [SIGN] 1*DIGIT
realnumber =/ [SIGN] 1*DIGIT ["." *DIGIT] [ "e" [SIGN] 1*DIGIT ]
realnumber =/ [SIGN] *DIGIT "." 1*DIGIT [ "e" [SIGN] 1*DIGIT ]


; RFC 5234 is case insensitive.
; Uppercase
eof = %d69.79.70
type = %d84.89.80.69
help = %d72.69.76.80
unit = %d85.78.73.84
; Lowercase
counter = %d99.111.117.110.116.101.114
gauge = %d103.97.117.103.101
histogram = %d104.105.115.116.111.103.114.97.109
gaugehistogram = gauge histogram
stateset = %d115.116.97.116.101.115.101.116
info = %d105.110.102.111
summary = %d115.117.109.109.97.114.121
unknown = %d117.110.107.110.111.119.110

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

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

metricname-char = metricname-initial-char / DIGIT
metricname-initial-char = ALPHA / "_" / ":"

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-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

总体结构

必须使用 UTF-8。不得使用字节顺序标记 (BOM)。提醒实现者的一个重点是,字节 0 是有效的 UTF-8,而字节 255 则不是。

内容类型必须是

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

换行必须用换行符 (\n) 表示,并且不得包含回车符 (\r)。Exposition 必须以 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_sum{path="/api/v1",method="GET"} 9036.32
acme_http_router_request_seconds_count{path="/api/v1",method="GET"} 807283.0
acme_http_router_request_seconds_created{path="/api/v1",method="GET"} 1605281325.0
acme_http_router_request_seconds_sum{path="/api/v2",method="POST"} 479.3
acme_http_router_request_seconds_count{path="/api/v2",method="POST"} 34.0
acme_http_router_request_seconds_created{path="/api/v2",method="POST"} 1605281325.0
# TYPE go_goroutines gauge
# HELP go_goroutines Number of goroutines that currently exist.
go_goroutines 69
# 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_total 4.20072246e+06
# EOF
转义

当 ABNF 中提到转义时,必须应用以下转义规则:换行符,\n (0x0A) -> 字面量 \\n (字节码 0x5c 0x6e) 双引号 -> \\" (字节码 0x5c 0x22) 反斜杠 -> \\\\ (字节码 0x5c 0x5c)

应该使用双反斜杠来表示一个反斜杠字符。不应该对未定义的转义序列使用单个反斜杠。例如,\\\\a 等效于 \\a 并且更推荐使用前者。

数字

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

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

如“规范数字”一节中所述,“quantile”和“le”标签值不得使用任意的整数和浮点数表示。在其他任何使用数字的地方,都可以使用这种表示。

注意事项:规范数字

直方图的 "le" 标签值和摘要指标的 "quantile" 标签值中的数字是特殊的,因为它们是标签值,而标签值旨在作为不透明的字符串。由于最终用户可能会直接与这些字符串值交互,并且许多监控系统缺乏将它们作为一等公民数字处理的能力,因此如果一个给定的数字有完全相同的文本表示,将会非常有益。

一致性非常重要,但现实世界中各种语言及其运行时的实现使得强制要求这一点不切实际。最重要的常见分位数是 0.5、0.95、0.9、0.99、0.999,以及代表从一毫秒到 10.0 秒值的桶值,因为这些覆盖了典型 Web 服务的延迟 SLA 和 Apdex 等情况。涵盖 10 的幂是为了确保在定点和指数表示之间的切换是一致的,因为这在不同的运行时中会有所不同。目标渲染等同于 Go 语言 float64 值的默认渲染(即 %g),并在没有小数点或指数的情况下追加 .0,以明确它们是浮点数。

Exposer 必须将正无穷大输出为 +Inf。

对于 0.0 到 10.0 之间以 0.001 为增量的值,Exposer 应该按照以下示例生成输出:0.0 0.001 0.002 0.01 0.1 0.9 0.95 0.99 0.999 1.0 1.7 10.0

对于 1e-10 到 1e+10 之间的 10 的幂次方值,Exposer 应该按照以下示例生成输出:1e-10 1e-09 1e-05 0.0001 0.1 1.0 100000.0 1e+06 1e+10

解析器不得仅仅因为输入值不符合规范值而拒绝它们。例如,不能拒绝 1.1e-4,尽管它不是 0.00011 的一致性表示。

对于非规范数字,Exposer 应该遵循这些模式。其目的是通过调整渲染算法使这些值保持一致,从而使绝大多数其他值也具有一致的渲染。只使用少数特定 le/quantile 值的 Exposer 也可以进行硬编码。在 C 等语言中,如果 Grisu3 这样的最小化浮点数渲染算法不易获得,Exposer 可以使用不同的渲染方式。

给 C 语言及其他共享其 printf 实现的语言的实现者的一个警告是:%f、%e 和 %g 的标准精度只有六位有效数字。要达到完整精度需要 17 位有效数字,例如 printf("%.17g", d)

时间戳

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

MetricFamily

MetricFamily 之间不得有显式分隔符。下一个 MetricFamily 必须通过元数据或一个新的样本指标名称来表示,该名称不能是前一个 MetricFamily 的一部分。

MetricFamily 不得交错出现。

MetricFamily 元数据

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

# TYPE foo counter

如果未暴露 TYPE,则 MetricFamily 必须为 Unknown 类型。

如果指定了单位,必须在 UNIT 元数据行中提供。此外,下划线和单位必须是 MetricFamily 名称的后缀。

一个有效的示例,用于一个单位为 "seconds" 的 foo_seconds 指标:

# TYPE foo_seconds counter
# UNIT foo_seconds seconds

一个无效的示例,其中单位不是名称的后缀:

# TYPE foo counter
# UNIT foo seconds

以下也是有效的:

# TYPE foo_seconds counter

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

UNIT 或 HELP 行的值可以为空。这必须被视为该 MetricFamily 不存在相应的元数据行。

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

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

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

指标

指标不得交错出现。

请参阅“文本格式 -> MetricPoint”中的示例。一个没有标签或时间戳且值为 0 的样本必须渲染为以下两种形式之一:

bar_seconds_count 0

bar_seconds_count{} 0

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

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

MetricPoint 值的渲染可以包含额外的标签(例如,Histogram 类型的 "le" 标签),这些标签的渲染方式必须与 Metric 自身的 LabelSet 相同。

MetricPoint

MetricPoints 不得交错出现。

一个正确的示例,其中一个 MetricFamily 内有多个 MetricPoints 和 Samples:

# TYPE foo_seconds summary
# UNIT foo_seconds seconds
foo_seconds_count{a="bb"} 0 123
foo_seconds_sum{a="bb"} 0 123
foo_seconds_count{a="bb"} 0 456
foo_seconds_sum{a="bb"} 0 456
foo_seconds_count{a="ccc"} 0 123
foo_seconds_sum{a="ccc"} 0 123
foo_seconds_count{a="ccc"} 0 456
foo_seconds_sum{a="ccc"} 0 456

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

# TYPE foo_seconds summary
# UNIT foo_seconds seconds
foo_seconds_count{a="bb"} 0 123
foo_seconds_count{a="ccc"} 0 123
foo_seconds_count{a="bb"} 0 456
foo_seconds_count{a="ccc"} 0 456

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

# TYPE foo_seconds summary
# UNIT foo_seconds seconds
foo_seconds_count{a="bb"} 0 123
foo_seconds_count{a="bb"} 0 456
foo_seconds_sum{a="bb"} 0 123
foo_seconds_sum{a="bb"} 0 456

指标类型

Gauge

对于 Gauge 类型的 MetricFamily,其 MetricPoint 值的样本 MetricName 不得有后缀。

一个 MetricFamily 示例,包含一个无标签的 Metric 和一个无时间戳的 MetricPoint:

# TYPE foo gauge
foo 17.0

一个 MetricFamily 示例,包含两个带标签的 Metric 和无时间戳的 MetricPoint:

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

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

# TYPE foo gauge

一个示例,包含一个带标签的 Metric 和一个带时间戳的 MetricPoint:

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

一个示例,包含一个无标签的 Metric 和一个带时间戳的 MetricPoint:

# TYPE foo gauge
foo 17.0 1520879607.789

一个示例,包含一个无标签的 Metric 和两个带时间戳的 MetricPoint:

# TYPE foo gauge
foo 17.0 123
foo 18.0 456
Counter

MetricPoint 的总值样本 MetricName 必须有 _total 后缀。如果存在,MetricPoint 的创建时间值样本 MetricName 必须有 _created 后缀。

一个示例,包含一个无标签的 Metric,以及一个无时间戳且无 created 值的 MetricPoint:

# TYPE foo counter
foo_total 17.0

一个示例,包含一个无标签的 Metric,以及一个带时间戳且无 created 值的 MetricPoint:

# TYPE foo counter
foo_total 17.0 1520879607.789

一个示例,包含一个无标签的 Metric,以及一个无时间戳但有 created 值的 MetricPoint:

# TYPE foo counter
foo_total 17.0
foo_created 1520430000.123

一个示例,包含一个无标签的 Metric,以及一个带时间戳且有 created 值的 MetricPoint:

# TYPE foo counter
foo_total 17.0 1520879607.789
foo_created 1520430000.123 1520879607.789

Exemplar 可以附加到 MetricPoint 的 Total 样本上。

StateSet

对于 StateSet 类型的 MetricFamily,其 MetricPoint 值的样本 MetricName 不得有后缀。

StateSet 的 MetricPoint 中必须为每个状态包含一个样本。每个状态的样本必须有一个标签,其标签名为 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
Info

对于 Info 类型的 MetricFamily,其 MetricPoint 值的样本 MetricName 必须有 _info 后缀。样本值必须始终为 1。

一个示例,包含一个无标签的 Metric,以及一个带有 "name" 和 "version" 标签的 MetricPoint 值:

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

一个示例,包含一个带 "entity" 标签的 Metric,以及一个带有 "name" 和 "version" 标签的 MetricPoint 值:

# TYPE foo 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

Metric 标签和 MetricPoint 值标签可以按任意顺序排列。

总结

如果存在,MetricPoint 的 Sum 值样本 MetricName 必须有 _sum 后缀。如果存在,MetricPoint 的 Count 值样本 MetricName 必须有 _count 后缀。如果存在,MetricPoint 的 Created 值样本 MetricName 必须有 _created 后缀。如果存在,MetricPoint 的 Quantile 值必须使用一个名为 "quantile" 的标签来指定测量的分位数,标签值为测量的分位数值。

一个示例,包含一个无标签的 Metric,以及一个带有 Sum、Count 和 Created 值的 MetricPoint:

# TYPE foo summary
foo_count 17.0
foo_sum 324789.3
foo_created 1520430000.123

一个示例,包含一个无标签的 Metric,以及一个带有两个分位数的 MetricPoint:

# TYPE foo summary
foo{quantile="0.95"} 123.7
foo{quantile="0.99"} 150.0

分位数可以按任意顺序排列。

Histogram

MetricPoint 的桶值样本 MetricNames 必须有 _bucket 后缀。如果存在,MetricPoint 的 Sum 值样本 MetricName 必须有 _sum 后缀。如果存在,MetricPoint 的 Created 值样本 MetricName 必须有 _created 后缀。当且仅当一个 MetricPoint 中存在 Sum 值时,该 MetricPoint 的 +Inf 桶值也必须出现在一个带有 _count 后缀的 MetricName 的样本中。

桶必须按照 "le" 值的数字升序排列,并且 "le" 标签的值必须遵循规范数字的规则。

一个示例,包含一个无标签的 Metric,以及一个带有 Sum、Count 和 Created 值并有 12 个桶的 MetricPoint。示例中特意展示了各种宽泛且不典型但有效的 "le" 值:

# TYPE foo histogram
foo_bucket{le="0.0"} 0
foo_bucket{le="1e-05"} 0
foo_bucket{le="0.0001"} 5
foo_bucket{le="0.1"} 8
foo_bucket{le="1.0"} 10
foo_bucket{le="10.0"} 11
foo_bucket{le="100000.0"} 11
foo_bucket{le="1e+06"} 15
foo_bucket{le="1e+23"} 16
foo_bucket{le="1.1e+23"} 17
foo_bucket{le="+Inf"} 17
foo_count 17
foo_sum 324789.3
foo_created 1520430000.123
Exemplar

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

一个展示了多个有效案例的 Exemplar 示例:“0.01” 桶没有 Exemplar。0.1 桶有一个无标签的 Exemplar。1 桶有一个带一个标签的 Exemplar。10 桶有一个带标签和时间戳的 Exemplar。在实践中,所有桶都应该有相同风格的 Exemplar。

# TYPE foo histogram
foo_bucket{le="0.01"} 0
foo_bucket{le="0.1"} 8 # {} 0.054
foo_bucket{le="1"} 11 # {trace_id="KOO5S4vxi0o"} 0.67
foo_bucket{le="10"} 17 # {trace_id="oHg5SJYRHA0"} 9.8 1520879607.789
foo_bucket{le="+Inf"} 17
foo_count 17
foo_sum 324789.3
foo_created  1520430000.123
GaugeHistogram

MetricPoint 的桶值样本 MetricNames 必须有 _bucket 后缀。如果存在,MetricPoint 的 Sum 值样本 MetricName 必须有 _gsum 后缀。当且仅当一个 MetricPoint 中存在 Sum 值时,该 MetricPoint 的 +Inf 桶值也必须出现在一个带有 _gcount 后缀的 MetricName 的样本中。

桶必须按照 "le" 值的数字升序排列,并且 "le" 标签的值必须遵循规范数字的规则。

一个示例,包含一个无标签的 Metric,以及一个 MetricPoint 值,该值在桶中没有 Exemplar。

# TYPE foo gaugehistogram
foo_bucket{le="0.01"} 20.0
foo_bucket{le="0.1"} 25.0
foo_bucket{le="1"} 34.0
foo_bucket{le="10"} 34.0
foo_bucket{le="+Inf"} 42.0
foo_gcount 42.0
foo_gsum 3289.3
Unknown

对于 Unknown 类型的 MetricFamily,其 MetricPoint 值的样本指标名称不得有后缀。

一个示例,包含一个无标签的 Metric 和一个无时间戳的 MetricPoint:

# TYPE foo unknown
foo 42.23

Protobuf 格式

总体结构

Protobuf 消息必须以二进制格式编码,并且其内容类型必须为 application/openmetrics-protobuf; version=1.0.0

所有有效载荷必须是单个二进制编码的 MetricSet 消息,该消息由 OpenMetrics protobuf 模式定义。

版本

Protobuf 格式必须遵循 proto3 版本的协议缓冲区语言。

字符串

所有字符串字段必须使用 UTF-8 编码。

时间戳

OpenMetrics protobuf 模式中的时间戳表示必须遵循已发布的 google.protobuf.Timestamp [timestamp] 消息。时间戳消息必须是 Unix 纪元秒,以 int64 表示,以及一个纳秒级分辨率的非负秒数部分,以 int32 表示,从秒时间戳部分开始向前计数。该值必须在 0 到 999,999,999(含)之间。

Protobuf 模式

Protobuf 模式目前可在此处获取

注意Prometheus 及其生态系统不支持 OpenMetrics protobuf 模式,而是使用类似的 io.prometheus.client 格式。关于 OpenMetrics 2.0 中 protobuf 模式未来的讨论正在进行中

设计考虑

范围

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

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

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

范围之外

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

扩展与改进

OpenMetrics 的第一个版本基于公认且事实标准的 Prometheus 文本格式 0.0.4,并特意没有在其之上添加主要的句法或语义扩展或优化。例如,没有尝试使直方图桶的文本表示更紧凑,而是依赖底层堆栈中的压缩来处理其重复性。

这是一个刻意的选择,以便该标准能够利用现有用户群的采用和势头。这确保了从 Prometheus 文本格式 0.0.4 的过渡相对容易。

它还确保有一个易于实现的基本标准。这可以在标准的未来版本中加以扩展。其意图是,标准的未来版本将始终要求支持这个 1.0 版本,无论是在句法上还是语义上。

我们希望允许监控系统能够从 OpenMetrics exposition 中获取有用的信息,而不会带来过重的负担。如果剥离所有元数据和结构,仅将 OpenMetrics exposition 视为一组无序的样本,那么它本身也应该是可用的。因此,也没有不透明的二进制类型,如 sketch 或 t-digest,这些类型无法表示为 gauge 和 counter 的混合,因为它们需要自定义的解析和处理。

这一原则在整个标准中得到了一贯的应用。例如,MetricFamily 的单位在名称中重复出现,以便不理解单位元数据的系统也能获得单位信息。“le”标签是一个普通的标签值,而不是拥有自己特殊的语法,这样摄取方就不必添加特殊的直方图处理代码来摄取它们。再举一个例子,没有复合数据类型。例如,没有用于纬度/经度的地理位置类型,因为这可以通过单独的 gauge 指标来完成。

单位和基本单位

为了在系统间保持一致性并避免混淆,单位主要基于国际单位制(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 MetricPoints 的值添加标签不是破坏性变更。这样做是为了您可以向现有的 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”格式。

举个例子,“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 中使用多个指标或使用多个 MetricFamily 似乎都是合理的。对 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 标头。虽然这可以为推送式摄取方传输目标元数据,并且本标准也不禁止这样做,但它的缺点是,即使拉取式摄取方应该使用自己的目标元数据,能够访问暴露方自身知道的元数据通常仍然很有用。

首选的解决方案是将此目标元数据作为 exposition 的一部分提供,但方式不影响整个 exposition。Info MetricFamily 就是为此设计的。一个暴露方可以包含一个名为“target”的 Info MetricFamily,其中只有一个没有标签的指标,包含了元数据。文本格式的一个示例如下:

# TYPE target info
# HELP target 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 暴露暴露方元数据。

以上讨论是在单个暴露方的背景下进行的。来自通用监控系统的 exposition 可能包含来自许多单个目标的指标,因此可能暴露多个目标信息指标。这些指标在摄取过程中可能已经将目标元数据作为标签添加。指标名称不得根据目标元数据而改变。例如,即使所有指标都源自暂存环境中的目标,将所有指标都加上 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 中暴露具有相同时间戳的相同指标 MetricPoint 值是有效的,但是如果底层指标现在已经丢失,这样做是无效的。

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

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

所有这些都说明,通常情况下,不应暴露 MetricPoint 时间戳,因为应由摄取方自行决定为其摄取的数据样本附加时间戳。

跟踪指标最后变更时间

假设你有一个计数器 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 的时间戳可能相对于摄取方的系统时钟或来自同一 exposition 的其他指标而言,看起来略微超前。同样,一个 MetricPoint 的“_created”时间戳也可能看起来略晚于该 MetricPoint 的 exemplar 或样本时间戳。

请记住,常用的监控系统支持从纳秒到秒的各种分辨率,因此,如果两个 MetricPoint 的时间戳在截断到秒级分辨率后相同,可能会导致在摄取方出现明显的重复。在这种情况下,必须使用时间戳最早的 MetricPoint。

阈值

暴露系统的期望边界可能是有意义的,但需要谨慎处理。对于普遍适用的值,为这些阈值发出 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的唯一字符串可能表明该使用场景更像是事件日志记录,而不是指标时间序列。

Exemplar长度有128个UTF-8字符的硬性限制,以防止滥用该功能进行追踪跨度数据和其他事件日志记录。

安全性

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

所有公开方实现都应该(SHOULD)能够使用TLS 1.2或更高版本来保护其HTTP流量。如果公开方实现不支持加密,操作员应该(SHOULD)在可行的情况下使用反向代理、防火墙和/或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-textapplication/openmetrics-proto

本页内容