searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

OpenMetrics 数据传输规范 -- 一种云原生、高度可扩展的指标协议

2023-08-21 07:34:29
358
0

简介

Prometheus于2012年创建,自2015年起一直是云原生可观测性的默认产品。Prometheus的核心是其文本指标展示格式,即Prometheus展示格式0.0.4,该格式自2014年起就已经非常稳定。这种格式特别强调可生成性、可采集性和可读性。

截至2020年,已有700多家公开的exporter、数量不详的非公开exporter以及成千上万个使用该格式的原生库集成。来自不同项目和公司的几十个采集器都支持采用它。

通过OpenMetrics,我们正在规范和收紧该标准,以将其提交给IETF。我们正在记录一个广泛而有机的工作标准,同时引入最小的、基本上向后兼容的、经过深思熟虑的变更。截至2020年,已有几十个exporter、集成商和采集器开始使用并优先协商OpenMetrics。

鉴于生态系统的广泛采用和重要的协调需求,后续不会对Prometheus展示格式0.0.4或OpenMetrics 1.0进行全面的改动。

概况

指标是一种特殊的遥测数据,它们反映了一组数据当前的状态快照。指标不同于日志或事件,后者侧重于记录或关于单个事件的信息。

OpenMetrics格式与该格式的任何具体传输方法无关。该格式的数据会被长期暴露,并且被定期消费。

实现者必须通过HTTP GET请求以OpenMetrics文本格式公开指标,该HTTP请求的URL通常定义为“/metrics”。实现者也可以通过HTTP将OpenMetrics格式的指标集定期推送到预先配置的端点来公开指标。

指标和时间序列

本标准用数值表达所有系统状态,常见例子有计数、当前值、枚举和布尔状态。指标倾向于在时间上聚合数据,而事件发生在特定时间。这样做虽然会损失信息,但能减少开销。这也是许多现代监控系统常见的工程权衡取舍。

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

常见的指标时间序列例子包括网络接口计数器、设备温度、BGP连接状态以及报警状态等。

数据模型

本节必须与 ABNF 部分一起阅读。如果两者之间存在分歧,必须以ABNF优先。

数据类型

OpenMetrics中的指标值必须是浮点数或整数。注意摄取器可能仅支持float64。非实值NaN、+Inf和-Inf必须支持。NaN不应被视为缺失值,但可用于表示除以零的情况。

布尔值

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

时间戳

时间戳必须为Unix纪元的秒数。可以使用负时间戳。

字符串

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

标签(label)

标签是由字符串组成的键值对。以下划线开头的标签名称是保留的,除非本标准另有规定,否则不能使用。

标签名称必须遵循ABNF部分中的限制。应将空标签值视为标签不存在。

标签集(LabelSet)

标签集必须由标签组成,可以为空。标签集内标签名称必须唯一。

指标点

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

示例(Exemplars )

示例是指向指标集外数据的引用。常见用例是程序跟踪的ID。

示例必须由标签集和值组成,并且可以有时间戳。它们可以与指标点的标签集和时间戳不同。示例的标签集中的标签名称和值的组合长度不能超过128个UTF-8字符代码点。为了实现简单性和文本与proto格式之间的一致性,示例中的其他字符(如",=")不包括在此限制内。

摄取器可以丢弃示例。

指标

指标由指标家族内的唯一标签集定义。指标必须包含一个或多个指标点的列表。

给定指标家族中具有相同名称的指标,其标签集中的标签名称应相同。

指标点不应具有显式时间戳。如果为指标公开多个指标点,则其指标点必须具有单调递增的时间戳。

指标族MetricFamily

一个指标族可以有零个或多个指标。

一个指标族必须有名称、HELP、TYPE和UNIT元数据。

一个指标族内的每个指标必须有一个唯一的标签集。

名称Name

指标族名称是一个字符串,在一个指标集中必须唯一。

名称应该是snake_case格式。指标名称必须遵循ABNF中的限制。指标族名称中的冒号保留用于表示指标族是通用监控系统的计算或聚合结果。

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

后缀 Suffixes

指标族的名称不得导致根据ABNF与指标集中的文本格式中的另一个指标族发生样本指标名称的潜在冲突。

一个示例是作为计数器类型的“foo”可以创建一个“foo_created”的gauge类型指标。

应该避免可能与文本格式样本指标名称使用的后缀相混淆的名称。

  • 各类型的后缀是:
  • Counter: '_total''_created'
  • Summary: '_count''_sum' '_created' '' (empty)
  • Histogram: '_count''_sum' '_bucket' '_created'
  • GaugeHistogram: '_gcount''_gsum' '_bucket'
  • Info: '_info'
  • Gauge: '' (empty)
  • StateSet: '' (empty)
  • Unknown: '' (empty)

类型Type

类型指定指标族的类型。有效值为“unknown”、“gauge”、“counter”、“stateset”、“info”、“histogram”、“gaugehistogram”和“summary”。

单位Unit

单位指定指标族的单位。

如果非空,它必须是指标族名称的后缀,用下划线分隔。

请注意,进一步的生成规则可能会使其在文本格式中成为中缀。

帮助HELP

帮助是一个字符串,应该是非空的。它用于给出指标族的简要描述,以供人类消费,并且应该足够短,可用作工具提示。

指标集 MetricSet

指标集是OpenMetrics公开的顶级对象。它必须由指标族组成,可以为空。

每个指标族名称在指标集中必须唯一。

同一个标签名称和值不应在指标集中的每个指标上出现。

指标集中不需要指标族的特定顺序。

需要让它更易于人类阅读,例如可以按字母顺序排序。

如果存在,按照下面的“在推送式和拉取式系统中都支持目标元数据”部分,应该首先出现一个称为“target”的信息指标族。

指标类型

Gauge

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

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

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

gauge可以用来编码多状态且随时间变化的枚举值,这是最高效但对用户最不友好的方式。

Counter

counter用于测量离散事件。常见的例子是收到的 HTTP 请求数、花费的 CPU 秒数或发送的字节数。

对于计数器,随着时间的推移它们增长的速度才是对用户感兴趣的。

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

类型为 counter 的指标中的指标点应该有一个名为 created 的时间戳值。这可以帮助摄取器区分新的指标和它以前没有见过的长期运行的指标。

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

类型为 counter 的指标的指标点的 total 值可以有示例。

StateSet

StateSet表示一系列相关的布尔值,也称为比特集。

如果需要对枚举进行编码,可以通过 StateSet 完成。

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

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

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

这适用于 enum 值随时间变化的情况,而状态数量不超过几个。

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

Info

info类型的指标用于公开不应在进程生命周期中改变的文本信息。常见的例子有应用程序的版本、版本控制提交和编译器的版本。

info指标的指标点包含一个标签集。

info指标点的标签集不能有与其指标的标签集的标签名称相同的标签名称。

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

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

Histogram

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

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

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

如果存在,Sum 的值必须等于所有测量事件值的总和。

指标点内的桶阈值必须唯一。

从语义上讲,Sum 和 buckets 的值是计数器,所以不能是 NaN 或负数。

可以使用负阈值桶,但同时直方图指标点必须不包含 sum 值,因为从语义上讲它不再是计数器。桶阈值不能等于 NaN。count和桶的值必须是整数。

直方图指标点应该有一个名为 Created 的时间戳值。这可以帮助摄取器区分新的指标和它以前没有见过的长期运行的指标。

直方图指标的标签集不能有一个叫做 “le” 的标签名称。

桶的值可以有exemplars。桶是累积的,以允许监控系统为了性能/防止拒绝服务的原因删除任何非 +Inf 桶,这会损失细粒度但仍然是一个有效的直方图。

注:第二句是一个注意事项,如果需要可以移除。

每个桶覆盖小于或等于它的值,并且exemplars的值必须在这个范围内。exemplars应该放入具有最高值的桶中。一个桶不能有多个exemplars。

GaugeHistogram

GaugeHistogram 测量当前的分布。常见的例子是项目在队列中已经等待的时间长度,或队列中的请求大小。

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

GaugeHistogram 的桶遵循与 Histogram 相同的所有规则。概念上,GaugeHistogram 的桶和 Gsum 是 gauges,但是桶的值不能是负数或 NaN。如果有负阈值桶,则 sum 可以是负数。Gsum 不能是 NaN。桶的值必须是整数。

GaugeHistogram 指标的标签集不能有一个叫做 “le” 的标签名称。

桶的值可以有 exemplars。每个桶覆盖小于或等于它的值,并且 exemplar 的值必须在这个范围内。exemplars 应该放入具有最高值的桶中。一个桶不能有多个 exemplar。

Summary

summary也用于测量离散事件的分布,在 Histogram 代价太高和/或平均事件大小就足够时,摘要是很好的选择。

summary也可以用于向后兼容,因为一些现有的仪表库只公开预计算的分位数,不支持 Histogram。不应该使用预计算的分位数,因为分位数不可聚合,用户通常无法推断它们覆盖的时间范围。

summary类型的指标点可以由count、sum、created和一组quantile组成。从语义上讲,count和sum是计数器,所以不能是NaN或负数。count必须是整数。

如果summary指标点包含count或sum,应该有一个名为Created的时间戳。这可以帮助区分新的指标和之前未见过的长期运行指标。Created与分位数值的收集周期无关。

quantile是从一个分位数映射到一个值。例如quantile 0.95值为0.2的指标myapp_http_request_duration_seconds表示95%延迟为200ms,时间范围不定。

如果无相关事件,quantile必须为NaN。quantile指标的标签集不能有“quantile”标签。quantile范围为0到1,它的值不能为负,它的值应代表近期值,通常是过去5-10分钟。

Unknown

不应使用unknown类型。只有当来自第三方系统的单个指标无法确定类型时,才可以使用unknown类型。

unknown类型的点必须具有单个值。

数据传输和线格式

文本线格式必须支持,这是默认格式。protobuf 线格式可以支持,但必须仅在协商后使用。

OpenMetrics 格式采用正规 Chomsky 语法,可以快速编写小的解析器。文本格式压缩良好,而 protobuf 已经是二进制高效编码格式。 部分或无效的指标展示必须完全视为错误。

协议协商

所有摄取器实现必须能够摄取使用 TLS 1.2 或更高版本加密的指标数据。所有展示器应当能够发出使用 TLS 1.2或更高版本加密的数据。摄取器实现应当能够摄取非TLS的HTTP数据。

所有实现应当使用TLS传输数据。关于使用哪个版本不在此讨论。例如,对于基于拉取的 HTTP 指标展示,应当使用标准的 HTTP 内容类型协商机制,如果没有请求更高版本,必须默认使用标准的最老版本(即 1.0.0)。

基于推送的协商本质上更复杂,因为通常是展示器来发起连接。除非被摄取器另行要求,生产者必须使用标准的最老版本(即 1.0.0)。

文本格式

ABNF

符合 RFC 5234标准的ABNF,后续需要升级到RFC 7405

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.123421" 和 "1.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。

展示器必须为正无穷输出 +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

