OpenMetrics

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

Prometheus 创建于 2012 年,自 2015 年以来一直是云原生可观测性的默认选择。Prometheus 设计的核心部分是其文本指标暴露格式,称为 Prometheus exposition format 0.0.4,自 2014 年以来保持稳定。该格式经过特别设计,易于生成、易于摄取,且易于人类理解。截至 2020 年,有超过 700 个公开列表的 exporter,数量不明的未列表 exporter,以及数千个使用此格式的原生库集成。来自各种项目和公司的数十个 ingestor 支持消费它。

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

鉴于在生态系统中的广泛采用和重要的协调要求,对 Prometheus exposition format 0.0.4 或 OpenMetrics 1.0 进行全面更改超出了范围。

注意: OpenMetrics 2.0 正在开发中。请在此阅读如何加入 Prometheus OM 2.0 工作组。

概述

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

OpenMetrics 主要是一种线格式(wire format),与该格式的任何特定传输方式无关。该格式预计会定期消费,并在连续的暴露中具有意义。

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

指标与时间序列

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

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

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

数据模型

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

数据类型

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

布尔值

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

时间戳

时间戳必须是 Unix Epoch 秒数。可以使用负时间戳。

字符串

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

标签

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

以单下划线开头的标签名是保留的,除非本标准另有规定,否则不得使用。标签名必须遵循 ABNF 节中的限制。

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

标签集

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

指标点

每个指标点包含一组值,具体取决于指标家族类型。

Exemplar

Exemplar 是对指标集外部数据的引用。一个常见的用例是程序跟踪的 ID。

Exemplar 必须包含一个标签集和一个值,并且可以有一个时间戳。它们可以各自与指标点的时间戳不同。

Exemplar 标签集的标签名和标签值的总长度不得超过 128 个 UTF-8 字符码点。Exemplar 文本渲染中的其他字符,如 ",=,为了实现简单性和文本与 protobuf 格式的一致性,不包含在此限制内。

Ingestor 可以丢弃 exemplar。

指标

指标由指标家族中唯一的标签集定义。指标必须包含一个或多个指标点列表。给定指标家族中同名的指标应该具有相同的标签名集。

指标点不应该有显式的时间戳。

如果为一个指标暴露了多个指标点,则其指标点必须具有单调递增的时间戳。

指标家族

一个指标家族可以有零个或多个指标。一个指标家族必须具有名称、HELP、TYPE 和 UNIT 元数据。指标家族内的每个指标必须具有唯一的标签集。

名称

指标家族名称是一个字符串,并且在指标集中必须唯一。名称应该使用 snake_case 格式。指标名称必须遵循 ABNF 节中的限制。

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

以单下划线开头的指标家族名称是保留的,除非本标准另有规定,否则不得使用。

后缀

指标家族的名称不得导致文本格式中与同一指标集中另一个指标家族的样本指标名称发生潜在冲突(根据 ABNF)。例如,一个名为 "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 指定指标家族类型。有效值包括 "unknown"、"gauge"、"counter"、"stateset"、"info"、"histogram"、"gaugehistogram" 和 "summary"。

单位

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

Help

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

指标集

指标集是 OpenMetrics 暴露的顶级对象。它必须由指标家族组成,并且可以为空。

每个指标家族名称必须唯一。相同的标签名和值不应该出现在指标集中的每个指标上。

指标集内不需要特定的指标家族顺序。如果性能权衡合理,暴露器可以使暴露更易于人类阅读,例如按字母顺序排序。

如果存在,根据下面的“支持基于推送和基于拉取的系统中的目标元数据”一节,一个名为 "target" 的 Info 指标家族应该排在第一位。

指标类型

Gauge

Gauge 是当前测量值,例如当前使用的内存字节数或队列中的项目数。对于 gauge,用户感兴趣的是其绝对值。

Gauge 类型的指标家族中的指标点的指标点必须只有一个值。

Gauge 值可以随时间增加、减少或保持不变。即使它们只朝一个方向变化,它们可能仍然是 gauge 而不是 counter。日志文件的大小通常只会增加,资源可能会减少,而队列大小的限制可能保持不变。

当枚举有许多状态并且随时间变化时,可以使用 gauge 来编码枚举,它是效率最高的但对用户最不友好。

Counter

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

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

Counter 类型的指标的指标点应该有一个名为 Created 的时间戳值。这有助于 ingestor 区分新指标和之前未见的长期运行指标。

Counter 类型的指标的指标点的 Total 值可以重置为 0。如果存在,对应的 Created 时间也必须设置为重置的时间戳。

