编写客户端库

本文档介绍了 Prometheus 客户端库应提供的功能和 API,旨在保持各库之间的一致性,使常见用例易于实现,并避免提供可能误导用户的功能。

截至本文档撰写时,已支持 10 种语言,因此我们已经很好地了解了如何编写客户端。这些指南旨在帮助新客户端库的作者创建高质量的库。

约定

MUST/MUST NOT/SHOULD/SHOULD NOT/MAY 具有 https://www.ietf.org/rfc/rfc2119.txt 中赋予的含义

此外,ENCOURAGED 意味着某个特性对于库来说是可取的,但如果不存在也无妨。换句话说,这是个锦上添花的功能。

需要牢记的事项

  • 利用每种语言的特性。

  • 常见用例应该易于实现。

  • 做某事的正确方法应该就是简单的方法。

  • 更复杂的用例应该也是可能实现的。

常见用例(按顺序)是

  • 在库/应用中广泛使用无标签的计数器。

  • 在摘要/直方图中对函数/代码块进行计时。

  • 使用计量器跟踪事物的当前状态(及其限制)。

  • 监控批处理作业。

总体结构

客户端内部 MUST 基于回调编写。客户端 SHOULD 通常遵循此处描述的结构。

核心类是 Collector。它有一个方法(通常称为 ‘collect’),返回零个或多个指标及其样本。Collector 通过 CollectorRegistry 进行注册。通过将 CollectorRegistry 传递给类/方法/函数“桥”,数据被暴露出来,该“桥”以 Prometheus 支持的格式返回指标。每次 CollectorRegistry 被抓取时,它都必须回调每个 Collector 的 collect 方法。

大多数用户与之交互的接口是 Counter、Gauge、Summary 和 Histogram Collector。这些代表单个指标,应覆盖用户为其自己的代码进行插桩的绝大多数用例。

更高级的用例(例如从另一个监控/插桩系统进行代理)需要编写自定义 Collector。有人可能还希望编写一个接收 CollectorRegistry 并生成不同监控/插桩系统理解的数据格式的“桥”,从而让用户只需考虑一个插桩系统。

CollectorRegistry SHOULD 提供 register()/unregister() 函数,且 Collector SHOULD 允许注册到多个 CollectorRegistry。

客户端库 MUST 是线程安全的。

对于 C 等非面向对象语言,客户端库应尽可能在实践中遵循此结构的精髓。

命名

客户端库 SHOULD 遵循本文档中提到的函数/方法/类名,同时考虑到其所用语言的命名约定。例如,set_to_current_time() 在 Python 中是一个好的方法名,而在 Go 中 SetToCurrentTime() 更好,Java 的约定是 setToCurrentTime()。当名称因技术原因而不同时(例如不允许函数重载),文档/帮助字符串 SHOULD 指引用户查找其他名称。

库 MUST NOT 提供与此处给出的名称相同或相似但语义不同的函数/方法/类。

指标

Counter、Gauge、Summary 和 Histogram 指标类型是用户主要与之交互的接口。

Counter 和 Gauge MUST 是客户端库的一部分。Summary 和 Histogram 中至少有一个 MUST 提供。

这些主要应作为文件静态变量使用,即与进行插桩的代码在同一文件中定义的全局变量。客户端库 SHOULD 支持这一点。常见用例是对整个代码段进行插桩,而不是在对象实例的上下文中对代码段进行插桩。用户不必担心将指标连接到代码的各个地方,客户端库应该为他们做到这一点(如果不支持,用户会编写一个包装器使库“更容易”使用——但这很少会顺利)。

MUST 存在一个默认的 CollectorRegistry,标准指标 MUST 默认隐式注册到其中,无需用户进行特殊操作。MUST 有一种方式使指标不注册到默认的 CollectorRegistry,以便用于批处理作业和单元测试。自定义收集器 SHOULD 也应遵循这一点。

指标的具体创建方式因语言而异。对于某些语言(Java、Go),Builder 模式最好,而对于其他语言(Python),函数参数足够丰富,可以在一次调用中完成。

例如在 Java Simpleclient 中,我们有

class YourClass {
  static final Counter requests = Counter.build()
      .name("requests_total")
      .help("Requests.").register();
}

This will register requests with the default CollectorRegistry. By calling build() rather than register() the metric won’t be registered (handy for unittests), you can also pass in a CollectorRegistry to register() (handy for batch jobs).

这将把请求注册到默认的 CollectorRegistry。通过调用 build() 而不是 register(),指标将不会被注册(方便单元测试),你也可以将一个 CollectorRegistry 传入 register() 方法(方便批处理作业)。

