机器人实时通知

1,155 阅读2分钟

企业微信机器人通知

描述

生产环境对异常监控实时性要求比较高,通常情况大家都会使用邮件报警,非常重要的会使用短信报警。邮件大家容易忽略,而且邮箱空间有限需要定期清理,短信又要收费,所以我们要采用一种既要免费又要实时性高的方式,那就是大家平时工作所用的沟通工具(企微,钉钉)

目的

ERROR级别日志实时报警

自定义业务预警通知

对系统无侵入设计

能够快速定位问题

最重要能够实时群发通知

邮件通知

image.png

可以看到邮件通知非常多,而且及时性也不够

方案

既然要无侵入设计,就不能到处埋点,最终选择自定义Appender的方式处理

由于公司内部工作沟通工具是企业微信,所以就借助企业微信机器人发送实时通知消息,根据请求链路ID结合ELK工具就可以从消息通知直接跳转到ELK,查看完整的链路日志从而快速定位问题

废话不多说直接上代码。。。

ErrorWarnAppender

public class ErrorWarnAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
 
    @Setter
    private String packageNames;
 
    @Override
    protected void append(ILoggingEvent event) {
        if (null == event || "DEV".equalsIgnoreCase(ApplicationHelper.getEnv()) || StrUtil.isEmpty(MDC.get(ScmConstant.REQUEST_ID))) {
            return;
        }
        boolean success = Arrays.stream(packageNames.split(",")).anyMatch(event.getLoggerName()::startsWith);
        if (!success) {
            return;
        }
 
        if (null == event.getThrowableProxy()) {
            RobotUtil.sendWarn(event.getFormattedMessage());
        } else {
            RobotUtil.sendWarn(event.getFormattedMessage(), event.getThrowableProxy().getClassName() + ":" + event.getThrowableProxy().getMessage());
        }
    }
}

RobotUtil

public class RobotUtil {
 
    /**
     * 警告地址
     */
    private static final String WARN_URL = "XXX";
 
    /**
     * 通知地址
     */
    private static final String NOTICE_URL = "XXX";
 
    /**
     * 测试环境ELK地址
     */
    private static final String ELK_URL = "XXX";
 
    /**
     * 生产环境ELK地址
     */
    private static final String PRD_ELK_URL = "XXX";
 
    /**
     * 发送企业微信机器人通知消息
     *
     * @param message
     */
    public static void sendNotice(StringBuilder message) {
        try {
            // 发送通知消息
            if (isPro()) {
                HttpUtil.post(NOTICE_URL, RobotUtil.getMessage(message));
            } else {
                message.insert(0, "env:" + ApplicationHelper.getEnv() + "\n");
                HttpUtil.post(WARN_URL, RobotUtil.getMessage(message));
            }
        } catch (Exception ignored) {
        }
    }
 
    /**
     * 发送企业微信机器人警告消息
     *
     * @param message
     */
    public static void sendWarn(String message) {
        sendWarn(message, null);
    }
 
    /**
     * 发送企业微信机器人警告消息
     *
     * @param message
     * @param exception
     */
    public static void sendWarn(String message, String exception) {
        if (!ApplicationHelper.getSendWarnEnable()) {
            return;
        }
        try {
            String requestId = MDC.get(ScmConstant.REQUEST_ID);
            StringBuilder text = new StringBuilder();
            text.append("env:").append(isPro() ? "<font color=\"red\">**生产环境**</font>" : ApplicationHelper.getEnv()).append("\n");
            text.append("applicationName:").append(ApplicationHelper.getApplicationName()).append("\n");
            if (StrUtil.isNotEmpty(requestId)) {
                text.append("requestId:").append("[").append(requestId).append("](").append(String.format(isPro() ? PRD_ELK_URL : ELK_URL, requestId)).append(")").append("\n");
            }
            text.append("message:").append(StrUtil.subPre(message, 1000)).append("\n");
            if (StrUtil.isNotEmpty(exception)) {
                text.append("exception:").append(StrUtil.subPre(exception, 1000)).append("\n");
            }
            // 发送警告消息
            HttpUtil.post(WARN_URL, RobotUtil.getMessage(text));
        } catch (Exception ignored) {
        }
    }
 
    /**
     * 消息内容
     *
     * @param message
     * @return
     */
    private static String getMessage(Object message) {
        Map<String, Object> textMap = new HashMap<>();
        textMap.put("msgtype", "markdown");
        Map<String, Object> contentMap = new HashMap<>();
        contentMap.put("content", message);
        textMap.put("markdown", contentMap);
        return JSONUtil.toJsonStr(textMap);
    }
 
    /**
     * 是否生产环境
     *
     * @return
     */
    private static boolean isPro() {
        return "PRO".equalsIgnoreCase(ApplicationHelper.getEnv());
    }
 
}

logback配置

1、添加appender 配置
<appender name="ERROR_WARN" class="org.slf4j.ErrorWarnAppender">
    <packageNames>org.xx,com.xx,com.yy</packageNames>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
        <level>ERROR</level>
    </filter>
</appender>

2、各个环境增加appender
<springProfile name="test">
    <root level="INFO">
        <appender-ref ref="INFO_FILE"/>
        <appender-ref ref="ERROR_FILE"/>
        <appender-ref ref="ERROR_WARN"/>
    </root>
</springProfile>

<springProfile name="prd">
    <root level="INFO">
        <appender-ref ref="INFO_FILE"/>
        <appender-ref ref="ERROR_FILE"/>
        <appender-ref ref="ERROR_WARN"/>
    </root>
</springProfile>

效果展示

image.png image.png

可以看到消息能够区分环境,应用名称,链路ID,错误信息(生产环境红色醒目提醒)

注意事项

1、消息每分钟最多发送20\color{red}消息每分钟最多发送20条

发送企业微信消息是https请求接口,每分钟限制最多20条消息,所以异常报警只发送error级别的通知

同时还可以借助于kafka异步消息,发送邮件,短信 等等消息通知

2、消息体长度最大支持4000字符\color{red}消息体长度最大支持4000字符

我们会把message和exception 截取前1000个字符

3、需要简单了解markdown语法\color{red}需要简单了解markdown语法