Counter 类型的指标的指标点的 Total 值可以有一个 exemplar。

StateSet

StateSet 表示一系列相关的布尔值,也称为位集。如果需要编码枚举 (ENUMs),可以使用 StateSet 来完成。

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

StateSet 指标的标签集不得有与其指标家族名称相同的标签名。

如果编码为 StateSet,枚举 (ENUMs) 在一个指标点中必须只有一个布尔值为 true。

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

StateSet 类型的指标家族必须具有空单位字符串。

Info

Info 指标用于暴露不应在进程生命周期内更改的文本信息。常见的例子是应用程序版本、版本控制提交和编译器版本。

Info 指标的指标点包含一个标签集。Info 指标点的标签集不得有与其指标的标签集中标签名相同的标签名。

Info 可以用于编码值不随时间变化的枚举 (ENUMs),例如网络接口的类型。

Info 类型的指标家族必须具有空单位字符串。

Histogram

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

Histogram 指标点必须至少包含一个桶 (bucket),并且应该包含 Sum 和 Created 值。每个桶必须有一个阈值 (threshold) 和一个值。

Histogram 指标点必须有一个带有 +Inf 阈值的桶。桶必须是累积的。例如,对于一个表示请求延迟(秒)的指标,其阈值为 1、2、3 和 +Inf 的桶的值必须遵循 value_1 <= value_2 <= value_3 <= value_+Inf。如果十个请求每个都花费了 1 秒,则阈值为 1、2、3 和 +Inf 的桶的值必须等于 10。

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

从语义上讲,Sum 和桶值是 counter,因此不得为 NaN 或负数。可以使用负阈值桶,但这样 Histogram 指标点就不能包含 sum 值,因为它在语义上不再是 counter。桶阈值不得等于 NaN。Count 和桶值必须是整数。

Histogram 指标点应该有一个名为 Created 的时间戳值。这有助于 ingestor 区分新指标和之前未见的长期运行指标。

Histogram 指标的标签集不得有名为 "le" 的标签。

桶值可以有 exemplar。桶是累积的,以便监控系统可以为了性能/反拒绝服务原因丢弃任何非 +Inf 的桶,这样虽然会损失粒度,但仍然是一个有效的 Histogram。

每个桶包含小于或等于其阈值的值,exemplar 的值必须在该范围内。Exemplar 应该放在值最高的桶中。一个桶不得有多个 exemplar。

GaugeHistogram

GaugeHistogram 测量当前的分布。常见的例子包括项目在队列中等待的时间或队列中请求的大小。

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

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

GaugeHistogram 的桶和 Gsum 在概念上是 gauge,但是桶值不得为负数或 NaN。如果存在负阈值桶,则 sum 可以为负数。Gsum 不得为 NaN。桶值必须是整数。

GaugeHistogram 指标的标签集不得有名为 "le" 的标签。

桶值可以有 exemplar。

每个桶包含小于或等于其阈值的值,exemplar 的值必须在该范围内。Exemplar 应该放在值最高的桶中。一个桶不得有多个 exemplar。

Summary

Summary 也测量离散事件的分布,当 Histogram 太昂贵和/或平均事件大小足够时,可以使用 Summary。

它们也可以用于向后兼容,因为一些现有的埋点库暴露预计算的分位数 (quantiles),并且不支持 Histogram。不应该使用预计算的分位数,因为分位数不可聚合,用户通常无法推断它们涵盖的时间范围。

Summary 指标点可以包含 Count、Sum、Created 和一组分位数。

从语义上讲,Count 和 Sum 值是 counter,因此不得为 NaN 或负数。Count 必须是整数。

Summary 类型的指标的指标点如果包含 Count 或 Sum 值,应该有一个名为 Created 的时间戳值。这有助于 ingestor 区分新指标和之前未见的长期运行指标。Created 不得与分位数值的收集周期相关。

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

Unknown

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

Unknown 类型的指标中的点必须只有一个值。

数据传输与线格式

必须支持文本线格式,且其为默认格式。可以支持 Protobuf 线格式,并且必须仅在协商后使用。

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

部分或无效的暴露必须被视为整个暴露的错误。

协议协商

所有 ingestor 实现必须能够摄取使用 TLS 1.2 或更高版本加密的数据。所有暴露器应该能够发送使用 TLS 1.2 或更高版本加密的数据。ingestor 实现应该能够从不使用 TLS 的 HTTP 摄取数据。所有实现都应该使用 TLS 传输数据。

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

基于推送的协商本身更复杂,因为暴露器通常发起连接。生产者必须使用标准的最低版本(即 1.0.0),除非 ingestor 另有要求。

