今天要和你們分享的是在實際工做中「如何優雅地自定義Prometheus監控指標」!目前大部分使用Spring Boot構建微服務體系的公司,大都在使用Prometheus來構建微服務的度量指標(Metrics)類監控系統。而通常作法是經過在微服務應用中集成Prometheus指標採集SDK,從而使得Spring Boot暴露相關Metrics採集端點來實現。 java
但通常來講,Spring Boot默認暴露的Metrics數量及類型是有限的,若是想要創建針對微服務應用更豐富的監控維度(例如TP90/TP99分位值指標之類),那麼還須要咱們在Spring Boot默認已經打開的Metrics基礎之上,配置Prometheus類庫(micrometer-registry-prometheus)所提供的其餘指標類型。web
但怎麼樣才能在Spring Boot框架中以更優雅地方式實現呢?難道須要在業務代碼中編寫各類自定義監控指標代碼的暴露邏輯嗎?接下來的內容咱們將經過@註解+AOP的方式來演示如何以更加優雅的方式來實現Prometheus監控指標的自定義!spring
須要說明的是在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這樣的分位值指標,那麼就能夠經過該註解標註。除此以外,還能夠定義上報其餘指標類型的註解,例如:app
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,那麼也能夠定義一個通用註解,用於同時上報多個指標類型,例如:ide
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代理類來實現,具體實現代碼以下:微服務
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; //計時器Metric Timer 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); //計數器Metric Counter 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; @Override public 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 @ConditionalOnMissingBean public MeterRegistryCustomizer<MeterRegistry> meterRegistryCustomizer(Environment environment) { return registry -> { registry.config() .commonTags("application", environment.getProperty("spring.application.name")); }; } @Bean @ConditionalOnMissingBean public 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 { @Autowired private 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相關文章,學習資料都會在裏面更新,整理的資料也會放在裏面。
以爲寫的還不錯的就點個贊,加個關注唄!點關注,不迷路,持續更新!!!