编写客户端库
本文档涵盖了 Prometheus 客户端库应提供的功能和 API,旨在实现库之间的一致性,简化常见用例,并避免提供可能误导用户的功能。
在撰写本文时,已有 10 种语言得到支持,因此我们对如何编写客户端有了一个很好的理解。这些指南旨在帮助新客户端库的作者开发出优秀的库。
约定
MUST/MUST NOT/SHOULD/SHOULD NOT/MAY 的含义如 https://www.ietf.org/rfc/rfc2119.txt 中所述
此外,ENCOURAGED 表示某个特性对于库来说是可取的,但即使不存在也无妨。换句话说,这是一个锦上添花的功能。
注意事项
-
利用每种语言的特性。
-
常见用例应易于实现。
-
正确的方法应该也是简单的方法。
-
更复杂的用例应可实现。
常见用例(按顺序列出)
-
无标签计数器在库/应用程序中广泛使用。
-
在摘要/直方图中对函数/代码块进行计时。
-
使用 Gauge 跟踪事物的当前状态(及其限制)。
-
监控批处理作业。
整体结构
客户端内部 MUST(必须)基于回调。客户端 SHOULD(应该)通常遵循此处描述的结构。
关键类是 Collector。它有一个方法(通常称为 'collect'),返回零个或多个指标及其样本。Collector 会注册到 CollectorRegistry。数据通过将 CollectorRegistry 传递给一个类/方法/函数“桥接器”来暴露,该桥接器以 Prometheus 支持的格式返回指标。每次抓取 CollectorRegistry 时,它都必须回调每个 Collector 的 collect 方法。
大多数用户交互的接口是 Counter、Gauge、Summary 和 Histogram 收集器。它们代表单个指标,应涵盖用户在自己的代码中进行埋点的大多数用例。
更高级的用例(例如从另一个监控/埋点系统进行代理)需要编写自定义 Collector。用户可能还希望编写一个“桥接器”,它接受一个 CollectorRegistry 并以不同监控/埋点系统可理解的格式生成数据,从而允许用户只需考虑一个埋点系统。
CollectorRegistry SHOULD(应该)提供 register()
/unregister()
函数,并且 Collector SHOULD(应该)允许注册到多个 CollectorRegistry。
客户端库 MUST(必须)是线程安全的。
对于 C 等非面向对象语言,客户端库应尽可能遵循这种结构的精神。
命名
客户端库 SHOULD(应该)遵循本文档中提及的函数/方法/类名称,同时考虑到其所用语言的命名约定。例如,set_to_current_time()
在 Python 中是一个好的方法名,但 SetToCurrentTime()
在 Go 中更好,而 setToCurrentTime()
是 Java 中的约定。如果名称因技术原因(例如不允许函数重载)而不同,文档/帮助字符串 SHOULD(应该)将用户指向其他名称。
库 MUST NOT(绝不能)提供与此处给出的名称相同或相似但语义不同的函数/方法/类。
指标
计数器、Gauge、Summary 和 Histogram 指标类型 是用户主要使用的接口。
Counter 和 Gauge MUST(必须)是客户端库的一部分。Summary 和 Histogram 至少提供其中一个 MUST(必须)。
这些主要应作为文件静态变量使用,即在与埋点代码相同的文件中定义的全局变量。客户端库 SHOULD(应该)支持这一点。常见用例是对整个代码进行埋点,而不是在对象实例的上下文中对代码进行埋点。用户不应该担心在代码中传递指标,客户端库应该为他们完成这项工作(如果它不这样做,用户会围绕该库编写一个包装器使其“更简单”——这很少会进展顺利)。
MUST(必须)存在一个默认的 CollectorRegistry,标准指标 MUST(必须)默认隐式注册到其中,无需用户进行特殊操作。MUST(必须)有一种方法使指标不注册到默认的 CollectorRegistry,以用于批处理作业和单元测试。自定义收集器 SHOULD(应该)也应遵循此规则。
指标的确切创建方式因语言而异。对于某些语言(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) 是一个单调递增的计数器。它 MUST NOT(绝不能)允许值减小,但 MAY(可以)重置为 0(例如通过服务器重启)。
计数器 MUST(必须)具有以下方法
inc()
: 将计数器增加 1inc(double v)
: 将计数器增加给定值。MUST(必须)检查 v >= 0。
计数器 ENCOURAGED(鼓励)具有
一种计算给定代码块中抛出/引发的异常的方法,并且可选地只计算特定类型的异常。这在 Python 中是 count_exceptions。
计数器 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) 对滑动时间窗口内的观测值(通常是请求持续时间等)进行采样,并提供对其分布、频率和总和的即时洞察。
摘要 MUST NOT(绝不能)允许用户将“quantile”设置为标签名称,因为这在内部用于指定摘要分位数。摘要 ENCOURAGED(鼓励)提供分位数作为导出,尽管这些分位数无法聚合且往往速度较慢。摘要 MUST(必须)允许不包含分位数,因为仅 _count
/_sum
已经非常有用,并且这 MUST(必须)是默认设置。
摘要 MUST(必须)具有以下方法
observe(double v)
: 观察给定值
摘要 SHOULD(应该)具有以下方法
某种以秒为单位为用户计时代码的方法。在 Python 中,这是 time()
装饰器/上下文管理器。在 Java 中,这是 startTimer/observeDuration。MUST NOT(绝不能)提供除秒以外的单位(如果用户想要其他单位,可以手动实现)。这应遵循与 Gauge/Histogram 相同的模式。
Summary 的 _count
/_sum
MUST(必须)从 0 开始。
Histogram (直方图)
直方图 (Histograms) 允许对事件(例如请求延迟)进行可聚合的分布。其核心是每个桶一个计数器。
直方图 MUST NOT(绝不能)允许用户将 le
作为标签,因为 le
在内部用于指定桶。
直方图 MUST(必须)提供手动选择桶的方式。SHOULD(应该)提供以 linear(start, width, count)
和 exponential(start, factor, count)
方式设置桶的方法。计数 MUST(必须)包含 +Inf
桶。
直方图 SHOULD(应该)具有与其他客户端库相同的默认桶。一旦指标创建,桶 MUST NOT(绝不能)可更改。
直方图 MUST(必须)具有以下方法
observe(double v)
: 观察给定值
直方图 SHOULD(应该)具有以下方法
某种以秒为单位为用户计时代码的方法。在 Python 中,这是 time()
装饰器/上下文管理器。在 Java 中,这是 startTimer
/observeDuration
。MUST NOT(绝不能)提供除秒以外的单位(如果用户想要其他单位,可以手动实现)。这应遵循与 Gauge/Summary 相同的模式。
Histogram 的 _count
/_sum
和桶 MUST(必须)从 0 开始。
进一步的指标考量
ENCOURAGED(鼓励)在指标中提供超出上述文档的额外功能,只要这对于给定语言是合理的。
如果有可以简化的常见用例,那就去实现它,只要它不会鼓励不良行为(例如次优的指标/标签布局,或在客户端进行计算)。
标签
标签是 Prometheus 最强大的方面之一,但也容易被滥用。因此,客户端库在向用户提供标签时必须非常谨慎。
客户端库 MUST NOT(绝不能)允许用户为 Gauge/Counter/Summary/Histogram 或库提供的任何其他 Collector 的同一指标使用不同的标签名称。
自定义收集器的指标几乎总是应该具有一致的标签名称。由于在极少数情况下存在有效的不一致用例,客户端库不应验证这一点。
虽然标签功能强大,但大多数指标将不带有标签。因此,API 应该允许使用标签,但不能被标签支配。
客户端库 MUST(必须)允许在创建 Gauge/Counter/Summary/Histogram 时可选地指定标签名称列表。客户端库 SHOULD(应该)支持任意数量的标签名称。客户端库 MUST(必须)验证标签名称是否符合文档要求。
提供对指标带标签维度访问的通用方法是通过 labels()
方法,该方法接受标签值列表或从标签名称到标签值的映射,并返回一个“子对象 (Child)”。然后可以在该子对象上调用常用的 .inc()
/.dec()
/.observe()
等方法。
labels()
返回的子对象 (Child) SHOULD(应该)可由用户缓存,以避免再次查找——这在对延迟敏感的代码中很重要。
带有标签的指标 SHOULD(应该)支持一个与 labels()
签名相同的 remove()
方法,该方法将从指标中移除不再导出的子对象 (Child),以及一个 clear()
方法,该方法将从指标中移除所有子对象 (Children)。这些操作会使子对象的缓存失效。
SHOULD(应该)有一种方法可以使用默认值初始化给定的子对象 (Child),通常只需调用 labels()
。不带标签的指标 MUST(必须)始终被初始化,以避免缺少指标的问题。
指标名称
指标名称必须遵循规范。与标签名称一样,这对于使用 Gauge/Counter/Summary/Histogram 和库提供的任何其他 Collector 都 MUST(必须)满足。
许多客户端库提供分三部分设置名称:namespace_subsystem_name
,其中只有 name
是强制性的。
MUST NOT(绝不能)鼓励使用动态/生成的指标名称或指标名称的子部分,除非自定义 Collector 正在从其他埋点/监控系统进行代理。生成/动态的指标名称表明您应该改用标签。
指标描述和帮助
Gauge/Counter/Summary/Histogram MUST(必须)要求提供指标描述/帮助。
客户端库提供的任何自定义收集器 MUST(必须)具有其指标的描述/帮助。
建议将其设为强制参数,但不要检查其是否达到一定长度,因为如果有人真的不想编写文档,我们也不会说服他们。库中提供的收集器(以及生态系统中所有可能的地方)SHOULD(应该)具有良好的指标描述,以身作则。
暴露
客户端 MUST(必须)实现暴露格式文档中概述的基于文本的暴露格式。
如果可以在不产生显著资源成本的情况下实现,ENCOURAGED(鼓励)暴露指标的可重现顺序(特别是对于人类可读的格式)。
标准和运行时收集器
客户端库 SHOULD(应该)提供以下文档中列出的标准导出。
这些 SHOULD(应该)作为自定义收集器实现,并默认注册到默认的 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 修改相同内存位的方法效果最好,例如 Java simpleclient 中的 DoubleAdder。不过这会带来内存开销。
如上所述,labels()
的结果应该是可缓存的。支持带标签指标的并发映射通常相对较慢。对不带标签的指标进行特殊处理以避免类似 labels()
的查找可以帮助很多。
指标 SHOULD(应该)避免在递增/递减/设置等操作时阻塞,因为在抓取进行时整个应用程序被阻塞是不希望的。
ENCOURAGED(鼓励)对主要的埋点操作(包括标签)进行基准测试。
在执行指标暴露时,应注意资源消耗,特别是 RAM。考虑通过流式传输结果来减少内存占用,并可能限制并发抓取的数量。