展示器应该以10的幂次,以以下示例中显示的方式,输出从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语言等没有可用最小浮点渲染算法的语言中,展示器可以使用不同的渲染。警告:C和其他共享其printf实现的语言中:%f、%e和%g的标准精度仅为6个有效数字。需要17位有效数字才能达到完整精度,例如  printf("%.17g", d)

时间戳

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

metricFamily

MetricFamily之间不得有显式分隔符。下一个MetricFamily必须用元数据或新的样本指标名称进行信号通知,该名称不能是前一个MetricFamily的一部分。

MetricFamily不能交错。

MetricFamily元数据

MetricFamily有四个元数据:NAME、TYPE、UNIT和HELP。

计数器指标foo的元数据示例如下:

TYPE foo counter

如果没有暴露TYPE,则MetricFamily必须是Unknown类型。如果指定了单位,则必须在UNIT元数据行中提供。另外,单位的下划线和单位必须是MetricFamily名称的后缀。

带有“秒”单位的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”标签),它必须以与指标自己的LabelSet相同的方式渲染。

指标点

指标点之间不能交错放置。

在MetricFamily中有多个MetricPoint和Sample的正确示例如下:

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

MetricPoint交错的错误示例:

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的值的样本指标名称不能有后缀。

指标没有标签,没有时间戳的MetricPoint的MetricFamily示例:

TYPE foo gauge
foo 17.0

一个具有标签的指标和没有时间戳的MetricPoint的MetricFamily示例:

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

一个没有指标的MetricFamily示例:

TYPE foo gauge

一个具有标签的指标和具有时间戳的MetricPoint的示例:

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

指标没有标签,具有时间戳的MetricPoint的示例:

TYPE foo gauge
foo 17.0 1520879607.789

指标没有标签,两个具有时间戳的MetricPoint的示例:

TYPE foo gauge
foo 17.0 123
foo 18.0 456

Counter

MetricPoint的总值样本指标名称必须有"_total"后缀。如果存在,MetricPoint的创建值样本指标名称必须有"_created"后缀。

指标没有标签,也没有时间戳和created的MetricPoint的示例:

TYPE foo counter
foo_total 17.0

指标没有标签,具有时间戳但没有created的MetricPoint的示例:

TYPE foo counter
foo_total 17.0 1520879607.789

指标没有标签,没有时间戳但有created的MetricPoint的示例:

TYPE foo counter
foo_total 17.0
foo_created 1520430000.123

指标没有标签,同时具有时间戳和created的MetricPoint的示例:

TYPE foo counter
foo_total 17.0 1520879607.789
foo_created 1520430000.123 1520879607.789

Exemplars可以附加到总值样本上。

StateSet

StateSet类型的MetricFamily的MetricPoint的值的样本指标名称不应有后缀。

StateSet必须在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

一个在指标上有“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的值的样本指标名称必须有"_info"后缀。样本的值必须始终为1。

一个具有"name"和"version"标签的MetricPoint值的示例:

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

一个具有"entity"、“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

指标标签和MetricPoint值标签可以以任何顺序出现。

Summary

如果存在,MetricPoint的求和值样本指标名称必须有"_sum"后缀。

如果存在,MetricPoint的计数值样本指标名称必须有"_count"后缀。

如果存在,MetricPoint的创建值样本指标名称必须有"_created"后缀。

如果存在,MetricPoint的分位数值必须使用一个标签指定所测量的分位数,该标签的名称为"quantile",标签值为所测量的分位数。

指标没有标签,具有求和、计数和创建值的MetricPoint的示例:

TYPE foo summary
foo_count 17.0
foo_sum 324789.3
foo_created 1520430000.123

指标没有标签,具有两个分位数的MetricPoint的示例:

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

分位数可以以任意顺序出现

Histogram

MetricPoint的桶值样本指标名称必须有"_bucket"后缀。

如果存在,MetricPoint的求和值样本指标名称必须有"_sum"后缀。

如果存在,MetricPoint的创建值样本指标名称必须有"_created"后缀。

如果且仅当MetricPoint中存在求和值时,则MetricPoint的+Inf桶的值也必须以后缀为"_count"的样本指标名称出现。

桶必须按"le"的数值递增顺序排序,并且"le"标签的值必须遵循规范数字的规则。

一个没有标签的指标和一个具有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

Exemplars

没有标签的Exemplars必须将空的LabelSet表示为{}。

下面是一个展示了几种有效情况的Exemplars示例:

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的桶值样本指标名称必须有"_bucket"后缀。

如果存在,MetricPoint的求和值样本指标名称必须有"_gsum"后缀。

如果且仅当MetricPoint中存在求和值时,则MetricPoint的+Inf桶的值也必须以后缀为"_gcount"的样本指标名称出现。

桶必须按"le"的数值递增顺序排序,并且"le"标签的值必须遵循规范数字的规则。

指标没有标签,MetricPoint值没有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

GaugeHistogram与普通的Histogram不同之处在于其桶值和求和值都是瞬时采样值,而不是累积变化值。它适用于直方图但不累积的场景。标签名称也有明确的区分。

Unknown类型

Unknown类型MetricFamily的MetricPoint指标名称不能有后缀。

一个没有标签的指标和一个没有时间戳的MetricPoint的示例:

TYPE foo unknown
foo 42.23

对于Unknown类型,它的指标点的值不附加任何后缀。这表示它是一个未知语义的原始样本值。

Protobuf格式

OpenMetrics的Protobuf schema定义了MetricFamily和Metric点等核心结构的二进制编码格式。使用Protobuf可以获得更高的编码效率、跨语言互操作性等优势。总体上遵循和文本格式类似的模型和语义。

总体结构

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

所有payload必须是一个单一的二进制编码的MetricSet消息,如OpenMetrics protobuf schema所定义。

版本

Protobuf格式必须遵循proto3版本的protocol buffer语言。

字符串

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

时间戳

OpenMetrics protobuf schema中的时间戳表示必须遵循已发布的google.protobuf.Timestamp [timestamp] 类型。

时间戳必须为Unix纪元秒,是以int64和int32表示的正数,分辨率为纳秒。它必须在0到999,999,999(包含)之间。

Protobuf schema

syntax = "proto3";

// The OpenMetrics protobuf schema which defines the protobuf wire
// format.
// Ensure to interpret "required" as semantically required for a valid
// message.
// All string fields MUST be UTF-8 encoded strings.
package openmetrics;

import "google/protobuf/timestamp.proto";

// The top-level container type that is encoded and sent over the wire.
message MetricSet {
// Each MetricFamily has one or more MetricPoints for a single Metric.
repeated MetricFamily metric_families = 1;
}

// One or more Metrics for a single MetricFamily, where each Metric
// has one or more MetricPoints.
message MetricFamily {
// Required.
string name = 1;

// Optional.
MetricType type = 2;

// Optional.
string unit = 3;

// Optional.
string help = 4;

// Optional.
repeated Metric metrics = 5;
}

// The type of a Metric.
enum MetricType {
// Unknown must use unknown MetricPoint values.
UNKNOWN = 0;
// Gauge must use gauge MetricPoint values.
GAUGE = 1;
// Counter must use counter MetricPoint values.
COUNTER = 2;
// State set must use state set MetricPoint values.
STATE_SET = 3;
// Info must use info MetricPoint values.
INFO = 4;
// Histogram must use histogram value MetricPoint values.
HISTOGRAM = 5;
// Gauge histogram must use histogram value MetricPoint values.
GAUGE_HISTOGRAM = 6;
// Summary quantiles must use summary value MetricPoint values.
SUMMARY = 7;
}

// A single metric with a unique set of labels within a metric family.
message Metric {
// Optional.
repeated Label labels = 1;

// Optional.
repeated MetricPoint metric_points = 2;
}

// A name-value pair. These are used in multiple places: identifying
// timeseries, value of INFO metrics, and exemplars in Histograms.
message Label {
// Required.
string name = 1;

// Required.
string value = 2;
}

// A MetricPoint in a Metric.
message MetricPoint {
// Required.
oneof value {
UnknownValue unknown_value = 1;
GaugeValue gauge_value = 2;
CounterValue counter_value = 3;
HistogramValue histogram_value = 4;
StateSetValue state_set_value = 5;
InfoValue info_value = 6;
SummaryValue summary_value = 7;
  }

// Optional.
google.protobuf.Timestamp timestamp = 8;
}

// Value for UNKNOWN MetricPoint.
message UnknownValue {
// Required.
oneof value {
double double_value = 1;
int64 int_value = 2;
  }
}

// Value for GAUGE MetricPoint.
message GaugeValue {
// Required.
oneof value {
double double_value = 1;
int64 int_value = 2;
  }
}

// Value for COUNTER MetricPoint.
message CounterValue {
// Required.
oneof total {
double double_value = 1;
uint64 int_value = 2;
  }

// The time values began being collected for this counter.
// Optional.
google.protobuf.Timestamp created = 3;

// Optional.
Exemplar exemplar = 4;
}

// Value for HISTOGRAM or GAUGE_HISTOGRAM MetricPoint.
message HistogramValue {
// Optional.
oneof sum {
double double_value = 1;
int64 int_value = 2;
  }

// Optional.
uint64 count = 3;

// The time values began being collected for this histogram.
// Optional.
google.protobuf.Timestamp created = 4;

// Optional.
repeated Bucket buckets = 5;

// Bucket is the number of values for a bucket in the histogram
// with an optional exemplar.
message Bucket {
// Required.
uint64 count = 1;

// Optional.
double upper_bound = 2;

// Optional.
Exemplar exemplar = 3;
  }
}

message Exemplar {
// Required.
double value = 1;

// Optional.
google.protobuf.Timestamp timestamp = 2;

// Labels are additional information about the exemplar value
// (e.g. trace id).
// Optional.
repeated Label label = 3;
}

// Value for STATE_SET MetricPoint.
message StateSetValue {
// Optional.
repeated State states = 1;

message State {
// Required.
bool enabled = 1;

// Required.
string name = 2;
  }
}

// Value for INFO MetricPoint.
message InfoValue {
// Optional.
repeated Label info = 1;
}

// Value for SUMMARY MetricPoint.
message SummaryValue {
// Optional.
oneof sum {
double double_value = 1;
int64 int_value = 2;
  }

// Optional.
uint64 count = 3;

// The time sum and count values began being collected for this summary.
// Optional.
google.protobuf.Timestamp created = 4;

// Optional.
repeated Quantile quantile = 5;

message Quantile {
// Required.
double quantile = 1;

// Required.
double value = 2;
  }
}

设计考量

范围

OpenMetrics旨在为在线系统提供遥测。它运行在无法提供硬实时或软实时保证的协议之上,因此它本身也无法做出任何实时保证。

OpenMetrics的延迟和抖动特性与底层网络、操作系统、CPU等一样不精确。它足够精确到可以作为决策的基础进行聚合,但无法反映单个事件。它应支持所有规模的系统,从每小时接收几个请求的应用程序,到监控400Gb网络端口的带宽使用。可以对传输的遥测数据进行任意时间段的聚合和分析。它旨在以固定间隔传输实时状态快照。

摄入器如何发现展示器不在本标准的范围内,也未在本标准中定义。反之亦然。

对于监控系统而言,标准只定义了指标数据格式和传输,而不涉及具体的发现机制,这属于监控系统实现的细节。设计保持开放性和灵活性非常重要。

