使用Java自定义Exporter 本小节将带领读者了解Promrtheus提供的client_java的基本用法,并且在最后在Spring Boot应用程序中使用client_java,直接在应用程序层面提供对Prometheus的支持。
使用Client Java构建Exporter程序 client_java是Prometheus针对JVM类开发语言的client library库,我们可以直接基于client_java用户可以快速实现独立运行的Exporter程序,也可以在我们的项目源码中集成client_java以支持Prometheus。
自定义Collector 在client_java的simpleclient模块中提供了自定义监控指标的核心接口。
如果使用Gradle作为项目构建工具,可以通过向build.gradle添加simpleclient依赖:
compile 'io.prometheus:simpleclient:0.3.0'
当无法直接修改监控目标时,可以通过自定义Collector的方式,实现对监控样本收集,该收集器需要实现collect()方法并返回一组监控样本,如下所示:
public class YourCustomCollector extends Collector { public List<MetricFamilySamples> collect () { List<MetricFamilySamples> mfs = new ArrayList <MetricFamilySamples>(); String metricName = "my_guage_1" ; MetricFamilySamples.Sample sample = new MetricFamilySamples .Sample(metricName, Arrays.asList("l1" ), Arrays.asList("v1" ), 4 ); MetricFamilySamples.Sample sample2 = new MetricFamilySamples .Sample(metricName, Arrays.asList("l1" , "l2" ), Arrays.asList("v1" , "v2" ), 3 ); MetricFamilySamples samples = new MetricFamilySamples (metricName, Type.GAUGE, "help" , Arrays.asList(sample, sample2)); mfs.add(samples); return mfs; } }
这里定义了一个名为my_guage的监控指标,该监控指标的所有样本数据均转换为一个MetricFamilySamples.Sample实例,该实例中包含了该样本的指标名称、标签名数组、标签值数组以及样本数据的值。
监控指标my_guage的所有样本值,需要持久化到一个MetricFamilySamples实例中,MetricFamilySamples指定了当前监控指标的名称、类型、注释信息等。需要注意的是MetricFamilySamples中所有样本的名称必须保持一致,否则生成的数据将无法符合Prometheus的规范。
直接使用MetricFamilySamples.Sample和MetricFamilySamples的方式适用于当某监控指标的样本之间的标签可能不一致的情况,例如,当监控容器时,不同容器实例可能包含一些自定义的标签,如果需要将这些标签反应到样本上,那么每个样本的标签则不可能保持一致。而如果所有样本的是一致的情况下,我们还可以使用client_java针对不同指标类型的实现GaugeMetricFamily,CounterMetricFamily,SummaryMetricFamily等,例如:
class YourCustomCollector2 extends Collector { List<MetricFamilySamples> collect () { List<MetricFamilySamples> mfs = new ArrayList <MetricFamilySamples>(); mfs.add(new GaugeMetricFamily ("my_gauge_2" , "help" , 42 )); GaugeMetricFamily labeledGauge = new GaugeMetricFamily ("my_other_gauge" , "help" , Arrays.asList("labelname" )); labeledGauge.addMetric(Arrays.asList("foo" ), 4 ); labeledGauge.addMetric(Arrays.asList("bar" ), 5 ); mfs.add(labeledGauge); return mfs; } }
使用HTTP Server暴露样本数据 client_java下的simpleclient_httpserver模块实现了一个简单的HTTP服务器,当向该服务器发送获取样本数据的请求后,它会自动调用所有Collector的collect()方法,并将所有样本数据转换为Prometheus要求的数据输出格式规范。如果用户使用了Gradle构建项目,可以添加以下依赖:
compile 'io.prometheus:simpleclient_httpserver:0.3.0'
添加依赖之后,就可以在Exporter程序的main方法中启动一个HTTPServer实例:
public class CustomExporter { public static void main (String[] args) throws IOException { HTTPServer server = new HTTPServer (1234 ); } }
而在启动之前,别忘记调用Collector的register()方法。否则HTTPServer是找不到任何的Collector实例的:
new YourCustomCollector ().register();new YourCustomCollector2 ().register();
运行CustomExporter并访问http://127.0.0.1:1234/metrics,即可获取到以下数据:
$ curl http://127.0.0.1:1234/metrics # HELP my_gauge help # TYPE my_gauge gauge my_gauge 42.0 # HELP my_other_gauge help # TYPE my_other_gauge gauge my_other_gauge{labelname="foo",} 4.0 my_other_gauge{labelname="bar",} 5.0 # HELP my_guage help # TYPE my_guage gauge my_guage{l1="v1",} 4.0 my_guage{l1="v1",l2="v2",} 3.0
当然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:
compile 'io.prometheus:simpleclient_hotspot:0.3.0'
通过调用io.prometheus.client.hotspot.DefaultExport的initialize方法注册该模块中所有的Collector实例:
DefaultExports.initialize();
重新运行CustomExporter,并获取样本数据:
$ curl http://127.0.0.1:1234/metrics # HELP jvm_buffer_pool_used_bytes Used bytes of a given JVM buffer pool. # TYPE jvm_buffer_pool_used_bytes gauge jvm_buffer_pool_used_bytes{pool="direct",} 8192.0 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为例,当我们需要监控某个业务当前正在处理的请求数量,可以使用以下方式实现:
public class YourClass { static final Gauge inprogressRequests = Gauge.build() .name("inprogress_requests" ).help("Inprogress requests." ).register(); void processRequest () { inprogressRequests.inc(); inprogressRequests.dec(); } }
Gauge继承自Collector,registoer()方法会将该Gauge实例注册到CollectorRegistry中。这里创建了一个名为inprogress_requests的监控指标,其注释信息为”Inprogress requests”。
Gauge对象主要包含两个方法inc()和dec(),分别用于计数器+1和-1。
如果监控指标中还需要定义标签,则可以使用Gauge构造器的labelNames()方法,声明监控指标的标签,同时在样本计数时,通过指标的labels()方法指定标签的值,如下所示:
public class YourClass { static final Gauge inprogressRequests = Gauge.build() .name("inprogress_requests" ) .labelNames("method" ) .help("Inprogress requests." ).register(); void processRequest () { inprogressRequests.labels("get" ).inc(); inprogressRequests.labels("get" ).dec(); } }
Counter与Gauge的使用方法一致,唯一的区别在于Counter实例只包含一个inc()方法,用于计数器+1。
复杂类型Summary和Histogram Summary和Histogram用于统计和分析样本的分布情况。如下所示,通过Summary可以将HTTP请求的字节数以及请求处理时间作为统计样本,直接统计其样本的分布情况。
class YourClass { static final Summary receivedBytes = Summary.build() .name("requests_size_bytes" ).help("Request size in bytes." ).register(); static final Summary requestLatency = Summary.build() .name("requests_latency_seconds" ).help("Request latency in seconds." ).register(); void processRequest (Request req) { Summary.Timer requestTimer = requestLatency.startTimer(); try { } finally { receivedBytes.observe(req.size()); requestTimer.observeDuration(); } } }
除了使用Timer进行计时以外,Summary实例也提供了timer()方法,可以对线程或者Lamda表达式运行时间进行统计:
class YourClass { static final Summary requestLatency = Summary.build() .name("requests_latency_seconds" ).help("Request latency in seconds." ).register(); void processRequest (Request req) { requestLatency.timer(new Runnable () { public abstract void run () { } }); requestLatency.timer(() -> { }); } }
Summary和Histogram的用法基本保持一致,区别在于Summary可以指定在客户端统计的分位数,如下所示:
static final Summary requestLatency = Summary.build() .quantile(0.5 , 0.05 ) .quantile(0.9 , 0.01 ) .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()方法指定,如下所示:
static final Histogram requestLatency = Histogram.build() .name("requests_latency_seconds" ).help("Request latency in seconds." ) .buckets(0.1 , 0.2 , 0.4 , 0.8 ) .register();
与PushGateway集成 对于一些短周期或者临时采集的样本数据,client_java还提供了对PushGateway的支持:
添加依赖:
compile 'io.prometheus:simpleclient_pushgateway:0.3.0'
如下所示,PushGateway的实现类可以从所有注册到defaultRegistry的Collector实例中获取样本数据并直接推送 到外部部署的PushGateway服务中。
public class PushGatewayIntegration { public void push () throws IOException { CollectorRegistry registry = CollectorRegistry.defaultRegistry; PushGateway pg = new PushGateway ("127.0.0.1:9091" ); pg.pushAdd(registry, "my_batch_job" ); } }
在应用中内置Prometheus支持 本小节将以Spring Boot为例,介绍如何在应用代码中集成client_java。
添加Prometheus Java Client相关的依赖:
dependencies { compile 'io.prometheus:simpleclient:0.0.24' compile "io.prometheus:simpleclient_spring_boot:0.0.24" compile "io.prometheus:simpleclient_hotspot:0.0.24" }
通过注解@EnablePrometheusEndpoint启用Prometheus Endpoint,这里同时使用了simpleclient_hotspot中提供的DefaultExporter。该Exporter会在metrics endpoint中统计当前应用JVM的相关信息:
@SpringBootApplication @EnablePrometheusEndpoint public class SpringApplication implements CommandLineRunner { public static void main (String[] args) { SpringApplication.run(GatewayApplication.class, args); } @Override public void run (String... strings) throws Exception { DefaultExports.initialize(); } }
默认情况下Prometheus暴露的metrics endpoint为 /prometheus,可以通过endpoint配置进行修改:
endpoints: prometheus: id: metrics metrics: id: springmetrics sensitive: false enabled: true
启动应用程序访问http://localhost:8080/metrics可以看到以下输出内容:
# HELP jvm_gc_collection_seconds Time spent in a given JVM garbage collector in seconds. # TYPE jvm_gc_collection_seconds summary jvm_gc_collection_seconds_count{gc="PS Scavenge",} 11.0 jvm_gc_collection_seconds_sum{gc="PS Scavenge",} 0.18 jvm_gc_collection_seconds_count{gc="PS MarkSweep",} 2.0 jvm_gc_collection_seconds_sum{gc="PS MarkSweep",} 0.121 # HELP jvm_classes_loaded The number of classes that are currently loaded in the JVM # TYPE jvm_classes_loaded gauge jvm_classes_loaded 8376.0 # HELP jvm_classes_loaded_total The total number of classes that have been loaded since the JVM has started execution # TYPE jvm_classes_loaded_total counter ...
添加拦截器,为监控埋点做准备 除了获取应用JVM相关的状态以外,我们还可能需要添加一些自定义的监控Metrics实现对系统性能,以及业务状态进行采集,以提供日后优化的相关支撑数据。首先我们使用拦截器处理对应用的所有请求。
继承WebMvcConfigurerAdapter类并复写addInterceptors方法,对所有请求/**添加拦截器
@SpringBootApplication @EnablePrometheusEndpoint public class SpringApplication extends WebMvcConfigurerAdapter implements CommandLineRunner { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new PrometheusMetricsInterceptor ()).addPathPatterns("/**" ); } }
PrometheusMetricsInterceptor继承自HandlerInterceptorAdapter,通过复写父方法preHandle和afterCompletion可以拦截一个HTTP请求生命周期的不同阶段:
public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return super .preHandle(request, response, handler); } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { super .afterCompletion(request, response, handler, ex); } }
自定义监控指标 一旦PrometheusMetricsInterceptor能够成功拦截和处理请求之后,我们就可以使用client java自定义多种监控指标。
计数器可以用于记录只会增加不会减少的指标类型,比如记录应用请求的总量(http_requests_total),cpu使用时间(process_cpu_seconds_total)等。 一般而言,Counter类型的metrics指标在命名中我们使用_total结束。
使用Counter.build()创建Counter类型的监控指标,并且通过name()方法定义监控指标的名称,通过labelNames()定义该指标包含的标签。最后通过register()将该指标注册到Collector的defaultRegistry中中。
public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter { static final Counter requestCounter = Counter.build() .name("io_namespace_http_requests_total" ).labelNames("path" , "method" , "code" ) .help("Total requests." ).register(); @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { String requestURI = request.getRequestURI(); String method = request.getMethod(); int status = response.getStatus(); requestCounter.labels(requestURI, method, String.valueOf(status)).inc(); super .afterCompletion(request, response, handler, ex); } }
在afterCompletion方法中,可以获取到当前请求的请求路径、请求方法以及状态码。 这里通过labels指定了当前样本各个标签对应的值,最后通过.inc()计数器+1:
requestCounter.labels(requestURI, method, String.valueOf(status)).inc();
通过指标io_namespace_http_requests_total我们可以实现:
# PromQL sum(io_namespace_http_requests_total)
# PromQL sum(rate(io_wise2c_gateway_requests_total[5m]))
# PromQL topk(10, sum(io_namespace_http_requests_total) by (path))
使用Gauge可以反映应用的当前状态 ,例如在监控主机时,主机当前空闲的内容大小(node_memory_MemFree),可用内存大小(node_memory_MemAvailable)。或者容器当前的CPU使用率,内存使用率。这里我们使用Gauge记录当前应用正在处理的Http请求数量。
public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter { ...省略的代码 static final Gauge inprogressRequests = Gauge.build() .name("io_namespace_http_inprogress_requests").labelNames("path", "method") .help("Inprogress requests.").register(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ...省略的代码 // 计数器+1 inprogressRequests.labels(requestURI, method).inc(); return super.preHandle(request, response, handler); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ...省略的代码 // 计数器-1 inprogressRequests.labels(requestURI, method).dec(); super.afterCompletion(request, response, handler, ex); } }
通过指标io_namespace_http_inprogress_requests我们可以直接查询应用当前正在处理中的Http请求数量:
# PromQL io_namespace_http_inprogress_requests{}
Histogram主要用于在指定分布范围内(Buckets)记录大小(如http request bytes)或者事件发生的次数。以请求响应时间requests_latency_seconds为例。
public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter { static final Histogram requestLatencyHistogram = Histogram.build().labelNames("path", "method", "code") .name("io_namespace_http_requests_latency_seconds_histogram").help("Request latency in seconds.") .register(); private Histogram.Timer histogramRequestTimer; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ...省略的代码 histogramRequestTimer = requestLatencyHistogram.labels(requestURI, method, String.valueOf(status)).startTimer(); ...省略的代码 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ...省略的代码 histogramRequestTimer.observeDuration(); ...省略的代码 } }
Histogram会自动创建3个指标,分别为:
# 实际含义: 当前一共发生了2次http请求 io_namespace_http_requests_latency_seconds_histogram_count{path="/",method="GET",code="200",} 2.0
所有事件产生值的大小的总和: basename_sum
# 实际含义: 发生的2次http请求总的响应时间为13.107670803000001 秒 io_namespace_http_requests_latency_seconds_histogram_sum{path="/",method="GET",code="200",} 13.107670803000001
事件产生的值分布在bucket中的次数: basename_bucket{le=”上包含”}
# 在总共2次请求当中。http请求响应时间 <=0.005 秒 的请求次数为0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.005",} 0.0 # 在总共2次请求当中。http请求响应时间 <=0.01 秒 的请求次数为0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.01",} 0.0 # 在总共2次请求当中。http请求响应时间 <=0.025 秒 的请求次数为0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.025",} 0.0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.05",} 0.0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.075",} 0.0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.1",} 0.0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.25",} 0.0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.5",} 0.0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.75",} 0.0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="1.0",} 0.0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="2.5",} 0.0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="5.0",} 0.0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="7.5",} 2.0 # 在总共2次请求当中。http请求响应时间 <=10 秒 的请求次数为0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="10.0",} 2.0 # 在总共2次请求当中。http请求响应时间 10 秒 的请求次数为0 io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="+Inf",} 2.0
Summary和Histogram非常类型相似,都可以统计事件发生的次数或者发小,以及其分布情况。Summary和Histogram都提供了对于事件的计数_count以及值的汇总_sum。 因此使用_count,和_sum时间序列可以计算出相同的内容,例如http每秒的平均响应时间:rate(basename_sum[5m]) / rate(basename_count[5m])。同时Summary和Histogram都可以计算和统计样本的分布情况,比如中位数,9分位数等等。其中 0.0<= 分位数Quantiles <= 1.0。
不同在于Histogram可以通过histogram_quantile函数在服务器端计算分位数,而Sumamry的分位数则是直接在客户端进行定义。因此对于分位数的计算。 Summary在通过PromQL进行查询时有更好的性能表现,而Histogram则会消耗更多的资源。相对的对于客户端而言Histogram消耗的资源更少。
public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter { static final Summary requestLatency = Summary.build() .name("io_namespace_http_requests_latency_seconds_summary") .quantile(0.5, 0.05) .quantile(0.9, 0.01) .labelNames("path", "method", "code") .help("Request latency in seconds.").register(); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { ...省略的代码 requestTimer = requestLatency.labels(requestURI, method, String.valueOf(status)).startTimer(); ...省略的代码 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ...省略的代码 requestTimer.observeDuration(); ...省略的代码 } }
使用Summary指标,会自动创建多个时间序列:
# 含义:当前http请求发生总次数为12次 io_namespace_http_requests_latency_seconds_summary_count{path="/",method="GET",code="200",} 12.0
# 含义:这12次http请求的总响应时间为 51.029495508s io_namespace_http_requests_latency_seconds_summary_sum{path="/",method="GET",code="200",} 51.029495508
# 含义:这12次http请求响应时间的中位数是3.052404983s io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.5",} 3.052404983 # 含义:这12次http请求响应时间的9分位数是8.003261666s io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.9",} 8.003261666
使用Collector暴露其它指标 除了在拦截器中使用Prometheus提供的Counter,Summary,Gauage等构造监控指标以外,我们还可以通过自定义的Collector实现对相关业务指标的暴露。例如,我们可以通过自定义Collector直接从应用程序的数据库中统计监控指标.
@SpringBootApplication @EnablePrometheusEndpoint public class SpringApplication extends WebMvcConfigurerAdapter implements CommandLineRunner { @Autowired private CustomExporter customExporter; ...省略的代码 @Override public void run (String... args) throws Exception { ...省略的代码 customExporter.register(); } }
CustomExporter集成自io.prometheus.client.Collector,在调用Collector的register()方法后,当访问/metrics时,则会自动从Collector的collection()方法中获取采集到的监控指标。
由于这里CustomExporter存在于Spring的IOC容器当中,这里可以直接访问业务代码,返回需要的业务相关的指标。
import io.prometheus.client.Collector;import io.prometheus.client.GaugeMetricFamily;import org.springframework.stereotype.Component;import java.util.ArrayList;import java.util.Collections;import java.util.List;@Component public class CustomExporter extends Collector { @Override public List<MetricFamilySamples> collect () { List<MetricFamilySamples> mfs = new ArrayList <>(); # 创建metrics指标 GaugeMetricFamily labeledGauge = new GaugeMetricFamily ("io_namespace_custom_metrics" , "custom metrics" , Collections.singletonList("labelname" )); # 设置指标的label以及value labeledGauge.addMetric(Collections.singletonList("labelvalue" ), 1 ); mfs.add(labeledGauge); return mfs; } }
这里也可以使用CounterMetricFamily,SummaryMetricFamily声明其它的指标类型。