一、前言

本章节开始将为大家展示如何在 SpringBoot 应用中去使用 Metrics 监控。本系列使用的 SpringBoot 版本为笔者当前的最新 RELAESE 版本 2.4.0,整个 SpringBoot 2 关于这边都是大同小异,所以大家不用担心版本问题。

二、依赖包

除了常规开发 SpringBoot Web 所需要的两个包外:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

我们还需要导入 SpringBoot 的监控端点 actuator 包和 Prometheus 包:

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

另外为了便于开发,我还使用了 Lombok:

1
2
3
4
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

三、Metrics Enums

对于刚刚入门使用的同学,经常会出现 Metrics 指标的管理混乱的情况。在代码里这处注册一个指标,那处注册一个,很混乱。因此有必要提供一个类,专门管理所有的 Metrics 指标并统一注册。这里我使用枚举类实现,当然你也可以根据自己需要使用其他方式实现。

3.1 IMetricsEnum

Metrics 监控指标枚举,定义了指标的类型和名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface IMetricsEnum {
enum Type {GAUGE, COUNTER, TIMER}

String getName();

Type getType();

String getDesc();

default IMetricsTagEnum createVirtualMetricsTagEnum(String[] tags) {
IMetricsEnum iMetricsEnum = this;
return new IMetricsTagEnum() {
@Override
public IMetricsEnum getMetricsEnum() {
return iMetricsEnum;
}

@Override
public String[] getTags() {
return tags;
}
};
}
}

3.2 IMetricsTagEnum

负责维护所有的 Metrics 指标,是实际生效的 Metrics 指标。相较于 IMetricsEnum,需要额外指定 tag 属性。

1
2
3
4
5
6
7
public interface IMetricsTagEnum {
String FUNCTION = "function";

IMetricsEnum getMetricsEnum();

String[] getTags();
}

四、Metrics Support

首先编写一些使用 Prometheus 的 Metrics 的工具类,一共有以下几个类:

  • MetricsRegisterConfig 交于 Spring 容器管理,负责获取到 Prometheus 的全局实例对象
  • BaseMetricsUtil 基础的 Metrics 工具类,抽取一些共用方法
  • MetricsUtil Metrics 注册和各个类型指标值记录的工具类

4.1 MetricsRegisterConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MetricsRegisterConfig implements BeanPostProcessor {
@Value("${spring.application.name}")
private String applicationName;

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(bean instanceof MeterRegistry) {
MeterRegistry registry = (MeterRegistry) bean;
registry.config().commonTags("application", applicationName);

BaseMetricsUtil.meterRegistry = registry;
}

return bean;
}
}

4.2 BaseMetricsUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Slf4j
public class BaseMetricsUtil {
public static MeterRegistry meterRegistry;

protected static String CLASS_NAME = new Object() {
public String getClassName() {
String clazzName = this.getClass().getName();
return clazzName.substring(0, clazzName.lastIndexOf("$"));
}
}.getClassName();

public static boolean basicCheck(final IMetricsTagEnum metricsTagEnum) {
if (meterRegistry == null) {
log.warn("metrics registry is null,class={}", CLASS_NAME);
return false;
}

final String[] tags = metricsTagEnum.getTags();

if (tags != null && tags.length % 2 != 0) {
log.error("metrics count error,class={},tags={}", CLASS_NAME, tags);
return false;
}

return true;
}
}

4.3 MetricsUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131

