今天要和大家分享的是在实际工作中**“如何优雅地自定义Prometheus监控指标”**!目前大部分使用Spring Boot构建微服务体系的公司,大都在使用Prometheus来构建微服务的度量指标(Metrics)类监控系统。而一般做法是通过在微服务应用中集成Prometheus指标采集SDK,从而使得Spring Boot暴露相关Metrics采集端点来实现。

但一般来说,Spring Boot默认暴露的Metrics数量及类型是有限的,如果想要建立针对微服务应用更丰富的监控维度(例如TP90/TP99分位值指标之类),那么还需要我们在Spring Boot默认已经打开的Metrics基础之上,配置Prometheus类库(micrometer-registry-prometheus)所提供的其他指标类型。

但怎么样才能在Spring Boot框架中以更优雅地方式实现呢?难道需要在业务代码中编写各种自定义监控指标代码的暴露逻辑吗?接下来的内容我们将通过**@注解+AOP**的方式来演示如何以更加优雅的方式来实现Prometheus监控指标的自定义!

自定义监控指标配置注解

需要说明的是在Spring Boot应用中,对程序运行信息的收集(如指标、日志),比较常用的方法是通过Spring的AOP代理拦截来实现,但这种拦截程序运行过程的逻辑多少会损耗点系统性能,因此在自定义Prometheus监控指标的过程中,可以将是否上报指标的选择权交给开发人员,而从易用性角度来说,可以通过注解的方式实现。例如:

package com.wudimanong.monitor.metrics.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Tp {String description() default "";
}

如上所示代码,我们定义了一个用于标注上报计时器指标类型的注解,如果想统计接口的想TP90、TP99这样的分位值指标,那么就可以通过该注解标注。除此之外,还可以定义上报其他指标类型的注解,例如:

package com.wudimanong.monitor.metrics.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Count {String description() default "";
}

如上所示,我们定义了一个用于上报计数器类型指标的注解!如果要统计接口的平均响应时间、接口的请求量之类的指标,那么可以通过该注解标注!

而如果觉得分别定义不同指标类型的注解比较麻烦,对于某些接口上述各种指标类型都希望上报到Prometheus,那么也可以定义一个通用注解,用于同时上报多个指标类型,例如:

package com.wudimanong.monitor.metrics.annotation;import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Monitor {String description() default "";
}

总之,无论是分开定义特定指标注解还是定义一个通用的指标注解,其目标都是希望以更灵活的方式来扩展Spring Boot微服务应用的监控指标类型。

自定义监控指标注解AOP代理逻辑实现

上面我们灵活定义了上报不同指标类型的注解,而上述注解的具体实现逻辑,可以通过定义一个通用的AOP代理类来实现,具体实现代码如下:

package com.wudimanong.monitor.metrics.aop;import com.wudimanong.monitor.metrics.Metrics;
import com.wudimanong.monitor.metrics.annotation.Count;
import com.wudimanong.monitor.metrics.annotation.Monitor;
import com.wudimanong.monitor.metrics.annotation.Tp;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import java.lang.reflect.Method;
import java.util.function.Function;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;@Aspect
@Component
public class MetricsAspect {/*** Prometheus指标管理*/private MeterRegistry registry;private Function<ProceedingJoinPoint, Iterable<Tag>> tagsBasedOnJoinPoint;public MetricsAspect(MeterRegistry registry) {this.init(registry, pjp -> Tags.of(new String[]{"class", pjp.getStaticPart().getSignature().getDeclaringTypeName(), "method",pjp.getStaticPart().getSignature().getName()}));}public void init(MeterRegistry registry, Function<ProceedingJoinPoint, Iterable<Tag>> tagsBasedOnJoinPoint) {this.registry = registry;this.tagsBasedOnJoinPoint = tagsBasedOnJoinPoint;}/*** 针对@Tp指标配置注解的逻辑实现*/@Around("@annotation(com.wudimanong.monitor.metrics.annotation.Tp)")public Object timedMethod(ProceedingJoinPoint pjp) throws Throwable {Method method = ((MethodSignature) pjp.getSignature()).getMethod();method = pjp.getTarget().getClass().getMethod(method.getName(), method.getParameterTypes());Tp tp = method.getAnnotation(Tp.class);Timer.Sample sample = Timer.start(this.registry);String exceptionClass = "none";try {return pjp.proceed();} catch (Exception ex) {exceptionClass = ex.getClass().getSimpleName();throw ex;} finally {try {String finalExceptionClass = exceptionClass;//创建定义计数器,并设置指标的Tags信息(名称可以自定义)Timer timer = Metrics.newTimer("tp.method.timed",builder -> builder.tags(new String[]{"exception", finalExceptionClass}).tags(this.tagsBasedOnJoinPoint.apply(pjp)).tag("description", tp.description()).publishPercentileHistogram().register(this.registry));sample.stop(timer);} catch (Exception exception) {}}}/*** 针对@Count指标配置注解的逻辑实现*/@Around("@annotation(com.wudimanong.monitor.metrics.annotation.Count)")public Object countMethod(ProceedingJoinPoint pjp) throws Throwable {Method method = ((MethodSignature) pjp.getSignature()).getMethod();method = pjp.getTarget().getClass().getMethod(method.getName(), method.getParameterTypes());Count count = method.getAnnotation(Count.class);String exceptionClass = "none";try {return pjp.proceed();} catch (Exception ex) {exceptionClass = ex.getClass().getSimpleName();throw ex;} finally {try {String finalExceptionClass = exceptionClass;//创建定义计数器,并设置指标的Tags信息(名称可以自定义)Counter counter = Metrics.newCounter("count.method.counted",builder -> builder.tags(new String[]{"exception", finalExceptionClass}).tags(this.tagsBasedOnJoinPoint.apply(pjp)).tag("description", count.description()).register(this.registry));counter.increment();} catch (Exception exception) {}}}/*** 针对@Monitor通用指标配置注解的逻辑实现*/@Around("@annotation(com.wudimanong.monitor.metrics.annotation.Monitor)")public Object monitorMethod(ProceedingJoinPoint pjp) throws Throwable {Method method = ((MethodSignature) pjp.getSignature()).getMethod();method = pjp.getTarget().getClass().getMethod(method.getName(), method.getParameterTypes());Monitor monitor = method.getAnnotation(Monitor.class);String exceptionClass = "none";try {return pjp.proceed();} catch (Exception ex) {exceptionClass = ex.getClass().getSimpleName();throw ex;} finally {try {String finalExceptionClass = exceptionClass;//计时器MetricTimer timer = Metrics.newTimer("tp.method.timed",builder -> builder.tags(new String[]{"exception", finalExceptionClass}).tags(this.tagsBasedOnJoinPoint.apply(pjp)).tag("description", monitor.description()).publishPercentileHistogram().register(this.registry));Timer.Sample sample = Timer.start(this.registry);sample.stop(timer);//计数器MetricCounter counter = Metrics.newCounter("count.method.counted",builder -> builder.tags(new String[]{"exception", finalExceptionClass}).tags(this.tagsBasedOnJoinPoint.apply(pjp)).tag("description", monitor.description()).register(this.registry));counter.increment();} catch (Exception exception) {}}}
}

上述代码完整的实现了前面我们定义的指标配置注解的逻辑,其中针对@Monitor注解的逻辑就是@Tp和@Count注解逻辑的整合。如果还需要定义其他指标类型,可以在此基础上继续扩展!

需要注意,在上述逻辑实现中对“Timer”及“Counter”等指标类型的构建这里并没有直接使用“micrometer-registry-prometheus”依赖包中的构建对象,而是通过自定义的Metrics.newTimer()这样的方式实现,其主要用意是希望以更简洁、灵活的方式去实现指标的上报,其代码定义如下:

package com.wudimanong.monitor.metrics;import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Counter.Builder;
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.lang.NonNull;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;public class Metrics implements ApplicationContextAware {private static ApplicationContext context;@Overridepublic void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {context = applicationContext;}public static ApplicationContext getContext() {return context;}public static Counter newCounter(String name, Consumer<Builder> consumer) {MeterRegistry meterRegistry = context.getBean(MeterRegistry.class);return new CounterBuilder(meterRegistry, name, consumer).build();}public static Timer newTimer(String name, Consumer<Timer.Builder> consumer) {return new TimerBuilder(context.getBean(MeterRegistry.class), name, consumer).build();}
}

上述代码通过接入Spring容器上下文,获取了MeterRegistry实例,并以此来构建像Counter、Timer这样的指标类型对象。而这里之所以将获取方法定义为静态的,主要是便于在业务代码中进行引用!

而在上述代码中涉及的CounterBuilder、TimerBuilder构造器代码定义分别如下:

package com.wudimanong.monitor.metrics;import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Counter.Builder;
import io.micrometer.core.instrument.MeterRegistry;
import java.util.function.Consumer;public class CounterBuilder {private final MeterRegistry meterRegistry;private Counter.Builder builder;private Consumer<Builder> consumer;public CounterBuilder(MeterRegistry meterRegistry, String name, Consumer<Counter.Builder> consumer) {this.builder = Counter.builder(name);this.meterRegistry = meterRegistry;this.consumer = consumer;}public Counter build() {consumer.accept(builder);return builder.register(meterRegistry);}
}

上述代码为CounterBuilder构造器代码!TimerBuilder构造器代码如下:

package com.wudimanong.monitor.metrics;import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.Timer.Builder;
import java.util.function.Consumer;public class TimerBuilder {private final MeterRegistry meterRegistry;private Timer.Builder builder;private Consumer<Builder> consumer;public TimerBuilder(MeterRegistry meterRegistry, String name, Consumer<Timer.Builder> consumer) {this.builder = Timer.builder(name);this.meterRegistry = meterRegistry;this.consumer = consumer;}public Timer build() {this.consumer.accept(builder);return builder.register(meterRegistry);}
}

之所以还特地将构造器代码单独定义,主要是从代码的优雅性考虑!如果涉及其他指标类型的构造,也可以通过类似的方法进行扩展!

自定义指标注解配置类

在上述代码中我们已经定义了几个自定义指标注解及其实现逻辑代码,为了使其在Spring Boot环境中运行,还需要编写如下配置类,代码如下:

package com.wudimanong.monitor.metrics.config;import com.wudimanong.monitor.metrics.Metrics;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;@Configuration
public class CustomMetricsAutoConfiguration {@Bean@ConditionalOnMissingBeanpublic MeterRegistryCustomizer<MeterRegistry> meterRegistryCustomizer(Environment environment) {return registry -> {registry.config().commonTags("application", environment.getProperty("spring.application.name"));};}@Bean@ConditionalOnMissingBeanpublic Metrics metrics() {return new Metrics();}
}

上述配置代码主要是约定了上报Prometheus指标信息中所携带的应用名称,并对自定义了Metrics类进行了Bean配置!

业务代码的使用方式及效果

接下来我们演示在业务代码中如果要上报Prometheus监控指标应该怎么写,具体如下:

package com.wudimanong.monitor.controller;import com.wudimanong.monitor.metrics.annotation.Count;
import com.wudimanong.monitor.metrics.annotation.Monitor;
import com.wudimanong.monitor.metrics.annotation.Tp;
import com.wudimanong.monitor.service.MonitorService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/monitor")
public class MonitorController {@Autowiredprivate MonitorService monitorServiceImpl;//监控指标注解使用//@Tp(description = "/monitor/test")//@Count(description = "/monitor/test")@Monitor(description = "/monitor/test")@GetMapping("/test")public String monitorTest(@RequestParam("name") String name) {monitorServiceImpl.monitorTest(name);return "监控示范工程测试接口返回->OK!";}
}

如上述代码所示,在实际的业务编程中就可以比较简单的通过注解来配置接口所上传的Prometheus监控指标了!此时在本地启动程序,可以通过访问微服务应用的“/actuator/prometheus”指标采集端点来查看相关指标,如下图所示:

有了这些自定义上报的监控指标,那么Promethues在采集后,我们就可以通过像Grafana这样的可视化工具,来构建起多维度界面友好地监控视图了,例如以TP90/TP99为例:

如上所示,在Grafana中可以同时定义多个PromeQL来定于不同的监控指标信息,这里我们分别通过Prometheus所提供的“histogram_quantile”函数统计了接口方法“monitorTest”的TP90及TP95分位值!而所使用的指标就是自定义的“tp_method_timed_xx”指标类型!

后记

以上就是我最近在工作中封装的一组关于Prometheus自定义监控指标的SDK代码,在实际工作中可以将其封住为Spring Boot Starter依赖的形式,从而更好地被Spring Boot项目集成!

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

实战|如何优雅地自定义Prometheus监控指标相关推荐