扩展和改进

OpenMetrics的第一个版本是基于事实标准Prometheus文本格式0.0.4,不在其上面添加主要的语法或语义扩展或优化。

例如,没有尝试使Histogram桶的文本表示更加紧凑,这依赖底层栈对其重复性质的压缩。

这是一个故意的选择,以便推动现有用户去采用它。这可以确保从Prometheus文本格式0.0.4轻松过渡。

这也确保它是一个易于实现的基础标准。未来版本的标准可以在此基础上进行构建。未来版本需要对这个1.0版本提供支持,无论在语法还是语义上。

我们希望监控系统可以从OpenMetrics中获取可用信息,而不会有过重的负担。如果剥离掉所有元数据和结构,仅将OpenMetrics看作是无序的样本集,它本身就应该是可用的。因此标准中没有不透明的二进制类型,例如草图或t-digest,因为它们需要自定义的解析和处理,不能用一组计数器和仪表表达。

这个原则贯穿了整个标准。例如,一个MetricFamily的单位在名称中重复,以便不理解单位元数据的系统可以使用单位;“le”标签是一个普通的标签值,而不是有自己的特殊语法,以便摄入器不需要添加特殊的直方图处理代码来摄入它们;例如没有复合数据类型;例如没有用于纬度/经度的地理位置类型,因为这可以用独立的gauge指标来完成。

单位和基本单位

为了系统之间的一致性并避免困惑,单位在很大程度上基于国际单位制的基本单位。基本单位包括秒、字节、焦耳、克、米、比率、伏特、安培和摄氏度。

在适用的情况下应提供单位。例如,将所有时长指标以秒为单位,就不会有猜测给定指标是纳秒、微秒、毫秒、秒、分钟、小时、天还是周的风险,也不会有处理混合单位的问题。通过选择无前缀的单位,我们避免了出现千毫秒(kilomilliseconds )这样由复杂系统的自发行为导致的情况。

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

同样,混合使用bit和byte会引起困惑,因此字节(byte)被选为基本单位。

从理论上讲,开尔文是更好的基本单位,但在实践中,大多数现有硬件公开摄氏度。

公斤是国际单位制的基本单位,但是kilo前缀会有问题,因此选择克作为基本单位。

虽然基本单位应该在所有可能的情况下使用,但开尔文是一个知名的单位,在色温或黑体温等使用案例中,可能不大会对比开尔文和摄氏度指标,这时可以使用开尔文代替摄氏度。

比率ratio是基本单位,而不是百分比。在可能的情况下,应该以原始数据的形式公开分子和分母的counter或gauge。这让摄入器的分析和聚合有更好的数学性能。

分贝decibel不是基本单位,deci是一个国际单位制前缀,而且bel是对数的。要表达信号/能量/功率比,直接公开比率ratio会更好,或者如果可能的话最好公开原始的功率/能量。浮点指数完全足以覆盖甚至极端的科学用例。从电子伏特(~1e-19 J)到超新星爆发的能量(~1e44 J)跨越了63个数量级,而64位浮点数可以覆盖2000个以上的数量级。

如果无法避免非基本单位且无法转换,则应该在指标名称中包含实际单位。例如,焦耳是能量和功率的基本单位,因为瓦特可以表示为具有焦耳单位的counter。在实践中,给定的第三方系统可能只公开瓦特,所以在这种情况下,以瓦特表示的仪表可能是唯一现实的选择。

并不是所有的MetricFamily都有单位。例如,HTTP请求的数量就没有单位。从技术上讲,单位是HTTP请求,但从这个意义上说,整个MetricFamily名称就是单位。走向那个极端没什么用处。要时刻记住,在人类可读的下游系统图表上拥有良好的坐标轴。

无状态

OpenMetrics定义的格式在展示之间是无状态的。之前公开的信息对未来的展示不能有任何影响。每个展示都是公开者当前状态的自包含快照。

必须为现有的和新的摄入器提供相同的自包含展示。一个核心的设计选择是,exporter不应该仅因为指标最近没有变化或观测就排除该指标。exporter不能对摄入器的摄入频率做任何假设。

无状态的设计可以确保任何时刻的展示都包含完整的监控信息。这为监控系统的弹性、可靠性和可扩展性提供了重要保障。同时也符合监控系统的实时性要求,不需要维护状态就可以获取最新的监控数据。

跨时间的展示和指标演进

当指标随时间演变进行分析时,指标最有用,因此展示必须随时间变化保持意义。因此,仅仅一个单独的展示本身是不够的。

指标语义的变化也可能会破坏下游用户。解析器通常通过缓存以前的结果来优化。因此,应该避免改变标签暴露的顺序,尽管从技术上讲这不算违背标准。这也有助于编写展示的单元测试。

指标和样本不应该出现然后消失,例如,计数器只有在具有历史记录时才有用。原则上,给定指标应该在进程启动到进程终止的整个时间内持续存在。

通常不可能提前知道一个MetricFamily在进程的生命周期中会有哪些指标(例如,http请求延迟指标是一个histogram,它的标签值有HTTP路径,是在运行时提供的),但是一旦暴露了一个类似计数器的指标,则应该持续暴露直到进程终止。计数器没有增量不意味着它当前的值无效。在某些情况下,停止公开给定指标可能是有意义的;请参阅缺失数据部分。

一般来说,改变 MetricFamily 的类型,或从其指标中添加或删除标签会破坏摄入器。

一个显著的例外是,向 Info 类型指标中添加标签不会破坏。这样可以向现有的 Info MetricFamily 添加其他有意义的信息,而不是被迫创建一个带有额外标签值的全新的信息指标。

摄入器系统应该确保它们对这样的添加具有弹性。改变一个 MetricFamily 的帮助信息不会破坏。对于样本值,在浮点数和整数之间切换不会破坏。向状态集添加新状态不会破坏。在不改变指标名称的情况下添加单元元数据不会破坏。

直方图的桶不应该变化,因为这可能会导致性能问题并破坏摄入器。类似地,应用程序的二进制文件在不同环境展示的指标,对于给定的直方图 MetricFamily 应该具有相同的桶,以便所有摄入器都可以聚合它们,而不需要实现异构桶的直方图合并逻辑。

一个例外可能是偶尔对桶进行手动更改,这被认为是破坏性的,但当由于新的软件版本导致性能特征发生变化时,它可能是一个有效的权衡。

即使更改在技术上不是破坏性的,它们仍然带来了成本。例如,频繁的更改可能会导致摄入器的性能问题。一个变化的帮助字符串可能导致每个帮助值被存储。频繁在 int 和 float 值之间切换可能会阻止有效的压缩。因此,为了避免问题,指标的演变应该谨慎对待,只在必要时进行。

NaN

在OpenMetrics中,NaN通常是由除零得到,例如最近没有观测到摘要分位数。 

NaN在OpenMetrics中没有任何特殊含义,特别是不能用作缺失或其他不良数据的标记。

缺失数据

数据不再出现时,有一些合理的情况。例如,可以卸载文件系统,因此磁盘剩余空间的gauge指标就不再存在。这个情况没有特殊的标记或信号。后续的展示可以不包括这个指标。

指标不应轻易删除,只有在确实合理的情况下才删除,例如相关资源不再存在。同时应该在可能的情况下添加文档或日志记录,以解释原因。

总体来说,OpenMetrics通过简单地忽略缺失的指标来优雅地处理缺失数据,而不是定义特殊值或标记。这简化了编写exporter和解析器的工作。

展示性能

只有在合理的时间内收集到指标,指标才有用。需要几分钟才能公开的指标被认为是无用的。

一个经验法则是,展示不应该超过一秒钟。

旧系统通过OpenMetric暴露指标可能需要更长时间。因此,无法做出硬性的性能假设。

展示应该是最新状态。例如,应该避免依赖缓存的值,在可能的范围内应该绕过这种缓存。

并发

为了高可用性和即兴访问,常见的方法是有多个摄入器。为支持这一点,必须支持并发展示。应遵循所有关于并发系统的最佳实践,常见的陷阱包括死锁、竞争条件和过于粗糙的锁,这会阻止展示并发地推进。

快速的展示对于监控系统至关重要,否则指标的数据会过时。同时,支持并发可以提高可用性,允许多个摄入器同时获取指标。

设计高性能的指标展示既要考虑算法和数据结构,也要考虑并发。关注这两方面可以让OpenMetrics监控系统以毫秒级的延迟可靠地运行。

指标命名和命名空间

我们在指标和标签名称的命名中追求可理解性、避免冲突和简洁之间的平衡。名称用下划线分隔,所以指标名称最终是“snake_case”。

举个例子,"http_request_seconds"很简洁,但在大量应用程序之间会发生冲突,而且不清楚这个指标在测量什么。例如,它可能在复杂系统的身份验证中间件之前或之后。

指标名称应该指示它们来自哪些代码。所以一个名为“A Company Manufacturing Everything”的公司可能会在其代码中的所有指标加上前缀“acme_”,如果他们有一个测量延迟的HTTP路由器库,它可能有一个指标,例如“acme_http_router_request_seconds”,并有一个帮助字符串指示它是总体延迟。

目标不是防止所有应用程序之间的所有潜在冲突,因为这需要像全局指标命名空间或非常长的基于DNS的命名空间这样的严格的解决方案。相反,目标是保持轻量级,以便对于给定的应用程序,其组成库之间不太可能发生冲突。

在整个监控系统的部署中,目标是减少不同事物使用同一指标名称的冲突。例如,acme_http_router_request_seconds可能最终存在于在A公司开发的数百个不同的应用程序中,这在正常不过。如果另一公司在其HTTP路由器中也使用指标名称acme_http_router_request_seconds,那么这也很好。如果来自两家公司的应用程序由同一监控系统进行监控,则会发生冲突,这种冲突是不理想的,但可以接受的,因为没有应用程序试图公开两个名称,也没有目标试图(错误地)两次公开相同的指标名称。如果一个应用程序希望同时包含两者,那将是一个问题,需要以某种方式更改其中一个指标名称。

作为推论,库越公开,其指标名称的命名空间就应越好,以降低这种场景出现的风险。公司内部使用,acme_不是一个坏的选择,但这些公司可能会分别选择前缀acmeverything_或acorpme_用于公司外共享的代码。

在按公司或组织进行命名空间划分之后,应根据需要继续按库/子系统/应用程序层层划分命名空间和命名,如上面的http_router库。目标是如果你熟悉代码库的整体结构,你可以根据指标名称很好地猜测其检测的位置。

对于常见的非常著名的现有软件,软件本身的名称可能足以区分。例如,对于 DNS 软件,bind_可能就足够了,尽管isc_bind_会更常规。

以 scrape_ 为前缀的指标名称由摄入器用于附加与单个exporter相关的信息,因此不应该由应用程序直接公开。已经被消费并通过通用监控系统传递的指标可能在后续展示上包含这样的指标名称。如果展示者希望提供关于单个展示的信息,可以使用诸如 myexposer_scrape_ 之类的指标前缀。一个常见的例子是 gauge指标 myexposer_scrape_duration_seconds,表示exporter的角度花费的时间。