文本格式

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)。暴露必须以 EOF 结束,并且应该以 EOF\n 结束。

一个完整暴露的示例

# 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

数字

整数不得有小数点。例如 23, 0042, 和 1341298465647914

浮点数必须使用小数点或科学计数法表示。例如 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 等情况。包含十的幂次方是为了确保定点表示和指数表示之间的切换是一致的,因为这在不同运行时之间有所差异。目标渲染方式等同于 Go 语言 float64 值的默认渲染方式(即 %g),并在没有小数点或指数的情况下追加 .0,以明确它们是浮点数。

暴露器必须将正无穷大输出为 +Inf。

暴露器应该按照以下示例生成 0.0 到 10.0(以 0.001 为增量)的值输出: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(以十的幂次方为增量)的值输出:1e-10 1e-09 1e-05 0.0001 0.1 1.0 100000.0 1e+06 1e+10

解析器不得仅因为输入与规范值不一致而拒绝它们。例如,即使 1.1e-4 不是 0.00011 的一致渲染,也不得拒绝它。

暴露器应该遵循这些模式来处理非规范数字,目的是通过调整渲染算法以使这些值保持一致,从而使绝大多数其他值也具有一致的渲染。仅使用少量特定 le/quantile 值的暴露器也可以硬编码。在 C 等共享其 printf 实现的语言中,如果无法轻易获得 Grisu3 等最小浮点渲染算法,暴露器可以使用不同的渲染方式。

提醒 C 和其他共享其 printf 实现的语言的实现者: %f、%e 和 %g 的标准精度只有六位有效数字。需要 17 位有效数字才能达到完全精度,例如 printf("%.17g", d)

时间戳

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

指标家族

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

指标家族不得交叉。

指标家族元数据

元数据有四个部分:指标家族名称、TYPE、UNIT 和 HELP。一个名为 foo 的 counter 指标家族的元数据示例是

# TYPE foo counter

如果没有暴露 TYPE,指标家族必须是 Unknown 类型。

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

一个 foo_seconds 指标(单位为 "seconds")的有效示例

# TYPE foo_seconds counter
# UNIT foo_seconds seconds

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

# TYPE foo counter
# UNIT foo seconds

以下形式也是有效的

# TYPE foo_seconds counter

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

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

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

一个指标家族的每种元数据行不得超过一行。顺序应该是 TYPE、UNIT、HELP。

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

指标

指标不得交叉。

参见“文本格式 -> 指标点”中的示例。标签 一个没有标签或时间戳、值为 0 的样本必须以下列方式之一渲染

bar_seconds_count 0

bar_seconds_count{} 0

标签值可以是任何有效的 UTF-8 值,因此必须按照 ABNF 应用转义。一个包含两个标签的有效示例

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

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

指标点

指标点不得交叉。

一个指标家族中包含多个指标点和样本的正确示例是

# 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

一个指标交叉的错误示例

# 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

一个指标点交叉的错误示例

# 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 类型的指标家族的指标点的值的样本指标名称不得有后缀。

一个包含一个没有标签的指标家族、一个没有标签的指标和一个没有时间戳的指标点的示例

# TYPE foo gauge
foo 17.0

一个包含一个有两个标签的指标家族、两个有标签的指标和没有时间戳的指标点的示例

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

一个没有指标的指标家族示例

# 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

指标点的 Total 值样本指标名称必须有后缀 _total。如果存在,指标点的 Created 值样本指标名称必须有后缀 _created

一个包含一个没有标签的指标、一个没有时间戳也没有 created 的指标点的示例

# TYPE foo counter
foo_total 17.0

一个包含一个没有标签的指标、一个有时间戳但没有 created 的指标点的示例

# TYPE foo counter
foo_total 17.0 1520879607.789

一个包含一个没有标签的指标、一个没有时间戳但有 created 的指标点的示例

# TYPE foo counter
foo_total 17.0
foo_created 1520430000.123

一个包含一个没有标签的指标、一个有时间戳且有 created 的指标点的示例

# TYPE foo counter
foo_total 17.0 1520879607.789
foo_created 1520430000.123 1520879607.789

Exemplar 可以附加到指标点的 Total 样本上。

StateSet

StateSet 类型的指标家族的指标点的值的样本指标名称不得有后缀。

StateSets 必须包含 MetricPoint 中每个 State 的一个样本。每个 State 的样本必须包含一个标签,标签名是 MetricFamily 的名称,标签值是 State 的名称。如果 State 为 true,State 样本的值必须是 1;如果 State 为 false,则必须是 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 值的 Sample MetricName 必须带有后缀 _info。Sample 值必须始终是 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 值标签可以以任何顺序出现。