计数器

计数器是一个单调递增的计数器。它 MUST NOT 允许值减少,但是它 MAY 被重置为 0(例如通过服务器重启)。

  • 计数器 MUST 具有以下方法
  • inc(): 将计数器增加 1

inc(double v): 将计数器增加给定数量。MUST 检查 v >= 0。

计数器 ENCOURAGED 具有

一种计算给定代码段中抛出/引发异常的方式,并且可选地只计算某些类型的异常。这在 Python 中是 count_exceptions。

计数器 MUST 从 0 开始。

计量器

计量器代表一个可以上升和下降的值。

  • 计量器 MUST 具有以下方法
  • inc(): 将计量器增加 1
  • inc(double v): 将计量器增加给定数量
  • dec(): 将计量器减少 1
  • dec(double v): 将计量器减少给定数量

set(double v): 将计量器设置为给定值

计量器 MUST 从 0 开始,你 MAY 提供一种方式使给定计量器从不同数字开始。

  • 计量器 SHOULD 具有以下方法

set_to_current_time(): 将计量器设置为当前的 Unix 时间(秒)。

计量器 ENCOURAGED 具有

一种跟踪代码段/函数中进行中请求的方式。这在 Python 中是 track_inprogress

一种对代码段进行计时并将计量器设置为其持续时间(秒)的方式。这对于批处理作业很有用。这在 Java 中是 startTimer/setDuration,在 Python 中是 time() 装饰器/上下文管理器。这 SHOULD 与 Summary/Histogram 中的模式匹配(尽管使用 set() 而非 observe())。

摘要

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

摘要 MUST NOT 允许用户将“quantile”设置为标签名,因为它在内部用于指定摘要分位数。摘要 ENCOURAGED 提供分位数作为导出,尽管这些无法聚合且往往很慢。摘要 MUST 允许不包含分位数,因为仅有 _count/_sum 已经非常有用,并且这 MUST 是默认设置。

  • 摘要 MUST 具有以下方法

observe(double v): 观测给定数量

摘要 SHOULD 具有以下方法

某种方式为用户提供代码计时(以秒为单位)。在 Python 中,这是 time() 装饰器/上下文管理器。在 Java 中,这是 startTimer/observeDuration。MUST NOT 提供秒以外的单位(如果用户需要其他单位,他们可以手动实现)。这应遵循与 Gauge/Histogram 相同的模式。

摘要的 _count/_sum MUST 从 0 开始。

直方图

直方图允许对事件(如请求延迟)的可聚合分布进行度量。其核心是对每个桶进行计数。

直方图 MUST NOT 允许用户将 le 设置为标签,因为 le 在内部用于指定桶(bucket)。

直方图 MUST 提供手动选择桶的方式。SHOULD 提供以 linear(start, width, count)exponential(start, factor, count) 方式设置桶的方法。Count MUST 包含 +Inf 桶。

直方图 SHOULD 具有与其他客户端库相同的默认桶。一旦指标创建后,桶 MUST NOT 可更改。

  • 摘要 MUST 具有以下方法

直方图 MUST 具有以下方法

某种为用户测量代码执行时间(以秒为单位)的方式。在 Python 中,这可以是 time() 装饰器/上下文管理器。在 Java 中,这可以是 startTimer/observeDuration。除了秒之外,不得提供其他单位(如果用户想要其他单位,他们可以手动转换)。这应该遵循与 Gauge/Summary 相同的模式。

直方图 SHOULD 具有以下方法

某种方式为用户提供代码计时(以秒为单位)。在 Python 中,这是 time() 装饰器/上下文管理器。在 Java 中,这是 startTimer/observeDuration。MUST NOT 提供秒以外的单位(如果用户需要其他单位,他们可以手动实现)。这应遵循与 Gauge/Summary 相同的模式。

直方图的 _count/_sum 和桶 MUST 从 0 开始。

进一步的指标考量

在指标中提供超出上述文档所述之外的、对特定语言有意义的额外功能是 ENCOURAGED 的。

如果有一个你可以使其更简单的常见用例,那就去做,只要它不会鼓励不良行为(例如次优的指标/标签布局,或在客户端进行计算)。

标签

标签是 Prometheus 最强大的特性之一,但也容易被滥用。因此,客户端库在如何向用户提供标签方面必须非常谨慎。

客户端库 MUST NOT 允许用户对 Gauge/Counter/Summary/Histogram 或库提供的任何其他 Collector 的同一指标使用不同的标签名称。

来自自定义收集器的指标几乎总是应具有一致的标签名称。由于仍然存在罕见但有效的用例并非如此,客户端库不应验证这一点。

