## 编写客户端库
---
这篇文档包括Prometheus客户端API应该提供的基础功能,目的是在客户端库之间保持一致性,轻松上手并避免提供导致用户出错的功能。
已经有10种客户端语言支持Prometheus客户端了,因此我们知道怎么写好一个客户端。这个指南旨在帮助写Prometheus客户端其他语言的作者写一个好的库。
### Conventions约定
MUST/MUST NOT/SHOULD/SHOULD NOT/MAY在[https://www.ietf.org/rfc/rfc2119.txt](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客户端,我们可以这样写:
```Java
class YourClass {
static final Counter requests = Counter.build()
.name("requests_total")
.help("Requests.").register();
}
```
上面的例子,使用默认的`CollectorRegistry`进行注册。如果只是调用build()方法, 度量指标将不会被注册(对于单元测试来说很方便),你还可以将`CollectorRegistry`传递给register()(方便批作业处理)。
#### Counter
`Counter`[https://prometheus.io/docs/concepts/metric_types/#counter]是一个单调递增的计数器。它不允许counter值下降,但是它可以被重置为0(例如:客户端服务重启)。
一个counter必须有以下方法:
- `inc()`: 增量为1.
- `inc(double v)`: 增加给定值v。必须检查v>=0。
Counter在给定代码段抛出/引发异常的方式,也可以只选择某些类型的一场,这是Python中的count_exceptions。
Counters必须从0开始。
#### Gauge
[Gauge](https://prometheus.io/docs/concepts/metric_types/#gauge)表示一个可以上下波动的值。
gauge必须有以下的方法:
- `inc()`: 每次增加1
- `inc(double v)`: 每次增加给定值v
- `dec()`: 每次减少1
- `dec(double v)`: 每次减少给定值v
- `set(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](https://prometheus.io/docs/concepts/metric_types/#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](https://prometheus.io/docs/concepts/metric_types/#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创建时间可选地指定标签名称列表。客户端库应该支持任意大小的标签列表。客户端库必须验证标签名称是否符合[要求](https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels)。
提供访问度量指标名称列表最常用的方法, 是通过`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,帮助理解其含义.
### 导出
客户端必须实现一个文档[导出格式](https://prometheus.io/docs/instrumenting/exposition_formats)。
客户端可以实现多种导出格式。而且是可读性非常好的格式。
如果有疑问,请去文本格式。它不具有依赖性(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传输结果来减少内存占用,并且可能对并发获取的数量有限制。