Summary

如果存在,MetricPoint 的 Sum Value Sample MetricName 必须带有后缀 _sum。如果存在,MetricPoint 的 Count Value Sample MetricName 必须带有后缀 _count。如果存在,MetricPoint 的 Created Value Sample MetricName 必须带有后缀 _created。如果存在,MetricPoint 的 Quantile Values 必须使用标签指定测量的分位数,标签名为 "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 的 Bucket Values Sample MetricNames 必须带有后缀 _bucket。如果存在,MetricPoint 的 Sum Value Sample MetricName 必须带有后缀 _sum。如果存在,MetricPoint 的 Created Value Sample MetricName 必须带有后缀 _created。当且仅当 MetricPoint 中存在 Sum Value 时,MetricPoint 的 +Inf Bucket 值也必须出现在一个带有后缀 "_count" 的 MetricName 的 Sample 中。

Buckets 必须按照 "le" 的数值递增顺序排序,并且 "le" 标签的值必须遵循 Canonical Numbers 的规则。

一个没有标签的 Metric 的示例,以及一个带有 Sum、Count 和 Created 值以及 12 个 buckets 的 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
Exemplars

没有 Labels 的 Exemplars 必须表示为一个空的 LabelSet,即 {}。

Exemplars 的示例,展示了几个有效情况:"0.01" bucket 没有 Exemplar。0.1 bucket 有一个没有 Labels 的 Exemplar。1 bucket 有一个带有一个 Label 的 Exemplar。10 bucket 有一个带有 Label 和 timestamp 的 Exemplar。实际上,所有 buckets 都应该使用相同风格的 Exemplars。

# 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 的 Bucket Values Sample MetricNames 必须带有后缀 _bucket。如果存在,MetricPoint 的 Sum Value Sample MetricName 必须带有后缀 _gsum。当且仅当 MetricPoint 中存在 Sum Value 时,MetricPoint 的 +Inf Bucket 值也必须出现在一个带有后缀 _gcount 的 MetricName 的 Sample 中。

Buckets 必须按照 "le" 的数值递增顺序排序,并且 "le" 标签的值必须遵循 Canonical Numbers 的规则。

一个没有标签的 Metric 的示例,以及一个没有 Exemplar 的 MetricPoint 值,buckets 中也没有 Exemplars

# 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 值的 sample metric name 不得带有后缀。

一个没有标签的 Metric 的示例,以及一个没有 timestamp 的 MetricPoint

# TYPE foo unknown
foo 42.23

Protobuf 格式

总体结构

Protobuf 消息必须使用二进制编码,其内容类型必须是 application/openmetrics-protobuf; version=1.0.0

所有载荷都必须是一个单一的二进制编码的 MetricSet 消息,其定义遵循 OpenMetrics protobuf schema。

版本

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

字符串

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

时间戳

OpenMetrics protobuf schema 中的时间戳表示必须遵循已发布的 google.protobuf.Timestamp [timestamp] 消息。时间戳消息必须使用 Unix epoch seconds(int64 类型)和非负的秒的小数部分(int32 类型,纳秒精度,从秒的时间戳分量向前计数)。该小数部分必须在 0 到 999,999,999 之间(包含边界)。

Protobuf schema

Protobuf schema 当前可在此处获取:https://github.com/prometheus/OpenMetrics/blob/3bb328ab04d26b25ac548d851619f90d15090e5d/proto/openmetrics_data_model.proto

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

设计考量

范围

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

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

它旨在以固定的频率传输数据传输时系统状态的快照。

超出范围

采集器如何发现哪些暴露器存在,反之亦然,超出了本标准的范围,因此本标准中未定义此内容。

扩展和改进

OpenMetrics 的第一个版本基于成熟的、事实上的标准 Prometheus text format 0.0.4,特意没有在此基础上添加主要的语法或语义扩展,也没有进行优化。例如,没有尝试使 Histogram buckets 的文本表示更紧凑,而是依赖底层栈的压缩来处理其重复性。

这是一个深思熟虑的选择,以便标准能够利用现有用户群的采用和势头。这确保了从 Prometheus text format 0.0.4 的相对轻松的过渡。

这也确保了有一个易于实现的基础标准。未来的标准版本可以在此基础上构建。其意图是,未来的标准版本将始终要求支持此 1.0 版本,无论是在语法上还是语义上。

