Prometheus自定義監控內容
相關內容原文地址:
博客園:Throwable:基於Prometheus搭建SpringCloud全方位立體監控體系
一、io.micrometer的使用
在SpringBoot2.X中,spring-boot-starter-actuator引入了io.micrometer,對1.X中的metrics進行了重構,主要特點是支持tag/label,配合支持tag/label的監控系統,使得我們可以更加方便地對metrics進行多維度的統計查詢及監控。io.micrometer目前支持Counter、Gauge、Timer、Summary等多種不同類型的度量方式(不知道有沒有遺漏),下面逐個簡單分析一下它們的作用和使用方式。 需要在SpringBoot項目下引入下面的依賴:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<version>${micrometer.version}</version>
</dependency>
目前最新的micrometer.version爲1.0.5。注意一點的是:io.micrometer支持Tag(標籤)的概念,Tag是其metrics是否能夠有多維度的支持的基礎,Tag必須成對出現,也就是必須配置也就是偶數個Tag,有點類似於K-V的關係。
1.1 Counter
Counter(計數器)簡單理解就是一種只增不減的計數器。它通常用於記錄服務的請求數量、完成的任務數量、錯誤的發生數量等等。舉個例子:
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
/**
* @author throwable
* @version v1.0
* @description
* @since 2018/7/19 23:10
*/
public class CounterSample {
public static void main(String[] args) throws Exception {
//tag必須成對出現,也就是偶數個
Counter counter = Counter.builder("counter")
.tag("counter", "counter")
.description("counter")
.register(new SimpleMeterRegistry());
counter.increment();
counter.increment(2D);
System.out.println(counter.count());
System.out.println(counter.measure());
//全局靜態方法
Metrics.addRegistry(new SimpleMeterRegistry());
counter = Metrics.counter("counter", "counter", "counter");
counter.increment(10086D);
counter.increment(10087D);
System.out.println(counter.count());
System.out.println(counter.measure());
}
}
輸出:
3.0
[Measurement{statistic='COUNT', value=3.0}]
20173.0
[Measurement{statistic='COUNT', value=20173.0}]
Counter的Measurement的statistic(可以理解爲度量的統計角度)只有COUNT,也就是它只具備計數(它只有增量的方法,因此只增不減),這一點從它的接口定義可知:
public interface Counter extends Meter {
default void increment() {
increment(1.0);
}
void increment(double amount);
double count();
//忽略其他方法或者成員
}
Counter還有一個衍生類型FunctionCounter,它是基於函數式接口ToDoubleFunction進行計數統計的,用法差不多。
1.2 Gauge
Gauge(儀表)是一個表示單個數值的度量,它可以表示任意地上下移動的數值測量。Gauge通常用於變動的測量值,如當前的內存使用情況,同時也可以測量上下移動的"計數",比如隊列中的消息數量。舉個例子:
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author throwable
* @version v1.0
* @description
* @since 2018/7/19 23:30
*/
public class GaugeSample {
public static void main(String[] args) throws Exception {
AtomicInteger atomicInteger = new AtomicInteger();
Gauge gauge = Gauge.builder("gauge", atomicInteger, AtomicInteger::get)
.tag("gauge", "gauge")
.description("gauge")
.register(new SimpleMeterRegistry());
atomicInteger.addAndGet(5);
System.out.println(gauge.value());
System.out.println(gauge.measure());
atomicInteger.decrementAndGet();
System.out.println(gauge.value());
System.out.println(gauge.measure());
//全局靜態方法,返回值竟然是依賴值,有點奇怪,暫時不選用
Metrics.addRegistry(new SimpleMeterRegistry());
AtomicInteger other = Metrics.gauge("gauge", atomicInteger, AtomicInteger::get);
}
}
輸出結果:
5.0
[Measurement{statistic='VALUE', value=5.0}]
4.0
[Measurement{statistic='VALUE', value=4.0}]
Gauge關注的度量統計角度是VALUE(值),它的構建方法中依賴於函數式接口ToDoubleFunction的實例(如例子中的實例方法引用AtomicInteger::get)和一個依賴於ToDoubleFunction改變自身值的實例(如例子中的AtomicInteger實例),它的接口方法如下:
public interface Gauge extends Meter {
double value();
//忽略其他方法或者成員
}
1.3 Timer
Timer(計時器)同時測量一個特定的代碼邏輯塊的調用(執行)速度和它的時間分佈。簡單來說,就是在調用結束的時間點記錄整個調用塊執行的總時間,適用於測量短時間執行的事件的耗時分佈,例如消息隊列消息的消費速率。舉個例子:
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import java.util.concurrent.TimeUnit;
/**
* @author throwable
* @version v1.0
* @description
* @since 2018/7/19 23:44
*/
public class TimerSample {
public static void main(String[] args) throws Exception{
Timer timer = Timer.builder("timer")
.tag("timer","timer")
.description("timer")
.register(new SimpleMeterRegistry());
timer.record(()->{
try {
TimeUnit.SECONDS.sleep(2);
}catch (InterruptedException e){
//ignore
}
});
System.out.println(timer.count());
System.out.println(timer.measure());
System.out.println(timer.totalTime(TimeUnit.SECONDS));
System.out.println(timer.mean(TimeUnit.SECONDS));
System.out.println(timer.max(TimeUnit.SECONDS));
}
}
輸出結果:
1
[Measurement{statistic='COUNT', value=1.0}, Measurement{statistic='TOTAL_TIME', value=2.000603975}, Measurement{statistic='MAX', value=2.000603975}]
2.000603975
2.000603975
2.000603975
Timer的度量統計角度主要包括記錄執行的最大時間、總時間、平均時間、執行完成的總任務數,它提供多種的統計方法變體:
public interface Timer extends Meter, HistogramSupport {
void record(long amount, TimeUnit unit);
default void record(Duration duration) {
record(duration.toNanos(), TimeUnit.NANOSECONDS);
}
<T> T record(Supplier<T> f);
<T> T recordCallable(Callable<T> f) throws Exception;
void record(Runnable f);
default Runnable wrap(Runnable f) {
return () -> record(f);
}
default <T> Callable<T> wrap(Callable<T> f) {
return () -> recordCallable(f);
}
//忽略其他方法或者成員
}
這些record或者包裝方法可以根據需要選擇合適的使用,另外,一些度量屬性(如下限和上限)或者單位可以自行配置,具體屬性的相關內容可以查看DistributionStatisticConfig類,這裏不詳細展開。
另外,Timer有一個衍生類LongTaskTimer,主要是用來記錄正在執行但是尚未完成的任務數,用法差不多。
1.4 Summary
Summary(摘要)用於跟蹤事件的分佈。它類似於一個計時器,但更一般的情況是,它的大小並不一定是一段時間的測量值。在micrometer中,對應的類是DistributionSummary,它的用法有點像Timer,但是記錄的值是需要直接指定,而不是通過測量一個任務的執行時間。舉個例子:
import io.micrometer.core.instrument.DistributionSummary;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
/**
* @author throwable
* @version v1.0
* @description
* @since 2018/7/19 23:55
*/
public class SummarySample {
public static void main(String[] args) throws Exception {
DistributionSummary summary = DistributionSummary.builder("summary")
.tag("summary", "summary")
.description("summary")
.register(new SimpleMeterRegistry());
summary.record(2D);
summary.record(3D);
summary.record(4D);
System.out.println(summary.measure());
System.out.println(summary.count());
System.out.println(summary.max());
System.out.println(summary.mean());
System.out.println(summary.totalAmount());
}
}
輸出結果:
[Measurement{statistic='COUNT', value=3.0}, Measurement{statistic='TOTAL', value=9.0}, Measurement{statistic='MAX', value=4.0}]
3
4.0
3.0
9.0
Summary的度量統計角度主要包括記錄過的數據中的最大值、總數值、平均值和總次數。另外,一些度量屬性(如下限和上限)或者單位可以自行配置,具體屬性的相關內容可以查看DistributionStatisticConfig類。
二、擴展
SpringCloud體系的監控,擴展一個功能,記錄一下每個有效的請求的執行時間。添加下面幾個類或者方法:
//註解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodMetric {
String name() default "";
String description() default "";
String[] tags() default {};
}
//切面類
@Aspect
@Component
public class HttpMethodCostAspect {
@Autowired
private MeterRegistry meterRegistry;
@Pointcut("@annotation(club.throwable.sample.aspect.MethodMetric)")
public void pointcut() {
}
@Around(value = "pointcut()")
public Object process(ProceedingJoinPoint joinPoint) throws Throwable {
Method targetMethod = ((MethodSignature) joinPoint.getSignature()).getMethod();
//這裏是爲了拿到實現類的註解
Method currentMethod = ClassUtils.getUserClass(joinPoint.getTarget().getClass())
.getDeclaredMethod(targetMethod.getName(), targetMethod.getParameterTypes());
if (currentMethod.isAnnotationPresent(MethodMetric.class)) {
MethodMetric methodMetric = currentMethod.getAnnotation(MethodMetric.class);
return processMetric(joinPoint, currentMethod, methodMetric);
} else {
return joinPoint.proceed();
}
}
private Object processMetric(ProceedingJoinPoint joinPoint, Method currentMethod,
MethodMetric methodMetric) throws Throwable {
String name = methodMetric.name();
if (!StringUtils.hasText(name)) {
name = currentMethod.getName();
}
String desc = methodMetric.description();
if (!StringUtils.hasText(desc)) {
desc = name;
}
String[] tags = methodMetric.tags();
if (tags.length == 0) {
tags = new String[2];
tags[0] = name;
tags[1] = name;
}
Timer timer = Timer.builder(name).tags(tags)
.description(desc)
.register(meterRegistry);
return timer.record(() -> {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throw new IllegalStateException(throwable);
}
});
}
}
//啓動類裏面添加方法
@SpringBootApplication
@EnableEurekaClient
@RestController
public class SampleApplication {
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
@MethodMetric
@GetMapping(value = "/hello")
public String hello(@RequestParam(name = "name", required = false, defaultValue = "doge") String name) {
return String.format("%s say hello!", name);
}
}
配置好Grafana的面板,重啓項目,多次調用/hello接口。