一、概述
目前博客上写的感觉都比较乱,很多都是基于java应用系统作埋点,并不是开发独立的exporter组件。**在开发自定义的exporter组件前,请自行了解一下什么是Spring Boot Actuator组件的作用。**简单点就是Actuator组件会将应用系统的健康检查,审计,指标收集,HTTP 跟踪等各项指标信息暴露给外部的模块(通常是接口的json格式),帮助我们监控和管理Spring Boot 应用。因为暴露内部信息的特性,Actuator 也可以和一些外部的应用监控系统整合(Prometheus, Graphite, DataDog, Influx, Wavefront, New Relic等)。这些监控系统提供了出色的仪表板,图形,分析和警报,可帮助你通过一个统一友好的界面,监视和管理你的应用程序。
二、开发思路
本篇文章的主要思路就是通过spring Boot开发Actuator组件接口转换成prometheus的对接格式。
三、项目搭建+对接
1. 新建项目
- IDEA新建spring Boot项目
- 填写项目包名信息
- 选择spring web组件以及对外暴露需要的Actuator组件
- 点击完成等待项目构建。
2. 配置Actuator组件
- application.yml文件新增项
##项目名称
spring:
application:
name: dm-prometheus
##对外的端口
server:
port: 8080
##actuator对外暴露所有的检查项
management:
endpoints:
web:
exposure:
include: '*'
- 启动项目
这个actuator会将一些指标暴露出来,
http://localhost:8080/actuator
具体的内容项看官方的图把
3. 对接prometheus接口
因为prometheus接口没有采用这种对接格式,需要通过转换依赖来解决格式转换问题。
- 引入依赖
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
2.调整配置yml文件
- 因为本次是开发自定义exporters,不需要其他的检查项,所以通过enabled-by-default直接关闭掉其他的检查项同时只提供prometheus接口
- 同时关闭jvm等检查项
spring:
application:
name: dm-prometheus
server:
port: 8080
management:
endpoints:
# 关闭所有的检查项
enabled-by-default: false
web:
exposure:
# 暴露监控接口,*为全部接口
include: 'prometheus'
## 省掉了/actuator前缀
base-path: "/"
# 对外暴露prometheus接口并显示更多健康信息
endpoint:
prometheus:
enabled: true
health:
show-details: always
## 关闭内置的检查项
metrics:
enable:
jvm: false
logback: false
files: false
tomcat: false
executor: false
disk: false
uptime: false
integration: false
- 重启项目
访问接口:http://localhost:8080/prometheus 就只要这些检查项,如果不需要可以通过注入bean形式去掉这些。在Q&A中第一条过滤掉查看方法。
四、自定义exporter组件
组件感觉是分为两种:
第一种是:业务系统埋点,意思是调用系统controller接口以后才会将信息更新到meter上。
第二种是:每次刷新接口时,都调用一次。
1. controller接口调用
controller
package com.dameng.prometheus_demo.controller;
import com.dameng.prometheus_demo.services.MeterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
public class MetricController {
@Autowired
MeterService meterService;
@RequestMapping("/metric1")
public String metric() {
meterService.test();
return "custom metric test";
}
}
service
package com.dameng.prometheus_demo.services;
public interface MeterService {
void test();
}
serviceImpl
package com.dameng.prometheus_demo.services;
import cn.hutool.core.util.RandomUtil;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import io.prometheus.client.CollectorRegistry;
import io.prometheus.client.Gauge;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.concurrent.atomic.AtomicInteger;
@Service
@Component
public class MeterServiceImpl implements MeterService {
@Autowired
MeterRegistry meterRegistry;
@Autowired
private CollectorRegistry collectorRegistry;
private AtomicInteger app_online_count;
private Gauge my_library_transactions_active ;
@PostConstruct
private void init(){
app_online_count = meterRegistry.gauge("dmdbms.temperature.gauge", Tags.of("site", "SiteA", "cab", "cab01"), new AtomicInteger(0));
my_library_transactions_active = Gauge.build()
.name("dmdbms_library_transactions_active")
.help("Active transactions.")
.labelNames("wy","zxjr","ocs","xxjf")
.register(collectorRegistry);
}
public void test(){
System.out.println("111");
app_online_count.set(RandomUtil.randomInt(10, 100));
meterRegistry.gauge("dmdbms.temperature.gauge1111", Tags.of("site", "SiteA", "cab", "cab222"), new AtomicInteger(2222));
//传参多个值
my_library_transactions_active.labels("site", "SiteA", "cab", "cab222").set(RandomUtil.randomInt(10, 100));
my_library_transactions_active.labels("site", "SiteB", "cab", "cab333").set(RandomUtil.randomInt(10, 100));
}
}
验证
先访问接口,在查看标签信息
http://localhost:8080/metric1 查看接口信息
2.MeterBinder接口
SpringBoot中提供了MeterBinder接口用于申明与注册meterRegistry。自定义Metrics只需要实现MeterBinder接口,Spring会自动发现并完成后续的杂活。
MeterBinder
增加dmdbms_system_memory_total接口
@Component
public class SystemQueryMeterBinder implements MeterBinder {
@Autowired
private SystemQueryService systemQueryService;
@Override
public void bindTo(@NonNull MeterRegistry registry) {
//获取内存总量
Gauge.builder("dmdbms_system_memory_total", systemQueryService, SystemQueryService::getOsCpuTotalPercentage)
// 这个 fen 会接到 order_amount的后面及在 prometheus 中的指标名称为 order_amount_fen
//.baseUnit("fen")
.description("os_system_memory_total")
// .tags("instance_name", hostName,"ip_addr",ipAddress)
//strongReference(true) 可以将 Gauge 引用的对象设置为强引用,但不要将一些将可能被垃圾收集的对象设置强引用,将会造成不必要的内存泄露, 如果指标数据中出现 NaN,可能是 gauge 弱引用的对象被垃圾回收
.strongReference(false)
.register(registry);
}
}
SystemQueryService
package com.dameng.prometheus_demo.services;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.system.oshi.OshiUtil;
import org.springframework.stereotype.Service;
@Service
public class SystemQueryService {
public double getOsCpuTotalPercentage() {
return RandomUtil.randomInt(10, 100);
}
}
Q&A
1.去掉指定格式的信息
package com.dameng.prometheus_demo.config;
import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CustomMeterFilter implements MeterFilter {
//只需要和dmdbms相关的指标数据,其余的数据一律拒绝
@Override
public MeterFilterReply accept(Meter.Id id) {
if (id.getName().startsWith("dmdbms")) {
return MeterFilterReply.ACCEPT;
}
return MeterFilterReply.DENY;
}
}
2.给所有标签加属性
package com.dameng.prometheus_demo.config;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
public class CustomMeterRegistryCustomizer implements MeterRegistryCustomizer<MeterRegistry> {
@Override
public void customize(MeterRegistry registry) {
//默认加参数
registry.config().commonTags("host_name", "localhost");
}
}
3.垃圾回收与NaN
指标值注册到registry中默认为弱引用,若函数调用调用周期结束,则该值会被 Java 给标记并 GC 掉。对应的指标输出的值则会很快会变成了NaN。
此种短状态适用于心跳类型的指标,在预警系统中可以及时发现没有按时上报的点。
但对于相对长时间想保持住特定指标值,需要显式给到对应变量强引用。比如使用一个实例化的HashMap来 cache 相关的值。
如上段代码中,方式一为强引用,方式二则弱引用。
4. 指标设计与选型
名称 + {一组tag} + 值 为一指标形式。
如某库位温度指标形式如下:
gn_temperature_gauge_value{application="xxx",cab="cab01",site="SiteA"} 37.0
tag中值为 String 型,尽量选取『可识别』,『有限集合』的值作为 tag,比如主机,柜号,库位等等。每一个tag值都会产生一个维度,不同的 tag值+名称会被记录成不同的时间序列。
某些**很有潜力膨胀到"无限量"**的值如各种ID、邮件地址、时间戳等,就不适宜选做 tag的值
后期若使用 grafana 绘制特定指标变化图,不同时间序列也会对应到不同的多条曲线。
指标值为 double 型: 对于自定义的数值型,如温度,访问次数等的指标,原样输出即可。
若输入某些状态类的值,可定义成数值型。比如 Prometheus 中的存活状态指标 UP = 1。类似如某后台 Job 状态(pending, running, stopped),可依样定义为10,20,30。
5. 两种常用指标类型(Metric Type)
gauge: 可增可减计数器,反应某值当前一刻状态。比如称重传感器的当前重量,温度传感器的当前温度。
方式一:
Gauge.builder("gn.temperature.gauge", new AtomicInteger(37), AtomicInteger::get)
方式二:
registry.gauge("gn.temperature.gauge", Tags.of("site", "SiteA", "cab", "cab01"), new AtomicInteger(37));
两者等价,会输出成如下指标:
# HELP gn_temperature_gauge_value for cab temperature # TYPE gn_temperature_gauge_value gn_temperature_gauge_value{application="xxx",cab="cab01",site="SiteA",} 37.0
注意定义的名称转换:其中".“被换成了”",gauge类型的指标最后加上了"value"做结尾。指标命名只能为ASCII字母、数字、下划线和冒号,且必须配正则表达式[a-zA-Z:][a-zA-Z0-9:]*
counter:只增不减计数器,是Gauge的一个特例。适用于只有服务器重启时候才会重置的计数场景。比如"用户访问次数",某接口失败次数"等等。API 使用方式类似。
Counter counter = Counter.builder("gn.beat.counter")
.tags("site", "SiteA", "function", "foo")
.description("for request errors")
.register(registry);
counter.increment();
会输出成如下指标:
# HELP gn_beat_counter_total for request errors # TYPE gn_beat_counter_total counter gn_beat_counter_total{application="xxx",function="foo",site="SiteA",} 1.0