本文档涵盖了 Prometheus 客户端库应提供的功能和 API,旨在实现库之间的一致性,使简单的用例变得容易,并避免提供可能导致用户走错方向的功能。
在撰写本文时,已经支持 10 种语言,因此我们现在对如何编写客户端有了很好的认识。这些指南旨在帮助新客户端库的作者生成优秀的库。
MUST/MUST NOT/SHOULD/SHOULD NOT/MAY 的含义在 https://www.ietf.org/rfc/rfc2119.txt 中给出
此外,ENCOURAGED 意味着一个功能对于库来说是期望拥有的,但即使不存在也没关系。换句话说,锦上添花。
需要记住的事情
利用每种语言的特性。
常见的用例应该很容易。
做某事的正确方法应该是简单的方法。
更复杂的用例应该是可能的。
常见的用例是(按顺序排列)
在库/应用程序中自由散布的无标签计数器。
摘要/直方图中的定时函数/代码块。
用于跟踪事物当前状态(及其限制)的仪表盘。
批量作业的监控。
客户端必须在内部编写为基于回调的。客户端通常应遵循此处描述的结构。
关键类是 Collector。它有一个方法(通常称为“collect”),返回零个或多个指标及其样本。Collector 在 CollectorRegistry 中注册。数据通过将 CollectorRegistry 传递给类/方法/函数“bridge”来公开,该“bridge”以 Prometheus 支持的格式返回指标。每次抓取 CollectorRegistry 时,它都必须回调到每个 Collector 的 collect 方法。
大多数用户与之交互的接口是 Counter、Gauge、Summary 和 Histogram Collector。这些代表单个指标,并且应该涵盖用户检测自己的代码的大多数用例。
更高级的用例(例如从另一个监控/检测系统代理)需要编写自定义 Collector。有人可能还想编写一个“bridge”,它接受 CollectorRegistry 并以不同的监控/检测系统理解的格式生成数据,从而允许用户只需考虑一个检测系统。
CollectorRegistry 应提供 register()
/unregister()
函数,并且应允许 Collector 注册到多个 CollectorRegistry。
客户端库必须是线程安全的。
对于非 OO 语言(如 C),客户端库应尽可能遵循此结构的原则。
客户端库应遵循本文档中提到的函数/方法/类名称,同时牢记它们所使用的语言的命名约定。例如,set_to_current_time()
对于 Python 中的方法名称来说是不错的,但 SetToCurrentTime()
在 Go 中更好,而 setToCurrentTime()
是 Java 中的约定。如果名称因技术原因而不同(例如,不允许函数重载),则文档/帮助字符串应将用户指向其他名称。
库不得提供与此处给出的名称相同或相似的函数/方法/类,但具有不同的语义。
Counter、Gauge、Summary 和 Histogram 指标类型 是用户的主要接口。
Counter 和 Gauge 必须是客户端库的一部分。Summary 和 Histogram 中至少必须提供一个。
这些应主要用作文件静态变量,即在与它们检测的代码相同的文件中定义的全局变量。客户端库应启用此功能。常见的用例是检测一段代码的整体,而不是对象的一个实例上下文中的一段代码。用户不必担心在整个代码中传递他们的指标,客户端库应该为他们做这件事(如果它没有这样做,用户将编写一个库的包装器以使其“更容易” - 这通常不太顺利)。
必须有一个默认的 CollectorRegistry,标准指标必须默认隐式注册到其中,而无需用户进行特殊工作。必须有一种方法使指标不注册到默认的 CollectorRegistry,以便用于批量作业和单元测试。自定义收集器也应遵循此原则。
指标的确切创建方式因语言而异。对于某些语言(Java、Go),构建器方法是最好的,而对于其他语言(Python),函数参数足够丰富,可以在一次调用中完成。
例如,在 Java Simpleclient 中,我们有
class YourClass {
static final Counter requests = Counter.build()
.name("requests_total")
.help("Requests.").register();
}
这将向默认的 CollectorRegistry 注册请求。通过调用 build()
而不是 register()
,指标将不会被注册(对于单元测试很有用),您还可以将 CollectorRegistry 传递给 register()
(对于批量作业很有用)。
计数器是一个单调递增的计数器。它不得允许值减少,但可以重置为 0(例如通过服务器重启)。
计数器必须具有以下方法
inc()
:将计数器增加 1inc(double v)
:按给定数量增加计数器。必须检查 v >= 0。鼓励计数器具有
一种计算给定代码段中抛出/引发的异常的方法,并且可以选择仅计算某些类型的异常。这在 Python 中是 count_exceptions。
计数器必须从 0 开始。
仪表盘表示可以上下波动的值。
仪表盘必须具有以下方法
inc()
:将仪表盘增加 1inc(double v)
:按给定数量增加仪表盘dec()
:将仪表盘减少 1dec(double v)
:按给定数量减少仪表盘set(double v)
:将仪表盘设置为给定值仪表盘必须从 0 开始,您可以提供一种方法使给定的仪表盘从不同的数字开始。
仪表盘应具有以下方法
set_to_current_time()
:将仪表盘设置为当前的 Unix 时间戳(秒)。鼓励仪表盘具有
一种跟踪某些代码/函数中正在进行的请求的方法。这在 Python 中是 track_inprogress
。
一种定时一段代码并将仪表盘设置为其持续时间(秒)的方法。这对于批量作业很有用。这在 Java 中是 startTimer/setDuration,在 Python 中是 time()
装饰器/上下文管理器。这应与 Summary/Histogram 中的模式匹配(尽管是 set()
而不是 observe()
)。
摘要 对时间滑动窗口内的观察结果(通常是请求持续时间之类)进行采样,并提供对其分布、频率和总和的即时洞察。
摘要不得允许用户将“quantile”设置为标签名称,因为这在内部用于指定摘要分位数。鼓励摘要提供分位数作为导出,尽管这些分位数无法聚合并且往往速度较慢。摘要必须允许不包含分位数,因为仅 _count
/_sum
就非常有用,并且这必须是默认设置。
摘要必须具有以下方法
observe(double v)
:观察给定的数量摘要应具有以下方法
某种以秒为单位为用户定时代码的方法。在 Python 中,这是 time()
装饰器/上下文管理器。在 Java 中,这是 startTimer/observeDuration。不得提供秒以外的单位(如果用户想要其他单位,他们可以手动完成)。这应遵循与 Gauge/Histogram 相同的模式。
摘要 _count
/_sum
必须从 0 开始。
直方图 允许事件(例如请求延迟)的可聚合分布。这在核心上是每个存储桶一个计数器。
直方图不得允许 le
作为用户设置的标签,因为 le
在内部用于指定存储桶。
直方图必须提供一种手动选择存储桶的方法。应提供以 linear(start, width, count)
和 exponential(start, factor, count)
方式设置存储桶的方法。计数必须包括 +Inf
存储桶。
直方图应具有与其他客户端库相同的默认存储桶。一旦创建指标,存储桶就不得更改。
直方图必须具有以下方法
observe(double v)
:观察给定的数量直方图应具有以下方法
某种以秒为单位为用户定时代码的方法。在 Python 中,这是 time()
装饰器/上下文管理器。在 Java 中,这是 startTimer
/observeDuration
。不得提供秒以外的单位(如果用户想要其他单位,他们可以手动完成)。这应遵循与 Gauge/Summary 相同的模式。
直方图 _count
/_sum
和存储桶必须从 0 开始。
进一步的指标考虑
鼓励在指标中提供超出上述文档中记录的其他功能,以使其更适合给定的语言。
如果存在您可以简化的常见用例,请尽情去做,只要它不会鼓励不良行为(例如次优的指标/标签布局,或在客户端中进行计算)。
标签是 Prometheus 最强大的方面之一,但也容易被滥用。因此,客户端库在如何向用户提供标签时必须非常谨慎。
客户端库不得允许用户为同一指标的 Gauge/Counter/Summary/Histogram 或库提供的任何其他 Collector 使用不同的标签名称。
来自自定义收集器的指标几乎总是应该具有一致的标签名称。由于仍然存在罕见但有效的用例并非如此,因此客户端库不应验证这一点。
虽然标签功能强大,但大多数指标将没有标签。因此,API 应该允许使用标签,但不要使其占主导地位。
客户端库必须允许在 Gauge/Counter/Summary/Histogram 创建时可选地指定标签名称列表。客户端库应支持任意数量的标签名称。客户端库必须验证标签名称是否符合文档中要求的。
提供对指标的标签维度访问的常用方法是通过 labels()
方法,该方法接受标签值列表或从标签名称到标签值的映射,并返回“Child”。然后可以在 Child 上调用常用的 .inc()
/.dec()
/.observe()
等方法。
用户应缓存 labels()
返回的 Child,以避免必须再次查找它 - 这在延迟关键代码中很重要。
带有标签的指标应支持 remove()
方法,该方法具有与 labels()
相同的签名,它将从指标中删除 Child,不再导出它,以及一个 clear()
方法,该方法从指标中删除所有 Child。这些方法会使 Child 的缓存失效。
应该有一种方法使用默认值初始化给定的 Child,通常只需调用 labels()
即可。必须始终初始化没有标签的指标,以避免缺少指标的问题。
指标名称必须遵循规范。与标签名称一样,对于 Gauge/Counter/Summary/Histogram 的使用以及库提供的任何其他 Collector,都必须满足此要求。
许多客户端库提供以三个部分设置名称:namespace_subsystem_name
,其中只有 name
是强制性的。
必须不鼓励动态/生成的指标名称或指标名称的子部分,除非自定义 Collector 从其他检测/监控系统代理。生成/动态指标名称表明您应该改用标签。
Gauge/Counter/Summary/Histogram 必须要求提供指标描述/帮助。
客户端库提供的任何自定义 Collector 都必须在其指标上具有描述/帮助。
建议将其设为强制性参数,但不检查其长度是否达到特定长度,因为如果有人真的不想编写文档,我们也不会说服他们。库提供的 Collector(以及生态系统中我们能够做到的任何地方)应具有良好的指标描述,以身作则。
客户端必须实现 暴露格式 文档中概述的基于文本的暴露格式。
如果可以在没有显着资源成本的情况下实现,则鼓励暴露的指标的顺序可重现(特别是对于人类可读的格式)。
客户端库应提供它们可以提供的标准导出,如下所述。
这些应作为自定义 Collector 实现,并在默认 CollectorRegistry 上默认注册。应该有一种禁用这些的方法,因为在某些非常小众的用例中,它们会妨碍工作。
这些指标具有前缀 process_
。如果使用所用语言或运行时获取必要值存在问题甚至不可能,则客户端库应首选省略相应的指标,而不是导出虚假的、不准确的或特殊的值(如 NaN
)。所有内存值以字节为单位,所有时间以 Unix 时间戳/秒为单位。
指标名称 | 帮助字符串 | 单位 |
---|---|---|
process_cpu_seconds_total |
在秒内花费的总用户和系统 CPU 时间。 | 秒 |
process_open_fds |
打开的文件描述符的数量。 | 文件描述符 |
process_max_fds |
打开的文件描述符的最大数量。 | 文件描述符 |
process_virtual_memory_bytes |
虚拟内存大小,以字节为单位。 | 字节 |
process_virtual_memory_max_bytes |
可用的最大虚拟内存量,以字节为单位。 | 字节 |
process_resident_memory_bytes |
常驻内存大小,以字节为单位。 | 字节 |
process_heap_bytes |
进程堆大小,以字节为单位。 | 字节 |
process_start_time_seconds |
自 Unix 纪元以来的进程启动时间,以秒为单位。 | 秒 |
process_threads |
进程中 OS 线程的数量。 | 线程 |
此外,鼓励客户端库也提供对其语言运行时的指标有意义的任何内容(例如,垃圾回收统计信息),并带有适当的前缀,例如 go_
、hotspot_
等。
客户端库应具有涵盖核心检测库和暴露的单元测试。
鼓励客户端库提供使用户可以轻松地对其检测代码的使用进行单元测试的方法。例如,Python 中的 CollectorRegistry.get_sample_value
。
理想情况下,客户端库可以包含在任何应用程序中以添加一些检测,而不会破坏应用程序。
因此,在向客户端库添加依赖项时,建议谨慎。例如,如果您添加的库使用的 Prometheus 客户端需要 x.y 版本的库,但应用程序在其他地方使用 x.z 版本,这会对应用程序产生不利影响吗?
建议在可能出现这种情况的地方,将核心检测与给定格式的指标的桥接/暴露分开。例如,Java simpleclient simpleclient
模块没有依赖项,而 simpleclient_servlet
具有 HTTP 位。
由于客户端库必须是线程安全的,因此需要某种形式的并发控制,并且必须考虑多核机器和应用程序的性能。
根据我们的经验,性能最差的是互斥锁。
处理器原子指令往往处于中间位置,并且通常可以接受。
避免不同 CPU 改变同一 RAM 位的方法效果最佳,例如 Java simpleclient 中的 DoubleAdder。但是,存在内存成本。
如上所述,labels()
的结果应该是可缓存的。倾向于支持带有标签的指标的并发映射往往相对较慢。特殊处理没有标签的指标以避免类似 labels()
的查找可以帮助很多。
指标应避免在递增/递减/设置等时阻塞,因为在抓取正在进行时阻止整个应用程序是不可取的。
鼓励对主要检测操作(包括标签)进行基准测试。
在执行暴露时,应牢记资源消耗,尤其是 RAM。考虑通过流式传输结果来减少内存占用,并可能限制并发抓取的数量。
本文档是开源的。请通过提交问题或拉取请求来帮助改进它。