报警工具:
SpringBoot集成钉钉报警sdk(解决Failed to introspect Class异常)
实现5秒内出现10个异常时触发报警。需要使用Redis来进行全局控制。
思路:
借助Redis的zSet集合,获取一定时间范围内的set集合。判断set个个数是否满足条件,若满足则触发报警。
注意点:
- 防止多次报警:加阻塞性的分布式锁,一个线程处理时,其他线程等待,若线程触发报警后,清空redis。
- 报警触发机制:每次收到新的异常时,触发报警逻辑。
- 业务配置的优先级要高于全局配置。
- 因为使用zSet存储,所有防止数据结构自带的去重逻辑。
1. 配置类
配置类中可以配置全局配置以及各个业务的配置。
@Component
@ConfigurationProperties(prefix = "monitor.config")
@Data
public class MonitorProperties {
/**
* 全局监控配置。
*/
private MonitorInfo globeMonitor;
/**
* key:模块的名字,模块优先全局配置
*/
private Map<String, MonitorInfo> monitorMapping;
/**
* 钉钉监控报警的相关信息
*/
@Data
public static class MonitorInfo {
/**
* 是否打开全局的监控报警(默认关闭)
*/
private boolean openMonitor = false;
/**
* 全局的监控报警-间隔时间(默认4分钟),单位:ms
*/
private long monitorIntervalTimeMillis = 4 * 60 * 1000L;
/**
* 全局的监控报警-间隔时间内的异常数阈值,在间隔时间内若超过改阈值,便会报警。(默认30个)
*/
private int errorCount = 30;
/**
* 监控报警的钉钉群地址(必须配置,若不配置,则不会开启报警)
*/
private String webHook;
/**
* (选填)监控报警的钉钉群机器人@的手机号(填写手机号,会自动@通知群的同事)。填写多个时,使用,分割
*/
private String atMobiles;
}
}
yml的配置:
monitor:
config:
# 1. 全局监控报警
globe-monitor:
# 开启监控报警
open-monitor: true
# 间隔时间6s
monitor-interval-time-millis: 6000
# 异常数量阈值,在监控时间间隔内,若触发阈值,会发起报警
error-count: 10
# 钉钉报警的机器人地址(必须配置,若不配置,则不会开启报警)
web-hook: xxx
# (选填)监控报警的钉钉群机器人@的手机号(填写手机号,会自动@通知群的同事)。填写多个时,使用,分割
at-mobiles:
# 2. 各业务监控报警(优先级高于全局监控报警)
monitor-mapping:
# MsgTypeEnum枚举值的名字
XxxType:
open-monitor: true
error-count: 30
使用@ConfigurationProperties
注解时,注意在启动类加@EnableConfigurationProperties
注解,pom文件增加:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2. Redis的工具类
Redis的工具类需要提供阻塞式的分布式锁。
@Service
@Slf4j
public class RedisService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final String SUCCESS = "OK";
public StringRedisTemplate getStringRedisTemplate(){
return stringRedisTemplate;
}
/**
* 加锁的方法
*/
public Boolean vSetIfAbsent(String key, String value, long timeoutMillisecond) {
return stringRedisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
String result;
Object nativeConnection = connection.getNativeConnection();
if (nativeConnection instanceof JedisCluster) {
result = ((JedisCluster) nativeConnection).set(key, value, "NX", "PX", timeoutMillisecond);
return SUCCESS.equals(result);
} else if (nativeConnection instanceof Jedis) {
result = ((Jedis) nativeConnection).set(key, value, "NX", "PX", timeoutMillisecond);
return SUCCESS.equals(result);
} else {
return false;
}
}
});
}
/**
* 加锁,若加锁失败,便一直重试。重试的等待时间为waitMillisecond控制。
*
* @param key - key名称
* @param expireMillisecond - 锁成功后的有效期,毫秒
* @param waitMillisecond - 没有获取到锁时,保持阻塞的最大时间
* @return 加锁成功后持有的字符串。
*/
public String lock(String key, long expireMillisecond, long waitMillisecond) {
//当前时间
long currentTimeMillis = System.currentTimeMillis();
//失效时间
long invalidTimeMillis = currentTimeMillis + waitMillisecond;
String lockKey = "lock:" + key;
String lockValue = UUID.randomUUID().toString();
//加锁失败,返回的是false,那么sleep一段时间,再次尝试加锁
boolean keySet = vSetIfAbsent(lockKey, lockValue, expireMillisecond);
//锁失败并且没有达到最大失效时间
while (!keySet && System.currentTimeMillis() < invalidTimeMillis) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
log.error("", e);
}
keySet = vSetIfAbsent(lockKey, lockValue, expireMillisecond);
}
//加锁成功,返回加锁的字符串。
if (keySet) {
return lockValue;
}
return null;
}
/**
* 解锁
*
* @param key
*/
public void unlock(String key, String value) {
if (StringUtils.isBlank(value)) {
return;
}
String lockKey = "lock:" + key;
String lockValueRedis = stringRedisTemplate.opsForValue().get(lockKey);
if (StringUtils.equals(lockValueRedis, value)) {
stringRedisTemplate.delete(lockKey);
}
}
}
3. 报警类
@Component
public class MonitorService {
@Autowired
private MonitorProperties monitorProperties;
@Autowired
private RedisService redisService;
/**
* 推送消息的模板
*/
private static final String alertTemplate = "【报警】\n" + "[异常类型] %s \n"
+ "于[%d]秒内发生[%d]次错误,\n";
/**
* 接受到异常类
*
* @param e 异常类
*/
public void exceptionMonitor(Exception e, String type) {
//是否开启异常报警
if (errorEntryMonitor(e)) {
//读取配置
MonitorProperties.MonitorInfo monitorInfo = openMonitorByProperties(type);
//存在配置且合法
if (monitorInfo != null) {
//redis加锁,防止多次通知
String lockKey = "monitor:" + type;
String lockValue = redisService.lock(lockKey, 30 * 1000, 5 * 1000);
try {
//加锁成功
if (lockValue != null) {
//查询异常
String monitorKey = "monitor:score:" + type;
long maxScore = System.currentTimeMillis();
long minScore = maxScore - monitorInfo.getMonitorIntervalTimeMillis();
//获取间隔时间的异常类信息(注入,此处是set可能去重,加入时应该防止去重)
Set<String> errors = redisService.getStringRedisTemplate().opsForZSet().rangeByScore(monitorKey, minScore, maxScore);
//获取到报警的内容,为防止去重,增加id
String monitorContent = monitorContent(e);
//当异常数量满足时
if (errors != null && errors.size() == monitorInfo.getErrorCount() - 1) {
//存储最新一次的异常信息
errors.add(monitorContent);
//获取到原始的异常集合(去重前缀id)
Set<String> originalSets = errors.stream().
map(k -> k.split(":")[1]).
filter(k -> !"null".equals(k)).
collect(Collectors.toSet());
//报警信息
String message;
if (originalSets.stream().anyMatch(StringUtils::isNotBlank)) {
//详细信息是真正由用户控制
message = String.format(alertTemplate + ",详细信息%s", type, monitorInfo.getMonitorIntervalTimeMillis() / 1000,
monitorInfo.getErrorCount(), JSON.toJSONString(originalSets));
} else {
message = String.format(alertTemplate, type, monitorInfo.getMonitorIntervalTimeMillis() / 1000,
monitorInfo.getErrorCount());
}
//报警
DingtalkUtils.dingtalk(monitorInfo.getWebHook(), message, monitorInfo.getAtMobiles());
//报警成功后,移除Redis的数据
redisService.getStringRedisTemplate().opsForZSet().removeRangeByScore(monitorKey, minScore, maxScore);
} else {
//,直接放入到Redis中
redisService.getStringRedisTemplate().opsForZSet().add(monitorKey, monitorContent, maxScore);
//设置key的失效时间
redisService.getStringRedisTemplate().expire(monitorKey,monitorInfo.getMonitorIntervalTimeMillis(), TimeUnit.MILLISECONDS);
}
}
} finally {
//释放分布式锁
redisService.unlock(lockKey, lockValue);
}
}
}
}
/**
* 异常监控的前缀信息,在回显的时候,会移除掉前缀信息
* 此处+uuid是为了防止被set去重(可以换其他id)
*
* @return 前缀信息
*/
private String monitorContent(Exception e) {
return UUID.randomUUID().toString() + ":" + errorMonitorContent(e);
}
/**
* 真正报警的内容报警的内容
*/
protected String errorMonitorContent(Exception e) {
return null;
}
/**
* 判断是否开启异常报警
*/
protected boolean errorEntryMonitor(Exception e) {
return true;
}
/**
* 根据配置决定是否开启监控
*/
protected MonitorProperties.MonitorInfo openMonitorByProperties(String type) {
//读取个性化配置
Map<String, MonitorProperties.MonitorInfo> monitorMapping = monitorProperties.getMonitorMapping();
MonitorProperties.MonitorInfo monitorInfo;
//若个性化没有配置,读取全局配置
if (monitorMapping == null) {
monitorInfo = monitorProperties.getGlobeMonitor();
} else {
monitorInfo = monitorMapping.getOrDefault(type, monitorProperties.getGlobeMonitor());
}
//此时是开启的状态,判断其他参数是否合法
if (monitorInfo != null && monitorInfo.isOpenMonitor()) {
if (monitorInfo.getErrorCount() != 0
&& monitorInfo.getMonitorIntervalTimeMillis() != 0 &&
StringUtils.isNotBlank(monitorInfo.getWebHook())) {
return monitorInfo;
}
}
return null;
}
}
注:继承钉钉报警可以参考SpringBoot集成钉钉报警sdk(解决Failed to introspect Class异常)
网友评论