  1. Java获取Prometheus监控指标数据

    Java获取Prometheus监控指标数据 一. 准备工作 1. 有可以被Prometheus监控的服务 没有的话可以参考以下链接本地搭建:SpringBoot应用接入Prometheus+Graf ...

  2. 自定义 Zabbix 监控指标及图表

    自定义 Zabbix 监控指标及图表 问题描述 有时候 Zabbix 监控系统的模版提供的监控指标并不能满足我们的需求,比如我们要监控服务器的线程数.TCP 连接数等,这些指标在 Zabbix 自带的 ...

  3. zabbix如何添加图表显示内容_自定义 Zabbix 监控指标及图表

    问题描述 有时候 Zabbix 监控系统的模版提供的监控指标并不能满足我们的需求,比如我们要监控服务器的线程数.TCP 连接数等,这些指标在 Zabbix 自带的模板中是没有的,这时候我们就需要自定义 ...

  4. Docker——Prometheus监控服务治理

    摘要 Prometheus是继Kubernetes之后,第二个从云原生计算基金会(CNCF)毕业的项目.Prometheus是Google监控系统BorgMon类似实现的开源版,整套系统由监控服务.告 ...

  5. prometheus监控_Prometheus监控简介

    prometheus监控 指标是代表系统整体运行状况以及您认为对监视和警报或可观察性很重要的任何其他特定信息的主要方式 . Prometheus是从2012年开始在SoundCloud上构建的领先的开 ...

