编写客户端库
本文档涵盖了 Prometheus 客户端库应提供的功能和 API,旨在实现跨库的一致性,使简单用例易于实现,并避免提供可能误导用户功能的。
在撰写本文时,已有 10 种语言支持,因此我们对如何编写客户端有了一定的认识。这些指南旨在帮助新客户端库的作者编写出色的库。
约定
此外,MUST/MUST NOT/SHOULD/SHOULD NOT/MAY 的含义同 https://www.ietf.org/rfc/rfc2119.txt 中给出的含义。
此外,ENCOURAGED 表示某个功能是库应具备的,但没有也没关系。换句话说,是“锦上添花”。
需要注意的事项
-
利用每种语言的特性。
-
常见的用例应易于实现。
-
正确做事的方式应该是简单的方式。
-
应能实现更复杂的用例。
常见的用例是(按顺序)
-
在库/应用程序中广泛使用的无标签计数器。
-
在摘要/直方图中对函数/代码块进行计时。
-
用于跟踪事物当前状态(及其限制)的计数器。
-
批量作业的监控。
整体结构
客户端在内部实现上必须是基于回调的。客户端通常应该遵循此处描述的结构。
关键类是 Collector。它有一个方法(通常称为 ‘collect’),该方法返回零个或多个指标及其样本。Collectors 被注册到 CollectorRegistry。数据通过将 CollectorRegistry 传递给一个名为“bridge”的类/方法/函数来暴露,该函数/方法/类会以 Prometheus 支持的格式返回指标。每次 CollectorRegistry 被抓取时,都必须回调到每个 Collector 的 collect 方法。
大多数用户交互的接口是 Counter、Gauge、Summary 和 Histogram Collectors。它们代表单个指标,应该涵盖用户自行检测代码的大部分用例。
更高级的用例(例如从其他监控/检测系统代理)需要编写自定义 Collector。有人可能还想编写一个“bridge”,它接收一个 CollectorRegistry 并生成另一种监控/检测系统理解格式的数据,从而使用户只需考虑一个检测系统。
CollectorRegistry 应该提供 register()/unregister() 函数,并且 Collector 应该允许被注册到多个 CollectorRegistry。
客户端库必须是线程安全的。
对于 C 等非面向对象语言,客户端库应在实际可行的情况下遵循此结构的精髓。
命名
客户端库应遵循本文档中提到的函数/方法/类名,并注意其工作语言的命名约定。例如,对于 Python 中的方法名,set_to_current_time() 是好的,但对于 Go 来说 SetToCurrentTime() 更好,而在 Java 中 setToCurrentTime() 是惯例。当名称因技术原因(例如不允许函数重载)而不同时,文档/帮助字符串应将用户指向其他名称。
库不得提供与此处给出的名称相同或相似但语义不同的函数/方法/类。
指标
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()(对批处理作业很有用)。
Counter(计数器)
Counter 是单调递增的计数器。它不能允许值减小,但可以重置为 0(例如,服务器重启)。
计数器必须具有以下方法
inc(): 将计数器增加 1inc(double v): 将计数器增加给定数量。必须检查 v >= 0。
鼓励计数器拥有
一种统计给定代码段中抛出/引发的异常数量的方法,并且可以选择只统计特定类型的异常。这是 Python 中的 count_exceptions。
计数器必须从 0 开始。
Gauge(仪表盘)
Gauge 代表一个可以上下变动的值。
Gauge 必须具有以下方法
inc(): 将 Gauge 增加 1inc(double v): 将 Gauge 增加给定数量dec(): 将 Gauge 减少 1dec(double v): 将 Gauge 减少给定数量set(double v): 将 Gauge 设置为给定值
Gauge 必须从 0 开始,您可以提供一种方式让给定的 Gauge 从不同的数字开始。
Gauge 应该具有以下方法
set_to_current_time(): 将 Gauge 设置为当前的 unixtime(以秒为单位)。
Gauge 被鼓励拥有
一种统计某个代码段/函数中进行中的请求的方法。这是 Python 中的 track_inprogress。
一种计时代码段并将其持续时间(以秒为单位)设置为 Gauge 的方法。这对于批处理作业很有用。在 Java 中是 startTimer/setDuration,在 Python 中是 time() 装饰器/上下文管理器。这应该匹配 Summary/Histogram 中的模式(尽管是 set() 而不是 observe())。
总结
一个summary 在滑动时间窗口内采样观测值(通常是请求持续时间等),并提供其分布、频率和总和的瞬时洞察。
Summary 不能允许用户将“quantile”作为标签名,因为它在内部用于指定 summary 的分位数。Summary 被鼓励提供分位数作为导出,尽管这些不能被聚合并且往往很慢。Summary 必须允许不带分位数,因为仅有 _count/_sum 非常有用,并且这是默认设置。
Summary 必须具有以下方法
observe(double v): 观测给定数量
Summary 应该具有以下方法
一种以秒为单位为用户计时代码的方法。在 Python 中是 time() 装饰器/上下文管理器。在 Java 中是 startTimer/observeDuration。不得提供除秒以外的单位(如果用户需要其他单位,可以手动实现)。这应该遵循 Gauge/Histogram 的模式。
Summary _count/_sum 必须从 0 开始。
Histogram(直方图)
Histograms 允许对事件进行可聚合的分布,例如请求延迟。这本质上是每个桶的计数器。
Histogram 不能允许 le 作为用户设置的标签,因为 le 在内部用于指定桶。
Histogram 必须提供一种手动选择桶的方法。应该提供以 linear(start, width, count) 和 exponential(start, factor, count) 方式设置桶的方法。Count 必须包含 +Inf 桶。
Histogram 应该具有与其他客户端库相同的默认桶。桶在指标创建后不得更改。
Histogram 必须具有以下方法
observe(double v): 观测给定数量
Histogram 应该具有以下方法
一种以秒为单位为用户计时代码的方法。在 Python 中是 time() 装饰器/上下文管理器。在 Java 中是 startTimer/observeDuration。不得提供除秒以外的单位(如果用户需要其他单位,可以手动实现)。这应该遵循 Gauge/Summary 的模式。
Histogram _count/_sum 和桶必须从 0 开始。
进一步的指标考虑
鼓励在指标中提供超出上述文档范围的功能,只要它适合给定语言。
如果您可以简化一个常见的用例,那么尽管去做吧,只要它不会鼓励不希望行为(例如次优的指标/标签布局,或在客户端进行计算)。
标签
标签是 Prometheus 最强大的方面之一,但也很容易滥用。因此,客户端库在如何向用户提供标签方面必须非常谨慎。
客户端库不应允许用户为 Gauge/Counter/Summary/Histogram 或库提供的任何其他 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 是必需的。
动态/生成的指标名称或指标名称的子部分应被禁止,除非自定义收集器正在代理其他检测/监控系统。生成/动态指标名称是您应该使用标签的标志。
指标描述和帮助
Gauge/Counter/Summary/Histogram 必须要求提供指标描述/帮助。
客户端库中提供的任何自定义收集器都必须在其指标上提供描述/帮助。
建议将其作为强制性参数,但不检查其长度是否符合特定要求,因为如果有人真的不想写文档,我们也不会说服他们。库中提供的收集器(以及在生态系统中我们能做的所有地方)应该有良好的指标描述,以身作则。
暴露
客户端必须实现暴露格式文档中概述的基于文本的暴露格式。
如果能够实现且不造成显著资源成本,则鼓励暴露指标时具有可重现的顺序(尤其是对于人类可读的格式)。
标准和运行时收集器
客户端库应该提供它们所能提供的标准导出,如下所述。
这些应该实现为自定义收集器,并默认注册到默认的 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 | 进程中的 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。考虑通过流式传输结果来减少内存占用,并可能限制并发抓取的数量。