美文网首页一些收藏
SpringBoot—实现n秒内出现x个异常报警

SpringBoot—实现n秒内出现x个异常报警

作者: 小胖学编程 | 来源:发表于2020-11-12 17:09 被阅读0次

报警工具:
SpringBoot集成钉钉报警sdk(解决Failed to introspect Class异常)

实现5秒内出现10个异常时触发报警。需要使用Redis来进行全局控制。

思路:
借助Redis的zSet集合,获取一定时间范围内的set集合。判断set个个数是否满足条件,若满足则触发报警。

注意点:

  1. 防止多次报警:加阻塞性的分布式锁,一个线程处理时,其他线程等待,若线程触发报警后,清空redis。
  2. 报警触发机制:每次收到新的异常时,触发报警逻辑。
  3. 业务配置的优先级要高于全局配置。
  4. 因为使用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异常)

相关文章

网友评论

    本文标题:SpringBoot—实现n秒内出现x个异常报警

    本文链接:https://www.haomeiwen.com/subject/qxjpbktx.html