使用 Client Java 构建 Exporter 程序

client_java是 Prometheus 针对JVM类开发语言的 client library 库,我们可以直接基于 client_java 用户可以快速实现独立运行的 Exporter 程序,也可以在我们的项目源码中集成 client_java 以支持 Prometheus。

自定义 Collector

在client_java的simpleclient模块中提供了自定义监控指标的核心接口。

如果使用Gradle作为项目构建工具,可以通过向build.gradle添加simpleclient依赖:

  1. compile 'io.prometheus:simpleclient:0.3.0'

当无法直接修改监控目标时,可以通过自定义Collector的方式,实现对监控样本收集,该收集器需要实现collect()方法并返回一组监控样本,如下所示:

  1. public class YourCustomCollector extends Collector {
  2. public List<MetricFamilySamples> collect() {
  3. List<MetricFamilySamples> mfs = new ArrayList<MetricFamilySamples>();
  4. String metricName = "my_guage_1";
  5. // Your code to get metrics
  6. MetricFamilySamples.Sample sample = new MetricFamilySamples.Sample(metricName, Arrays.asList("l1"), Arrays.asList("v1"), 4);
  7. MetricFamilySamples.Sample sample2 = new MetricFamilySamples.Sample(metricName, Arrays.asList("l1", "l2"), Arrays.asList("v1", "v2"), 3);
  8. MetricFamilySamples samples = new MetricFamilySamples(metricName, Type.GAUGE, "help", Arrays.asList(sample, sample2));
  9. mfs.add(samples);
  10. return mfs;
  11. }
  12. }

这里定义了一个名为my_guage的监控指标,该监控指标的所有样本数据均转换为一个MetricFamilySamples.Sample实例,该实例中包含了该样本的指标名称、标签名数组、标签值数组以及样本数据的值。

监控指标my_guage的所有样本值,需要持久化到一个MetricFamilySamples实例中,MetricFamilySamples指定了当前监控指标的名称、类型、注释信息等。需要注意的是MetricFamilySamples中所有样本的名称必须保持一致,否则生成的数据将无法符合Prometheus的规范。

直接使用MetricFamilySamples.Sample和MetricFamilySamples的方式适用于当某监控指标的样本之间的标签可能不一致的情况,例如,当监控容器时,不同容器实例可能包含一些自定义的标签,如果需要将这些标签反应到样本上,那么每个样本的标签则不可能保持一致。而如果所有样本的是一致的情况下,我们还可以使用client_java针对不同指标类型的实现GaugeMetricFamily,CounterMetricFamily,SummaryMetricFamily等,例如:

  1. class YourCustomCollector2 extends Collector {
  2. List<MetricFamilySamples> collect() {
  3. List<MetricFamilySamples> mfs = new ArrayList<MetricFamilySamples>();
  4. // With no labels.
  5. mfs.add(new GaugeMetricFamily("my_gauge_2", "help", 42));
  6. // With labels
  7. GaugeMetricFamily labeledGauge = new GaugeMetricFamily("my_other_gauge", "help", Arrays.asList("labelname"));
  8. labeledGauge.addMetric(Arrays.asList("foo"), 4);
  9. labeledGauge.addMetric(Arrays.asList("bar"), 5);
  10. mfs.add(labeledGauge);
  11. return mfs;
  12. }
  13. }

使用HTTP Server暴露样本数据

client_java下的simpleclient_httpserver模块实现了一个简单的HTTP服务器,当向该服务器发送获取样本数据的请求后,它会自动调用所有Collector的collect()方法,并将所有样本数据转换为Prometheus要求的数据输出格式规范。如果用户使用了Gradle构建项目,可以添加以下依赖:

  1. compile 'io.prometheus:simpleclient_httpserver:0.3.0'

添加依赖之后,就可以在Exporter程序的main方法中启动一个HTTPServer实例:

  1. public class CustomExporter {
  2. public static void main(String[] args) throws IOException {
  3. HTTPServer server = new HTTPServer(1234);
  4. }
  5. }

