编写客户端库

本文档涵盖了 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 应提供 register()/unregister() 函数,并且应允许将 Collector 注册到多个 CollectorRegistry 中。

客户端库必须是线程安全的。

对于非面向对象的语言(如 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():将计数器增加 1
  • inc(double v):将计数器增加给定数量。必须检查 v >= 0。

鼓励计数器具有

一种方法来计算给定代码块中抛出/引发的异常,并且可以选择仅计算某些类型的异常。这是 Python 中的 count_exceptions。

计数器必须从 0 开始。

仪表盘

仪表盘 表示一个可以上下波动的值。

仪表盘必须具有以下方法

  • inc():将仪表盘增加 1
  • inc(double v):将仪表盘增加给定数量
  • dec():将仪表盘减少 1
  • dec(double v):将仪表盘减少给定数量
  • set(double v):将仪表盘设置为给定值

仪表盘必须从 0 开始,您可以提供一种方法让给定的仪表盘从不同的数字开始。

仪表盘应具有以下方法

  • set_to_current_time():将仪表盘设置为当前以秒为单位的 Unix 时间。

鼓励仪表盘具有

一种方法来跟踪某些代码/函数中的进行中请求。这是 Python 中的 track_inprogress

一种方法来计时一段代码并将仪表盘设置为其持续时间(以秒为单位)。这对于批处理作业很有用。这在 Java 中是 startTimer/setDuration,在 Python 中是 time() 装饰器/上下文管理器。这应与 Summary/Histogram 中的模式匹配(尽管是 set() 而不是 observe())。

摘要

摘要 在时间滑动窗口上对观测值(通常是请求持续时间等)进行采样,并提供对其分布、频率和总和的即时洞察。

摘要不得允许用户将“分位数”设置为标签名称,因为这是在内部用于指定摘要分位数。鼓励摘要提供分位数作为导出,尽管这些分位数不能聚合并且往往速度较慢。摘要必须允许不包含分位数,因为仅 _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 使用相同的指标具有不同的标签名称。

来自自定义 Collector 的指标几乎总是应该具有一致的标签名称。由于仍然存在一些罕见但有效的用例,在这种用例中情况并非如此,因此客户端库不应对此进行验证。

虽然标签功能强大,但大多数指标将没有标签。因此,API 应该允许使用标签,但不要使其占据主导地位。

客户端库**必须**允许在 Gauge/Counter/Summary/Histogram 创建时可选地指定标签名称列表。客户端库**应该**支持任意数量的标签名称。客户端库**必须**验证标签名称是否满足 文档要求

提供对指标的带标签维度的访问的通用方法是通过一个 labels() 方法,该方法采用标签值的列表或从标签名称到标签值的映射,并返回一个“子项”。然后可以在子项上调用常用的 .inc()/.dec()/.observe() 等方法。

labels() 返回的子项**应该**由用户进行缓存,以避免不得不再次查找它——这在延迟关键代码中很重要。

带标签的指标**应该**支持一个 remove() 方法,该方法与 labels() 具有相同的签名,用于从指标中删除不再导出它的子项,以及一个 clear() 方法,用于从指标中删除所有子项。这些会使子项的缓存失效。

**应该**有一种方法可以将给定子项初始化为默认值,通常只需调用 labels() 即可。没有标签的指标**必须**始终初始化,以避免 缺少指标的问题

指标名称

指标名称必须遵循 规范。与标签名称一样,Gauge/Counter/Summary/Histogram 和库提供的任何其他 Collector 的使用都**必须**满足此要求。

许多客户端库提供三种方式来设置名称:namespace_subsystem_name,其中只有 name 是必需的。

**不鼓励**使用动态/生成的指标名称或指标名称的子部分,除非自定义 Collector 从其他检测/监控系统代理。生成的/动态指标名称表示您应该改用标签。

指标描述和帮助

Gauge/Counter/Summary/Histogram**必须**要求提供指标描述/帮助。

客户端库提供的任何自定义 Collector**必须**在其指标上提供描述/帮助。

建议将其设为必填参数,但不要检查其长度是否符合要求,因为如果有人真的不想编写文档,我们就无法说服他们。库提供的 Collector(实际上在我们可以在生态系统中提供的任何地方)**应该**具有良好的指标描述,以起到榜样作用。

公开

客户端**必须**实现 公开格式 文档中概述的基于文本的公开格式。

鼓励(尤其是在人类可读格式中)公开指标的可重复顺序,如果可以在不产生大量资源成本的情况下实现。

标准和运行时 Collector

客户端库**应该**提供他们可以提供的标准导出,如下所述。

这些**应该**作为自定义 Collector 实现,并在默认 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

打包和依赖项

理想情况下,客户端库可以包含在任何应用程序中以添加一些检测,而不会破坏应用程序。

因此,在向客户端库添加依赖项时,建议谨慎行事。例如,如果您添加了一个使用需要库的 x.y 版本的 Prometheus 客户端的库,但应用程序在其他地方使用了 x.z,这会对应用程序产生不利影响吗?

建议在可能出现这种情况时,将核心检测与以给定格式桥接/公开指标分开。例如,Java simpleclient simpleclient 模块没有任何依赖项,而 simpleclient_servlet 具有 HTTP 部分。

性能注意事项

由于客户端库必须是线程安全的,因此需要某种形式的并发控制,并且必须考虑在多核机器和应用程序上的性能。

根据我们的经验,性能最差的是互斥锁。

处理器原子指令往往处于中间位置,并且通常是可以接受的。

避免不同的 CPU 更改同一块 RAM 的方法效果最好,例如 Java 的 simpleclient 中的 DoubleAdder。但这会带来内存成本。

如上所述,labels() 的结果应该可以缓存。支持带标签指标的并发映射往往相对较慢。对没有标签的指标进行特殊处理以避免类似 labels() 的查找可以提供很大帮助。

指标**应该**避免在其被递增/递减/设置等时阻塞,因为在抓取正在进行时,整个应用程序被挂起是不可取的。

鼓励对主要检测操作(包括标签)进行基准测试。

在执行公开时,应牢记资源消耗,尤其是 RAM。考虑通过流式传输结果来减少内存占用,并可能限制并发抓取的数量。

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