基于企业微信的错误日志报警工具开发

2,558 阅读7分钟
原文链接: mp.weixin.qq.com

女神镇楼

背景

对于服务端系统,保障系统稳定性是一个非常值得关注的问题,也是一个很复杂的工程问题。

监控和报警是稳定性工作中最基础的一环:监控系统的运行情况(cpu,内存,磁盘,网络等),如果出现了异常情况,则发出对应的报警通知(比如钉钉消息、微信、短信、邮件或者是电话通知)。

代码异常突然增多(error级别日志增多)也是系统异常的一种,对于这种情况,收到报警消息之后,开发同学一般需要登录到线上机器,查看错误日志来排查具体的原因。

这种情况下,如果报警消息中能够包括出现异常的上下文以及异常堆栈,不仅能第一时间发现问题,这样的话还能够一定程度上提高问题的排查修复效率。

本文就基于企业微信和logback日志系统来实现error级别异常日志发送企业微信群消息报警功能,消息中包括了异常上下文以及异常堆栈消息。

代码地址:https://github.com/rio-2607/ErrorLogMonitor

过程

企业微信群消息接口

企业微信给所有的企业微信群提供了机器人功能,通过群机器人,可以提供一些自定义的消息推送。

  1. 在群名称右键点击添加群机器人。

  2. 点击添加机器人之后,会出现当前公司已经在使用的群机器人,可以直接添加已存在的机器人,或者新创建一个机器人。

  3. 点击新创建一个机器人之后,会要求设置机器人的名称(必选)以及机器人的头像(非必选)。

  4. 设置好之后就会出现机器人的webhook地址,同时也会有使用说明

    简单来说就是直接通过http调用webhook接口来发送群消息。

Logback

Logback是一个很优秀的开源的日志框架,国内外很多公司和项目都会使用它来记录系统的日志。实际使用时,在配置好配置文件后,只需要一行语句即可记录相应的日志信息,比如

logger.error("exception,",e);

Logback中有一些重要的概念:

  • LoggingEvent

    LoggingEvent表示日志事件,其中包括了所有与打印日志相关的信息,比如当前请求线程、当前时间、消息内容、请求级别等。

  • Logger

    Logger表示日志记录器,是打印日志的入口,打印日志时要先获取一个 Logger对象。

  • Appender

    Appender表示日志输出的目的地,即日志会发送到哪里进行处理。Logback允许一个日志输出到多个不同的目的地进行处理。常用的 Appender有控制台、文件、socket服务器、数据库等。一个Logger可以关联多个 Appender

  • Layout

    Layout负责对日志消息进行格式化,用户可以自主设置日志输出的格式。

实现

要实现error级别异常日志钉钉报警,就是要捕获所有的error级别的日志,然后解析出异常数据,调用企业微信接口发送消息即可。

Let's do it.

首先需要明确,微信报警消息中需要发送哪些数据。

新建MoitorRecord类,来定义微信报警中需要发送哪些数据。

