實戰|如何優雅地自定義Prometheus監控指標

今天要和大家分享的是在實際工作中“如何優雅地自定義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;
                //計時器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相關文章,學習資料都會在裏面更新,整理的資料也會放在裏面。

覺得寫的還不錯的就點個贊,加個關注唄!點關注,不迷路,持續更新!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章