虽然标签功能强大,但大多数指标没有标签。因此,API 应支持标签,但不应以其为主导。

客户端库 MUST 允许在 Gauge/Counter/Summary/Histogram 创建时可选地指定标签名称列表。客户端库 SHOULD 支持任意数量的标签名称。客户端库 MUST 验证标签名称是否符合文档中要求

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

labels() 返回的 Child SHOULD 对用户可缓存,以避免再次查找——这在延迟敏感的代码中很重要。

带有标签的指标 SHOULD 支持 remove() 方法,其签名与 labels() 相同,用于从指标中移除不再导出的 Child,以及 clear() 方法用于移除指标中的所有 Children。这些方法会使 Child 的缓存失效。

SHOULD 有一种方式用默认值初始化给定的 Child,通常只需调用 labels()。没有标签的指标 MUST 始终被初始化,以避免指标丢失问题

指标名称

指标名称必须遵循规范。与标签名称一样,对于 Gauge/Counter/Summary/Histogram 以及库提供的任何其他 Collector 的使用,都 MUST 满足这一点。

许多客户端库提供将名称设置为三个部分:namespace_subsystem_name,其中只有 name 是必需的。

MUST discourage 动态/生成的指标名称或指标名称的子部分,除非自定义 Collector 正在从其他插桩/监控系统代理。生成的/动态的指标名称表明你应该使用标签代替。

指标描述与帮助信息

Gauge/Counter/Summary/Histogram MUST 要求提供指标描述/帮助信息。

客户端库提供的任何自定义 Collector MUST 在其指标上具有描述/帮助信息。

建议将其设为一个强制参数,但不要检查其长度,因为如果某人真的不想写文档,我们也无法说服他们。库中提供的 Collector(以及生态系统中我们力所能及的任何地方)SHOULD 具有良好的指标描述,以起到表率作用。

暴露

客户端 MUST 实现暴露格式文档中概述的基于文本的暴露格式。

如果可以在不显著增加资源开销的情况下实现,ENCOURAGED 暴露的指标具有可重现的顺序(特别是对于人类可读格式)。

标准与运行时收集器

客户端库 SHOULD 提供其能实现的标准导出(Standard exports),如下所述。

这些 SHOULD 作为自定义 Collector 实现,并默认注册到默认的 CollectorRegistry。SHOULD 有一种方法来禁用它们,因为在一些非常小众的用例中它们可能会碍事。

进程指标 这些指标带有 process_ 前缀。如果使用所用语言或运行时获取必要值存在问题甚至不可能,客户端库 SHOULD 优先省略相应的指标,而不是导出虚假、不准确或特殊值(如 NaN)。所有内存值以字节为单位,所有时间以 Unix 时间/秒为单位。 指标名称
帮助字符串 单位 process_cpu_seconds_total
总用户和系统 CPU 时间(秒)。 process_open_fds
打开的文件描述符数量。 文件描述符 process_open_fds
process_max_fds 最大打开文件描述符数量。 process_virtual_memory_bytes
虚拟内存大小(字节)。 字节 process_virtual_memory_bytes
process_virtual_memory_max_bytes 最大可用虚拟内存量(字节)。 process_virtual_memory_bytes
process_resident_memory_bytes 常驻内存大小(字节)。 process_virtual_memory_bytes
process_heap_bytes 进程堆大小(字节)。 process_cpu_seconds_total
process_start_time_seconds 进程启动时间(自 Unix 纪元以来的秒数)。 process_threads

进程中的操作系统线程数量。

线程

运行时指标

此外,ENCOURAGED 客户端库也提供与其语言运行时相关的指标(例如垃圾收集统计信息),并使用适当的前缀,如 go_hotspot_ 等。

单元测试

客户端库 SHOULD 具有涵盖核心插桩库和暴露的单元测试。

ENCOURAGED 客户端库提供使单元测试用户插桩代码使用变得容易的方法。例如,Python 中的 CollectorRegistry.get_sample_value

打包与依赖

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

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

建议在这种情况下,将核心插桩与给定格式的指标桥接/暴露分离开来。例如,Java simpleclient 的 simpleclient 模块没有依赖项,而 simpleclient_servlet 包含 HTTP 相关部分。

性能考量

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

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

处理器原子指令(atomic instructions)往往居中,通常可以接受。

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

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

指标 SHOULD 避免在增加/减少/设置等操作时阻塞,因为在抓取正在进行时整个应用程序被阻塞是不可取的。

ENCOURAGED 对主要的插桩操作(包括标签)进行基准测试。