在Prometheus生态系统中,对于通用的每个进程指标,社区约定使用process_作为前缀,比如process_open_fds和process_max_fds。这有助于不同实现达成一致的指标语义。不过如果今天重新定义,可能会采用更清晰的名称,如process_fds_open和process_fds_limit。

在指标名称中,应该避免包含如“metric”、“timer”、“stats”、“counter”、“total”、“float64”等冗余子字符串。这些信息通过指标的类型和单位已经隐含,不需要显式包含。

同样,不要在指标名称中包含其标签名称。标签后续可能会被监控系统聚合,包含标签名称会导致指标名称不准确。

指标名称也不应该包含监控系统实现的细节,比如特定的输出格式(OpenMetrics)或监控系统名称(Prometheus)。这些实现细节与指标本身语义无关,包含在名称里会造成困扰。

标签命名空间

标签名称不建议由公司或库进行明确命名空间划分,指标名称的命名空间划分就足够了,考虑到会增加标签长度。但仍建议采取些微注意措施,避免常见冲突。

标签名称如region、zone、cluster、availability_zone、az、datacenter、dc、owner、customer、stage、service、team、job、instance、environment和env很容易与监控系统添加的目标标识标签冲突。应尽量避免,或添加最小命名空间。

标签名称“type”过于通用,应该避免。例如HTTP指标,如果要区分GET、POST,“method”会更好。

指标名有HELP、TYPE等元数据,但标签没有。这是因为会给格式增加很少收益。暴露者可通过带外文档向摄取器提供这些信息。

指标名称与标签

使用多个指标或指标族有时候都合理。求和或平均指标族应当仍有意义,虽不总是有用。如把电压和风扇速度混合就无意义。

需要注意的是,OpenMetrics假设摄取器可以处理和聚合数据。

不应与其他指标一起公开总和,这会在下游重复计算。

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

指标的标签应仅保持所需的最小唯一性,因为每个额外的标签都增加了下游用户的处理复杂度。可应用于多个指标家族的标签,可以考虑移动到类似数据库规范化的info指标中。如果几乎所有指标用户都需要额外的标签,将其添加到所有指标家族可能是一个更好的权衡。

例如,如果一个指标家族与不同的SQL语句相关,其唯一性是通过包含SQL语句哈希的标签提供的,那么为了人类可读性,添加包含SQL前500字符的标签是可以的。 

经验表明,下游摄取器更倾向于处理独立的总计数和失败指标家族,而不是在一个家族内用标签区分。同样,独立公开读写和发送接收指标家族也较好,这更方便下游分别处理。

这需要公开者和系统专家根据经验和工程权衡找到最佳平衡。

指标和标签字符

OpenMetrics建立在现有的Prometheus文本格式和生态系统上。向后兼容是核心设计目标。扩展或缩减它的字符支持会破坏兼容性,并影响关联的查询语言。标签值支持UTF-8,可表示多语言指标。

元数据类型

元数据可来自不同源头,主要有两大类:

“目标元数据”通常是公开者外部的元数据,如服务发现、CMDB等的数据,包含数据中心区域、服务部署信息、生产或测试环境等。这可以通过公开者或摄取器给所有指标添加标签来实现,以捕获此元数据。通过摄取器实现更佳,因为它更灵活,开销也更小。如硬件团队可能关心服务器机架位置,而数据库团队可能关心包含了生产数据库的副本。通过摄取器实现也无需额外分发配置。

“公开者元数据”来自公开者内部,如软件版本、编译信息或 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

当公开者提供目标元数据时,应将其放在公开内容的最前面。这是为了效率,以便依赖它的摄取器可以先应用业务逻辑,而不必缓存整个公开内容。

除非针对特定摄取器明确配置,公开者不应将目标元数据标签添加到所有指标。公开者也不应基于目标元数据为指标家族添加前缀或变更名称。通常同一个标签不应出现在所有指标上,但在少数情况下可能由紧急行为导致。同样在非常小的公开中,所有指标家族名可能碰巧共享前缀。

例如,由一家生产各种产品的公司编写的应用程序可能会包含以 acme_、go_、process_ 为前缀的指标,以及使用的任何第三方库的指标前缀。

公开者可以通过 Info 指标族公开公开者元数据。

以上讨论是单个公开者的背景。通用监控系统的公开可能包含多个目标的指标,因此可能公开多个目标信息指标。目标元数据可能在摄取时已作为标签添加到指标中。指标名称不应基于目标元数据变化。

客户端计算以及派生指标

公开者应将计算和派生指标的工作留给摄取器。一个例外是汇总分位数,需要它来保持向后兼容。公开应该是在任意时间内有用的原始值。

例如,不应公开过去5分钟计数器增长的平均速率仪表板。让摄取器基于各次公开计算增长更好,也更容错。

另一个例子是不应公开直方图/汇总的平均事件大小。公开自指标创建以来的平均增长速率,会阻止聚合。

标准差也属于这个类别。公开平方和是正确的方法。在本标准histogram不包含平方和,因为64位浮点精度在实际中不够用。求平方后精度只有53位小数位数的一半。例如,观测每秒1万个事件的Histogram指标,在2小时内就会丢失精度。使用64位整数也不会更好,因为缺少了浮点小数,通常用于跟踪每秒事件长度的纳秒级整数计数器在19次观测后就会溢出。在普遍使用128位浮点数时,可以重新审视这个设计决策。

另一个例子是避免公开请求失败比率,而是分别公开失败请求数和总请求数的计数器。

数字类型

对于每秒增长一百万次的计数器,用 float64 表示需要一个世纪才会开始丢失精度。然而 100Gbps 网络接口的吞吐量精度,在 float64 中可能在20小时内就开始丢失。尽管对 100Gbps 网络接口在数年内丢失 1KB 精度不太可能成问题,但对于高吞吐量的整数数据,int64 是一个选择。

summary的分位数必须是 float64,因为它们是估计值,从根本上不精确。

公开时间戳

OpenMetrics 的核心假设是,公开者公开最新的快照。

尽管为公开数据添加时间戳的使用案例有限,但这很少见。之前已添加时间戳的数据,特别是已摄取入通用监控系统的,可能携带时间戳。实时或原始数据不应携带时间戳。多次为同一指标公开相同的指标点值和时间戳是有效的,但是如果基础指标现在缺失则无效。

时间同步是一个难题,每个系统的数据应在内部一致。因此,摄取器应从其角度为数据添加当前时间戳,而不是基于公开者的系统时间。

对于带时间戳的指标,通常无法检测指标在多次公开间的消失时间。但是无时间戳的指标,摄取器可以使用其在指标消失的公开中的时间戳。

综上,一般不应公开指标点时间戳,应由摄取器为所摄样本添加自己的时间戳。

跟踪指标上次变化

假设你有一个计数器 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

更好。

相反,示例的时间戳没有限制。请注意,由于竞争条件或时间不同步,示例时间戳可能快于摄取器时钟或同一公开的其他指标。同样,指标点的 "_created" 可能晚于该指标点的示例或样本时间戳。

常用监控系统支持纳秒到秒的分辨率。因此截断到秒,两个指标点时间戳相同可能在摄取器造成重复。这时必须使用时间戳最早的指标点。

阈值

公开系统期望的界限有时是有意义的,但需要谨慎。对于普遍真实的数值,公开这样的阈值指标为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

这为摄取器提供了期望的完整上下文。

谨慎地公开阈值指标可以提供额外的上下文和洞察,但需要注意它们可能随环境改变。

大小限制

本标准没有规定单次公开的样本数量、标签数量、状态集状态数量、信息值中的标签数量或指标名称/标签名称/标签值/帮助文本的字符限制。

具体限制存在阻止合理用例的风险。相反,提供一些关于任何单个公开合理大小的指导是有用的。例如经过通用监控系统后一个公开可能具有合适数量的标签,但添加的几个目标标签可能将其推过限制。

摄取器可以自己施加限制,特别是为了预防攻击或停机。尽管如此,摄取器需要考虑合理的使用场景,并努力不对其产生不成比例的影响。如果任一值/指标/公开超过这样的限制,则必须拒绝整个公开。

影响监控系统性能的主要是唯一时间序列的数量、时间序列的样本数量和各种唯一字符串的数量。

唯一时间序列数量大致等于文本格式中的非注释行数。总计1000万时间序列是一个很大的量级,通常是单实例摄取器的上限。任何单个公开不应超过1万个时间序列,除非经过尽职调查。重要的是时间序列的总量级,而不是MetricFamilies或标签的基数。

对样本数量和唯一字符串数量也有类似的经验法则。总体上,任何单个公开不应显著高于大规模部署的整体指标量级。若有疑问,10万时间序列和100万唯一字符串是一个合理上限,100万样本需要仔细评估。

在合理用例下,单个公开不应远高于典型大规模部署的整体指标量级。超过时需要审慎评估对性能的影响。

如果所有同类型目标公开相同的时间序列集,则每个额外目标的字符串对大多数合理的现代监控系统来说不会带来额外成本。但是,如果每个目标都有唯一的字符串,则会带来这样的成本。例如,一个被许多目标使用的单一的1万字符的指标名称,其本身很难在实践中成为问题。相反,如果有成千上万个目标各自公开一个唯一的36字符UUID,与单个1万字符指标名称相比,考虑到需要存储的字符串,其成本要高出3倍以上,即使采用现代方法。此外,如果这些字符串随时间变化,旧字符串仍需要至少在一段时间内保存,带来额外成本。假设上一段提到的1000万时间序列,如果每小时有100MB的唯一字符串,则可能表示用例更像是事件日志记录,而不是指标时间序列。

可以硬性限制examplar长度在128个UTF-8字符以下,以防止滥用该功能来传递跟踪数据等事件日志,避免监控系统过载。

安全考量

实现者可以选择提供认证、授权和计费功能。如果选择提供,则应该在OpenMetrics之外处理。

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

指标exporter应该独立于面向终端用户的生产服务。因此,对于使用OpenMetrics的公开服务,通常不建议在像TCP/80、TCP/443、TCP/8080和TCP/8443这样的端口上设置/metrics接口。

总体来说,安全性主要考虑适当的网络隔离和加密,避免指标接口被非授权方访问或遭受攻击。实现方和操作方都应采取适当措施。

IANA注意事项

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

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

如果需要在常见的IP地址和端口上达到多个指标端点,操作者可以考虑使用与公开方通过本地地址通信的反向代理。

为了方便多路复用,端点本身应该在其路径中携带自己的名称,即`/node_exporter/metrics`。基于“在推送和拉取系统中都支持目标元数据”部分的讨论,以及允许独立摄取而不出现单点故障的考虑,不应该将公开合并到一个公开中。

OpenMetrics希望注册两个MIME类型:application/openmetrics-text和`application/openmetrics-proto`。

编辑注: application/openmetrics-text自2018年以来一直在活跃使用,application/openmetrics-proto尚未活跃使用。

编辑注:我们想感谢Sumeer Bhola,但kramdown 2.x不再支持`Contributor:`,所以一旦达成共识我们会手动添加。

0条评论
0 / 1000
唐****程
14文章数
1粉丝数
唐****程
14 文章 | 1 粉丝
原创

