编写客户端库

本文档涵盖了 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 被允许注册到多个 CollectorRegistrys。

客户端库 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 支持这种用法。常见的用例是对一段代码进行整体埋点,而不是在某个对象实例的上下文中对代码进行埋点。用户不应该担心在整个代码中传递指标,客户端库应该为他们处理好(如果它不这样做,用户会围绕库编写一个包装器使其“更容易”——但这很少会有好的结果)。

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

指标的确切创建方式因语言而异。对于某些语言(Java、Go),构建器(builder)方法是最好的,而对于另一些语言(Python),函数参数足够丰富,可以在一次调用中完成。

例如,在 Java Simpleclient 中,我们有

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

这将把 requests 注册到默认的 CollectorRegistry。通过调用 build() 而不是 register(),指标将不会被注册(这对于单元测试很方便),你也可以将 CollectorRegistry 传递给 register()(这对于批处理作业很方便)。

Counter(计数器)

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

一个计数器 MUST 具有以下方法

  • inc(): 将计数器加 1
  • inc(double v): 将计数器增加给定值。必须检查 v >= 0。

鼓励计数器具有

一种方法来计算在给定代码片段中抛出/引发的异常,并且可以选择只计算特定类型的异常。这在 Python 中是 count_exceptions。

计数器 MUST 从 0 开始。

Gauge(仪表盘)

Gauge 代表一个可以上升和下降的值。

一个仪表盘 MUST 具有以下方法

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

仪表盘 MUST 从 0 开始,你 MAY 提供一种方法让给定的仪表盘从一个不同的数字开始。

仪表盘 SHOULD 具有以下方法

  • set_to_current_time(): 将仪表盘设置为当前的 unix 时间戳(秒)。

鼓励仪表盘具有以下功能

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

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

总结

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

摘要 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 开始。

Histogram(直方图)

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

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

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

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

一个直方图 MUST 具有以下方法

  • observe(double v): 观察给定值

直方图 SHOULD 具有以下方法

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

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

更多指标考量

在指标中提供超出上述文档记录的功能,只要对特定语言有意义,是 ENCOURAGED 的。

如果你能简化一个常见的用例,那就去做吧,只要它不会鼓励不良行为(比如不理想的指标/标签布局,或者在客户端进行计算)。

标签

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

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

来自自定义收集器的指标几乎总是应该有统一的标签名。由于在极少数但有效的用例中情况并非如此,客户端库不应该对此进行验证。

尽管标签功能强大,但大多数指标将不带标签。因此,API 应该允许使用标签,但不应让其占据主导地位。

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

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

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

带标签的指标 SHOULD 支持一个与 `labels()` 签名相同的 `remove()` 方法,该方法将从指标中移除一个子项,使其不再被导出;以及一个 `clear()` 方法,该方法从指标中移除所有子项。这些操作会使子项的缓存失效。

应该有一种方法可以用默认值初始化给定的子项,通常只需调用 labels()。不带标签的指标 MUST 始终被初始化,以避免指标丢失的问题

指标名称

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

许多客户端库提供三部分来设置名称:`namespace_subsystem_name`,其中只有 `name` 是强制的。

必须不鼓励使用动态/生成的指标名称或指标名称的子部分,除非自定义收集器在代理其他埋点/监控系统时。生成/动态的指标名称表明你应该改用标签。

指标描述和帮助信息

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

客户端库附带的任何自定义收集器 MUST 在其指标上提供描述/帮助信息。

建议将其作为强制参数,但不要检查其长度,因为如果有人真的不想写文档,我们也无法说服他们。库中提供的收集器(以及生态系统中我们能做到的任何地方)SHOULD 有良好的指标描述,以身作则。

暴露

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

如果可以在没有显著资源成本的情况下实现,鼓励(ENCOURAGED)暴露指标的顺序可重现(尤其对于人类可读的格式)。

标准和运行时收集器

客户端库 SHOULD 尽可能提供标准导出,具体如下文所述。

这些 SHOULD 作为自定义收集器实现,并默认注册在默认的 CollectorRegistry 上。应该有一种方法可以禁用这些,因为在某些非常特殊的用例中它们会造成干扰。

进程指标

这些指标的前缀是 process_。如果在使用特定语言或运行时获取必要值存在问题甚至不可能,客户端库 SHOULD 宁愿省略相应的指标,也不要导出虚假、不准确或特殊的值(如 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进程中的操作系统线程数。线程

运行时指标

此外,鼓励客户端库提供对其语言运行时有意义的任何指标(例如垃圾回收统计信息),并使用适当的前缀,如 `go_`、`hotspot_` 等。

单元测试

客户端库 SHOULD 包含覆盖核心埋点库和暴露功能的单元测试。

鼓励客户端库提供方便用户对埋点代码进行单元测试的方法。例如,Python 中的 `CollectorRegistry.get_sample_value`。

打包和依赖项

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

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

建议在可能出现这种情况时,将核心埋点与特定格式的指标桥接/暴露部分分离开。例如,Java simpleclient 的 simpleclient 模块没有依赖项,而 simpleclient_servlet 模块包含 HTTP 相关部分。

性能考量

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

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

处理器原子指令的性能通常居中,并且通常是可接受的。

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

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

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

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

在进行暴露时,应考虑资源消耗,特别是 RAM。考虑通过流式传输结果来减少内存占用,并可能限制并发抓取的数量。

本页内容