而在启动之前,别忘记调用Collector的register()方法。否则HTTPServer是找不到任何的Collector实例的:

  1. new YourCustomCollector().register();
  2. new YourCustomCollector2().register();

运行CustomExporter并访问 http://127.0.0.1:1234/metrics ,即可获取到以下数据:

  1. $ curl http://127.0.0.1:1234/metrics
  2. # HELP my_gauge help
  3. # TYPE my_gauge gauge
  4. my_gauge 42.0
  5. # HELP my_other_gauge help
  6. # TYPE my_other_gauge gauge
  7. my_other_gauge{labelname="foo",} 4.0
  8. my_other_gauge{labelname="bar",} 5.0
  9. # HELP my_guage help
  10. # TYPE my_guage gauge
  11. my_guage{l1="v1",} 4.0
  12. my_guage{l1="v1",l2="v2",} 3.0

当然 HTTPServer 中并不存在什么黑魔法,其内部实现如下所示:

HTTPServer处理流程

HTTPServer 处理流程

当调用 Collector 实例 register() 方法时,会将该实例保存到 CollectorRegistry 当中,CollectorRegistry 负责维护当前系统中所有的 Collector 实例。 HTTPServer 在接收到 HTTP 请求之后,会从 CollectorRegistry 中拿到所有的 Collector 实例,并调用其 collect() 方法获取所有样本,最后格式化为 Prometheus 的标准输出。

除了直接使用 HTTPServer 以外暴露样本数据以外,client_java 中还提供了对 Spring Boot、Spring Web 以及 Servlet 的支持。

使用内置的 Collector

通过 client_java 中定义的标准接口,用户可以快速实现自己的监控数据收集器,并通过 HTTPServer 将样本数据输出给 Prometheus。除了提供接口规范以外,client_java 还提供了多个内置的 Collector 模块,以simpleclient_hotspot 为例,该模块中内置了对JVM虚拟机运行状态(GC,内存池,JMX,类加载,线程池等)数据的 Collector 实现,用户可以通过在Gradle中添加以下依赖,导入 simpleclient_hotspot:

  1. compile 'io.prometheus:simpleclient_hotspot:0.3.0'

通过调用 io.prometheus.client.hotspot.DefaultExport 的 initialize 方法注册该模块中所有的Collector 实例:

  1. DefaultExports.initialize();

重新运行 CustomExporter,并获取样本数据:

  1. $ curl http://127.0.0.1:1234/metrics
  2. # HELP jvm_buffer_pool_used_bytes Used bytes of a given JVM buffer pool.
  3. # TYPE jvm_buffer_pool_used_bytes gauge
  4. jvm_buffer_pool_used_bytes{pool="direct",} 8192.0
  5. jvm_buffer_pool_used_bytes{pool="mapped",} 0.0

除了之前自定义的监控指标以外,在响应内容中还会得到当前JVM的运行状态数据。在 client_java 项目中除了使用内置了对 JVM 监控的 Collector 以外,还实现了对 Hibernate,Guava Cache,Jetty,Log4j、Logback 等监控数据收集的支持。用户只需要添加相应的依赖,就可以直接进行使用。

在业务代码中进行监控埋点

在 client_java 中除了使用 Collector 直接采集样本数据以外,还直接提供了对Prometheus中4种监控类型的实现分别是:Counter、Gauge、Summary和Histogram。 基于这些实现,开发人员可以非常方便的在应用程序的业务流程中进行监控埋点。

简单类型 Gauge 和 Counter

以Gauge为例,当我们需要监控某个业务当前正在处理的请求数量,可以使用以下方式实现:

  1. public class YourClass {
  2. static final Gauge inprogressRequests = Gauge.build()
  3. .name("inprogress_requests").help("Inprogress requests.").register();
  4. void processRequest() {
  5. inprogressRequests.inc();
  6. // Your code here.
  7. inprogressRequests.dec();
  8. }
  9. }