OpenMetrics 数据传输规范 -- 一种云原生、高度可扩展的指标协议

2023-08-21 07:34:29
358
0

简介

Prometheus于2012年创建,自2015年起一直是云原生可观测性的默认产品。Prometheus的核心是其文本指标展示格式,即Prometheus展示格式0.0.4,该格式自2014年起就已经非常稳定。这种格式特别强调可生成性、可采集性和可读性。

截至2020年,已有700多家公开的exporter、数量不详的非公开exporter以及成千上万个使用该格式的原生库集成。来自不同项目和公司的几十个采集器都支持采用它。

通过OpenMetrics,我们正在规范和收紧该标准,以将其提交给IETF。我们正在记录一个广泛而有机的工作标准,同时引入最小的、基本上向后兼容的、经过深思熟虑的变更。截至2020年,已有几十个exporter、集成商和采集器开始使用并优先协商OpenMetrics。

鉴于生态系统的广泛采用和重要的协调需求,后续不会对Prometheus展示格式0.0.4或OpenMetrics 1.0进行全面的改动。

概况

指标是一种特殊的遥测数据,它们反映了一组数据当前的状态快照。指标不同于日志或事件,后者侧重于记录或关于单个事件的信息。

OpenMetrics格式与该格式的任何具体传输方法无关。该格式的数据会被长期暴露,并且被定期消费。

实现者必须通过HTTP GET请求以OpenMetrics文本格式公开指标,该HTTP请求的URL通常定义为“/metrics”。实现者也可以通过HTTP将OpenMetrics格式的指标集定期推送到预先配置的端点来公开指标。

指标和时间序列

本标准用数值表达所有系统状态,常见例子有计数、当前值、枚举和布尔状态。指标倾向于在时间上聚合数据,而事件发生在特定时间。这样做虽然会损失信息,但能减少开销。这也是许多现代监控系统常见的工程权衡取舍。

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

常见的指标时间序列例子包括网络接口计数器、设备温度、BGP连接状态以及报警状态等。

数据模型

本节必须与 ABNF 部分一起阅读。如果两者之间存在分歧,必须以ABNF优先。

数据类型

OpenMetrics中的指标值必须是浮点数或整数。注意摄取器可能仅支持float64。非实值NaN、+Inf和-Inf必须支持。NaN不应被视为缺失值,但可用于表示除以零的情况。

布尔值

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

时间戳

时间戳必须为Unix纪元的秒数。可以使用负时间戳。

字符串

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

标签(label)

标签是由字符串组成的键值对。以下划线开头的标签名称是保留的,除非本标准另有规定,否则不能使用。

标签名称必须遵循ABNF部分中的限制。应将空标签值视为标签不存在。

标签集(LabelSet)

标签集必须由标签组成,可以为空。标签集内标签名称必须唯一。

指标点

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

示例(Exemplars )

示例是指向指标集外数据的引用。常见用例是程序跟踪的ID。

示例必须由标签集和值组成,并且可以有时间戳。它们可以与指标点的标签集和时间戳不同。示例的标签集中的标签名称和值的组合长度不能超过128个UTF-8字符代码点。为了实现简单性和文本与proto格式之间的一致性,示例中的其他字符(如",=")不包括在此限制内。

摄取器可以丢弃示例。

指标

指标由指标家族内的唯一标签集定义。指标必须包含一个或多个指标点的列表。

给定指标家族中具有相同名称的指标,其标签集中的标签名称应相同。

指标点不应具有显式时间戳。如果为指标公开多个指标点,则其指标点必须具有单调递增的时间戳。

指标族MetricFamily

一个指标族可以有零个或多个指标。

一个指标族必须有名称、HELP、TYPE和UNIT元数据。

一个指标族内的每个指标必须有一个唯一的标签集。

名称Name

指标族名称是一个字符串,在一个指标集中必须唯一。

名称应该是snake_case格式。指标名称必须遵循ABNF中的限制。指标族名称中的冒号保留用于表示指标族是通用监控系统的计算或聚合结果。

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

后缀 Suffixes

指标族的名称不得导致根据ABNF与指标集中的文本格式中的另一个指标族发生样本指标名称的潜在冲突。

一个示例是作为计数器类型的“foo”可以创建一个“foo_created”的gauge类型指标。

应该避免可能与文本格式样本指标名称使用的后缀相混淆的名称。

  • 各类型的后缀是:
  • Counter: '_total''_created'
  • Summary: '_count''_sum' '_created' '' (empty)
  • Histogram: '_count''_sum' '_bucket' '_created'
  • GaugeHistogram: '_gcount''_gsum' '_bucket'
  • Info: '_info'
  • Gauge: '' (empty)
  • StateSet: '' (empty)
  • Unknown: '' (empty)

类型Type

类型指定指标族的类型。有效值为“unknown”、“gauge”、“counter”、“stateset”、“info”、“histogram”、“gaugehistogram”和“summary”。

单位Unit

单位指定指标族的单位。

如果非空,它必须是指标族名称的后缀,用下划线分隔。

请注意,进一步的生成规则可能会使其在文本格式中成为中缀。

帮助HELP

帮助是一个字符串,应该是非空的。它用于给出指标族的简要描述,以供人类消费,并且应该足够短,可用作工具提示。

指标集 MetricSet

指标集是OpenMetrics公开的顶级对象。它必须由指标族组成,可以为空。

每个指标族名称在指标集中必须唯一。

同一个标签名称和值不应在指标集中的每个指标上出现。

指标集中不需要指标族的特定顺序。

需要让它更易于人类阅读,例如可以按字母顺序排序。

如果存在,按照下面的“在推送式和拉取式系统中都支持目标元数据”部分,应该首先出现一个称为“target”的信息指标族。

指标类型

Gauge

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

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

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

gauge可以用来编码多状态且随时间变化的枚举值,这是最高效但对用户最不友好的方式。

Counter

counter用于测量离散事件。常见的例子是收到的 HTTP 请求数、花费的 CPU 秒数或发送的字节数。

对于计数器,随着时间的推移它们增长的速度才是对用户感兴趣的。

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

类型为 counter 的指标中的指标点应该有一个名为 created 的时间戳值。这可以帮助摄取器区分新的指标和它以前没有见过的长期运行的指标。

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

类型为 counter 的指标的指标点的 total 值可以有示例。

StateSet

StateSet表示一系列相关的布尔值,也称为比特集。

如果需要对枚举进行编码,可以通过 StateSet 完成。

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

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

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

这适用于 enum 值随时间变化的情况,而状态数量不超过几个。

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

Info

info类型的指标用于公开不应在进程生命周期中改变的文本信息。常见的例子有应用程序的版本、版本控制提交和编译器的版本。

info指标的指标点包含一个标签集。

info指标点的标签集不能有与其指标的标签集的标签名称相同的标签名称。

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

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

Histogram

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

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

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

如果存在,Sum 的值必须等于所有测量事件值的总和。

指标点内的桶阈值必须唯一。

从语义上讲,Sum 和 buckets 的值是计数器,所以不能是 NaN 或负数。

可以使用负阈值桶,但同时直方图指标点必须不包含 sum 值,因为从语义上讲它不再是计数器。桶阈值不能等于 NaN。count和桶的值必须是整数。

直方图指标点应该有一个名为 Created 的时间戳值。这可以帮助摄取器区分新的指标和它以前没有见过的长期运行的指标。

直方图指标的标签集不能有一个叫做 “le” 的标签名称。

桶的值可以有exemplars。桶是累积的,以允许监控系统为了性能/防止拒绝服务的原因删除任何非 +Inf 桶,这会损失细粒度但仍然是一个有效的直方图。

注:第二句是一个注意事项,如果需要可以移除。

每个桶覆盖小于或等于它的值,并且exemplars的值必须在这个范围内。exemplars应该放入具有最高值的桶中。一个桶不能有多个exemplars。

GaugeHistogram

GaugeHistogram 测量当前的分布。常见的例子是项目在队列中已经等待的时间长度,或队列中的请求大小。

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

GaugeHistogram 的桶遵循与 Histogram 相同的所有规则。概念上,GaugeHistogram 的桶和 Gsum 是 gauges,但是桶的值不能是负数或 NaN。如果有负阈值桶,则 sum 可以是负数。Gsum 不能是 NaN。桶的值必须是整数。

GaugeHistogram 指标的标签集不能有一个叫做 “le” 的标签名称。

桶的值可以有 exemplars。每个桶覆盖小于或等于它的值,并且 exemplar 的值必须在这个范围内。exemplars 应该放入具有最高值的桶中。一个桶不能有多个 exemplar。

Summary

summary也用于测量离散事件的分布,在 Histogram 代价太高和/或平均事件大小就足够时,摘要是很好的选择。

summary也可以用于向后兼容,因为一些现有的仪表库只公开预计算的分位数,不支持 Histogram。不应该使用预计算的分位数,因为分位数不可聚合,用户通常无法推断它们覆盖的时间范围。

summary类型的指标点可以由count、sum、created和一组quantile组成。从语义上讲,count和sum是计数器,所以不能是NaN或负数。count必须是整数。

如果summary指标点包含count或sum,应该有一个名为Created的时间戳。这可以帮助区分新的指标和之前未见过的长期运行指标。Created与分位数值的收集周期无关。

quantile是从一个分位数映射到一个值。例如quantile 0.95值为0.2的指标myapp_http_request_duration_seconds表示95%延迟为200ms,时间范围不定。

如果无相关事件,quantile必须为NaN。quantile指标的标签集不能有“quantile”标签。quantile范围为0到1,它的值不能为负,它的值应代表近期值,通常是过去5-10分钟。

Unknown

不应使用unknown类型。只有当来自第三方系统的单个指标无法确定类型时,才可以使用unknown类型。

unknown类型的点必须具有单个值。

数据传输和线格式

文本线格式必须支持,这是默认格式。protobuf 线格式可以支持,但必须仅在协商后使用。

OpenMetrics 格式采用正规 Chomsky 语法,可以快速编写小的解析器。文本格式压缩良好,而 protobuf 已经是二进制高效编码格式。 部分或无效的指标展示必须完全视为错误。

协议协商

所有摄取器实现必须能够摄取使用 TLS 1.2 或更高版本加密的指标数据。所有展示器应当能够发出使用 TLS 1.2或更高版本加密的数据。摄取器实现应当能够摄取非TLS的HTTP数据。

所有实现应当使用TLS传输数据。关于使用哪个版本不在此讨论。例如,对于基于拉取的 HTTP 指标展示,应当使用标准的 HTTP 内容类型协商机制,如果没有请求更高版本,必须默认使用标准的最老版本(即 1.0.0)。

基于推送的协商本质上更复杂,因为通常是展示器来发起连接。除非被摄取器另行要求,生产者必须使用标准的最老版本(即 1.0.0)。

文本格式

ABNF

符合 RFC 5234标准的ABNF,后续需要升级到RFC 7405

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.123421" 和 "1.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。

展示器必须为正无穷输出 +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

