暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

天凉好个秋 好个秋 2020-02-17
407

女神镇楼


背景


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

监控和报警是稳定性工作中最基础的一环:监控系统的运行情况(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);
}
}

其中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


文章转载自天凉好个秋 好个秋,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论