Gauge继承自Collector,registoer()方法会将该Gauge实例注册到CollectorRegistry中。这里创建了一个名为inprogress_requests的监控指标,其注释信息为”Inprogress requests”。

Gauge对象主要包含两个方法inc()和dec(),分别用于计数器+1和-1。

如果监控指标中还需要定义标签,则可以使用Gauge构造器的labelNames()方法,声明监控指标的标签,同时在样本计数时,通过指标的labels()方法指定标签的值,如下所示:

  1. public class YourClass {
  2. static final Gauge inprogressRequests = Gauge.build()
  3. .name("inprogress_requests")
  4. .labelNames("method")
  5. .help("Inprogress requests.").register();
  6. void processRequest() {
  7. inprogressRequests.labels("get").inc();
  8. // Your code here.
  9. inprogressRequests.labels("get").dec();
  10. }
  11. }

Counter与Gauge的使用方法一致,唯一的区别在于Counter实例只包含一个inc()方法,用于计数器+1。

复杂类型Summary和Histogram

Summary和Histogram用于统计和分析样本的分布情况。如下所示,通过Summary可以将HTTP请求的字节数以及请求处理时间作为统计样本,直接统计其样本的分布情况。

  1. class YourClass {
  2. static final Summary receivedBytes = Summary.build()
  3. .name("requests_size_bytes").help("Request size in bytes.").register();
  4. static final Summary requestLatency = Summary.build()
  5. .name("requests_latency_seconds").help("Request latency in seconds.").register();
  6. void processRequest(Request req) {
  7. Summary.Timer requestTimer = requestLatency.startTimer();
  8. try {
  9. // Your code here.
  10. } finally {
  11. receivedBytes.observe(req.size());
  12. requestTimer.observeDuration();
  13. }
  14. }
  15. }

除了使用Timer进行计时以外,Summary实例也提供了timer()方法,可以对线程或者Lamda表达式运行时间进行统计:

  1. class YourClass {
  2. static final Summary requestLatency = Summary.build()
  3. .name("requests_latency_seconds").help("Request latency in seconds.").register();
  4. void processRequest(Request req) {
  5. requestLatency.timer(new Runnable() {
  6. public abstract void run() {
  7. // Your code here.
  8. }
  9. });
  10. // Or the Java 8 lambda equivalent
  11. requestLatency.timer(() -> {
  12. // Your code here.
  13. });
  14. }
  15. }

Summary和Histogram的用法基本保持一致,区别在于Summary可以指定在客户端统计的分位数,如下所示:

  1. static final Summary requestLatency = Summary.build()
  2. .quantile(0.5, 0.05) // 其中0.05为误差
  3. .quantile(0.9, 0.01) // 其中0.01为误差
  4. .name("requests_latency_seconds").help("Request latency in seconds.").register();

对于Histogram而言,默认的分布桶为[.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10],如果需要指定自定义的桶分布,可以使用buckets()方法指定,如下所示:

  1. static final Histogram requestLatency = Histogram.build()
  2. .name("requests_latency_seconds").help("Request latency in seconds.")
  3. .buckets(0.1, 0.2, 0.4, 0.8)
  4. .register();

与 PushGateway 集成

对于一些短周期或者临时采集的样本数据,client_java 还提供了对 PushGateway 的支持:

添加依赖:

  1. compile 'io.prometheus:simpleclient_pushgateway:0.3.0'

如下所示,PushGateway的实现类可以从所有注册到defaultRegistry的Collector实例中获取样本数据并直接推送 到外部部署的PushGateway服务中。

  1. public class PushGatewayIntegration {
  2. public void push() throws IOException {
  3. CollectorRegistry registry = CollectorRegistry.defaultRegistry;
  4. PushGateway pg = new PushGateway("127.0.0.1:9091");
  5. pg.pushAdd(registry, "my_batch_job");
  6. }
  7. }