• 在应用中内置Prometheus支持
    • 添加拦截器,为监控埋点做准备
    • 自定义监控指标
    • 使用Collector暴露其它指标

    在应用中内置Prometheus支持

    本小节将以Spring Boot为例,介绍如何在应用代码中集成client_java。

    添加Prometheus Java Client相关的依赖:

    1. dependencies {
    2. compile 'io.prometheus:simpleclient:0.0.24'
    3. compile "io.prometheus:simpleclient_spring_boot:0.0.24"
    4. compile "io.prometheus:simpleclient_hotspot:0.0.24"
    5. }

    通过注解@EnablePrometheusEndpoint启用Prometheus Endpoint,这里同时使用了simpleclient_hotspot中提供的DefaultExporter。该Exporter会在metrics endpoint中统计当前应用JVM的相关信息:

    1. @SpringBootApplication
    2. @EnablePrometheusEndpoint
    3. public class SpringApplication implements CommandLineRunner {
    4. public static void main(String[] args) {
    5. SpringApplication.run(GatewayApplication.class, args);
    6. }
    7. @Override
    8. public void run(String... strings) throws Exception {
    9. DefaultExports.initialize();
    10. }
    11. }

    默认情况下Prometheus暴露的metrics endpoint为 /prometheus,可以通过endpoint配置进行修改:

    1. endpoints:
    2. prometheus:
    3. id: metrics
    4. metrics:
    5. id: springmetrics
    6. sensitive: false
    7. enabled: true

    启动应用程序访问http://localhost:8080/metrics可以看到以下输出内容:

    1. # HELP jvm_gc_collection_seconds Time spent in a given JVM garbage collector in seconds.
    2. # TYPE jvm_gc_collection_seconds summary
    3. jvm_gc_collection_seconds_count{gc="PS Scavenge",} 11.0
    4. jvm_gc_collection_seconds_sum{gc="PS Scavenge",} 0.18
    5. jvm_gc_collection_seconds_count{gc="PS MarkSweep",} 2.0
    6. jvm_gc_collection_seconds_sum{gc="PS MarkSweep",} 0.121
    7. # HELP jvm_classes_loaded The number of classes that are currently loaded in the JVM
    8. # TYPE jvm_classes_loaded gauge
    9. jvm_classes_loaded 8376.0
    10. # HELP jvm_classes_loaded_total The total number of classes that have been loaded since the JVM has started execution
    11. # TYPE jvm_classes_loaded_total counter
    12. ...
    添加拦截器,为监控埋点做准备

    除了获取应用JVM相关的状态以外,我们还可能需要添加一些自定义的监控Metrics实现对系统性能,以及业务状态进行采集,以提供日后优化的相关支撑数据。首先我们使用拦截器处理对应用的所有请求。

    继承WebMvcConfigurerAdapter类并复写addInterceptors方法,对所有请求/**添加拦截器

    1. @SpringBootApplication
    2. @EnablePrometheusEndpoint
    3. public class SpringApplication extends WebMvcConfigurerAdapter implements CommandLineRunner {
    4. @Override
    5. public void addInterceptors(InterceptorRegistry registry) {
    6. registry.addInterceptor(new PrometheusMetricsInterceptor()).addPathPatterns("/**");
    7. }
    8. }

    PrometheusMetricsInterceptor继承自HandlerInterceptorAdapter,通过复写父方法preHandle和afterCompletion可以拦截一个HTTP请求生命周期的不同阶段:

    1. public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter {
    2. @Override
    3. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    4. return super.preHandle(request, response, handler);
    5. }
    6. @Override
    7. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    8. super.afterCompletion(request, response, handler, ex);
    9. }
    10. }
    自定义监控指标

    一旦PrometheusMetricsInterceptor能够成功拦截和处理请求之后,我们就可以使用client java自定义多种监控指标。

    计数器可以用于记录只会增加不会减少的指标类型,比如记录应用请求的总量(http_requests_total),cpu使用时间(process_cpu_seconds_total)等。 一般而言,Counter类型的metrics指标在命名中我们使用_total结束。

    使用Counter.build()创建Counter类型的监控指标,并且通过name()方法定义监控指标的名称,通过labelNames()定义该指标包含的标签。最后通过register()将该指标注册到Collector的defaultRegistry中中。

    1. public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter {
    2. static final Counter requestCounter = Counter.build()
    3. .name("io_namespace_http_requests_total").labelNames("path", "method", "code")
    4. .help("Total requests.").register();
    5. @Override
    6. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    7. String requestURI = request.getRequestURI();
    8. String method = request.getMethod();
    9. int status = response.getStatus();
    10. requestCounter.labels(requestURI, method, String.valueOf(status)).inc();
    11. super.afterCompletion(request, response, handler, ex);
    12. }
    13. }

    在afterCompletion方法中,可以获取到当前请求的请求路径、请求方法以及状态码。 这里通过labels指定了当前样本各个标签对应的值,最后通过.inc()计数器+1:

    1. requestCounter.labels(requestURI, method, String.valueOf(status)).inc();

    通过指标io_namespace_http_requests_total我们可以实现:

    • 查询应用的请求总量
    1. # PromQL
    2. sum(io_namespace_http_requests_total)
    • 查询每秒Http请求量
    1. # PromQL
    2. sum(rate(io_wise2c_gateway_requests_total[5m]))
    • 查询当前应用请求量Top N的URI
    1. # PromQL
    2. topk(10, sum(io_namespace_http_requests_total) by (path))

    使用Gauge可以反映应用的当前状态,例如在监控主机时,主机当前空闲的内容大小(node_memory_MemFree),可用内存大小(node_memory_MemAvailable)。或者容器当前的CPU使用率,内存使用率。这里我们使用Gauge记录当前应用正在处理的Http请求数量。

    1. public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter {
    2. ...省略的代码
    3. static final Gauge inprogressRequests = Gauge.build()
    4. .name("io_namespace_http_inprogress_requests").labelNames("path", "method")
    5. .help("Inprogress requests.").register();
    6. @Override
    7. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    8. ...省略的代码
    9. // 计数器+1
    10. inprogressRequests.labels(requestURI, method).inc();
    11. return super.preHandle(request, response, handler);
    12. }
    13. @Override
    14. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    15. ...省略的代码
    16. // 计数器-1
    17. inprogressRequests.labels(requestURI, method).dec();
    18. super.afterCompletion(request, response, handler, ex);
    19. }
    20. }

    通过指标io_namespace_http_inprogress_requests我们可以直接查询应用当前正在处理中的Http请求数量:

    1. # PromQL
    2. io_namespace_http_inprogress_requests{}

    Histogram主要用于在指定分布范围内(Buckets)记录大小(如http request bytes)或者事件发生的次数。以请求响应时间requests_latency_seconds为例。

    1. public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter {
    2. static final Histogram requestLatencyHistogram = Histogram.build().labelNames("path", "method", "code")
    3. .name("io_namespace_http_requests_latency_seconds_histogram").help("Request latency in seconds.")
    4. .register();
    5. private Histogram.Timer histogramRequestTimer;
    6. @Override
    7. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    8. ...省略的代码
    9. histogramRequestTimer = requestLatencyHistogram.labels(requestURI, method, String.valueOf(status)).startTimer();
    10. ...省略的代码
    11. }
    12. @Override
    13. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    14. ...省略的代码
    15. histogramRequestTimer.observeDuration();
    16. ...省略的代码
    17. }
    18. }

    Histogram会自动创建3个指标,分别为:

    • 事件发生总次数: basename_count
    1. # 实际含义: 当前一共发生了2次http请求
    2. io_namespace_http_requests_latency_seconds_histogram_count{path="/",method="GET",code="200",} 2.0
    • 所有事件产生值的大小的总和: basename_sum
    1. # 实际含义: 发生的2次http请求总的响应时间为13.107670803000001 秒
    2. io_namespace_http_requests_latency_seconds_histogram_sum{path="/",method="GET",code="200",} 13.107670803000001
    • 事件产生的值分布在bucket中的次数: basename_bucket{le=”上包含”}
    1. # 在总共2次请求当中。http请求响应时间 <=0.005 秒 的请求次数为0
    2. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.005",} 0.0
    3. # 在总共2次请求当中。http请求响应时间 <=0.01 秒 的请求次数为0
    4. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.01",} 0.0
    5. # 在总共2次请求当中。http请求响应时间 <=0.025 秒 的请求次数为0
    6. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.025",} 0.0
    7. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.05",} 0.0
    8. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.075",} 0.0
    9. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.1",} 0.0
    10. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.25",} 0.0
    11. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.5",} 0.0
    12. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="0.75",} 0.0
    13. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="1.0",} 0.0
    14. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="2.5",} 0.0
    15. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="5.0",} 0.0
    16. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="7.5",} 2.0
    17. # 在总共2次请求当中。http请求响应时间 <=10 秒 的请求次数为0
    18. io_namespace_http_requests_latency_seconds_histogram_bucket{path="/",method="GET",code="200",le="10.0",} 2.0
    19. # 在总共2次请求当中。http请求响应时间 10 秒 的请求次数为0
    20. 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消耗的资源更少。

    1. public class PrometheusMetricsInterceptor extends HandlerInterceptorAdapter {
    2. static final Summary requestLatency = Summary.build()
    3. .name("io_namespace_http_requests_latency_seconds_summary")
    4. .quantile(0.5, 0.05)
    5. .quantile(0.9, 0.01)
    6. .labelNames("path", "method", "code")
    7. .help("Request latency in seconds.").register();
    8. @Override
    9. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    10. ...省略的代码
    11. requestTimer = requestLatency.labels(requestURI, method, String.valueOf(status)).startTimer();
    12. ...省略的代码
    13. }
    14. @Override
    15. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    16. ...省略的代码
    17. requestTimer.observeDuration();
    18. ...省略的代码
    19. }
    20. }

    使用Summary指标,会自动创建多个时间序列:

    • 事件发生总的次数
    1. # 含义:当前http请求发生总次数为12次
    2. io_namespace_http_requests_latency_seconds_summary_count{path="/",method="GET",code="200",} 12.0
    • 事件产生的值的总和
    1. # 含义:这12次http请求的总响应时间为 51.029495508s
    2. io_namespace_http_requests_latency_seconds_summary_sum{path="/",method="GET",code="200",} 51.029495508
    • 事件产生的值的分布情况
    1. # 含义:这12次http请求响应时间的中位数是3.052404983s
    2. io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.5",} 3.052404983
    3. # 含义:这12次http请求响应时间的9分位数是8.003261666s
    4. io_namespace_http_requests_latency_seconds_summary{path="/",method="GET",code="200",quantile="0.9",} 8.003261666
    使用Collector暴露其它指标

    除了在拦截器中使用Prometheus提供的Counter,Summary,Gauage等构造监控指标以外,我们还可以通过自定义的Collector实现对相关业务指标的暴露。例如,我们可以通过自定义Collector直接从应用程序的数据库中统计监控指标.

    1. @SpringBootApplication
    2. @EnablePrometheusEndpoint
    3. public class SpringApplication extends WebMvcConfigurerAdapter implements CommandLineRunner {
    4. @Autowired
    5. private CustomExporter customExporter;
    6. ...省略的代码
    7. @Override
    8. public void run(String... args) throws Exception {
    9. ...省略的代码
    10. customExporter.register();
    11. }
    12. }

    CustomExporter集成自io.prometheus.client.Collector,在调用Collector的register()方法后,当访问/metrics时,则会自动从Collector的collection()方法中获取采集到的监控指标。

    由于这里CustomExporter存在于Spring的IOC容器当中,这里可以直接访问业务代码,返回需要的业务相关的指标。

    1. import io.prometheus.client.Collector;
    2. import io.prometheus.client.GaugeMetricFamily;
    3. import org.springframework.stereotype.Component;
    4. import java.util.ArrayList;
    5. import java.util.Collections;
    6. import java.util.List;
    7. @Component
    8. public class CustomExporter extends Collector {
    9. @Override
    10. public List<MetricFamilySamples> collect() {
    11. List<MetricFamilySamples> mfs = new ArrayList<>();
    12. # 创建metrics指标
    13. GaugeMetricFamily labeledGauge =
    14. new GaugeMetricFamily("io_namespace_custom_metrics", "custom metrics", Collections.singletonList("labelname"));
    15. # 设置指标的label以及value
    16. labeledGauge.addMetric(Collections.singletonList("labelvalue"), 1);
    17. mfs.add(labeledGauge);
    18. return mfs;
    19. }
    20. }

    这里也可以使用CounterMetricFamily,SummaryMetricFamily声明其它的指标类型。