  6. 1.Prometheus监控入门之基础架构介绍

    0x00 前言简述 0.学习导读 1.开源监控系统简史 2.Prometheus 基础简介 3.Prometheus 架构组件 4.Prometheus 基本原理 5.Prometheus 数据模型和 ...

  7. Prometheus监控运维实战十: 主机监控指标

    在上一篇文章中我们学习了如何通过node-exporter获取主机监控信息.但安装好监控只是第一步,我们还需要知道哪些监控指标是我们需要关注的. 本文将对Prometheus相关的主机监控指标进行介绍 ...

  8. Prometheus+Grafana PG监控部署以及自定义监控指标

    点击上方"蓝字" 关注我们,享更多干货! 1.前言 Prometheus:是从云原生计算基金会(CNCF)毕业的项目.Prometheus是Google监控系统BorgMon类似实 ...

  9. 服务器运维监控指标,运维体系~指标监控~Prometheus监控告警与日志

    一 Prometheus 入门 1.1 入门介绍 运维体系~指标监控 先来一张图,说明一下Prometheus监控相关的软件和知识点. 1: 首先要安装:Prometheus, 负责收集各种监控指标, ...

最新文章

  1. 1107 Social Clusters
  2. 皮一皮:程序员为什么只能吃青春饭?
  3. python中国大学排名爬虫写明详细步骤-Python之爬虫-中国大学排名
  4. javascript 数组排序
  5. jieba 分词的三种模式
  6. java坐标移动题目case_用java怎样编写一个二维坐标平移程序
  7. 如何使用recordMyDesktop录制截屏视频
  8. 请求之前~HttpHandler实现媒体文件和图像文件的盗链
  9. 恢复html的初始选定状态,jQuery实现点击旋转,再点击恢复初始状态动画效果示例...
  10. Java图形化:JComponent组件
  11. Java保留小数位数两种简单方法
  12. 当时明月在,曾照彩云归。
  13. 怎么用dw修改PHP网页模板,DW基础篇:如何使用DW运用网页模板
  14. Python的学习笔记案例4--52周存钱挑战2.0
  15. 当代计算机网络技术带来的影响,浅析当代网络技术
  16. 办公电脑远程软件有哪些、这几款你知道吗
  17. 如何创建您自己的I爱纽约T恤
  18. C++之字符串大小写转换
  19. 我的世界无限法则服务器怎么用,我的世界无限法则版
  20. 利用Office365进行个税改革员工信息收集

热门文章

  1. 盗墓笔记之缘起与大结局∶张起灵与汪藏海的500年恩怨情仇
  2. Cadence OrCAD Capture 自顶而下的设计流程
  3. 大端字节序码流中取出2字节_graalvm字节码到位码
  4. 第六周项目1-分数类的雏形
  5. 用人单位与劳动者达成的不缴纳社会保险费的协议是否有效?
  6. Django-登陆验证
  7. JAVA一条龙持续集成方案(完整步骤+脚本+配置)
  8. 关于强交互类APP全球加速解决方案
  9. 【Python】输入圆的半径,根据自定义的方法返回圆的周长/面积等
  10. “我曾认为开源是有钱闲人的游戏,不要试图快速从中变现” | 对话《大教堂与集市》译者卫剑钒