我们希望监控系统能够从 OpenMetrics 暴露中获得可用信息,而不会带来不必要的负担。如果剥离所有元数据和结构,只将 OpenMetrics 暴露视为一个无序的样本集合,那么它本身就应该可用。因此,也没有不透明的二进制类型,例如 sketches 或 t-digests,这些类型无法通过 gauge 和 counter 的组合来表达,因为它们需要自定义解析和处理。

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

单位和基本单位

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

例如,将所有持续时间指标都以秒为单位,就没有必要猜测某个给定指标是以纳秒、微秒、毫秒、秒、分钟、小时、天还是周为单位,也无需处理混合单位。通过选择不带前缀的单位,我们避免了诸如千毫秒是复杂系统涌现行为的结果之类的情况。

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

类似地,混合位和字节会令人困惑,因此选择字节作为基本单位。虽然理论上开尔文是更好的基本单位,但实际上大多数现有硬件都暴露摄氏度。千克是 SI 基本单位,但 kilo 前缀有问题,因此选择克作为基本单位。

尽管在所有可能的情况下都应使用基本单位,但开尔文是一个成熟的单位,在颜色或黑体温度等用例中,可能不会将摄氏度和开尔文指标进行比较,因此可以使用开尔文代替摄氏度。

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

分贝不是基本单位,首先,deci 是 SI 前缀,其次,bels 是对数单位。要暴露信号/能量/功率比率,最好直接暴露比率,或者如果可能的话,暴露原始功率/能量。浮点指数足以涵盖即使是极端的科学用途。一个电子伏特(约 1e-19 J)一直到超新星释放的能量(约 1e44 J)跨越 63 个数量级,而一个 64 位浮点数可以覆盖 2000 多个数量级。

如果无法避免非基本单位且转换不可行,则仍应在指标名称中包含实际单位以提高清晰度。例如,焦耳既是能量的基本单位,也是功率的基本单位,因为瓦特可以表示为带有焦耳单位的 counter。实际上,给定的第三方系统可能只暴露瓦特,在这种情况下,以瓦特表示的 gauge 是唯一现实的选择。

并非所有 MetricFamilies 都有单位。例如,HTTP 请求计数就没有单位。从技术上讲,单位是 HTTP 请求,但在这个意义上,整个 MetricFamily 名称就是单位。走向那个极端是没有用的。应该始终记住,下游系统的图表应易于人类理解,并有良好的坐标轴。

无状态性

OpenMetrics 定义的传输格式在各次暴露之间是无状态的。之前暴露的信息对未来的暴露没有任何影响。每次暴露都是暴露器当前状态的一个自包含的快照。

必须向现有和新的采集器提供相同的自包含暴露。

一个核心设计选择是,暴露器不得仅仅因为指标最近没有变化或观察就排除该指标。暴露器不得对采集器采集暴露的频率做出任何假设。

跨时间暴露和指标演进

指标只有在其随时间演进可以分析时才最有意义,因此暴露必须在时间上是有意义的。因此,仅仅一次单独的暴露有用且有效是不够的。指标语义的一些变化也会破坏下游用户。

解析器通常通过缓存先前结果进行优化。因此,尽管技术上并非破坏性更改,但应避免在不同暴露中更改标签的暴露顺序。这通常也会使暴露的单元测试更容易编写。

指标和样本不应该在不同的暴露中出现和消失,例如,counter 只有在有历史时才有用。原则上,一个给定的 Metric 应该从进程启动时开始一直存在于暴露中,直到进程终止。通常无法预先知道 MetricFamily 在给定进程的生命周期内将拥有哪些 Metrics(例如,延迟直方图的标签值是 HTTP 路径,由最终用户在运行时提供),但一旦暴露了 counter 类型的 Metric,它就应该继续暴露,直到进程终止。counter 未获得增量并不表示其当前值失效。在某些情况下,停止暴露给定的 Metric 可能是有意义的;请参见关于 Missing Data 的部分。

一般来说,更改 MetricFamily 的类型,或为其 Metrics 添加或删除标签,将对采集器造成破坏性影响。

一个值得注意的例外是,向 Info MetricPoints 的值添加标签不会造成破坏性影响。这是为了允许您向现有的 Info MetricFamily 中添加额外信息,而无需被迫创建一个带有额外标签值的新 Info 指标。采集系统应确保它们能够弹性应对此类添加。

更改 MetricFamily 的 Help 不会造成破坏性影响。对于可能的值,在浮点数和整数之间切换不会造成破坏性影响。向 stateset 添加新状态不会造成破坏性影响。在不改变指标名称的情况下添加 unit 元数据不会造成破坏性影响。