public class MonitorRecord {    private String appName; // 发出报警的应用名称    private String ip; // 发出报警消息的机器所在的ip    private String hostName; // 发出报警消息的机器所在的主机名    private String env; // 哪个环境发出的报警,线上/预发/线下    private String userLoggedMsg; // 用户打印的消息,一般这个消息中包含了异常的上下文    private String stackMessage; // 异常堆栈消息    private String time; // 异常产生的时间}

接着新建AlarmService接口,来定义发送报警操作:

public interface AlarmService {    /**     * 发出报警消息     * @param record     * @return     */    boolean alarm(MonitorRecord record);}

由于这次是使用企业微信报警,所以新建类WechatAlarm类来实现企业微信发送消息操作:

public class WechatAlarm implements AlarmService {    private ExecutorService executorService;    private HttpClient client = new HttpClient();    private String webHookUrl;    public WechatAlarm() {}    public WechatAlarm(String webHookUrl, int coreThreadNum, int maxThreadNum) {        this.webHookUrl = webHookUrl;        executorService = ThreadPoolFactory.createExecutorService(coreThreadNum,maxThreadNum);    }    private Map<String,Object> buildParam(MonitorRecord record) {        Map<String,Object> map = new HashMap<>();        map.put("msgtype","text");        Map<String,String> content = new HashMap<>();        content.put("content",record.toString());        map.put("text",content);        return map;    }    @Override    public boolean alarm(MonitorRecord record) {        executorService.submit(() -> {            try {                // 在线程池中调用http接口发送微信消息                Map<String,Object> map = buildParam(record);                client.sendPostRequest(webHookUrl,map);            } catch (Exception e) {            }        });        return true;    }}

接下来是拦截error级别的日志。

前面说了,Logback中的Appender类用来表示日志的输出的目的地。所以我们只需要自定义一个 Appeder,然后在Logback的配置文件中的所有的Logger配置中(或者是所有Error级别的 Logger配置)增加这个自定义的Appeder就可以以拦截所有的(异常)日志。

在Logback中,要自定义Appeder,只需要继承 AppenderBase类实现append()方法即可。

我们首先定义一个抽象类AbstractMonitorAppender,该类继承自 AppenderBase类,并实现了append()方法:

public abstract class AbstractAlarmAppender extends AppenderBase<LoggingEvent> {    @Override    protected void append(LoggingEvent eventObject) {        try {            Level level = eventObject.getLevel();            if(Level.ERROR != level) {                // 只处理error级别的报错                return;            }            // 获取用户在日志中输出的语句,一般涵盖异常上下文            String userLogedErrorMessage = eventObject.getFormattedMessage();            String stackTraceInfo = "";            IThrowableProxy proxy = eventObject.getThrowableProxy();            if(null != proxy) {                // 获取异常堆栈                Throwable t = ((ThrowableProxy) proxy).getThrowable();                stackTraceInfo = ThrowableUtils.getThrowableStackTrace(t);            }            MonitorRecord record = MonitorRecord.buildRecord(stackTraceInfo,userLogedErrorMessage,                    getAppName(),getEnv());            monitor(record);        } catch (Exception e) {            addError("日志报警异常,异常原因:{}",e);        }    }    protected abstract String getAppName();    protected abstract String getEnv();    // 执行具体的监控报警操作    protected abstract void monitor(MonitorRecord monitorRecord);}

append()方法中,获取所有error级别的日志之后,解析出异常堆栈以及用户在日志中打印的数据,并构造 MonitorRecord对象,然后调用monitor()方法发送微信报警, monitor()方法是抽象方法,由子类实现。

接着新建WechatAlarmAppender类,继承自 AbstractAlarmAppender抽象类,实现monitor()方法。

public class WechatAlarmAppender extends AbstractAlarmAppender {    private String appName; // 使用报警工具的应用的名称    private int coreThreadNum; // 发送微信消息的线程池的核心线程池数量    private int maxThreadNum; // 发送微信消息的线程池的最大线程池数量    private String env; // 报警的环境    private String webHookUrl; // 企业微信报警接口url    private AlarmService alarmService;    public WechatAlarmAppender() {    }    public void setWebHookUrl(String webHookUrl) {        this.webHookUrl = webHookUrl;    }    public void setAppName(String appName) {        this.appName = appName;    }    public void setCoreThreadNum(int coreThreadNum) {        this.coreThreadNum = coreThreadNum;    }    public void setMaxThreadNum(int maxThreadNum) {        this.maxThreadNum = maxThreadNum;    }    public void setEnv(String env) {        this.env = env;    }    @Override    protected String getAppName() {        return this.appName;    }    @Override    protected String getEnv() {        return this.env;    }    @Override    protected void monitor(MonitorRecord monitorRecord) {        if(null == alarmService) {            synchronized (this) {                if(null == alarmService) {                    //  monitorService需要保持单例且要懒加载                    alarmService = new WechatAlarm(webHookUrl,coreThreadNum,maxThreadNum);                }            }        }        alarmService.alarm(monitorRecord);    }}

其中appNamecoreThreadNummaxThreadNumenvwebHookUrl这几个参数是在配置 WechatAlarmAppender的时候需要传入的。

使用

前面已经实现了WechatAlarmAppender类,现在需要在Logback的配置文件 logback-boot.xml中配置这个自定义的Appender,然后在 Logger配置中新增这个Appender即可:

<appender name="WECHAT_APPENDER" class="com.beautyboss.slogen.errorlog.monitor.appender.WechatAlarmAppender">    <!--使用该组件的应用名称    -->    <appName>test</appName>    <!-- 发送微信消息的线程池的核心线程数量-->    <coreThreadNum>1</coreThreadNum>    <!-- 发送微信消息的线程池的最大线程数量-->    <maxThreadNum>2</maxThreadNum>    <!--环境-->    <env>dev</env>    <!--企业微信群机器人webhookurl地址-->    <webHookUrl>这里配置微信群机器人webhook地址</webHookUrl></appender><root level="${root.log.level}">    <appender-ref ref="APP_FILE"/>    <appender-ref ref="ERROR_FILE" />    <appender-ref ref="STDOUT"/>    <!--新增appender-->    <appender-ref ref="WECHAT_APPENDER"/></root>

这样配置以后,项目中所有使用log.error()方法打印的日志(即error级别日志)都会通过企业微信发出消息报警。

测试

测试代码如下:

public void test() {    int num1 = 10;    int num2 = 0;    try {        int i = num1 / num2;    } catch (Exception e) {        log.error("{} / {} exception",num1,num2,e);    }}

结果如下图所示:

result.png