展示器应该以10的幂次,以以下示例中显示的方式,输出从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语言等没有可用最小浮点渲染算法的语言中,展示器可以使用不同的渲染。警告:C和其他共享其printf实现的语言中:%f、%e和%g的标准精度仅为6个有效数字。需要17位有效数字才能达到完整精度,例如  printf("%.17g", d)

时间戳

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

metricFamily

MetricFamily之间不得有显式分隔符。下一个MetricFamily必须用元数据或新的样本指标名称进行信号通知,该名称不能是前一个MetricFamily的一部分。

MetricFamily不能交错。

MetricFamily元数据

MetricFamily有四个元数据:NAME、TYPE、UNIT和HELP。

计数器指标foo的元数据示例如下:

TYPE foo counter

如果没有暴露TYPE,则MetricFamily必须是Unknown类型。如果指定了单位,则必须在UNIT元数据行中提供。另外,单位的下划线和单位必须是MetricFamily名称的后缀。

带有“秒”单位的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”标签),它必须以与指标自己的LabelSet相同的方式渲染。

指标点

指标点之间不能交错放置。

在MetricFamily中有多个MetricPoint和Sample的正确示例如下:

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

MetricPoint交错的错误示例:

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的值的样本指标名称不能有后缀。

指标没有标签,没有时间戳的MetricPoint的MetricFamily示例:

TYPE foo gauge
foo 17.0

一个具有标签的指标和没有时间戳的MetricPoint的MetricFamily示例:

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

一个没有指标的MetricFamily示例:

TYPE foo gauge

一个具有标签的指标和具有时间戳的MetricPoint的示例:

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

指标没有标签,具有时间戳的MetricPoint的示例:

TYPE foo gauge
foo 17.0 1520879607.789

指标没有标签,两个具有时间戳的MetricPoint的示例:

TYPE foo gauge
foo 17.0 123
foo 18.0 456

Counter

MetricPoint的总值样本指标名称必须有"_total"后缀。如果存在,MetricPoint的创建值样本指标名称必须有"_created"后缀。

指标没有标签,也没有时间戳和created的MetricPoint的示例:

TYPE foo counter
foo_total 17.0

指标没有标签,具有时间戳但没有created的MetricPoint的示例:

TYPE foo counter
foo_total 17.0 1520879607.789

指标没有标签,没有时间戳但有created的MetricPoint的示例:

TYPE foo counter
foo_total 17.0
foo_created 1520430000.123

指标没有标签,同时具有时间戳和created的MetricPoint的示例:

TYPE foo counter
foo_total 17.0 1520879607.789
foo_created 1520430000.123 1520879607.789

Exemplars可以附加到总值样本上。

StateSet

StateSet类型的MetricFamily的MetricPoint的值的样本指标名称不应有后缀。

StateSet必须在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

一个在指标上有“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的值的样本指标名称必须有"_info"后缀。样本的值必须始终为1。

一个具有"name"和"version"标签的MetricPoint值的示例:

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

一个具有"entity"、“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

指标标签和MetricPoint值标签可以以任何顺序出现。

Summary

如果存在,MetricPoint的求和值样本指标名称必须有"_sum"后缀。

如果存在,MetricPoint的计数值样本指标名称必须有"_count"后缀。

如果存在,MetricPoint的创建值样本指标名称必须有"_created"后缀。

如果存在,MetricPoint的分位数值必须使用一个标签指定所测量的分位数,该标签的名称为"quantile",标签值为所测量的分位数。

指标没有标签,具有求和、计数和创建值的MetricPoint的示例:

TYPE foo summary
foo_count 17.0
foo_sum 324789.3
foo_created 1520430000.123

指标没有标签,具有两个分位数的MetricPoint的示例:

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

分位数可以以任意顺序出现

Histogram

MetricPoint的桶值样本指标名称必须有"_bucket"后缀。

如果存在,MetricPoint的求和值样本指标名称必须有"_sum"后缀。

如果存在,MetricPoint的创建值样本指标名称必须有"_created"后缀。

如果且仅当MetricPoint中存在求和值时,则MetricPoint的+Inf桶的值也必须以后缀为"_count"的样本指标名称出现。

桶必须按"le"的数值递增顺序排序,并且"le"标签的值必须遵循规范数字的规则。

一个没有标签的指标和一个具有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

Exemplars

没有标签的Exemplars必须将空的LabelSet表示为{}。

下面是一个展示了几种有效情况的Exemplars示例:

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的桶值样本指标名称必须有"_bucket"后缀。

如果存在,MetricPoint的求和值样本指标名称必须有"_gsum"后缀。

如果且仅当MetricPoint中存在求和值时,则MetricPoint的+Inf桶的值也必须以后缀为"_gcount"的样本指标名称出现。

桶必须按"le"的数值递增顺序排序,并且"le"标签的值必须遵循规范数字的规则。

指标没有标签,MetricPoint值没有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

GaugeHistogram与普通的Histogram不同之处在于其桶值和求和值都是瞬时采样值,而不是累积变化值。它适用于直方图但不累积的场景。标签名称也有明确的区分。

Unknown类型

Unknown类型MetricFamily的MetricPoint指标名称不能有后缀。

一个没有标签的指标和一个没有时间戳的MetricPoint的示例:

TYPE foo unknown
foo 42.23

对于Unknown类型,它的指标点的值不附加任何后缀。这表示它是一个未知语义的原始样本值。

Protobuf格式

OpenMetrics的Protobuf schema定义了MetricFamily和Metric点等核心结构的二进制编码格式。使用Protobuf可以获得更高的编码效率、跨语言互操作性等优势。总体上遵循和文本格式类似的模型和语义。

总体结构

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

所有payload必须是一个单一的二进制编码的MetricSet消息,如OpenMetrics protobuf schema所定义。

版本

Protobuf格式必须遵循proto3版本的protocol buffer语言。

字符串

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

时间戳

OpenMetrics protobuf schema中的时间戳表示必须遵循已发布的google.protobuf.Timestamp [timestamp] 类型。

时间戳必须为Unix纪元秒,是以int64和int32表示的正数,分辨率为纳秒。它必须在0到999,999,999(包含)之间。

Protobuf schema

syntax = "proto3";

// The OpenMetrics protobuf schema which defines the protobuf wire
// format.
// Ensure to interpret "required" as semantically required for a valid
// message.
// All string fields MUST be UTF-8 encoded strings.
package openmetrics;

import "google/protobuf/timestamp.proto";

// The top-level container type that is encoded and sent over the wire.
message MetricSet {
// Each MetricFamily has one or more MetricPoints for a single Metric.
repeated MetricFamily metric_families = 1;
}

// One or more Metrics for a single MetricFamily, where each Metric
// has one or more MetricPoints.
message MetricFamily {
// Required.
string name = 1;

// Optional.
MetricType type = 2;

// Optional.
string unit = 3;

// Optional.
string help = 4;

// Optional.
repeated Metric metrics = 5;
}

// The type of a Metric.
enum MetricType {
// Unknown must use unknown MetricPoint values.
UNKNOWN = 0;
// Gauge must use gauge MetricPoint values.
GAUGE = 1;
// Counter must use counter MetricPoint values.
COUNTER = 2;
// State set must use state set MetricPoint values.
STATE_SET = 3;
// Info must use info MetricPoint values.
INFO = 4;
// Histogram must use histogram value MetricPoint values.
HISTOGRAM = 5;
// Gauge histogram must use histogram value MetricPoint values.
GAUGE_HISTOGRAM = 6;
// Summary quantiles must use summary value MetricPoint values.
SUMMARY = 7;
}

// A single metric with a unique set of labels within a metric family.
message Metric {
// Optional.
repeated Label labels = 1;

// Optional.
repeated MetricPoint metric_points = 2;
}

// A name-value pair. These are used in multiple places: identifying
// timeseries, value of INFO metrics, and exemplars in Histograms.
message Label {
// Required.
string name = 1;

// Required.
string value = 2;
}

// A MetricPoint in a Metric.
message MetricPoint {
// Required.
oneof value {
UnknownValue unknown_value = 1;
GaugeValue gauge_value = 2;
CounterValue counter_value = 3;
HistogramValue histogram_value = 4;
StateSetValue state_set_value = 5;
InfoValue info_value = 6;
SummaryValue summary_value = 7;
  }

// Optional.
google.protobuf.Timestamp timestamp = 8;
}

// Value for UNKNOWN MetricPoint.
message UnknownValue {
// Required.
oneof value {
double double_value = 1;
int64 int_value = 2;
  }
}

// Value for GAUGE MetricPoint.
message GaugeValue {
// Required.
oneof value {
double double_value = 1;
int64 int_value = 2;
  }
}

// Value for COUNTER MetricPoint.
message CounterValue {
// Required.
oneof total {
double double_value = 1;
uint64 int_value = 2;
  }

// The time values began being collected for this counter.
// Optional.
google.protobuf.Timestamp created = 3;

// Optional.
Exemplar exemplar = 4;
}

// Value for HISTOGRAM or GAUGE_HISTOGRAM MetricPoint.
message HistogramValue {
// Optional.
oneof sum {
double double_value = 1;
int64 int_value = 2;
  }

// Optional.
uint64 count = 3;

// The time values began being collected for this histogram.
// Optional.
google.protobuf.Timestamp created = 4;

// Optional.
repeated Bucket buckets = 5;

// Bucket is the number of values for a bucket in the histogram
// with an optional exemplar.
message Bucket {
// Required.
uint64 count = 1;

// Optional.
double upper_bound = 2;

// Optional.
Exemplar exemplar = 3;
  }
}

message Exemplar {
// Required.
double value = 1;

// Optional.
google.protobuf.Timestamp timestamp = 2;

// Labels are additional information about the exemplar value
// (e.g. trace id).
// Optional.
repeated Label label = 3;
}

// Value for STATE_SET MetricPoint.
message StateSetValue {
// Optional.
repeated State states = 1;

message State {
// Required.
bool enabled = 1;

// Required.
string name = 2;
  }
}

// Value for INFO MetricPoint.
message InfoValue {
// Optional.
repeated Label info = 1;
}

// Value for SUMMARY MetricPoint.
message SummaryValue {
// Optional.
oneof sum {
double double_value = 1;
int64 int_value = 2;
  }

// Optional.
uint64 count = 3;

// The time sum and count values began being collected for this summary.
// Optional.
google.protobuf.Timestamp created = 4;

// Optional.
repeated Quantile quantile = 5;

message Quantile {
// Required.
double quantile = 1;

// Required.
double value = 2;
  }
}

设计考量

范围

OpenMetrics旨在为在线系统提供遥测。它运行在无法提供硬实时或软实时保证的协议之上,因此它本身也无法做出任何实时保证。

OpenMetrics的延迟和抖动特性与底层网络、操作系统、CPU等一样不精确。它足够精确到可以作为决策的基础进行聚合,但无法反映单个事件。它应支持所有规模的系统,从每小时接收几个请求的应用程序,到监控400Gb网络端口的带宽使用。可以对传输的遥测数据进行任意时间段的聚合和分析。它旨在以固定间隔传输实时状态快照。

摄入器如何发现展示器不在本标准的范围内,也未在本标准中定义。反之亦然。

