编写客户端库
本文档涵盖了 Prometheus 客户端库应提供的功能和 API,旨在实现跨库的一致性,使常见用例变得简单,并避免提供可能误导用户的功能。
在撰写本文时,已经支持 10 种语言,因此我们现在对如何编写客户端有了很好的了解。这些指南旨在帮助新客户端库的作者编写出优秀的库。
约定
MUST/MUST NOT/SHOULD/SHOULD NOT/MAY 具有 https://www.ietf.org/rfc/rfc2119.txt 中给出的含义。
此外,ENCOURAGED 表示库最好具备某个功能,但如果没有也可以。换句话说,这是一个锦上添花的功能。
需要牢记的事项
-
利用每种语言的特性。
-
常见的用例应该简单易行。
-
正确的方法应该是简单的方法。
-
更复杂的用例也应该可以实现。
常见的用例(按顺序)是
-
在库/应用程序中广泛分布的无标签计数器。
-
在摘要/直方图中计时函数/代码块。
-
用于跟踪事物当前状态(及其限制)的仪表盘。
-
监控批处理作业。
整体结构
客户端内部必须是基于回调的。客户端通常应该遵循这里描述的结构。
关键的类是 Collector。它有一个方法(通常称为 ‘collect’),返回零个或多个指标及其样本。Collector 会在 CollectorRegistry 中注册。数据通过将 CollectorRegistry 传递给一个“桥接”的类/方法/函数来暴露,该桥接返回 Prometheus 支持的格式的指标。每次 CollectorRegistry 被抓取时,都必须回调每个 Collector 的 collect 方法。
大多数用户交互的接口是 Counter、Gauge、Summary 和 Histogram Collector。这些代表单个指标,应该涵盖用户在仪表化自己代码时的绝大多数用例。
更高级的用例(例如从另一个监控/仪表化系统代理)需要编写一个自定义的 Collector。有人可能还想编写一个“桥接”,接收 CollectorRegistry 并以另一个监控/仪表化系统理解的格式生成数据,这样用户就只需要考虑一个仪表化系统。
CollectorRegistry SHOULD 提供 register()/unregister() 函数,并且一个 Collector SHOULD 被允许注册到多个 CollectorRegistrys。
客户端库必须是线程安全的。
对于像 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()(这对于批处理作业很方便)。
Counter(计数器)
Counter 是一个单调递增的计数器。它绝不能允许值减少,但可以重置为 0(例如通过服务器重启)。
计数器必须有以下方法
inc():将计数器加 1inc(double v):将计数器增加给定的值。必须检查 v >= 0。
鼓励计数器具有
一种方法来计算在给定代码块中抛出/引发的异常,并且可以选择只计算某些类型的异常。在 Python 中这是 count_exceptions。
计数器必须从 0 开始。
Gauge(仪表盘)
Gauge 表示一个可以上下浮动的值。
仪表盘必须有以下方法
inc():将仪表盘值增加 1inc(double v):将仪表盘值增加给定数量dec():将仪表盘值减少 1dec(double v):将仪表盘值减少给定数量set(double v):将仪表盘值设置为给定值
Gauge 必须从 0 开始,你可以提供一种方式让给定的 Gauge 从不同的数字开始。
仪表盘应该有以下方法
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 开始。
Histogram(直方图)
直方图允许对事件(如请求延迟)进行可聚合的分布统计。其核心是每个桶一个计数器。
直方图不得允许用户设置 `le` 作为标签,因为 `le` 在内部用于指定桶。
直方图必须提供手动选择桶的方式。应提供以 `linear(start, width, count)` 和 `exponential(start, factor, count)` 方式设置桶的方法。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() 方法,该方法接受一个标签值列表或一个从标签名到标签值的映射,并返回一个“子项”。然后可以在子项上调用通常的 .inc()/.dec()/.observe() 等方法。
labels() 返回的子项应该可以被用户缓存,以避免再次查找——这在对延迟敏感的代码中很重要。
带标签的指标应该支持一个与 `labels()` 签名相同的 `remove()` 方法,该方法将从指标中移除一个子项,不再导出它;以及一个 `clear()` 方法,该方法从指标中移除所有子项。这些操作会使子项的缓存失效。
应该有一种方法来使用默认值初始化给定的子项,通常只需调用 `labels()`。没有标签的指标必须始终被初始化,以避免缺少指标的问题。
指标名称
指标名称必须遵循规范。与标签名称一样,对于 Gauge/Counter/Summary/Histogram 以及库中提供的任何其他 Collector 的使用,都必须满足此要求。
许多客户端库提供三部分设置名称的方式:`namespace_subsystem_name`,其中只有 `name` 是强制性的。
必须不鼓励使用动态/生成的指标名称或指标名称的子部分,除非自定义 Collector 从其他仪表化/监控系统代理数据。生成/动态的指标名称表明您应该改用标签。
指标描述和帮助信息
Gauge/Counter/Summary/Histogram 必须要求提供指标描述/帮助信息。
客户端库提供的任何自定义收集器都必须在其指标上提供描述/帮助信息。
建议将其设为强制参数,但不要检查其长度是否达到一定要求,因为如果有人真的不想写文档,我们是无法说服他们的。库中提供的收集器(以及生态系统中我们能做到的任何地方)都应该有好的指标描述,以身作则。
暴露
客户端必须实现暴露格式文档中概述的基于文本的暴露格式。
如果可以在不产生显著资源成本的情况下实现,鼓励(尤其对于人类可读的格式)暴露指标的可复现顺序。
标准和运行时收集器
客户端库应该提供尽可能多的标准导出,如下文所述。
这些应该作为自定义收集器实现,并默认在默认的 CollectorRegistry 上注册。应该有一种方法可以禁用这些,因为在一些非常小众的用例中,它们会造成妨碍。
进程指标
这些指标的前缀是 `process_`。如果获取必要的值在使用语言或运行时方面存在问题甚至不可能,客户端库应该倾向于省略相应的指标,而不是导出虚假、不准确或特殊的值(如 `NaN`)。所有内存值以字节为单位,所有时间以 unixtime/秒为单位。
| 指标名称 | 帮助字符串 | 单位 |
|---|---|---|
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 | 进程中的操作系统线程数。 | 线程 |
运行时指标
此外,鼓励客户端库为其语言的运行时提供有意义的指标(例如垃圾回收统计),并带有适当的前缀,如 `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()` 的查找可以有很大帮助。
指标在增加/减少/设置等操作时应避免阻塞,因为在抓取进行中时整个应用程序被挂起是不可取的。
鼓励对主要仪表化操作(包括标签)进行基准测试。
在进行暴露时应考虑资源消耗,特别是内存。考虑通过流式传输结果来减少内存占用,并可能限制并发抓取的数量。