@Slf4j
public class MetricsUtil extends BaseMetricsUtil {
private static final Map<IMetricsTagEnum, MetricsWrapper> METRICS_MAP = new HashMap<>();

static {
ThreadPoolUtil.newScheduledExecutor(1, "metrics-manager-thread-pool").scheduleWithFixedDelay(() -> METRICS_MAP.values().stream()
.filter(e -> e.getType() != IMetricsEnum.Type.COUNTER).filter(e -> TimeUtils.diffMs(e.getLastTime()) > 10_000)
.forEach(e -> e.recordTimerOrGauge(0L)), 15, 15, TimeUnit.SECONDS);
}

/**
* For IMetricsTagEnum
*/
public static <T extends IMetricsTagEnum> void init(Class<T> clazz) {
try {
Method method = clazz.getDeclaredMethod("values");
simpleRegister((T[]) method.invoke(null));
} catch (final Exception e) {
log.error("metrics gauge error,class={}", clazz.getSimpleName(), e);
}
}

/**
* For RingBuffer
*/
public static void registerGauge(final IMetricsTagEnum metricsTagEnum,
final Supplier<Number> supplier) {
if (!basicCheck(metricsTagEnum)) {
return;
}

Gauge.builder(metricsTagEnum.getMetricsEnum().getName(), supplier)
.tags(metricsTagEnum.getTags())
.description(metricsTagEnum.getMetricsEnum().getDesc())
.register(meterRegistry);
}

/**
* For Common
*/
public static void simpleRegister(IMetricsTagEnum... metricsTagEnums) {
if (metricsTagEnums != null && metricsTagEnums.length > 0) {
for (IMetricsTagEnum metricsTagEnum : metricsTagEnums) {
if (!basicCheck(metricsTagEnum)) {
continue;
}

final IMetricsEnum.Type type = metricsTagEnum.getMetricsEnum().getType();

if (type == IMetricsEnum.Type.GAUGE) {
Gauge.builder(metricsTagEnum.getMetricsEnum().getName(), METRICS_MAP, m -> (long) m.get(metricsTagEnum).getMetrics())
.tags(metricsTagEnum.getTags())
.description(metricsTagEnum.getMetricsEnum().getDesc())
.register(meterRegistry);

METRICS_MAP.put(metricsTagEnum, MetricsWrapper.newInstance(type, 0L));
} else if (type == IMetricsEnum.Type.COUNTER) {
final Counter cnt = Counter.builder(metricsTagEnum.getMetricsEnum().getName())
.tags(metricsTagEnum.getTags())
.description(metricsTagEnum.getMetricsEnum().getDesc())
.register(meterRegistry);

METRICS_MAP.put(metricsTagEnum, MetricsWrapper.newInstance(type, cnt));
} else if (type == IMetricsEnum.Type.TIMER) {
final Timer timer = Timer.builder(metricsTagEnum.getMetricsEnum().getName())
.tags(metricsTagEnum.getTags())
.description(metricsTagEnum.getMetricsEnum().getDesc())
.publishPercentiles(0.5, 0.9, 0.95, 0.99)
.register(meterRegistry);

METRICS_MAP.put(metricsTagEnum, MetricsWrapper.newInstance(type, timer));
}
}
}
}

/**
* For Counter
*/
public static void recordCounter(IMetricsTagEnum metricsTagEnum) {
recordCounter(metricsTagEnum, 1.0D);
}

/**
* For Counter
*/
public static void recordCounter(IMetricsTagEnum metricsTagEnum, double size) {
if (METRICS_MAP.containsKey(metricsTagEnum)) {
METRICS_MAP.get(metricsTagEnum).recordCounter(size);
}
}

/**
* For Timer、Gauge
*/
public static void recordTimerOrGauge(final IMetricsTagEnum metricsTagEnum, final long value) {
if (METRICS_MAP.containsKey(metricsTagEnum)) {
METRICS_MAP.get(metricsTagEnum).recordTimerOrGauge(value);
}
}

@Data
@NoArgsConstructor
@AllArgsConstructor
private static class MetricsWrapper {
private IMetricsEnum.Type type;

private Object metrics;

private long lastTime = TimeUtils.nowMs();

public static MetricsWrapper newInstance(IMetricsEnum.Type type, Object metrics) {
return new MetricsWrapper(type, metrics, TimeUtils.nowMs());
}

private void recordCounter(final double value) {
((Counter) this.metrics).increment(value);
this.lastTime = TimeUtils.nowMs();
}

public void recordTimerOrGauge(final long value) {
if (this.type == IMetricsEnum.Type.TIMER) {
((Timer) this.metrics).record(value, TimeUnit.MILLISECONDS);
} else if (this.type == IMetricsEnum.Type.GAUGE) {
this.metrics = value;
}
this.lastTime = TimeUtils.nowMs();
}
}
}

五、applicaiton.yaml

编辑程序的配置文件,主要是 management 相关的配置。

  • management.server.port 指定暴露的监控端点
  • management.endpoints.web.base-path SpringBoot 程序监控默认的根路径是 /actuator,我嫌它麻烦,给改成 /
  • management.endpoints.web.exposure.include SpringBoot 默认情况会将所有信息都暴露出去,这里我改成只暴露一部分,主要用的就是那个 prometheus【生产环境这些内容要么要加权限控制,要么尽量减少暴露部分,减少泄露信息的可能】
1
2
3
4
5
6
7
8
9
10
11
management:
server:
port: 7002
endpoints:
web:
base-path: /
exposure:
include: health, info, prometheus
spring:
application:
name: metrics-sample

六、结语

至此完成 SpringBoot Metrics 监控系统框架的搭建,下一节将开始演示指标的使用。