对于监控系统而言,标准只定义了指标数据格式和传输,而不涉及具体的发现机制,这属于监控系统实现的细节。设计保持开放性和灵活性非常重要。

扩展和改进

OpenMetrics的第一个版本是基于事实标准Prometheus文本格式0.0.4,不在其上面添加主要的语法或语义扩展或优化。

例如,没有尝试使Histogram桶的文本表示更加紧凑,这依赖底层栈对其重复性质的压缩。

这是一个故意的选择,以便推动现有用户去采用它。这可以确保从Prometheus文本格式0.0.4轻松过渡。

这也确保它是一个易于实现的基础标准。未来版本的标准可以在此基础上进行构建。未来版本需要对这个1.0版本提供支持,无论在语法还是语义上。

我们希望监控系统可以从OpenMetrics中获取可用信息,而不会有过重的负担。如果剥离掉所有元数据和结构,仅将OpenMetrics看作是无序的样本集,它本身就应该是可用的。因此标准中没有不透明的二进制类型,例如草图或t-digest,因为它们需要自定义的解析和处理,不能用一组计数器和仪表表达。

这个原则贯穿了整个标准。例如,一个MetricFamily的单位在名称中重复,以便不理解单位元数据的系统可以使用单位;“le”标签是一个普通的标签值,而不是有自己的特殊语法,以便摄入器不需要添加特殊的直方图处理代码来摄入它们;例如没有复合数据类型;例如没有用于纬度/经度的地理位置类型,因为这可以用独立的gauge指标来完成。

单位和基本单位

为了系统之间的一致性并避免困惑,单位在很大程度上基于国际单位制的基本单位。基本单位包括秒、字节、焦耳、克、米、比率、伏特、安培和摄氏度。

在适用的情况下应提供单位。例如,将所有时长指标以秒为单位,就不会有猜测给定指标是纳秒、微秒、毫秒、秒、分钟、小时、天还是周的风险,也不会有处理混合单位的问题。通过选择无前缀的单位,我们避免了出现千毫秒(kilomilliseconds )这样由复杂系统的自发行为导致的情况。

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

同样,混合使用bit和byte会引起困惑,因此字节(byte)被选为基本单位。

从理论上讲,开尔文是更好的基本单位,但在实践中,大多数现有硬件公开摄氏度。

公斤是国际单位制的基本单位,但是kilo前缀会有问题,因此选择克作为基本单位。

虽然基本单位应该在所有可能的情况下使用,但开尔文是一个知名的单位,在色温或黑体温等使用案例中,可能不大会对比开尔文和摄氏度指标,这时可以使用开尔文代替摄氏度。

比率ratio是基本单位,而不是百分比。在可能的情况下,应该以原始数据的形式公开分子和分母的counter或gauge。这让摄入器的分析和聚合有更好的数学性能。

分贝decibel不是基本单位,deci是一个国际单位制前缀,而且bel是对数的。要表达信号/能量/功率比,直接公开比率ratio会更好,或者如果可能的话最好公开原始的功率/能量。浮点指数完全足以覆盖甚至极端的科学用例。从电子伏特(~1e-19 J)到超新星爆发的能量(~1e44 J)跨越了63个数量级,而64位浮点数可以覆盖2000个以上的数量级。

如果无法避免非基本单位且无法转换,则应该在指标名称中包含实际单位。例如,焦耳是能量和功率的基本单位,因为瓦特可以表示为具有焦耳单位的counter。在实践中,给定的第三方系统可能只公开瓦特,所以在这种情况下,以瓦特表示的仪表可能是唯一现实的选择。

并不是所有的MetricFamily都有单位。例如,HTTP请求的数量就没有单位。从技术上讲,单位是HTTP请求,但从这个意义上说,整个MetricFamily名称就是单位。走向那个极端没什么用处。要时刻记住,在人类可读的下游系统图表上拥有良好的坐标轴。

无状态

OpenMetrics定义的格式在展示之间是无状态的。之前公开的信息对未来的展示不能有任何影响。每个展示都是公开者当前状态的自包含快照。

必须为现有的和新的摄入器提供相同的自包含展示。一个核心的设计选择是,exporter不应该仅因为指标最近没有变化或观测就排除该指标。exporter不能对摄入器的摄入频率做任何假设。

无状态的设计可以确保任何时刻的展示都包含完整的监控信息。这为监控系统的弹性、可靠性和可扩展性提供了重要保障。同时也符合监控系统的实时性要求,不需要维护状态就可以获取最新的监控数据。

跨时间的展示和指标演进

当指标随时间演变进行分析时,指标最有用,因此展示必须随时间变化保持意义。因此,仅仅一个单独的展示本身是不够的。

指标语义的变化也可能会破坏下游用户。解析器通常通过缓存以前的结果来优化。因此,应该避免改变标签暴露的顺序,尽管从技术上讲这不算违背标准。这也有助于编写展示的单元测试。

指标和样本不应该出现然后消失,例如,计数器只有在具有历史记录时才有用。原则上,给定指标应该在进程启动到进程终止的整个时间内持续存在。

通常不可能提前知道一个MetricFamily在进程的生命周期中会有哪些指标(例如,http请求延迟指标是一个histogram,它的标签值有HTTP路径,是在运行时提供的),但是一旦暴露了一个类似计数器的指标,则应该持续暴露直到进程终止。计数器没有增量不意味着它当前的值无效。在某些情况下,停止公开给定指标可能是有意义的;请参阅缺失数据部分。

一般来说,改变 MetricFamily 的类型,或从其指标中添加或删除标签会破坏摄入器。

一个显著的例外是,向 Info 类型指标中添加标签不会破坏。这样可以向现有的 Info MetricFamily 添加其他有意义的信息,而不是被迫创建一个带有额外标签值的全新的信息指标。

摄入器系统应该确保它们对这样的添加具有弹性。改变一个 MetricFamily 的帮助信息不会破坏。对于样本值,在浮点数和整数之间切换不会破坏。向状态集添加新状态不会破坏。在不改变指标名称的情况下添加单元元数据不会破坏。

直方图的桶不应该变化,因为这可能会导致性能问题并破坏摄入器。类似地,应用程序的二进制文件在不同环境展示的指标,对于给定的直方图 MetricFamily 应该具有相同的桶,以便所有摄入器都可以聚合它们,而不需要实现异构桶的直方图合并逻辑。

一个例外可能是偶尔对桶进行手动更改,这被认为是破坏性的,但当由于新的软件版本导致性能特征发生变化时,它可能是一个有效的权衡。

即使更改在技术上不是破坏性的,它们仍然带来了成本。例如,频繁的更改可能会导致摄入器的性能问题。一个变化的帮助字符串可能导致每个帮助值被存储。频繁在 int 和 float 值之间切换可能会阻止有效的压缩。因此,为了避免问题,指标的演变应该谨慎对待,只在必要时进行。

NaN

在OpenMetrics中,NaN通常是由除零得到,例如最近没有观测到摘要分位数。 

NaN在OpenMetrics中没有任何特殊含义,特别是不能用作缺失或其他不良数据的标记。

缺失数据

数据不再出现时,有一些合理的情况。例如,可以卸载文件系统,因此磁盘剩余空间的gauge指标就不再存在。这个情况没有特殊的标记或信号。后续的展示可以不包括这个指标。

指标不应轻易删除,只有在确实合理的情况下才删除,例如相关资源不再存在。同时应该在可能的情况下添加文档或日志记录,以解释原因。

总体来说,OpenMetrics通过简单地忽略缺失的指标来优雅地处理缺失数据,而不是定义特殊值或标记。这简化了编写exporter和解析器的工作。

展示性能

只有在合理的时间内收集到指标,指标才有用。需要几分钟才能公开的指标被认为是无用的。

一个经验法则是,展示不应该超过一秒钟。

旧系统通过OpenMetric暴露指标可能需要更长时间。因此,无法做出硬性的性能假设。

展示应该是最新状态。例如,应该避免依赖缓存的值,在可能的范围内应该绕过这种缓存。

并发

为了高可用性和即兴访问,常见的方法是有多个摄入器。为支持这一点,必须支持并发展示。应遵循所有关于并发系统的最佳实践,常见的陷阱包括死锁、竞争条件和过于粗糙的锁,这会阻止展示并发地推进。

快速的展示对于监控系统至关重要,否则指标的数据会过时。同时,支持并发可以提高可用性,允许多个摄入器同时获取指标。

设计高性能的指标展示既要考虑算法和数据结构,也要考虑并发。关注这两方面可以让OpenMetrics监控系统以毫秒级的延迟可靠地运行。

指标命名和命名空间

我们在指标和标签名称的命名中追求可理解性、避免冲突和简洁之间的平衡。名称用下划线分隔,所以指标名称最终是“snake_case”。

举个例子,"http_request_seconds"很简洁,但在大量应用程序之间会发生冲突,而且不清楚这个指标在测量什么。例如,它可能在复杂系统的身份验证中间件之前或之后。

指标名称应该指示它们来自哪些代码。所以一个名为“A Company Manufacturing Everything”的公司可能会在其代码中的所有指标加上前缀“acme_”,如果他们有一个测量延迟的HTTP路由器库,它可能有一个指标,例如“acme_http_router_request_seconds”,并有一个帮助字符串指示它是总体延迟。

目标不是防止所有应用程序之间的所有潜在冲突,因为这需要像全局指标命名空间或非常长的基于DNS的命名空间这样的严格的解决方案。相反,目标是保持轻量级,以便对于给定的应用程序,其组成库之间不太可能发生冲突。

在整个监控系统的部署中,目标是减少不同事物使用同一指标名称的冲突。例如,acme_http_router_request_seconds可能最终存在于在A公司开发的数百个不同的应用程序中,这在正常不过。如果另一公司在其HTTP路由器中也使用指标名称acme_http_router_request_seconds,那么这也很好。如果来自两家公司的应用程序由同一监控系统进行监控,则会发生冲突,这种冲突是不理想的,但可以接受的,因为没有应用程序试图公开两个名称,也没有目标试图(错误地)两次公开相同的指标名称。如果一个应用程序希望同时包含两者,那将是一个问题,需要以某种方式更改其中一个指标名称。

作为推论,库越公开,其指标名称的命名空间就应越好,以降低这种场景出现的风险。公司内部使用,acme_不是一个坏的选择,但这些公司可能会分别选择前缀acmeverything_或acorpme_用于公司外共享的代码。

在按公司或组织进行命名空间划分之后,应根据需要继续按库/子系统/应用程序层层划分命名空间和命名,如上面的http_router库。目标是如果你熟悉代码库的整体结构,你可以根据指标名称很好地猜测其检测的位置。

对于常见的非常著名的现有软件,软件本身的名称可能足以区分。例如,对于 DNS 软件,bind_可能就足够了,尽管isc_bind_会更常规。

以 scrape_ 为前缀的指标名称由摄入器用于附加与单个exporter相关的信息,因此不应该由应用程序直接公开。已经被消费并通过通用监控系统传递的指标可能在后续展示上包含这样的指标名称。如果展示者希望提供关于单个展示的信息,可以使用诸如 myexposer_scrape_ 之类的指标前缀。一个常见的例子是 gauge指标 myexposer_scrape_duration_seconds,表示exporter的角度花费的时间。

