OpenMetrics 2.0
- 版本:2.0
- 状态:草稿
- 日期:待定
- 作者: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 进行全面修改被认为是超出范围的。
注意本文档为早期草稿,预计会有重大更改。请阅读此处,了解如何加入 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 Epoch(秒)。可以使用负时间戳。
字符串
字符串必须只包含有效的 UTF-8 字符,并且可以是零长度。NULL (ASCII 0x0) 必须得到支持。
标签
标签是由字符串组成的键值对。
以 Underscore(下划线)开头的标签名是保留的,不得使用,除非本标准另有规定。标签名必须遵循 ABNF 部分的限制。
空标签值应被视为标签不存在。
标签集
标签集必须由标签组成,并且可以为空。标签名在标签集内必须是唯一的。
指标点
每个指标点包含一组值,具体取决于指标族类型。
示例
示例是对指标集外部数据的引用。常见的用例是程序跟踪的 ID。
示例必须由一个标签集和一个值组成,并且可以有一个时间戳。它们可以分别与指标点的时间戳和标签集不同。
示例标签集的标签名和值的总长度不得超过 128 个 UTF-8 字符代码点。示例文本表示中的其他字符,如 ",=
不包括在此限制内,以简化实现并保持文本格式和 Proto 格式之间的一致性。
摄取器可以丢弃示例。
指标
指标由指标族内唯一的标签集定义。指标必须包含一个或多个指标点的列表。给定指标族中同名的指标应在其标签集中具有相同的标签名集合。
指标点不应具有显式时间戳。
如果一个指标公开了多个指标点,则其指标点必须具有单调递增的时间戳。
指标族
一个指标族可以包含零个或多个指标。一个指标族必须具有名称、HELP(帮助信息)、TYPE(类型)和 UNIT(单位)元数据。指标族中的每个指标必须具有唯一的标签集。
名称
指标族名称是字符串,并且在指标集内必须是唯一的。名称应采用 snake_case 命名。指标名称必须遵循 ABNF 部分的限制。
指标族名称中的冒号保留用于表示该指标族是通用监控系统计算或聚合的结果。
以 Underscore(下划线)开头的指标族名称是保留的,不得使用,除非本标准另有规定。
后缀
指标族的名称不得导致指标集中文本格式中与其他指标族在 ABNF 规定的样本指标名称上发生潜在冲突。例如,名为“foo_total”的 Gauge 可能会与名为“foo”的 Counter 在文本格式中产生“foo_total”的冲突。
公开者应避免使用可能与文本格式样本指标名称所用的后缀混淆的名称。
- 各种类型的后缀如下:
- Counter(计数器):
_total
- Summary(摘要):
_count
、_sum
、`` (空) - Histogram(直方图):
_count
、_sum
、_bucket
- GaugeHistogram(仪表直方图):
_gcount
、_gsum
、_bucket
- Info(信息):
_info
- Gauge(仪表):`` (空)
- StateSet(状态集):`` (空)
- Unknown(未知):`` (空)
类型
Type 指定指标族的类型。有效值包括“unknown”(未知)、“gauge”(仪表)、“counter”(计数器)、“stateset”(状态集)、“info”(信息)、“histogram”(直方图)、“gaugehistogram”(仪表直方图)和“summary”(摘要)。
单位
Unit 指定指标族单位。如果非空,它必须是指标族名称的一个后缀,并用下划线分隔。请注意,进一步的生成规则可能会使其在文本格式中成为中缀。
帮助信息
Help 是一个字符串,应非空。它用于提供指标族简要的、供人类理解的描述,并且应足够短以便用作工具提示。
指标集
指标集是 OpenMetrics 公开的顶级对象。它必须由指标族组成,并且可以为空。
每个指标族名称必须是唯一的。在指标集内的每个指标上不应出现相同的标签名和值。
指标集内部的指标族没有特定的顺序要求。公开者可以为了人类阅读方便而对公开内容进行排序,例如在性能权衡合理的情况下按字母顺序排序。
如果存在,根据下方“在推拉式系统中支持目标元数据”一节,名为“target”的信息指标族应放在首位。
指标类型
仪表
仪表是当前的测量值,例如当前使用的内存字节数或队列中的项目数量。对于仪表,用户关心的是其绝对值。
类型为仪表(gauge)的指标中的指标点必须只有一个值。
仪表值可以随时间增加、减少或保持不变。即使它们只朝一个方向变化,它们仍然可能是仪表而不是计数器。日志文件的大小通常只会增加,资源可能会减少,而队列大小的限制可能是恒定的。
仪表可以用于编码具有许多状态且随时间变化的枚举,这是最有效但用户友好性最差的方法。
计数器
计数器衡量离散事件。常见示例包括收到的 HTTP 请求数量、CPU 使用的秒数或发送的字节数。对于计数器,用户感兴趣的是它们随时间增长的速度。
类型为计数器(Counter)的指标中的指标点必须有一个名为 Total 的值。Total 是一个非 NaN 值,并且必须随时间单调不递减,从 0 开始。
类型为计数器(Counter)的指标中的指标点应有一个名为 Created Timestamp 的时间戳值。这可以帮助摄取器区分新指标和之前未见过的长期运行指标。
指标的计数器(Counter)中的指标点的 Total 值可以重置为 0。如果存在,对应的 Created Timestamp 也必须设置为重置的时间戳。
指标的计数器(Counter)中的指标点的 Total 值可以有一个示例。
状态集
状态集表示一系列相关的布尔值,也称为位集。如果需要编码枚举(ENUM),可以通过状态集完成。
状态集指标的一个点可以包含多个状态,并且每个状态必须包含一个布尔值。状态具有字符串类型的名称。
状态集指标的标签集不得拥有与该指标族名称相同的标签名。
如果作为状态集编码,枚举必须在指标点内恰好有一个为 true 的布尔值。
这适用于枚举值随时间变化,并且状态数量不多的情况。
类型为状态集(StateSets)的指标族必须具有空的 Unit 字符串。
信息
信息指标用于公开在进程生命周期内不应改变的文本信息。常见示例包括应用程序版本、版本控制提交和编译器版本。
信息指标的指标点包含一个标签集。信息指标点的标签集不得拥有与该指标标签集的标签名相同的标签名。
信息可以用于编码其值不随时间变化的枚举,例如网络接口类型。
类型为信息(Info)的指标族必须具有空的 Unit 字符串。
直方图
直方图衡量离散事件的分布。常见示例包括 HTTP 请求的延迟、函数运行时或 I/O 请求大小。
直方图指标点必须至少包含一个桶,并应包含 Sum(总和)和 Created Timestamp(创建时间戳)值。每个桶必须有一个阈值和一个值。
直方图指标点必须有一个具有 +Inf 阈值的桶。桶必须是累积的。例如,对于表示请求延迟(以秒为单位)的指标,其阈值为 1、2、3 和 +Inf 的桶的值必须遵循 value_1 <= value_2 <= value_3 <= value_+Inf。如果十个请求每个耗时 1 秒,则 1、2、3 和 +Inf 桶的值必须等于 10。
+Inf 桶计算所有请求。如果存在,Sum 值必须等于所有测量事件值的总和。指标点内的桶阈值必须是唯一的。
从语义上讲,Sum 和桶值是计数器,因此不得是 NaN 或负数。可以使用负阈值桶,但此时直方图指标点不得包含总和值,因为它在语义上将不再是计数器。桶阈值不得等于 NaN。Count 和桶值必须是整数。
直方图指标点应具有一个名为 Created Timestamp 的时间戳值。这可以帮助摄取器区分新指标和之前未见过的长期运行指标。
直方图的指标的标签集不得包含“le”标签名。
桶值可以有示例。桶是累积的,以允许监控系统出于性能/反拒绝服务原因丢弃任何非 +Inf 的桶,这种方式会损失粒度但仍然是有效的直方图。
每个桶覆盖小于或等于其自身的值,并且示例的值必须在此范围内。示例应放入值最高的桶中。一个桶不得有多个示例。
仪表直方图
仪表直方图测量当前分布。常见示例包括项目在队列中等待了多长时间,或队列中请求的大小。
仪表直方图指标点必须有一个具有 +Inf 阈值的桶,并应包含一个 Gsum 值。每个桶必须有一个阈值和一个值。
仪表直方图的桶遵循与直方图相同的所有规则。
仪表直方图的桶和 Gsum 在概念上是仪表,但桶值不得为负数或 NaN。如果存在负阈值桶,则总和可以为负。Gsum 不得为 NaN。桶值必须是整数。
仪表直方图的指标的标签集不得包含“le”标签名。
桶值可以有示例。
每个桶覆盖小于或等于其自身的值,并且示例的值必须在此范围内。示例应放入值最高的桶中。一个桶不得有多个示例。
摘要
摘要也衡量离散事件的分布,并且可以在直方图过于昂贵和/或平均事件大小足够时使用。
它们可以用于向后兼容,因为一些现有仪表库公开了预计算的分位数而不支持直方图。不应使用预计算的分位数,因为分位数不可聚合,并且用户通常无法推断它们覆盖的时间范围。
摘要指标点可以由 Count(计数)、Sum(总和)、Created Timestamp(创建时间戳)和一组分位数组成。
从语义上讲,Count 和 Sum 值是计数器,因此不得是 NaN 或负数。Count 必须是整数。
类型为摘要(Summary)的指标中的指标点(如果包含 Count 或 Sum 值)应具有一个名为 Created Timestamp 的时间戳值。这可以帮助摄取器区分新指标和之前未见过的长期运行指标。Created Timestamp 不得与分位数值的收集周期相关。
分位数是从分位数到值的映射。例如,一个名为 myapp_http_request_duration_seconds 的指标中,分位数 0.95 的值为 0.2,这意味着在未知的时间范围内,第 95 百分位的延迟是 200 毫秒。如果在相关时间范围内没有事件,分位数的值必须为 NaN。分位数的指标的标签集不得包含“quantile”标签名。分位数必须介于 0 和 1 之间(包含 0 和 1)。分位数的值不得为负数。分位数的值应代表最近的值。通常这指的是过去 5-10 分钟内的值。
未知
不应使用 Unknown(未知)类型。当无法确定来自第三方系统的单个指标类型时,可以使用 Unknown。
类型为未知(unknown)的指标中的一个点必须只有一个值。
数据传输与线路格式
文本线路格式必须受支持并且是默认格式。Protobuf 线路格式可以受支持,并且只能在协商后使用。
OpenMetrics 格式是正则乔姆斯基文法(Regular Chomsky Grammars),这使得编写快速且小型的解析器成为可能。文本格式压缩效果良好,而 Protobuf 已经是二进制且高效编码的。
部分或无效的公开内容必须被视为整体错误。
协议协商
所有摄取器实现必须能够摄取使用 TLS 1.2 或更高版本保护的数据。所有公开者应能够发出使用 TLS 1.2 或更高版本保护的数据。摄取器实现应能够摄取不带 TLS 的 HTTP 数据。所有实现应使用 TLS 传输数据。
OpenMetrics 格式版本的协商是带外进行的。例如,对于基于 HTTP 的拉取式公开,使用标准的 HTTP 内容类型协商,并且如果没有请求更新的版本,则必须默认为标准的最旧版本(即 1.0.0)。
推送式协商本质上更为复杂,因为通常由公开者发起连接。除非摄取器另有要求,生产者必须使用标准的最旧版本(即 1.0.0)。
文本格式
ABNF
根据 RFC 5234 的 ABNF
“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] [SP created] [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
; Lowercase ct @ timestamp
created = %d99.116 "@" timestamp
整体结构
必须使用 UTF-8。不得使用字节顺序标记(BOM)。作为对实现者的重要提醒,字节 0 是有效的 UTF-8,而例如字节 255 则不是。
内容类型必须是
application/openmetrics-text; version=1.0.0; charset=utf-8
行尾必须以换行符(\n)表示,并且不得包含回车符(\r)。Expositions 必须以 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 [email protected]
acme_http_router_request_seconds_count{path="/api/v1",method="GET"} 807283.0 [email protected]
acme_http_router_request_seconds_sum{path="/api/v2",method="POST"} 479.3 [email protected]
acme_http_router_request_seconds_count{path="/api/v2",method="POST"} 34.0 [email protected]
# 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.123421
和 1.89e-7
。浮点数必须符合 IEEE 754 定义的 64 位浮点值范围,但尾数可能需要大量位而导致精度丢失。这可以用于编码纳秒级时间戳。
数字的任意整数和浮点表示形式不得用于“Canonical Numbers”部分中的 "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.001 的增量输出从 0.0 到 10.0 的值,示例如下: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 等语言中,如果像 Grisu3 这样的最小浮点渲染算法不易获得,暴露器可以使用不同的渲染方式。
对 C 语言和其他共享其 printf 实现的语言的实现者的警告:%f、%e 和 %g 的标准精度仅为六位有效数字。需要 17 位有效数字才能达到完整精度,例如 printf("%.17g", d)
。
时间戳
如果需要纳秒精度,时间戳不应使用指数浮点表示,因为 float64 的表示没有足够的精度,例如 1604676851.123456789
。
指标家族
指标家族之间不得有明确的分隔符。下一个指标家族必须通过元数据或不能作为前一个指标家族一部分的新样本指标名称来标识。
指标家族不得交错。
指标家族元数据
元数据包含四个部分:指标家族名称、TYPE、UNIT 和 HELP。一个名为 foo 的计数器指标的元数据示例如下:
# TYPE foo counter
如果未暴露 TYPE,则指标家族必须是 Unknown 类型。
如果指定了单位,则必须在 UNIT 元数据行中提供。此外,下划线和单位必须是指标家族名称的后缀。
一个名为 foo_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 行之外,不得暴露以 # 开头的行。
指标
指标不得交错。
请参见“文本格式 -> 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" 标签),这些标签必须以与指标自身 LabelSet 相同的方式渲染。
指标点
指标点不得交错。
一个指标家族中包含多个指标点和样本的正确示例如下:
# 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 类型指标家族的 MetricPoint 值的样本指标名称不得有后缀。
一个不带标签的指标和不带时间戳的指标点的指标家族示例
# 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
MetricPoint 的总值样本指标名称必须带有 _total
后缀。如果存在,MetricPoint 的创建时间戳必须与带有 ct@
前缀的指标点内联。如果值的时间戳存在,创建时间戳必须紧随其后添加。如果 exemplar 存在,创建时间戳必须在其之前添加。
一个不带标签的指标,以及不带时间戳且不带创建时间戳的指标点的示例
# TYPE foo counter
foo_total 17.0
一个不带标签的指标,以及带时间戳但不带创建时间戳的指标点的示例
# TYPE foo counter
foo_total 17.0 1520879607.789
一个不带标签的指标,以及不带时间戳但带创建时间戳的指标点的示例
# TYPE foo counter
foo_total 17.0 [email protected]
一个不带标签的指标,以及带时间戳且带创建时间戳的指标点的示例
# TYPE foo counter
foo_total 17.0 1520879607.789 [email protected]
Exemplars 可以附加到 MetricPoint 的 Total 样本。
一个不带标签的指标,以及带时间戳、创建时间戳和 exemplar 的指标点的示例
# TYPE foo counter
foo_total 17.0 1520879607.789 [email protected] # {trace_id="KOO5S4vxi0o"} 0.67
StateSet
StateSet 类型指标家族的 MetricPoint 值的样本指标名称不得有后缀。
StateSet 必须在 MetricPoint 中为每个 State 提供一个样本。每个 State 的样本必须有一个标签,其中标签名称是指标家族名称,标签值是 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
指标上“实体”标签的示例
# 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 类型指标家族的 MetricPoint 值的样本指标名称必须带有 _info
后缀。样本值必须始终为 1。
一个不带标签的指标,以及一个带有“name”和“version”标签的指标点值的示例
# TYPE foo info
foo_info{name="pretty name",version="8.2.7"} 1
一个带有“entity”标签的指标,以及一个带有“name”和“version”标签的指标点值的示例
# 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
指标标签和指标点值标签可以按任意顺序排列。
Summary
如果存在,MetricPoint 的总和值样本指标名称必须带有 _sum
后缀。如果存在,MetricPoint 的计数值指标名称必须带有 _count
后缀。如果存在,MetricPoint 的分位数(Quantile)值必须使用一个标签来指定所测量的分位数,其中标签名称为 "quantile",标签值为所测量的分位数。
如果存在,MetricPoint 的创建时间戳必须与带有 ct@
前缀的指标点内联。如果值的时间戳存在,创建时间戳必须紧随其后添加。如果 exemplar 存在,创建时间戳必须在其之前添加。创建时间戳必须附加到所有分位数(Quantile)值、MetricPoint 的总和(Sum)和 MetricPoint 的计数(Count)中。
一个不带标签的指标和带总和、计数及创建时间戳值的指标点的示例
# TYPE foo summary
foo_count 17.0 [email protected]
foo_sum 324789.3 [email protected]
一个不带标签的指标和带两个分位数及创建时间戳值的指标点的示例
# TYPE foo summary
foo{quantile="0.95"} 123.7 [email protected]
foo{quantile="0.99"} 150.0 [email protected]
分位数可以按任意顺序排列。
Histogram
MetricPoint 的桶值样本指标名称必须带有 _bucket
后缀。如果存在,MetricPoint 的总和值样本指标名称必须带有 _sum
后缀。
如果存在,MetricPoint 的创建时间戳必须与带有 ct@
前缀的指标点内联。如果值的时间戳存在,创建时间戳必须紧随其后添加。如果 exemplar 存在,创建时间戳必须在其之前添加。创建时间戳必须附加到所有桶值、MetricPoint 的总和(Sum)和 MetricPoint 的计数(Count)中。
当且仅当 MetricPoint 中存在总和值时,MetricPoint 的 +Inf 桶值也必须出现在带有 "_count" 后缀的样本指标名称中。
桶必须按 "le" 的数值递增顺序排序,且 "le" 标签的值必须遵循规范数字的规则。
一个不带标签的指标和带总和、计数及创建时间戳值的指标点的示例,以及包含 12 个桶。特意展示了各种广泛且非典型的但有效的“le”值
# TYPE foo histogram
foo_bucket{le="0.0"} 0 [email protected]
foo_bucket{le="1e-05"} 0 [email protected]
foo_bucket{le="0.0001"} 5 [email protected]
foo_bucket{le="0.1"} 8 [email protected]
foo_bucket{le="1.0"} 10 [email protected]
foo_bucket{le="10.0"} 11 [email protected]
foo_bucket{le="100000.0"} 11 [email protected]
foo_bucket{le="1e+06"} 15 [email protected]
foo_bucket{le="1e+23"} 16 [email protected]
foo_bucket{le="1.1e+23"} 17 [email protected]
foo_bucket{le="+Inf"} 17 [email protected]
foo_count 17 [email protected]
foo_sum 324789.3 [email protected]
Exemplars
不带标签的 Exemplars 必须将空 LabelSet 表示为 {}。
Exemplars 的一个示例,展示了几个有效情况:“0.01”桶没有 Exemplar。0.1 桶有一个没有标签的 Exemplar。1 桶有一个带一个标签的 Exemplar。10 桶有一个带标签和时间戳的 Exemplar。实际上,所有桶都应具有相同风格的 Exemplars。
# TYPE foo histogram
foo_bucket{le="0.01"} 0 [email protected]
foo_bucket{le="0.1"} 8 [email protected] # {} 0.054
foo_bucket{le="1"} 11 [email protected] # {trace_id="KOO5S4vxi0o"} 0.67
foo_bucket{le="10"} 17 [email protected] # {trace_id="oHg5SJYRHA0"} 9.8 1520879607.789
foo_bucket{le="+Inf"} 17 [email protected]
foo_count 17 [email protected]
foo_sum 324789.3 [email protected]
GaugeHistogram
MetricPoint 的桶值样本指标名称必须带有 _bucket
后缀。如果存在,MetricPoint 的总和值样本指标名称必须带有 _gsum
后缀。当且仅当 MetricPoint 中存在总和值时,MetricPoint 的 +Inf 桶值也必须出现在带有 _gcount
后缀的样本指标名称中。
桶必须按 "le" 的数值递增顺序排序,且 "le" 标签的值必须遵循规范数字的规则。
一个不带标签的指标,以及一个没有 Exemplar 且桶中也没有 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 类型指标家族的 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 模式目前可在此处获取:here。
注意Prometheus 及其生态系统不支持 OpenMetrics protobuf 模式,而是使用类似的io.prometheus.client
format。关于 OpenMetrics 2.0 中 protobuf 模式未来的讨论正在进行中。
设计考量
范围
OpenMetrics 旨在为在线系统提供遥测数据。它运行在不提供硬实时或软实时保证的协议之上,因此它本身无法提供任何实时保证。OpenMetrics 的延迟和抖动特性与底层网络、操作系统、CPU 等一样不精确。它对于聚合作为决策基础来说足够准确,但不能反映单个事件。
应支持各种规模的系统,从每小时接收少量请求的应用程序到监控 400Gb 网口带宽使用情况。应能够在任意时间段内对传输的遥测数据进行聚合和分析。
它旨在以规律的节奏传输数据传输时的状态快照。
范围之外
摄取器如何发现存在哪些暴露器,反之亦然,超出了本标准的范围,因此未在本标准中定义。
扩展和改进
OpenMetrics 的第一个版本基于成熟且事实上的标准 Prometheus 文本格式 0.0.4,刻意不添加主要的语法或语义扩展,也不进行优化。例如,没有尝试使 Histogram 桶的文本表示更紧凑,而是依赖底层堆栈中的压缩来处理它们的重复性。
这是一个故意的选择,以便该标准可以利用现有用户群的采用和发展势头。这确保了从 Prometheus 文本格式 0.0.4 的相对轻松过渡。
它还确保存在一个易于实现的基础标准。这可以在未来的标准版本中进行构建。其意图是未来的标准版本将始终要求在语法和语义上支持此 1.0 版本。
我们希望监控系统能够从 OpenMetrics 暴露中获得可用信息,而不会造成过重负担。如果剥离所有元数据和结构,仅将 OpenMetrics 暴露视为一个无序的样本集,它应该能够独立使用。因此,也没有不透明的二进制类型,例如草图(sketches)或 t-摘要(t-digests),它们无法表示为仪表(gauges)和计数器(counters)的混合,因为它们需要自定义解析和处理。
这一原则在整个标准中始终如一地应用。例如,MetricFamily 的单位在名称中重复出现,以便不理解单位元数据的系统也能获得单位信息。“le”标签是一个普通的标签值,而不是有其特殊的语法,这样摄取器就不必添加特殊的直方图处理代码来摄取它们。再举一个例子,没有复合数据类型。例如,没有用于经纬度的地理位置类型,因为这可以通过单独的 Gauge 指标来完成。
单位和基本单位
为了系统间的一致性并避免混淆,单位主要基于 SI 基本单位。基本单位包括秒、字节、焦耳、克、米、比率、伏特、安培和摄氏度。应在适用时提供单位。
例如,将所有持续时间指标都以秒为单位,就无需猜测给定指标是纳秒、微秒、毫秒、秒、分钟、小时、天还是周,也无需处理混合单位。通过选择不带前缀的单位,我们避免了像在复杂系统的紧急行为中出现千毫秒这样的情况。
由于值可以是浮点数,因此标准中内置了次基本单位精度。
同样,混合使用位(bits)和字节(bytes)会造成混淆,因此选择字节作为基本单位。虽然理论上开尔文(Kelvin)是更好的基本单位,但实际上大多数现有硬件都暴露摄氏度(Celsius)。千克(Kilograms)是 SI 基本单位,但“千”前缀存在问题,因此选择克(grams)作为基本单位。
虽然在所有可能的情况下都应该使用基本单位,但开尔文(Kelvin)是一个成熟的单位,可以在颜色或黑体温度等用例中替代摄氏度(Celsius),因为在这些情况下,摄氏度与开尔文指标之间的比较不太可能发生。
比率(Ratios)是基本单位,而不是百分比。在可能的情况下,应以给定分子和分母的 Gauge 或 Counter 形式暴露原始数据。这在摄取器中进行分析和聚合时具有更好的数学特性。
分贝(Decibels)不是基本单位,首先,deci 是一个 SI 前缀,其次,贝尔(bels)是对数单位。要暴露信号/能量/功率比,直接暴露比率会更好,如果可能,最好是原始功率/能量。浮点指数足以覆盖即使是极端的科学用途。从电子伏特(约 1e-19 J)到超新星释放的能量(约 1e44 J)是 63 个数量级,而 64 位浮点数可以覆盖超过 2000 个数量级。
如果无法避免非基本单位且转换不可行,实际单位仍应包含在指标名称中以保持清晰。例如,焦耳(joule)是能量和功率的基本单位,因为瓦特(watts)可以用带有焦耳单位的计数器表示。实际上,给定的第三方系统可能只暴露瓦特,因此在这种情况下,以瓦特表示的 Gauge 将是唯一现实的选择。
并非所有指标家族都具有单位。例如,HTTP 请求计数就没有单位。技术上讲,单位将是 HTTP 请求,但从这个意义上说,整个指标家族名称就是单位。走到这个极端是没有用的。在下游系统中为人类消费提供良好的图表轴的可能性应始终牢记在心。
无状态性
OpenMetrics 定义的传输格式在不同暴露之间是无状态的。之前暴露的信息不得对未来的暴露产生影响。每次暴露都是暴露器当前状态的自包含快照。
必须向现有和新的摄取器提供相同的自包含暴露。
一个核心设计选择是,暴露器不得仅仅因为指标最近没有变化或观察到而将其排除。暴露器不得对摄取器消费暴露的频率做出任何假设。
跨时间暴露和指标演进
当指标随时间的变化可以分析时,它们最为有用,因此相应的暴露必须随时间推移而有意义。因此,单个暴露本身不足以有用和有效。指标语义的一些变化也可能破坏下游用户。
解析器通常通过缓存先前结果进行优化。因此,应避免在不同暴露之间改变标签的暴露顺序,即使这在技术上不会导致中断。这也往往会使暴露的单元测试编写更容易。
指标和样本不应在不同暴露之间出现和消失,例如,计数器只有在有历史记录时才有用。原则上,给定指标应从进程启动时开始暴露,直到进程终止。通常无法提前知道给定进程生命周期内指标家族将包含哪些指标(例如,延迟直方图的标签值是 HTTP 路径,由最终用户在运行时提供),但一旦暴露了类似计数器的指标,它就应继续暴露直到进程终止。计数器没有增加并不意味着它不再具有当前值。在某些情况下,停止暴露给定指标可能是有意义的;请参阅“缺失数据”部分。
通常,更改指标家族的类型,或从其指标中添加或删除标签,将对摄取器造成破坏。
一个值得注意的例外是,向 Info MetricPoints 的值添加标签不会造成破坏。这是为了您可以在现有 Info 指标家族中添加额外信息,而无需强制创建带有额外标签值的新信息指标。摄取系统应确保它们能够适应此类添加。
更改指标家族的 Help 不会造成破坏。对于可能的值,在浮点数和整数之间切换不会造成破坏。向 StateSet 添加新状态不会造成破坏。在不更改指标名称的情况下添加单位元数据不会造成破坏。
直方图桶不应在不同暴露之间更改,因为这可能会导致性能问题并破坏摄取器。同样,应用程序的任何一致二进制和环境的所有暴露都应为给定 Histogram 指标家族提供相同的桶,以便所有摄取器可以聚合它们,而无需摄取器实现异构桶的直方图合并逻辑。一个例外可能是对桶的偶尔手动更改,这被认为是破坏性的,但在新软件发布导致性能特性变化时,这可能是一个有效的权衡。
即使更改在技术上不具有破坏性,它们仍然会带来成本。例如,频繁的更改可能会导致摄取器的性能问题。从一个暴露到另一个暴露变化的 Help 字符串可能会导致每个 Help 值都被存储。频繁地在 int 和 float 值之间切换可能会阻碍有效的压缩。
NaN
NaN 在 OpenMetrics 中与任何其他数字一样,通常是由于除以零而产生,例如在最近没有观察值的情况下,用于摘要分位数。NaN 在 OpenMetrics 中没有任何特殊含义,特别是不得用作缺失或损坏数据的标记。
缺失数据
在某些情况下,数据会停止存在,这是有效的。例如,文件系统可以被卸载,因此其用于空闲磁盘空间的 Gauge 指标就不再存在。这种情况没有特殊的标记或信号。后续的暴露简单地不再包含此指标。
暴露性能
指标只有在合理的时间范围内能够被收集时才有用。需要数分钟才能暴露的指标被认为没有用。
作为经验法则,暴露时间不应超过一秒。
通过 OpenMetrics 序列化的遗留系统指标可能需要更长时间。因此,无法做出严格的性能假设。
暴露应是最新的状态。例如,处理暴露请求的线程不应依赖缓存值,只要它能够绕过任何此类缓存。
并发
为了高可用性和即席访问,常见的方法是拥有多个摄取器。为支持这一点,必须支持并发暴露。应遵循并发系统的所有 BCP,常见陷阱包括死锁、竞态条件以及阻止暴露并发进行过于粗粒度的锁定。
指标命名和命名空间
我们旨在在指标和标签名称的命名中,在可理解性、避免冲突和简洁性之间取得平衡。名称通过下划线分隔,因此指标名称最终采用“snake_case”格式。
举例来说,“http_request_seconds”虽然简洁,但会在大量应用程序之间发生冲突,而且也不清楚这个指标到底在测量什么。例如,在一个复杂系统中,它可能是在认证中间件之前或之后。
指标名称应指示它们来自哪段代码。因此,一家名为“A Company Manufacturing Everything”的公司可能会在所有指标代码中加上“acme_”前缀,如果他们有一个测量延迟的 HTTP 路由库,它可能有一个指标,例如“acme_http_router_request_seconds”,并附带一个 Help 字符串,表明这是总体延迟。
其目的不是阻止所有应用程序之间所有潜在的冲突,因为那将需要像全局指标命名空间注册表或基于 DNS 的超长命名空间这样过于强硬的解决方案。相反,其目的是保持一种轻量级的非正式方法,这样对于给定应用程序,其组成库之间发生冲突的可能性非常小。
在一个监控系统的整体部署中,其目标是避免相同的指标名称表示不同含义的冲突。例如,acme_http_router_request_seconds 可能会出现在 A Company Manufacturing Everything 公司开发的数百个不同应用程序中,这是正常的。如果 Another Corporation Making Entities 也在其 HTTP 路由器中使用了指标名称 acme_http_router_request_seconds,那也没关系。如果来自两家公司的应用程序由同一个监控系统监控,冲突是不 desirable 的,但可接受,因为没有应用程序试图同时暴露这两个名称,也没有任何一个目标试图(错误地)暴露同一个指标名称两次。如果一个应用程序希望同时包含 My Example Company 和 Mega Exciting Company 的 HTTP 路由器库,那将是一个问题,并且其中一个指标名称需要以某种方式更改。
作为推论,库越公开,其指标名称的命名空间就应该越好,以降低出现此类情况的风险。acme_ 在公司内部使用不是一个糟糕的选择,但这些公司例如可以选择 acmeverything_ 或 acorpme_ 作为在公司外部共享代码的前缀。
在按公司或组织划分命名空间之后,命名空间和命名应根据需要按库/子系统/应用程序分形地继续,例如上面的 http_router 库。目标是,如果您熟悉代码库的整体结构,您可以根据给定指标的指标名称很好地猜测其仪器的位置。
对于一个常见且非常知名的现有软件,软件本身的名称可能足以区分。例如,对于 DNS 软件,bind_ 可能就足够了,尽管 isc_bind_ 会是更常见的命名方式。
以 scrape_ 为前缀的指标名称由摄取器用于附加与单个暴露相关的信息,因此不应由应用程序直接暴露。已经通过通用监控系统消费和传递的指标可能在后续暴露中包含此类指标名称。如果暴露器希望提供有关单个暴露的信息,可以使用例如 myexposer_scrape_ 的指标前缀。一个常见的示例是 myexposer_scrape_duration_seconds 的 Gauge,表示从暴露器的角度看该暴露所花费的时间。
在 Prometheus 生态系统中,出现了一组跨所有实现一致的每个进程指标,前缀为 process_。例如,对于打开文件限制(ulimits),MetricFamily process_open_fds 和 process_max_fds 的 Gauge 提供了当前值和最大值。(这些名称是遗留的,如果今天定义这些指标,它们更可能被称为 process_fds_open 和 process_fds_limit)。总的来说,要获得具有相同语义的名称是非常具有挑战性的,这就是为什么不同的检测应该使用不同的名称。
避免指标名称中的冗余。避免使用“metric”、“timer”、“stats”、“counter”、“total”、“float64”等子字符串——因为作为通过 OpenMetrics 暴露的具有给定类型(可能还有单位)的指标,此类信息已被隐含,因此不应明确包含。出于同样的原因,您不应将指标的标签名称包含在指标名称中,此外,监控系统对指标的后续聚合可能会导致此类信息不正确。
避免在您的检测中包含的指标名称中包含来自监控系统其他层的实现细节。例如,指标家族名称不应仅仅因为当前通过 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,但没有关于标签名称的元数据。这是因为那样会膨胀格式而收益甚微。带外文档是暴露器可以向其摄取器呈现此信息的一种方式。
指标名称与标签
在某些情况下,在指标家族中使用多个指标或使用多个指标家族似乎都有道理。即使指标家族的总和或平均值不总是有效,它也应该是有意义的。例如,混合电压和风扇速度是没有意义的。
提醒一下,OpenMetrics 是基于摄取器可以处理数据并对其执行聚合的假设而构建的。
将总和与其他指标一起暴露是错误的,因为这会导致在下游摄取器进行聚合时重复计数。
wrong_metric{label="a"} 1
wrong_metric{label="b"} 6
wrong_metric{label="total"} 7
指标的标签应尽可能少,以确保唯一性,因为每增加一个标签,用户在确定要处理哪些下游标签时就需要考虑多一个。可以应用于多个指标家族的标签是移入类似数据库 {{normalization}} 的 _info 指标的候选。如果几乎所有指标用户都希望使用附加标签,那么将其添加到所有指标家族可能是一个更好的权衡。例如,如果您有一个与不同 SQL 语句相关的指标家族,其中唯一性由包含完整 SQL 语句哈希的标签提供,那么可以有一个包含 SQL 语句前 500 个字符的另一个标签,以提高可读性。
经验表明,下游摄取器发现处理独立的“总计”和“失败”指标家族比在单个指标家族中使用 {result="success"} 和 {result="failure"} 标签更容易。此外,通常最好暴露独立的“读写”和“收发”指标家族,因为全双工系统很常见,并且下游摄取器更可能单独关心这些值而不是聚合值。
所有这一切并不像听起来那么容易。这是一个需要暴露和被暴露系统领域专家在经验和工程权衡方面寻找良好平衡的领域。指标和标签名称字符
OpenMetrics 建立在现有广泛采用的 Prometheus 文本暴露格式及其周围形成的生态系统之上。向后兼容性是一个核心设计目标。扩展或缩小 Prometheus 文本格式支持的字符集将违背这一目标。破坏向后兼容性将产生比仅仅传输格式更广泛的影响。特别是,为处理 Prometheus 生态系统内传输的数据而创建或采用的查询语言依赖于这些精确的字符集。标签值支持完整的 UTF-8,因此该格式可以表示多语言指标。
元数据类型
元数据可以来自不同的来源。多年来,出现了两个主要来源。虽然它们在功能上通常相同,但讨论它们的概念性差异有助于理解。
“目标元数据”通常是暴露器外部的元数据。常见的例子是来自服务发现、CMDB 或类似系统的数据,例如关于数据中心区域、服务是否属于特定部署、生产或测试的信息。这可以通过暴露器或摄取器向捕获此元数据的所有指标添加标签来实现。通过摄取器来实现更受青睐,因为它更灵活且开销更小。就灵活性而言,硬件维护团队可能关心机器位于哪个服务器机架中,而使用同一台机器的数据库团队可能关心它包含生产数据库的副本 2。就开销而言,硬编码或配置此信息需要额外的分发路径。
“暴露器元数据”来自暴露器内部。常见的例子是软件版本、编译器版本或 Git 提交 SHA。
在推式和拉式系统中支持目标元数据
在推式消费中,暴露器通常会向摄取器提供相关的目标元数据。在拉式消费中,可以采用推式方法,但更常见的是摄取器已经预先知道目标的元数据,例如来自机器数据库或服务发现系统,并在消费暴露时将其与指标关联起来。
OpenMetrics 是无状态的,并向所有摄取器提供相同的暴露,这与推式方法相冲突。此外,推式方法会破坏拉式摄取器,因为会暴露不需要的元数据。
一种方法是让推式摄取器根据操作员的带外配置(例如 HTTP 头)提供目标元数据。虽然这会为推式摄取器传输目标元数据,并且本标准不排除这种做法,但它的缺点是,即使拉式摄取器应该使用自己的目标元数据,但能够访问暴露器本身知道的元数据通常仍然很有用。
首选的解决方案是将此目标元数据作为暴露的一部分提供,但方式不会影响整个暴露。Info 指标家族就是为此目的而设计的。暴露器可以包含一个名为“target”的 Info 指标家族,其中包含一个不带标签的单个指标和元数据。文本格式的示例如下:
# TYPE target info
# HELP target Target metadata
target_info{env="prod",hostname="myhost",datacenter="sdc",region="europe",owner="frontend"} 1
当暴露器为此目的提供此指标时,它应在暴露中排在首位。这是为了效率,以便依赖它获取目标元数据的摄取器无需在根据其内容应用业务逻辑之前缓冲其余暴露。
暴露器不得将目标元数据标签添加到一次暴露中的所有指标中,除非明确为特定摄取器配置。暴露器不得给指标家族名称添加前缀,或以其他方式根据目标元数据改变指标家族名称。通常,相同的标签不应出现在一次暴露的每个指标上,但在极少数情况下,这可能是由于出现行为所致。类似地,在非常小的暴露中,来自暴露器的所有指标家族名称可能会共享一个前缀。例如,一家“万物制造公司”用 Go 语言编写的应用程序很可能包含带有 acme_、go_、process_ 前缀以及所使用的任何第三方库的指标前缀的指标。
暴露器可以将暴露器元数据作为信息指标家族暴露。
以上讨论是在单个暴露器的上下文中进行的。通用监控系统的暴露可能包含来自许多单独目标的指标,因此可能暴露多个目标信息指标。这些指标在摄取时可能已经作为标签添加了目标元数据。指标名称不得根据目标元数据而变化。例如,如果所有指标最终都被加上 staging_ 前缀,即使它们都源自暂存环境中的目标,那也是不正确的。
客户端计算和派生指标
暴露器应将任何数学运算或计算留给摄取器。一个值得注意的例外是摘要分位数,它不幸地是向后兼容性所必需的。暴露应为原始值,这些原始值在任意时间段内都很有用。
例如,您不应暴露一个带有计数器在过去5分钟内平均增长率的计量器。让摄取器计算他们在暴露中消耗的数据点的增长,具有更好的数学特性,并且对抓取失败更具弹性。
另一个例子是直方图/摘要的平均事件大小。暴露自应用程序启动或指标创建以来计数器的平均增长率,存在与早期示例相同的问题,并且还会阻止聚合。
标准差也属于此类。将平方和作为计数器暴露将是正确的方法。它未被包含在此标准中作为直方图值,因为64位浮点精度在实践中不足以使其正常工作。由于平方运算,只有53位尾数的一半在精度方面可用。例如,一个每秒观察1万个事件的直方图将在2小时内失去精度。使用64位整数也好不到哪里去,因为浮点小数的丢失,纳秒分辨率的整数通常跟踪一秒长的事件,在19次观察后就会溢出。当128位浮点数变得普遍时,可以重新审视这一设计决策。
另一个例子是避免暴露请求失败率,而是分别暴露失败请求和总请求的计数器。
数字类型
对于一个每秒递增一百万次的计数器,使用 float64(因为它有53位尾数)需要一个多世纪才能开始失去精度。然而,一个100 Gbps 网络接口的八位组吞吐量精度可能在大约20小时内开始用 float64 失去精度。虽然100Gbps网络接口在多年内失去1KB的精度在实践中不太可能成为问题,但对于具有如此高吞吐量的整数数据,int64 是一种选择。
摘要分位数必须是 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 中,摄取器可以自由地将自己的时间戳附加到两个指标上。
经验表明,暴露绝对时间戳(此处将纪元时间视为绝对时间)比暴露经过时间、自某时以来的秒数或类似值更具鲁棒性。无论哪种情况,它们都将是计量器。例如
# 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
相反,对示例时间戳没有最佳实践限制。请记住,由于竞态条件或设备之间时间未完全同步,示例时间戳可能相对于摄取器的系统时钟或同一暴露中的其他指标略微超前。同样,一个 MetricPoint 的“ct@”可能看起来略晚于该 MetricPoint 的示例或样本时间戳。
请记住,常用的监控系统支持从纳秒到秒的各种分辨率,因此如果两个 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
大小限制
本标准没有规定单个暴露所暴露的样本数量、可能存在的标签数量、状态集可能拥有的状态数量、信息值中的标签数量或指标名称/标签名称/标签值/帮助字符限制的任何特定限制。
特定的限制存在阻碍合理用例的风险,例如,虽然给定的暴露在通过通用监控系统后可能具有适当数量的标签,但可能已经添加了一些目标标签,从而使其超出限制。对这些数字的特定限制也无法捕捉通用监控系统的真实成本所在。因此,这些指南旨在帮助暴露器和摄取器理解什么是合理的。
另一方面,在某些维度上过大的暴露可能会导致显著的性能问题,与暴露指标的收益相比得不偿失。因此,关于任何单个暴露的大小的一些指南将很有用。
摄取器可以选择自行施加限制,特别是为了防止攻击或中断。尽管如此,摄取器仍需考虑合理的用例,并尽量不要对其造成不成比例的影响。如果任何单个值/指标/暴露超过此类限制,则必须拒绝整个暴露。
一般来说,有三件事会影响通用监控系统摄取时间序列数据的性能:唯一时间序列的数量、这些序列中随时间变化的样本数量,以及指标名称、标签名称、标签值和 HELP 等唯一字符串的数量。摄取器可以控制摄取频率,因此这方面无需进一步考虑。
唯一时间序列的数量大致相当于文本格式中非注释行的数量。截至2020年,总共1000万个时间序列被认为是大量,通常是任何单一实例摄取器的上限量级。任何单个暴露不应在未经尽职调查的情况下超过1万个时间序列。一个常见的考虑是横向扩展:如果您的实例数量增加1-2个数量级会发生什么?30年前,在一个单一部署中拥有数千个机架顶交换机是难以想象的。如果一个目标是单例(例如暴露与整个集群相关的指标),那么几十万个时间序列可能是合理的。重要的不是唯一指标家族的数量或单个标签/桶/状态集的基数,而是时间序列的总量级。1000个各自只有一个指标的计量器与一个拥有1000个指标的单一计量器成本相同。
如果特定类型的所有目标都暴露相同的时间序列集,那么每个额外目标的字符串不会给大多数相当现代的监控系统带来增量成本。然而,如果每个目标都有唯一的字符串,则会产生这样的成本。举一个极端的例子,一个被许多目标使用的单一1万字符的指标名称,其本身在实践中不太可能成为问题。相反,假设采用现代方法,一千个目标各自暴露一个唯一的36字符UUID,在存储字符串方面,其成本是该单一1万字符指标名称的三倍多。此外,如果这些字符串随时间变化,旧字符串仍需要至少存储一段时间,从而产生额外成本。假设上段中的1000万个时间序列,每小时100MB的唯一字符串可能表明用例更像是事件日志记录,而不是指标时间序列。
示例长度有严格的128个UTF-8字符限制,以防止滥用该功能进行跟踪跨度数据和其他事件日志记录。
安全
实现者可以选择提供认证、授权和审计;如果他们选择这样做,这应在 OpenMetrics 之外处理。
所有暴露器实现都应能够使用 TLS 1.2 或更高版本保护其 HTTP 流量。如果暴露器实现不支持加密,操作员应在可行的情况下使用反向代理、防火墙和/或 ACL。
指标暴露应独立于暴露给最终用户的生产服务;因此,对于使用 OpenMetrics 的公开暴露服务,通常不鼓励在 TCP/80、TCP/443、TCP/8080 和 TCP/8443 等端口上设置 /metrics 端点。
IANA
虽然目前大多数 Prometheus 暴露格式的实现都使用来自 {{PrometheusPorts}} 非正式注册表的非 IANA 注册端口,但 OpenMetrics 可以在一个明确定义的端口上找到。
IANA 为暴露数据的客户端分配的端口是 <9099(为历史一致性而请求)>。
如果多个指标端点需要通过一个共同的 IP 地址和端口访问,操作员可以考虑使用一个反向代理,该代理通过 localhost 地址与暴露器通信。为了简化多路复用,端点应在其路径中带有自己的名称,即 /node_exporter/metrics
。暴露不应合并为一次暴露,原因在“支持推拉式系统中的目标元数据”中有所涵盖,并且为了允许独立摄取而没有单点故障。
OpenMetrics 希望注册两种 MIME 类型:application/openmetrics-text
和 application/openmetrics-proto
。