女神镇楼
背景
对于服务端系统,保障系统稳定性是一个非常值得关注的问题,也是一个很复杂的工程问题。
监控和报警是稳定性工作中最基础的一环:监控系统的运行情况(cpu,内存,磁盘,网络等),如果出现了异常情况,则发出对应的报警通知(比如钉钉消息、微信、短信、邮件或者是电话通知)。
代码异常突然增多(error级别日志增多)也是系统异常的一种,对于这种情况,收到报警消息之后,开发同学一般需要登录到线上机器,查看错误日志来排查具体的原因。
这种情况下,如果报警消息中能够包括出现异常的上下文以及异常堆栈,不仅能第一时间发现问题,这样的话还能够一定程度上提高问题的排查修复效率。
本文就基于企业微信和logback日志系统来实现error级别异常日志发送企业微信群消息报警功能,消息中包括了异常上下文以及异常堆栈消息。
代码地址:https://github.com/rio-2607/ErrorLogMonitor
过程
企业微信群消息接口
企业微信给所有的企业微信群提供了机器人功能,通过群机器人,可以提供一些自定义的消息推送。
-
在群名称右键点击添加群机器人。
-
点击添加机器人之后,会出现当前公司已经在使用的群机器人,可以直接添加已存在的机器人,或者新创建一个机器人。
-
点击新创建一个机器人之后,会要求设置机器人的名称(必选)以及机器人的头像(非必选)。
-
设置好之后就会出现机器人的webhook地址,同时也会有使用说明
简单来说就是直接通过http调用webhook接口来发送群消息。
Logback
Logback是一个很优秀的开源的日志框架,国内外很多公司和项目都会使用它来记录系统的日志。实际使用时,在配置好配置文件后,只需要一行语句即可记录相应的日志信息,比如
logger.error("exception,",e);
Logback中有一些重要的概念:
-
LoggingEventLoggingEvent表示日志事件,其中包括了所有与打印日志相关的信息,比如当前请求线程、当前时间、消息内容、请求级别等。 -
LoggerLogger表示日志记录器,是打印日志的入口,打印日志时要先获取一个Logger对象。 -
AppenderAppender表示日志输出的目的地,即日志会发送到哪里进行处理。Logback允许一个日志输出到多个不同的目的地进行处理。常用的Appender有控制台、文件、socket服务器、数据库等。一个Logger可以关联多个Appender。 -
LayoutLayout负责对日志消息进行格式化,用户可以自主设置日志输出的格式。
实现
要实现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); }}
其中appName、 coreThreadNum、maxThreadNum、 env和webHookUrl这几个参数是在配置 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