本文档介绍了 Prometheus 客户端库应该提供的功能和 API,旨在确保各库之间的一致性,使常见用例简单易用,并避免提供可能将用户引入歧途的功能。
在编写本文时,已有 10 种语言的支持,因此我们已经对如何编写客户端有了很好的认识。这些指南旨在帮助新客户端库的作者编写出高质量的库。
约定
MUST/MUST NOT/SHOULD/SHOULD NOT/MAY 的含义遵循 https://www.ietf.org/rfc/rfc2119.txt 中的定义。
此外,ENCOURAGED 表示某个特性对于库来说是理想的,但即使没有也可以接受。换句话说,是锦上添花的功能。
需要牢记的事项:
充分利用每种语言的特性。
常见用例应该简单易用。
正确的做法应该也是最简单的做法。
更复杂的用例应该是可实现的。
常见用例(按优先顺序):
在库/应用程序中广泛使用不带标签的 Counter。
使用 Summary/Histogram 对函数/代码块计时。
使用 Gauge 跟踪事物的当前状态(及其限制)。
监控批处理作业。
整体结构
客户端内部 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,以用于批处理作业和单元测试。自定义 Collector SHOULD 也遵循这一点。
指标具体如何创建因语言而异。对于某些语言(Java、Go)构建器模式最好,而对于其他语言(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 允许值减少,但可以被重置为 0(例如通过服务器重启)。
Counter MUST 具有以下方法:
inc():将计数器增加 1inc(double v):将计数器增加给定的量。MUST 检查 v >= 0。
Counter ENCOURAGED 具有:
一种计算给定代码段中抛出/引发异常数量的方法,并且可选地只计算某些类型的异常。在 Python 中这是 count_exceptions。
Counter MUST 从 0 开始。
Gauge
Gauge 表示一个可以上升和下降的值。
Gauge MUST 具有以下方法:
inc():将 Gauge 增加 1inc(double v):将 Gauge 增加给定的量dec():将 Gauge 减少 1dec(double v):将 Gauge 减少给定的量set(double v):将 Gauge 设置为给定的值
Gauge MUST 从 0 开始,你 MAY 提供一种方法使给定的 Gauge 从不同的数字开始。
Gauge SHOULD 具有以下方法:
set_to_current_time():将 Gauge 设置为当前的 Unix 时间戳(秒)。
Gauge ENCOURAGED 具有:
一种在某段代码/函数中跟踪进行中请求的方法。在 Python 中这是 track_inprogress。
一种对一段代码计时并将 Gauge 设置为其持续时间(秒)的方法。这对于批处理作业很有用。在 Java 中这是 startTimer/setDuration,在 Python 中是 time() 装饰器/上下文管理器。这 SHOULD 与 Summary/Histogram 的模式匹配(尽管是用 set() 而不是 observe())。
Summary
Summary 对观测值(通常是请求持续时间之类的值)在滑动时间窗口上进行采样,并提供对其分布、频率和总和的即时洞察。
Summary MUST NOT 允许用户将 "quantile" 设置为标签名称,因为这是内部用于指定 Summary 分位数的。Summary ENCOURAGED 提供分位数作为导出,但这些不能被聚合且往往较慢。Summary MUST 允许不包含分位数,因为仅 _count/_sum 就非常有用,且这 MUST 是默认设置。
Summary MUST 具有以下方法:
observe(double v):观测给定的量
Summary SHOULD 具有以下方法:
为用户提供某种以秒为单位对代码计时的方法。在 Python 中这是 time() 装饰器/上下文管理器。在 Java 中这是 startTimer/observeDuration。MUST NOT 提供秒以外的单位(如果用户需要其他单位,可以手动实现)。这应该与 Gauge/Histogram 遵循相同的模式。
Summary 的 _count/_sum MUST 从 0 开始。
Histogram
Histogram 允许对事件(如请求延迟)进行可聚合的分布统计。其核心是每个桶一个计数器。
Histogram MUST NOT 允许将 le 作为用户设置的标签,因为 le 在内部用于指定桶。
Histogram MUST 提供一种手动选择桶的方法。SHOULD 提供以 linear(start, width, count) 和 exponential(start, factor, count) 方式设置桶的方法。count MUST 包括 +Inf 桶。
Histogram SHOULD 具有与其他客户端库相同的默认桶。桶在指标创建后 MUST NOT 可更改。
Histogram MUST 具有以下方法:
observe(double v):观测给定的量
Histogram SHOULD 具有以下方法:
为用户提供某种以秒为单位对代码计时的方法。在 Python 中这是 time() 装饰器/上下文管理器。在 Java 中这是 startTimer/observeDuration。MUST NOT 提供秒以外的单位(如果用户需要其他单位,可以手动实现)。这应该与 Gauge/Summary 遵循相同的模式。
Histogram 的 _count/_sum 和桶 MUST 从 0 开始。
更多指标考虑事项
在上述文档之外,鼓励在给定语言中有意义地为指标提供额外功能。
如果你能简化一个常见用例就去做,只要它不会鼓励不良行为(如次优的指标/标签布局,或在客户端进行计算)。
标签
标签是 Prometheus 最强大的特性之一,但也容易被滥用。因此客户端库必须非常谨慎地向用户提供标签功能。
客户端库 SHOULD NOT 允许用户对同一个 Gauge/Counter/Summary/Histogram 或库提供的任何其他 Collector 使用不同的标签名称。
来自自定义 Collector 的指标几乎应该总是具有一致的标签名称。由于仍有少见但合理的用例不符合这种情况,客户端库不应该验证这一点。
虽然标签很强大,但大多数指标不会有标签。因此 API 应该允许使用标签但不让标签占据主导地位。
客户端库 MUST 允许在 Gauge/Counter/Summary/Histogram 创建时可选地指定标签名称列表。客户端库 SHOULD 支持任意数量的标签名称。客户端库 MUST 验证标签名称符合文档化的要求。
提供对指标标签维度访问的一般方式是通过 labels() 方法,它接受标签值列表或标签名称到标签值的映射,并返回一个"Child"。然后可以对该 Child 调用常用的 .inc()/.dec()/.observe() 等方法。
labels() 返回的 Child SHOULD 可被用户缓存,以避免重复查找——这在延迟敏感的代码中很重要。
带标签的指标 SHOULD 支持 remove() 方法,其签名与 labels() 相同,用于从指标中移除一个 Child 使其不再被导出,以及 clear() 方法用于从指标中移除所有 Child。这些操作会使 Child 的缓存失效。
SHOULD 有一种方法来用默认值初始化给定的 Child,通常只需调用 labels()。不带标签的指标 MUST 始终被初始化,以避免缺失指标问题。
指标名称
指标名称必须遵循规范。与标签名称一样,这对于 Gauge/Counter/Summary/Histogram 的使用以及库提供的任何其他 Collector MUST 满足此要求。
许多客户端库提供分三部分设置名称的方式:namespace_subsystem_name,其中只有 name 是必须的。
MUST 不鼓励动态/生成的指标名称或指标名称的子部分,除非自定义 Collector 是从其他检测/监控系统代理时。生成的/动态的指标名称意味着你应该改用标签。
指标描述和帮助信息
Gauge/Counter/Summary/Histogram MUST 要求提供指标描述/帮助信息。
随客户端库提供的任何自定义 Collector MUST 在其指标上有描述/帮助信息。
建议将其设为必填参数,但不必检查其长度,因为如果有人确实不想写文档,我们也说服不了他们。随库提供的 Collector(以及确实在生态系统中的所有地方)SHOULD 有好的指标描述,以身作则。
暴露
客户端 MUST 实现暴露格式文档中描述的基于文本的暴露格式。
如果不会造成显著的资源开销,ENCOURAGED 对暴露的指标进行可重现的排序(特别是对于人类可读的格式)。
标准和运行时收集器
客户端库 SHOULD 尽可能提供下面文档中描述的标准导出。
这些 SHOULD 作为自定义 Collector 实现,并默认注册到默认的 CollectorRegistry。SHOULD 有一种方法来禁用它们,因为在一些非常小众的用例中它们会造成干扰。
进程指标
这些指标的前缀为 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 |
进程中的操作系统线程数。 | 线程 |
运行时指标
此外,客户端库也 ENCOURAGED 提供对其语言运行时有意义的指标(例如垃圾回收统计信息),使用适当的前缀,如 go_、hotspot_ 等。
单元测试
客户端库 SHOULD 有覆盖核心检测库和暴露功能的单元测试。
客户端库 ENCOURAGED 提供使用户能够轻松对其检测代码使用进行单元测试的方法。例如 Python 中的 CollectorRegistry.get_sample_value。
打包和依赖
理想情况下,客户端库可以被包含在任何应用程序中以添加一些检测,而不会破坏应用程序。
因此,在向客户端库添加依赖时应谨慎。例如,如果你添加了一个使用 Prometheus 客户端的库,该客户端需要版本 x.y 的某个库,而应用程序在其他地方使用了 x.z 版本,这是否会对应用程序产生不良影响?
建议在可能出现这种情况时,将核心检测与特定格式的指标桥接/暴露分开。例如 Java simpleclient 中的 simpleclient 模块没有依赖,而 simpleclient_servlet 包含 HTTP 部分。
性能考虑
由于客户端库必须是线程安全的,因此需要某种形式的并发控制,并且必须考虑在多核机器和应用程序上的性能。
根据我们的经验,性能最差的是互斥锁。
处理器原子指令通常处于中间水平,并且通常可以接受。
避免不同 CPU 修改同一块 RAM 的方法效果最好,例如 Java simpleclient 中的 DoubleAdder。但这会有内存开销。
如上所述,labels() 的结果应该是可缓存的。支持带标签指标的并发映射通常相对较慢。对不带标签的指标进行特殊处理以避免 labels() 类型的查找会有很大帮助。
指标在被递增/递减/设置等操作时 SHOULD 避免阻塞,因为在抓取进行时阻塞整个应用程序是不可取的。
ENCOURAGED 对主要的检测操作(包括标签)进行基准测试。
在执行暴露时应注意资源消耗,特别是 RAM。考虑通过流式传输结果来减少内存占用,并可能限制并发抓取的数量。