编写客户端库
这篇文档包括Prometheus客户端API应该提供的基础功能,目的是在客户端库之间保持一致性,轻松上手并避免提供导致用户出错的功能。
已经有10种客户端语言支持Prometheus客户端了,因此我们知道怎么写好一个客户端。这个指南旨在帮助写Prometheus客户端其他语言的作者写一个好的库。
Conventions约定
MUST/MUST NOT/SHOULD/SHOULD NOT/MAY在https://www.ietf.org/rfc/rfc2119.txt
另一个ENCOURAGE的含义是,一个特性对于一个库是非常好的,但是如果关闭这个特性的话,不会影响库的使用。
记住下面的几点:
- 记住每个特性的好处。
- 常用用例应该很简单
- 做事情正确方式是简单的方法
- 更复杂的例子应该是可能的
常用用例(有序):
- 没有标签的Counters在库或者应用程序之间传播
- Summaries/Histograms的时序功能/代码块
- Gauges跟踪事情的当前状态
- 批量任务监控
总体结构
客户端必须
在内部写入回调。客户通常应该
遵循下面描述的结构。
这个关键类是Collector
。这个有一个典型的方法collect
, 返回0~N个度量指标和这些指标的样本数据。Collector
用CollectorRegistry
进行注册。通过传递CollectorRegistry
给称之为bridge
的class/method/function来暴露数据。该bridge
返回Prometheus支持的数据格式数据。每次这个CollectorRegistry
被收集时,都必须回调Collector
的collect方法。
和用户交互最多的接口是Counter
, Gauge
, Summary
和Histogram Collectors
。这些表示单个度量指标,写的代码覆盖绝大多数的用例。
更高级的用例(例如来自其他监控/检测系统的代理)需要编写一个自定义Collector
收集器。有人也可能像写一个带有CollectorRegistry
的"bridge",以不同的监控/测量系统理解的格式生产数据, 允许用户只需要考虑一个测量系统。
CollectorRegistry
应该提供register()/unregister()
方法,以及一个Collector
应该注册多个CollectorRegistrys
客户库必须是线程安全的。
对于非面向对象的客户端,如:C语言,客户库编写在实践中应该遵循这种结构的理念。
命名
客户库应该遵循function/method/class
在这个文档中提及的命名规则,记住他们正在使用的语言命名规范。例如:set_to_current_time()
对于python而言是非常好的方法名称,SetToCurrentTime
对于Go语言是更好的,setToCurrentTime()
对于Java是更好的。由于技术原因(例如:不允许功能重载),名称不能,文档/帮助文档应该将用户指向其他名称。
库禁止提供与此处给出的相同或者相似functions/methods/classes
,但具有不同的语义。
Metrics
Counter
、Gauge
、Summary
和Histogram
度量指标类型是最主要的接口。
Counter
和Gauge
必须是客户库的一部分。Summary
和Histogram
至少被提供一个。
这些主要用作文件静态变量,也就是说,全局变量与他们正在调试的代码在同一个文件中定义。客户端库应该启用此功能。常见的用例是整体测试一段代码,而不是在对象的一个实例上下文中的一段代码。用户不必担心在他们的代码中管理他们的指标,客户端库应该为他们做到这一点(如果不这样做,用户将会围绕库写一个wrapper
, 使其更容易,少即是多)。
必须有一个默认的CollectorRegistry
, 四种度量指标类型必须在不需要用户任何干预下,就能完成默认被注册,同时也提供一种别的注册方法,用于批处理作业和单元测试。自定义的Collectors
也应该遵循这点。
究竟应该如何创建度量指标因语言而异。对于某些语言(Go,Java),builder是最好的,对于其他(Python)函数参数足够丰富,可以在一个调用中执行。
例如,一个简单的Java客户端,我们可以这样写:
class YourClass {
static final Counter requests = Counter.build()
.name("requests_total")
.help("Requests.").register();
}
上面的例子,使用默认的CollectorRegistry
进行注册。如果只是调用build()方法, 度量指标将不会被注册(对于单元测试来说很方便),你还可以将CollectorRegistry
传递给register()(方便批作业处理)。
Counter
Counter是一个单调递增的计数器。它不允许counter值下降,但是它可以被重置为0(例如:客户端服务重启)。
一个counter必须有以下方法:
inc()
: 增量为1.inc(double v)
: 增加给定值v。必须检查v>=0。
Counter在给定代码段抛出/引发异常的方式,也可以只选择某些类型的一场,这是Python中的count_exceptions。
Counters必须从0开始。
Gauge
Gauge表示一个可以上下波动的值。
gauge必须有以下的方法:
inc()
: 每次增加1inc(double v)
: 每次增加给定值vdec()
: 每次减少1dec(double v)
: 每次减少给定值vset(double v)
: 设置gauge值成v
Gauges值必须从0开始,你可以提供一个从不等于0的值开始。
gauge应该有以下方法:
set_to_current_time()
: 将gauge设置为当前的unix时间(以秒为单位)。
gauge被建议有:
- 一种在某些代码/方法中跟踪正在进行的请求方法。这是python种的
track_inprogress
。
执行一段代码,设置gauge类型数据样本值为这段代码执行的时间,这对于批量任务是非常有用的。在Java中是startTimer/setDuration
, 在python中是time()
decorator/上下文管理器。这应该符合在Summary
和Histogram
中的pattern(通过set()
而不是observe()
)。
Summary
summary通过时间滑动窗口抽样观察(通常是要求持续时间),并提供对其分布、频率和总和的即时观察。
summary不允许用户设置"quantile"作为一个标签,因为这个名称已在内部使用,用来指定分位数。summary鼓励提供“quantile”导出,虽然这些不能被汇总,而且需要大量时间。summary必须允许没有quantiles,因为只有_count/_sum
是飞铲更拥有的,这必须是默认值。
summary必须有以下方法:
observe(double v)
: 观察被给定值
summary应该有以下方法:
- 统计用户执行代码的时间,以秒为单位。在python中,这是
time()
decorateor/context管理器。在Java中这是startTimer/observeDuration
。 不能提供秒意外的单位(如果用户想要别的,自己手动做)。这应该遵循Gauge/Histogram相同的模式。
Summary _count/_sum
必须从0开始。
Histogram
Histogram允许时间的可聚合分布,如:请求延迟。每个bucket中都会有一个count值, 表示累加的样本数量值
一个histogram直方图不允许使用le
作为一个标签,它已经内部用于在分bucket时的步长大小。
直方图必须提供一个方法来手动选择buckets。应该提供一linear(start, width, count)和exponential(start,factor, count)方式设置buckets的方法。参数count值必须是有界的
直方图应该具有与其他客户端库相同的默认值,创建度量指标后bucket不能再更改。
一个直方图必须有下面的方法:
observe(double v)
: 观察给定值
直方图应该有以下的方法:
统计代码执行时间的一些方法,以秒为单位。在Python中是time()
decorator/context管理器。在Java中是startTimer/observeDuration
。不提供秒以外的单位(如果用户需要别的,可以手动做)。这应该遵循与Gauge/Summary相同的模式。
直方图_count/_sum
和buckets必须从0开始。
进一步的度量指标考量
提供额外的功能,超出以上记录的指标,对于给定的语言是有意义的
如果有一个常见用例,例如:次优度量指标/标签布局或者在客户端进行计算,可以使其更简单。
标签
标签Labels是Prometheus系统最强大的特性之一,但是很容易被滥用。因此,客户端库必须非常小心地如何向用户提供labels。
客户库在任何情况下禁止用户对于"Gauge/counter/summary/histogram"或者由库提供的其他Collector的度量指标,提供不相同的标签列表。
如果你的客户库在收集样本数据时间内对其进行了度量指标的验证,那么它也可以为自定义Collector进行验证。
虽然标签功能很强大,但大多数度量指标没有标签。因此,API允许有标签,但不是强制的。
客户库必须允许在Gauge/Counter/Summary/Histogram创建时间可选地指定标签名称列表。客户端库应该支持任意大小的标签列表。客户端库必须验证标签名称是否符合要求。
提供访问度量指标名称列表最常用的方法, 是通过labels()
方法,该方法可以获取标签值列表,或者获取Map键值对(标签名称:标签值)列表,并返回“child”,然后在Child上调用常用的.inc()/.desc()/.observe()
等方法。
label()
返回Child应该由用户缓存,以避免再次查找,这在延迟至关重要的代码中很重要。
带有标签的度量指标应该支持一个具有与labels()
相同签名的remove()
方法,它将从不再导出它的度量标准中删除一个Child,另一个clear()方法可以从度量指标中删除所有的Child
。
应该有一种使用默认初始化给定Child的方法,通常只需要调用labels()。没有标签的度量指标必须被初始化,已避免缺少度量指标的问题。
度量指标名称
度量指标名称补习遵循规范。与标签名称一样,必须满足使用Counter/Gauge/Summary/Histogram
和库中提供的任意其他Collector
的使用。
许多客户库提供三个部分的名称:namespace_subsystem_name
, 其中只有该name
是强制性的。
不鼓励使用动态/自动生成的度量指标名称或者其子部分,除非自定义"Collector"是从其他工具/监控系统代理的。你可以使用标签名称替代动态或者自动生成的度量指标名称。
度量指标描述和帮助
Gauge/Counter/Summary/Histogram
要求必须提供度量指标的desc和help。
带有自定义Collector的客户库,在度量指标上必须有desc/help
建议将度量指标名称的desc/help作为强制性参数,但不需要检查其长度,提供Collectors的库应该要有一个比较好的desc,帮助理解其含不需要检查其长度,提供Collectors的库应该要有一个比较好的desc,帮助理解其含义.
导出
客户端必须实现一个文档导出格式。
客户端可以实现多种导出格式。而且是可读性非常好的格式。
如果有疑问,请去文本格式。它不具有依赖性(protobuf),往往易于生成,是可读取的,并且protobuf的性能优势对于大多数用例来说并不重要。
如果可以在没有显著的资源成本情况下实现,可以重现可用的度量指标顺序(特别是对于人类可读格式)。
标准化和运行时收集器
客户端库应该提供标准导出,如下所述:
这些应该作为自定义Collectors实现,默认情况下在默认的CollectorRegistry上注册。应该有一种方法来禁用这些,因为有一些非常适用于他们的使用方式。
处理度量指标
这些导出应该有前缀process_。如果一种语言或者运行时没有公开其中一个变量,它不会被导出它。所有内存值以字节为单位,以时间戳/秒为单位。
度量指标名称 | 含义 | 单位 |
---|---|---|
process_cpu_seconds_total | 用户和系统CPU花费的时间 | 秒 |
process_open_fds | 打开的文件描述符数量 | 文件描述符 |
process_max_fds | 打开描述符最大值 | 文件描述符 |
process_virtual_memory_bytes | 虚拟内存大小 | 字节 |
process_resident_memory_bytes | 驻留内存大小 | 字节 |
process_heap_bytes | 进程head堆大小 | 字节 |
process_start_time_seconds | unix时间 | 秒 |
运行时度量指标
另外,客户端库也被提供给他们的语言运行时(如:垃圾回收统计信息)的指标方面,提供了一些合适的前缀,比如: go_, hostspot_等。
单元测试
客户端库应该包含核心工具库和展示的单元测试。
客户端库被鼓励提供方便用户单元测试其使用的工具代码。例如,python中的CollectorRegistry.get_sample_value。
包和依赖
理想情况下,客户端库可以包含在任何应用程序中以添加一些工具,而无需担心它会破坏应用程序。
因此,当向客户端添加依赖关系时,建议谨慎。例如:如果用户添加使用添加版本1.4的Protobuf的Prometheus客户端库,但是应用程序在其他地方使用1.2,会发生什么?
建议在可能出现的情况下,将核心工具和给定格式的度量指标/展示分开。例如:Java简单客户端模块没有依赖关系,而simpleclient_servlet具有Http比特位。
性能考虑
由于客户端库必须是线程安全的,因此需要进行某种形式的并发控制,并且必须考虑多核机器和应用程序的性能。
在我们的经验中,性能最差的是互斥体。
处理器原子指令往往处于中间,并且通常可以接受。
避免不同CPU突然使用RAM的方法效果最好,例如:Java简单客户端中的DoubleAdder。有内存成本。
如上所述,labels()的结果应该是可缓存的。趋向于使用标签返回度量的并发映射往往相对较慢。没有标签的特殊套管指标,已避免labels(),像查找可以帮助很多。
度量指标应该避免阻塞,当它们递增/递减/设置等时,因为整个应用程序在持续获取时不会被组织。
主要工具操作的基准(包括labels)得到了鼓励。
应该牢记资源消耗,特别是RAM。考虑通过stream传输结果来减少内存占用,并且可能对并发获取的数量有限制。