Histogram buckets 不应在不同暴露之间发生变化,因为这很可能导致性能问题并破坏采集器。同样,应用程序的任何一致二进制和环境的暴露都应该对给定的 Histogram MetricFamily 使用相同的 buckets,以便所有采集器都可以对其进行聚合,而无需采集器实现异构 buckets 的直方图合并逻辑。例外情况可能是偶尔手动更改 buckets,这被视为破坏性更改,但在性能特性因新软件发布而改变时,这可能是一个有效的权衡。

即使更改在技术上不是破坏性的,它们仍然有成本。例如,频繁的更改可能会导致采集器出现性能问题。随暴露而变化的 Help 字符串可能会导致每个 Help 值都被存储。频繁在 int 和 float 值之间切换可能会阻止高效压缩。

NaN

在 OpenMetrics 中,NaN 与其他数字一样,通常是由于除以零而产生的,例如当最近没有观察值时,summary 分位数可能出现这种情况。NaN 在 OpenMetrics 中没有任何特殊含义,特别是不得用于标记缺失或异常数据。

缺失数据

数据停止出现是有效的情况。例如,文件系统可能被卸载,因此其表示空闲磁盘空间的 Gauge Metric 不再存在。这种情况没有特殊的标记或信号。后续的暴露只是不包含此 Metric。

暴露性能

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

通常来说,暴露时间不应超过一秒。

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

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

并发性

对于高可用性和临时访问,一种常见的方法是使用多个采集器。为了支持这一点,必须支持并发暴露。应遵循并发系统的所有最佳实践,常见的陷阱包括死锁、竞态条件以及粒度过粗的锁定,这些都会阻止暴露并发进行。

指标命名和命名空间

我们力求在指标名称和标签名称的可理解性、避免冲突和简洁性之间取得平衡。名称通过下划线分隔,因此指标名称最终采用“snake_case”格式。

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

指标名称应指示它们来自哪段代码。因此,一家名为“A Company Manufacturing Everything”的公司可能会在其代码中所有指标前加上“acme”前缀,如果他们有一个测量延迟的 HTTP 路由器库,其指标可能类似于“acmehttprouterrequest_seconds”,并带有一个 Help 字符串说明这是整体延迟。

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

在整个监控系统的给定部署中,目标是相同指标名称表示不同含义的冲突很少见。例如,acmehttprouterrequestseconds 可能最终出现在 A Company Manufacturing Everything 开发的数百个不同应用程序中,这是正常的。如果 Another Corporation Making Entities 在其 HTTP 路由器中也使用了指标名称 acmehttprouterrequestseconds,那也没问题。如果两个公司的应用程序都被同一个监控系统监控,这种冲突虽然不理想,但可以接受,因为没有哪个应用程序试图同时暴露这两个名称,也没有哪个目标试图(错误地)两次暴露同一个指标名称。如果一个应用程序希望同时包含 My Example Company 和 Mega Exciting Company 的 HTTP 路由器库,那就会成为问题,并且需要以某种方式更改其中一个指标名称。

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

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

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

scrape_ 前缀的指标名称由采集器用于附加与单个暴露相关的信息,因此不应由应用程序直接暴露。已经经过通用监控系统处理和传递的指标在后续暴露中可能包含此类指标名称。如果暴露器希望提供有关单个暴露的信息,可以使用诸如 myexposerscrape 之类的指标前缀。一个常见的例子是 myexposerscrapeduration_seconds 这个 gauge,表示从暴露器的角度来看,该次暴露花费了多长时间。

在 Prometheus 生态系统中,出现了一组跨所有实现一致的进程级指标,其前缀为 process。例如,对于打开的文件 ulimits,MetricFamilies processopenfds 和 processmaxfds gauges 分别提供了当前值和最大值。(这些名称是遗留名称,如果今天定义这些指标,它们更可能被称为 processfdsopen 和 processfds_limit)。一般来说,获得具有相同语义的名称是非常具有挑战性的,这也是为什么不同的 instrumentation 应该使用不同的名称。

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

避免在 instrumentation 中包含的指标名称中包含来自监控系统其他层的实现细节。例如,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 中的多个 Metrics 或多个 MetricFamilies 都是合理的。对 MetricFamily 求和或求平均应该是有意义的,即使并非总是有用。例如,混合电压和风扇转速就没有意义。

提醒一下,OpenMetrics 的构建基于采集器可以处理数据并对其执行聚合的假设。

将总和与其他指标一起暴露是错误的,因为这会导致下游采集器在聚合时出现重复计数。

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

