编写客户端库


这篇文档包括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个度量指标和这些指标的样本数据。CollectorCollectorRegistry进行注册。通过传递CollectorRegistry给称之为bridge的class/method/function来暴露数据。该bridge返回Prometheus支持的数据格式数据。每次这个CollectorRegistry被收集时,都必须回调Collector的collect方法。

和用户交互最多的接口是Counter, Gauge, SummaryHistogram 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

CounterGaugeSummaryHistogram度量指标类型是最主要的接口。

CounterGauge必须是客户库的一部分。SummaryHistogram至少被提供一个。

这些主要用作文件静态变量,也就是说,全局变量与他们正在调试的代码在同一个文件中定义。客户端库应该启用此功能。常见的用例是整体测试一段代码,而不是在对象的一个实例上下文中的一段代码。用户不必担心在他们的代码中管理他们的指标,客户端库应该为他们做到这一点(如果不这样做,用户将会围绕库写一个wrapper, 使其更容易,少即是多)。

必须有一个默认的CollectorRegistry, 四种度量指标类型必须在不需要用户任何干预下,就能完成默认被注册,同时也提供一种别的注册方法,用于批处理作业和单元测试。自定义的Collectors也应该遵循这点。

究竟应该如何创建度量指标因语言而异。对于某些语言(Go,Java),builder是最好的,对于其他(Python)函数参数足够丰富,可以在一个调用中执行。

例如,一个简单的Java客户端,我们可以这样写:

  1. class YourClass {
  2. static final Counter requests = Counter.build()
  3. .name("requests_total")
  4. .help("Requests.").register();
  5. }

上面的例子,使用默认的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表示一个可以上下波动的值。

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/上下文管理器。这应该符合在SummaryHistogram中的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传输结果来减少内存占用,并且可能对并发获取的数量有限制。