编写客户端库

本文档涵盖了 Prometheus 客户端库应提供的功能和 API,旨在确保库之间的一致性,简化常见用例,并避免提供可能误导用户的功能。

在撰写本文时,已经支持了 10 种语言,因此我们现在对如何编写客户端有很好的了解。这些指南旨在帮助新客户端库的作者编写优秀的库。

约定

MUST/MUST NOT/SHOULD/SHOULD NOT/MAY 的含义在 https://www.ietf.org/rfc/rfc2119.txt 中给出

此外,ENCOURAGED 意味着一个功能是库所期望拥有的,但即使没有也没关系。换句话说,锦上添花。

需要记住的事项

  • 充分利用每种语言的特性。

  • 常见的用例应该易于使用。

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

  • 更复杂的用例应该是可行的。

常见的用例是(按顺序)

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

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

  • 用于跟踪事物当前状态(及其限制)的仪表盘。

  • 批处理作业的监控。

总体结构

客户端的内部结构必须基于回调。客户端通常应遵循此处描述的结构。

关键类是 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,以便用于批处理作业和单元测试。自定义 Collector 也应该遵循这一点。

指标的确切创建方式因语言而异。对于某些语言(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/直方图中的模式匹配(尽管是 set() 而不是 observe())。

Summary

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

Summary 绝不允许用户将 “quantile” 设置为标签名称,因为内部使用它来指定 Summary 分位数。鼓励 Summary 提供分位数作为导出,尽管这些分位数无法聚合并且往往很慢。Summary 必须允许不使用分位数,因为仅 _count/_sum 就非常有用,并且这必须是默认设置。

Summary 必须具有以下方法

  • observe(double v):观测给定的量

Summary 应该具有以下方法

为用户提供以秒为单位的代码计时方法。在 Python 中,这是 time() 装饰器/上下文管理器。在 Java 中,这是 startTimer/observeDuration。绝不能提供秒以外的单位(如果用户想要其他单位,他们可以手动完成)。这应遵循与 Gauge/直方图相同的模式。

Summary _count/_sum 必须从 0 开始。

直方图

直方图 允许事件的可聚合分布,例如请求延迟。它的核心是每个 bucket 一个计数器。

直方图绝不允许将 le 作为用户设置的标签,因为 le 在内部用于指定 bucket。

直方图必须提供一种手动选择 bucket 的方法。应该提供以 linear(start, width, count)exponential(start, factor, count) 方式设置 bucket 的方法。Count 必须包含 +Inf bucket。

直方图应该具有与其他客户端库相同的默认 bucket。一旦创建指标,bucket 就绝不能更改。

直方图必须具有以下方法

  • observe(double v):观测给定的量

直方图应该具有以下方法

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

直方图 _count/_sum 和 bucket 必须从 0 开始。

进一步的指标考量

鼓励在指标中提供超出上述文档记录的其他功能,只要这些功能对于给定的语言有意义。

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

标签

标签是 Prometheus 最强大的方面之一,但 也很容易被滥用。因此,客户端库在向用户提供标签时必须非常小心。

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

来自自定义 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

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

这些应该作为自定义 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。考虑通过流式传输结果来减少内存占用,并可能限制并发抓取的数量。

本文档是开源的。请通过提交 issue 或 pull request 来帮助改进它。