指标的标签应尽可能少,以确保唯一性,因为每增加一个标签,用户在确定下游需要处理哪些 Labels 时就需要考虑更多。可以应用于许多 MetricFamilies 的标签是移入 _info 指标的候选者,类似于数据库 {{normalization}}。如果可以预期几乎所有 Metric 用户都需要附加标签,那么将其添加到所有 MetricFamilies 可能是一个更好的权衡。例如,如果您有一个与不同 SQL 语句相关的 MetricFamily,其唯一性由包含完整 SQL 语句哈希值的标签提供,那么添加一个带有 SQL 语句前 500 个字符的标签以提高可读性是可以接受的。

经验表明,下游采集器更容易处理独立的 total 和 failure MetricFamilies,而不是在同一个 MetricFamily 中使用 {result="success"} 和 {result="failure"} Labels。此外,通常最好暴露独立的 read & write 和 send & receive MetricFamilies,因为全双工系统很常见,并且下游采集器更可能分别关注这些值而不是聚合值。

这一切听起来可能并不容易。这是一个需要领域专家在暴露和被暴露系统两方面具备经验和工程权衡才能找到良好平衡的领域。指标和标签名称字符

OpenMetrics 构建在现有广泛采用的 Prometheus text exposition format 及其围绕它形成的生态系统之上。向后兼容性是核心设计目标。扩展或缩小 Prometheus text format 支持的字符集将与该目标背道而驰。破坏向后兼容性将不仅仅对传输格式产生更广泛的影响。特别是,为处理 Prometheus 生态系统中传输的数据而创建或采用的查询语言依赖于这些精确的字符集。标签值支持完整的 UTF-8,因此该格式可以表示多语言指标。

元数据类型

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

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

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

在推拉式系统中支持目标元数据

在推模式消费中,通常由暴露者向摄取者提供相关的目标元数据。在拉模式消费中,也可以采用推模式的方法,但更典型的情况是,摄取者预先知道目标的元数据(例如来自机器数据库或服务发现系统),并在消费暴露数据时将其与指标关联起来。

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

一种方法是让推模式摄取者根据操作员配置在带外提供目标元数据,例如作为 HTTP 头部。虽然这会为推模式摄取者传输目标元数据,并且本标准并未排除此方法,但它的缺点是,即使拉模式摄取者应使用自己的目标元数据,通常仍然需要访问暴露者本身知晓的元数据。

首选的解决方案是将这些目标元数据作为暴露数据的一部分提供,但其方式不会影响整个暴露数据。信息指标家族(Info MetricFamilies)就是为此设计的。暴露者可以包含一个名为 "target" 的信息指标家族,其中包含一个没有标签的指标,用于存放元数据。文本格式的示例可能如下所示:

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

当暴露者为此目的提供此指标时,它应该位于暴露数据中的第一个。这是出于效率考虑,这样依赖它获取目标元数据的摄取者就不必缓冲其余的暴露数据,就可以根据其内容应用业务逻辑。

暴露者绝不能(MUST NOT)向暴露数据中的所有指标添加目标元数据标签,除非为特定的摄取者明确配置。暴露者绝不能(MUST NOT)根据目标元数据为指标家族名称添加前缀或以其他方式更改指标家族名称。一般来说,同一个标签不应出现在暴露数据的每个指标上,但在极少数情况下,这可能是由于偶然行为造成的。类似地,在非常小的暴露数据中,来自暴露者的所有指标家族名称可能恰好共享一个前缀。例如,一个由“万物制造公司”用 Go 语言编写的应用程序可能会包含带有 acme*、go*、process* 前缀的指标,以及来自任何正在使用的第三方库的指标前缀。

暴露者可以将暴露者元数据作为信息指标家族暴露。

上述讨论是在单个暴露者的背景下进行的。来自通用监控系统的暴露数据可能包含来自许多独立目标的指标,因此可能暴露多个目标信息指标。这些指标可能已经在摄取过程中被添加了目标元数据作为标签。指标名称绝不能(MUST NOT)根据目标元数据而变化。例如,即使所有指标都来自预发布环境中的目标,也不应将所有指标都添加 staging_ 前缀,这是不正确的)。

客户端计算和派生指标

暴露者应将任何数学或计算留给摄取者处理。一个显著的例外是 Summary 的分位数,不幸的是出于向后兼容性的需要而保留。暴露的数据应该是原始值,这些值在任意时间段内都是有用的。

例如,您不应暴露一个仪表指标(gauge),其值表示一个计数器在过去 5 分钟内的平均增长率。让摄取者根据他们在不同暴露中消费的数据点来计算增长具有更好的数学特性,并且对抓取失败更有弹性。

