報(bào)警工具:
SpringBoot集成釘釘報(bào)警sdk(解決Failed to introspect Class異常)
實(shí)現(xiàn)5秒內(nèi)出現(xiàn)10個(gè)異常時(shí)觸發(fā)報(bào)警祷安。需要使用Redis來進(jìn)行全局控制蘸鲸。
思路:
借助Redis的zSet集合朵夏,獲取一定時(shí)間范圍內(nèi)的set集合妄辩。判斷set個(gè)個(gè)數(shù)是否滿足條件碍舍,若滿足則觸發(fā)報(bào)警埂奈。
注意點(diǎn):
- 防止多次報(bào)警:加阻塞性的分布式鎖若贮,一個(gè)線程處理時(shí)戳吝,其他線程等待浩销,若線程觸發(fā)報(bào)警后,清空redis听哭。
- 報(bào)警觸發(fā)機(jī)制:每次收到新的異常時(shí)慢洋,觸發(fā)報(bào)警邏輯。
- 業(yè)務(wù)配置的優(yōu)先級(jí)要高于全局配置陆盘。
- 因?yàn)槭褂脄Set存儲(chǔ)普筹,所有防止數(shù)據(jù)結(jié)構(gòu)自帶的去重邏輯。
1. 配置類
配置類中可以配置全局配置以及各個(gè)業(yè)務(wù)的配置隘马。
@Component
@ConfigurationProperties(prefix = "monitor.config")
@Data
public class MonitorProperties {
/**
* 全局監(jiān)控配置斑芜。
*/
private MonitorInfo globeMonitor;
/**
* key:模塊的名字,模塊優(yōu)先全局配置
*/
private Map<String, MonitorInfo> monitorMapping;
/**
* 釘釘監(jiān)控報(bào)警的相關(guān)信息
*/
@Data
public static class MonitorInfo {
/**
* 是否打開全局的監(jiān)控報(bào)警(默認(rèn)關(guān)閉)
*/
private boolean openMonitor = false;
/**
* 全局的監(jiān)控報(bào)警-間隔時(shí)間(默認(rèn)4分鐘),單位:ms
*/
private long monitorIntervalTimeMillis = 4 * 60 * 1000L;
/**
* 全局的監(jiān)控報(bào)警-間隔時(shí)間內(nèi)的異常數(shù)閾值祟霍,在間隔時(shí)間內(nèi)若超過改閾值杏头,便會(huì)報(bào)警。(默認(rèn)30個(gè))
*/
private int errorCount = 30;
/**
* 監(jiān)控報(bào)警的釘釘群地址(必須配置沸呐,若不配置醇王,則不會(huì)開啟報(bào)警)
*/
private String webHook;
/**
* (選填)監(jiān)控報(bào)警的釘釘群機(jī)器人@的手機(jī)號(hào)(填寫手機(jī)號(hào),會(huì)自動(dòng)@通知群的同事)崭添。填寫多個(gè)時(shí)寓娩,使用,分割
*/
private String atMobiles;
}
}
yml的配置:
monitor:
config:
# 1. 全局監(jiān)控報(bào)警
globe-monitor:
# 開啟監(jiān)控報(bào)警
open-monitor: true
# 間隔時(shí)間6s
monitor-interval-time-millis: 6000
# 異常數(shù)量閾值,在監(jiān)控時(shí)間間隔內(nèi),若觸發(fā)閾值棘伴,會(huì)發(fā)起報(bào)警
error-count: 10
# 釘釘報(bào)警的機(jī)器人地址(必須配置寞埠,若不配置,則不會(huì)開啟報(bào)警)
web-hook: xxx
# (選填)監(jiān)控報(bào)警的釘釘群機(jī)器人@的手機(jī)號(hào)(填寫手機(jī)號(hào)焊夸,會(huì)自動(dòng)@通知群的同事)仁连。填寫多個(gè)時(shí),使用,分割
at-mobiles:
# 2. 各業(yè)務(wù)監(jiān)控報(bào)警(優(yōu)先級(jí)高于全局監(jiān)控報(bào)警)
monitor-mapping:
# MsgTypeEnum枚舉值的名字
XxxType:
open-monitor: true
error-count: 30
使用@ConfigurationProperties
注解時(shí)阱穗,注意在啟動(dòng)類加@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;
}
}
});
}
/**
* 加鎖揪阶,若加鎖失敗昌抠,便一直重試。重試的等待時(shí)間為waitMillisecond控制鲁僚。
*
* @param key - key名稱
* @param expireMillisecond - 鎖成功后的有效期,毫秒
* @param waitMillisecond - 沒有獲取到鎖時(shí)炊苫,保持阻塞的最大時(shí)間
* @return 加鎖成功后持有的字符串。
*/
public String lock(String key, long expireMillisecond, long waitMillisecond) {
//當(dāng)前時(shí)間
long currentTimeMillis = System.currentTimeMillis();
//失效時(shí)間
long invalidTimeMillis = currentTimeMillis + waitMillisecond;
String lockKey = "lock:" + key;
String lockValue = UUID.randomUUID().toString();
//加鎖失敗冰沙,返回的是false侨艾,那么sleep一段時(shí)間,再次嘗試加鎖
boolean keySet = vSetIfAbsent(lockKey, lockValue, expireMillisecond);
//鎖失敗并且沒有達(dá)到最大失效時(shí)間
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. 報(bào)警類
@Component
public class MonitorService {
@Autowired
private MonitorProperties monitorProperties;
@Autowired
private RedisService redisService;
/**
* 推送消息的模板
*/
private static final String alertTemplate = "【報(bào)警】\n" + "[異常類型] %s \n"
+ "于[%d]秒內(nèi)發(fā)生[%d]次錯(cuò)誤声畏,\n";
/**
* 接受到異常類
*
* @param e 異常類
*/
public void exceptionMonitor(Exception e, String type) {
//是否開啟異常報(bào)警
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();
//獲取間隔時(shí)間的異常類信息(注入,此處是set可能去重插龄,加入時(shí)應(yīng)該防止去重)
Set<String> errors = redisService.getStringRedisTemplate().opsForZSet().rangeByScore(monitorKey, minScore, maxScore);
//獲取到報(bào)警的內(nèi)容愿棋,為防止去重,增加id
String monitorContent = monitorContent(e);
//當(dāng)異常數(shù)量滿足時(shí)
if (errors != null && errors.size() == monitorInfo.getErrorCount() - 1) {
//存儲(chǔ)最新一次的異常信息
errors.add(monitorContent);
//獲取到原始的異常集合(去重前綴id)
Set<String> originalSets = errors.stream().
map(k -> k.split(":")[1]).
filter(k -> !"null".equals(k)).
collect(Collectors.toSet());
//報(bào)警信息
String message;
if (originalSets.stream().anyMatch(StringUtils::isNotBlank)) {
//詳細(xì)信息是真正由用戶控制
message = String.format(alertTemplate + "均牢,詳細(xì)信息%s", type, monitorInfo.getMonitorIntervalTimeMillis() / 1000,
monitorInfo.getErrorCount(), JSON.toJSONString(originalSets));
} else {
message = String.format(alertTemplate, type, monitorInfo.getMonitorIntervalTimeMillis() / 1000,
monitorInfo.getErrorCount());
}
//報(bào)警
DingtalkUtils.dingtalk(monitorInfo.getWebHook(), message, monitorInfo.getAtMobiles());
//報(bào)警成功后糠雨,移除Redis的數(shù)據(jù)
redisService.getStringRedisTemplate().opsForZSet().removeRangeByScore(monitorKey, minScore, maxScore);
} else {
//,直接放入到Redis中
redisService.getStringRedisTemplate().opsForZSet().add(monitorKey, monitorContent, maxScore);
//設(shè)置key的失效時(shí)間
redisService.getStringRedisTemplate().expire(monitorKey,monitorInfo.getMonitorIntervalTimeMillis(), TimeUnit.MILLISECONDS);
}
}
} finally {
//釋放分布式鎖
redisService.unlock(lockKey, lockValue);
}
}
}
}
/**
* 異常監(jiān)控的前綴信息徘跪,在回顯的時(shí)候甘邀,會(huì)移除掉前綴信息
* 此處+uuid是為了防止被set去重(可以換其他id)
*
* @return 前綴信息
*/
private String monitorContent(Exception e) {
return UUID.randomUUID().toString() + ":" + errorMonitorContent(e);
}
/**
* 真正報(bào)警的內(nèi)容報(bào)警的內(nèi)容
*/
protected String errorMonitorContent(Exception e) {
return null;
}
/**
* 判斷是否開啟異常報(bào)警
*/
protected boolean errorEntryMonitor(Exception e) {
return true;
}
/**
* 根據(jù)配置決定是否開啟監(jiān)控
*/
protected MonitorProperties.MonitorInfo openMonitorByProperties(String type) {
//讀取個(gè)性化配置
Map<String, MonitorProperties.MonitorInfo> monitorMapping = monitorProperties.getMonitorMapping();
MonitorProperties.MonitorInfo monitorInfo;
//若個(gè)性化沒有配置,讀取全局配置
if (monitorMapping == null) {
monitorInfo = monitorProperties.getGlobeMonitor();
} else {
monitorInfo = monitorMapping.getOrDefault(type, monitorProperties.getGlobeMonitor());
}
//此時(shí)是開啟的狀態(tài)垮庐,判斷其他參數(shù)是否合法
if (monitorInfo != null && monitorInfo.isOpenMonitor()) {
if (monitorInfo.getErrorCount() != 0
&& monitorInfo.getMonitorIntervalTimeMillis() != 0 &&
StringUtils.isNotBlank(monitorInfo.getWebHook())) {
return monitorInfo;
}
}
return null;
}
}
注:繼承釘釘報(bào)警可以參考SpringBoot集成釘釘報(bào)警sdk(解決Failed to introspect Class異常)