女神镇楼
背景
对于服务端系统,保障系统稳定性是一个非常值得关注的问题,也是一个很复杂的工程问题。
监控和报警是稳定性工作中最基础的一环:监控系统的运行情况(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