另一个例子是直方图/Summary 的平均事件大小。暴露一个计数器自应用程序启动或指标创建以来的平均增长率存在前面示例中的问题,并且还会阻止聚合。

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

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

数值类型

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

Summary 的分位数必须是 float64,因为它们是估计值,因此本质上是不准确的。

暴露时间戳

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

虽然将时间戳附加到暴露的数据上有一些有限的用例,但这些非常罕见。以前附加过时间戳的数据,特别是已经被摄入通用监控系统的数据,可能会带有时间戳。实时或原始数据不应带有时间戳。在不同暴露中暴露相同指标的指标点(MetricPoint)值和相同时间戳是有效的,但是如果底层指标现在已缺失,则这样做是无效的。

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

对于带有时间戳的指标,通常无法检测到指标在不同暴露中何时消失。然而,对于不带时间戳的指标,摄取者可以使用该指标不再出现的暴露中的自身时间戳。

所有这一切都表明,一般来说,指标点(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)作为值,摄取者可以自由地为两个指标附加自己的时间戳。

经验表明,暴露绝对时间戳(此处 epoch 被视为绝对时间)比暴露已用时间、自某时以来的秒数或类似值更健壮。无论哪种情况,它们都应该是仪表指标(gauges)。例如:

# 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

反过来,对于 exemplars 的时间戳没有最佳实践限制。请记住,由于竞态条件或设备之间时间未完全同步,exemplar 的时间戳相对于摄取者的系统时钟或同一暴露中的其他指标可能会显得稍微超前。类似地,某个指标点(MetricPoint)的 "_created" 时间戳可能显得略晚于该指标点的 exemplar 或 sample 时间戳。

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

阈值

暴露系统的期望边界可能是有意义的,但需要采取适当的措施。对于普遍为真的值,为这些阈值发出仪表指标(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

大小限制

本标准没有规定单次暴露所暴露样本数量、可能存在的标签数量、状态集可能具有的状态数量、信息值中的标签数量,或指标名称/标签名称/标签值/帮助文本的字符限制。

具体限制存在阻止合理用例的风险,例如,尽管给定的暴露数据在通过通用监控系统后可能具有适当数量的标签,但可能会添加一些目标标签从而超出限制。对这些数量施加具体限制也无法反映通用监控系统的实际成本所在。因此,这些指南旨在帮助暴露者和摄取者理解什么是合理的。

另一方面,某个维度上过大的暴露数据可能会导致显著的性能问题,与其暴露指标的益处相比。因此,关于单次暴露数据大小的一些指南将是有益的。

摄取者可以选择自行施加限制,特别是为了防止攻击或中断。尽管如此,摄取者需要考虑合理的用例,并尽量避免对其产生不成比例的影响。如果任何单个值/指标/暴露数据超出此类限制,则必须(MUST)拒绝整个暴露数据。

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

唯一时序的数量大致相当于文本格式中非注释行的数量。截至 2020 年,总计 1000 万时序被认为是大量,通常是任何单实例摄取者上限的量级。在未充分考虑的情况下,任何单次暴露的时序数量不应超过 1 万。一个常见的考虑因素是水平扩展:如果将实例数量扩展 1-2 个数量级会发生什么?三十年前很难想象在单个部署中拥有数千台机架顶层交换机。如果一个目标是单例(例如暴露与整个集群相关的指标),那么几十万时序可能是合理的。重要的是时序的总量级,而不是唯一指标家族的数量或单个标签/桶/状态集的基数。1000 个每个包含一个指标的仪表指标与一个包含 1000 个指标的仪表指标成本相同。

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

exemplar 长度有严格的 128 个 UTF-8 字符限制,以防止该功能被滥用于追踪 span 数据和其他事件日志记录。

安全性

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

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

指标暴露应独立于暴露给最终用户的生产服务;因此,对于使用 OpenMetrics 并面向公众的服务,通常不建议在 TCP/80、TCP/443、TCP/8080 和 TCP/8443 等端口上设置 /metrics 端点。

IANA

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

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

如果需要在一个共同的 IP 地址和端口上访问多个指标端点,操作员可以考虑使用通过 localhost 地址与暴露者通信的反向代理。为了简化多路复用,端点应该(SHOULD)在其路径中携带自己的名称,即 /node_exporter/metrics。暴露数据不应该(SHOULD NOT)合并为一个暴露数据,原因如“在推模式和拉模式系统中支持目标元数据”所述,并且为了实现独立摄取而不出现单点故障。

OpenMetrics 希望注册两种 MIME 类型:application/openmetrics-textapplication/openmetrics-proto

本文档是开源的。请通过提交问题或拉取请求来帮助改进它。