在Prometheus生态系统中,对于通用的每个进程指标,社区约定使用process_作为前缀,比如process_open_fds和process_max_fds。这有助于不同实现达成一致的指标语义。不过如果今天重新定义,可能会采用更清晰的名称,如process_fds_open和process_fds_limit。

在指标名称中,应该避免包含如“metric”、“timer”、“stats”、“counter”、“total”、“float64”等冗余子字符串。这些信息通过指标的类型和单位已经隐含,不需要显式包含。

同样,不要在指标名称中包含其标签名称。标签后续可能会被监控系统聚合,包含标签名称会导致指标名称不准确。

指标名称也不应该包含监控系统实现的细节,比如特定的输出格式(OpenMetrics)或监控系统名称(Prometheus)。这些实现细节与指标本身语义无关,包含在名称里会造成困扰。

标签命名空间

标签名称不建议由公司或库进行明确命名空间划分,指标名称的命名空间划分就足够了,考虑到会增加标签长度。但仍建议采取些微注意措施,避免常见冲突。

标签名称如region、zone、cluster、availability_zone、az、datacenter、dc、owner、customer、stage、service、team、job、instance、environment和env很容易与监控系统添加的目标标识标签冲突。应尽量避免,或添加最小命名空间。

标签名称“type”过于通用,应该避免。例如HTTP指标,如果要区分GET、POST,“method”会更好。

指标名有HELP、TYPE等元数据,但标签没有。这是因为会给格式增加很少收益。暴露者可通过带外文档向摄取器提供这些信息。

指标名称与标签

使用多个指标或指标族有时候都合理。求和或平均指标族应当仍有意义,虽不总是有用。如把电压和风扇速度混合就无意义。

需要注意的是,OpenMetrics假设摄取器可以处理和聚合数据。

不应与其他指标一起公开总和,这会在下游重复计算。

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

指标的标签应仅保持所需的最小唯一性,因为每个额外的标签都增加了下游用户的处理复杂度。可应用于多个指标家族的标签,可以考虑移动到类似数据库规范化的info指标中。如果几乎所有指标用户都需要额外的标签,将其添加到所有指标家族可能是一个更好的权衡。

例如,如果一个指标家族与不同的SQL语句相关,其唯一性是通过包含SQL语句哈希的标签提供的,那么为了人类可读性,添加包含SQL前500字符的标签是可以的。 

经验表明,下游摄取器更倾向于处理独立的总计数和失败指标家族,而不是在一个家族内用标签区分。同样,独立公开读写和发送接收指标家族也较好,这更方便下游分别处理。

这需要公开者和系统专家根据经验和工程权衡找到最佳平衡。

指标和标签字符

OpenMetrics建立在现有的Prometheus文本格式和生态系统上。向后兼容是核心设计目标。扩展或缩减它的字符支持会破坏兼容性,并影响关联的查询语言。标签值支持UTF-8,可表示多语言指标。

元数据类型

元数据可来自不同源头,主要有两大类:

“目标元数据”通常是公开者外部的元数据,如服务发现、CMDB等的数据,包含数据中心区域、服务部署信息、生产或测试环境等。这可以通过公开者或摄取器给所有指标添加标签来实现,以捕获此元数据。通过摄取器实现更佳,因为它更灵活,开销也更小。如硬件团队可能关心服务器机架位置,而数据库团队可能关心包含了生产数据库的副本。通过摄取器实现也无需额外分发配置。

“公开者元数据”来自公开者内部,如软件版本、编译信息或 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

当公开者提供目标元数据时,应将其放在公开内容的最前面。这是为了效率,以便依赖它的摄取器可以先应用业务逻辑,而不必缓存整个公开内容。

除非针对特定摄取器明确配置,公开者不应将目标元数据标签添加到所有指标。公开者也不应基于目标元数据为指标家族添加前缀或变更名称。通常同一个标签不应出现在所有指标上,但在少数情况下可能由紧急行为导致。同样在非常小的公开中,所有指标家族名可能碰巧共享前缀。

例如,由一家生产各种产品的公司编写的应用程序可能会包含以 acme_、go_、process_ 为前缀的指标,以及使用的任何第三方库的指标前缀。

公开者可以通过 Info 指标族公开公开者元数据。

以上讨论是单个公开者的背景。通用监控系统的公开可能包含多个目标的指标,因此可能公开多个目标信息指标。目标元数据可能在摄取时已作为标签添加到指标中。指标名称不应基于目标元数据变化。

客户端计算以及派生指标

公开者应将计算和派生指标的工作留给摄取器。一个例外是汇总分位数,需要它来保持向后兼容。公开应该是在任意时间内有用的原始值。

例如,不应公开过去5分钟计数器增长的平均速率仪表板。让摄取器基于各次公开计算增长更好,也更容错。

另一个例子是不应公开直方图/汇总的平均事件大小。公开自指标创建以来的平均增长速率,会阻止聚合。

标准差也属于这个类别。公开平方和是正确的方法。在本标准histogram不包含平方和,因为64位浮点精度在实际中不够用。求平方后精度只有53位小数位数的一半。例如,观测每秒1万个事件的Histogram指标,在2小时内就会丢失精度。使用64位整数也不会更好,因为缺少了浮点小数,通常用于跟踪每秒事件长度的纳秒级整数计数器在19次观测后就会溢出。在普遍使用128位浮点数时,可以重新审视这个设计决策。

另一个例子是避免公开请求失败比率,而是分别公开失败请求数和总请求数的计数器。

数字类型

对于每秒增长一百万次的计数器,用 float64 表示需要一个世纪才会开始丢失精度。然而 100Gbps 网络接口的吞吐量精度,在 float64 中可能在20小时内就开始丢失。尽管对 100Gbps 网络接口在数年内丢失 1KB 精度不太可能成问题,但对于高吞吐量的整数数据,int64 是一个选择。

summary的分位数必须是 float64,因为它们是估计值,从根本上不精确。

公开时间戳

OpenMetrics 的核心假设是,公开者公开最新的快照。

尽管为公开数据添加时间戳的使用案例有限,但这很少见。之前已添加时间戳的数据,特别是已摄取入通用监控系统的,可能携带时间戳。实时或原始数据不应携带时间戳。多次为同一指标公开相同的指标点值和时间戳是有效的,但是如果基础指标现在缺失则无效。

时间同步是一个难题,每个系统的数据应在内部一致。因此,摄取器应从其角度为数据添加当前时间戳,而不是基于公开者的系统时间。

对于带时间戳的指标,通常无法检测指标在多次公开间的消失时间。但是无时间戳的指标,摄取器可以使用其在指标消失的公开中的时间戳。

综上,一般不应公开指标点时间戳,应由摄取器为所摄样本添加自己的时间戳。

跟踪指标上次变化

假设你有一个计数器 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

更好。

相反,示例的时间戳没有限制。请注意,由于竞争条件或时间不同步,示例时间戳可能快于摄取器时钟或同一公开的其他指标。同样,指标点的 "_created" 可能晚于该指标点的示例或样本时间戳。

常用监控系统支持纳秒到秒的分辨率。因此截断到秒,两个指标点时间戳相同可能在摄取器造成重复。这时必须使用时间戳最早的指标点。

阈值

公开系统期望的界限有时是有意义的,但需要谨慎。对于普遍真实的数值,公开这样的阈值指标为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

这为摄取器提供了期望的完整上下文。

谨慎地公开阈值指标可以提供额外的上下文和洞察,但需要注意它们可能随环境改变。

大小限制

本标准没有规定单次公开的样本数量、标签数量、状态集状态数量、信息值中的标签数量或指标名称/标签名称/标签值/帮助文本的字符限制。

具体限制存在阻止合理用例的风险。相反,提供一些关于任何单个公开合理大小的指导是有用的。例如经过通用监控系统后一个公开可能具有合适数量的标签,但添加的几个目标标签可能将其推过限制。

摄取器可以自己施加限制,特别是为了预防攻击或停机。尽管如此,摄取器需要考虑合理的使用场景,并努力不对其产生不成比例的影响。如果任一值/指标/公开超过这样的限制,则必须拒绝整个公开。

影响监控系统性能的主要是唯一时间序列的数量、时间序列的样本数量和各种唯一字符串的数量。

唯一时间序列数量大致等于文本格式中的非注释行数。总计1000万时间序列是一个很大的量级,通常是单实例摄取器的上限。任何单个公开不应超过1万个时间序列,除非经过尽职调查。重要的是时间序列的总量级,而不是MetricFamilies或标签的基数。

对样本数量和唯一字符串数量也有类似的经验法则。总体上,任何单个公开不应显著高于大规模部署的整体指标量级。若有疑问,10万时间序列和100万唯一字符串是一个合理上限,100万样本需要仔细评估。

在合理用例下,单个公开不应远高于典型大规模部署的整体指标量级。超过时需要审慎评估对性能的影响。

如果所有同类型目标公开相同的时间序列集,则每个额外目标的字符串对大多数合理的现代监控系统来说不会带来额外成本。但是,如果每个目标都有唯一的字符串,则会带来这样的成本。例如,一个被许多目标使用的单一的1万字符的指标名称,其本身很难在实践中成为问题。相反,如果有成千上万个目标各自公开一个唯一的36字符UUID,与单个1万字符指标名称相比,考虑到需要存储的字符串,其成本要高出3倍以上,即使采用现代方法。此外,如果这些字符串随时间变化,旧字符串仍需要至少在一段时间内保存,带来额外成本。假设上一段提到的1000万时间序列,如果每小时有100MB的唯一字符串,则可能表示用例更像是事件日志记录,而不是指标时间序列。

可以硬性限制examplar长度在128个UTF-8字符以下,以防止滥用该功能来传递跟踪数据等事件日志,避免监控系统过载。

安全考量

实现者可以选择提供认证、授权和计费功能。如果选择提供,则应该在OpenMetrics之外处理。

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

指标exporter应该独立于面向终端用户的生产服务。因此,对于使用OpenMetrics的公开服务,通常不建议在像TCP/80、TCP/443、TCP/8080和TCP/8443这样的端口上设置/metrics接口。

总体来说,安全性主要考虑适当的网络隔离和加密,避免指标接口被非授权方访问或遭受攻击。实现方和操作方都应采取适当措施。

IANA注意事项

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

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

如果需要在常见的IP地址和端口上达到多个指标端点,操作者可以考虑使用与公开方通过本地地址通信的反向代理。

为了方便多路复用,端点本身应该在其路径中携带自己的名称,即`/node_exporter/metrics`。基于“在推送和拉取系统中都支持目标元数据”部分的讨论,以及允许独立摄取而不出现单点故障的考虑,不应该将公开合并到一个公开中。

OpenMetrics希望注册两个MIME类型:application/openmetrics-text和`application/openmetrics-proto`。

编辑注: application/openmetrics-text自2018年以来一直在活跃使用,application/openmetrics-proto尚未活跃使用。

编辑注:我们想感谢Sumeer Bhola,但kramdown 2.x不再支持`Contributor:`,所以一旦达成共识我们会手动添加。

文章来自个人专栏
agent
14 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0