我正在参加「掘金·启航计划」
前言
在web项目中,日志文件是一个非常重要的文档,它可以在程序运行情况和报错情况进行留存,方便我们在程序报错时进行问题的查找与确认。在线上运行的服务中,如果我想实时得到报错信息该怎么解决呢?下面我会介绍一种实时错误推送的方式:logback+自定义appender+飞书捷径+webhook,效果如下:
步骤1:配置pom.xml文件
<!-- 环境 -->
<profiles>
<!-- 开发环境 -->
<profile>
<id>dev</id>
<activation>
<!-- 默认激活配置 -->
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<!-- 当前环境 -->
<profile.name>dev</profile.name>
<!-- 飞书告警开关 -->
<feishu.alarm>false</feishu.alarm>
<!-- 飞书告警webhook -->
<feishu.webhook>https://hzebin.cn</feishu.webhook>
</properties>
</profile>
<!-- 测试环境 -->
<profile>
<!-- 测试环境的相关配置 -->
</profile>
<!-- 生产环境 -->
<profile>
<!-- 生产环境的相关配置 -->
</profile>
</profiles>
pom.xml可以配置多个环境的配置,以dev开发环境为例,在properties标签里,我自己定义了<feishu.alarm>是否进行飞书告警和<feishu.webhook>飞书告警的webhook。
步骤2:配置logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="false">
<springProperty scop="context" name="spring.application.name" source="spring.application.name" defaultValue="logs"/>
<property name="log.path" value="logs/${spring.application.name}"/>
<!-- Console log output -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %cyan(%logger{50}) - %highlight(%msg) %n</pattern>
</encoder>
</appender>
<!-- Log file debug output -->
<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/%d{yyyy-MM}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
</encoder>
</appender>
<!-- Log ferrorsrror output -->
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<!--name表示环境,用逗号分隔-->
<springProfile name="test,product">
<!--推送ERROR信息到飞书-->
<appender name="FeiShuLogbackAppender" class="cn.hzebin.common.appender.FeiShuLogbackAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!-- 日志收集最低日志级别 -->
<level>ERROR</level>
</filter>
<feishuAlarm>@feishu.alarm@</feishuAlarm>
<feishuWebhook>@feishu.webhook@</feishuWebhook>
<appName>uaa</appName>
</appender>
</springProfile>
<!--开发环境:打印控制台-->
<springProfile name="dev">
<logger name="cn.hzebin" level="debug"/>
<logger name="java.sql.Connection" level="debug"/>
<logger name="java.sql.Statement" level="debug"/>
<logger name="java.sql.PreparedStatement" level="debug"/>
<logger name="com.alibaba.nacos.client.naming" level="off" />
</springProfile>
<!--测试环境:打印控制台-->
<springProfile name="test">
<logger name="cn.hzebin" level="debug"/>
<logger name="java.sql.Connection" level="debug"/>
<logger name="java.sql.Statement" level="debug"/>
<logger name="java.sql.PreparedStatement" level="debug"/>
<logger name="com.alibaba.nacos.client.naming" level="off" />
</springProfile>
<!--生产环境:打印控制台-->
<springProfile name="product">
<logger name="cn.hzebin" level="debug"/>
<logger name="java.sql.Connection" level="debug"/>
<logger name="java.sql.Statement" level="debug"/>
<logger name="java.sql.PreparedStatement" level="debug"/>
<logger name="com.alibaba.nacos.client.naming" level="off" />
</springProfile>
<!-- Level: FATAL 0 ERROR 3 WARN 4 INFO 6 DEBUG 7 -->
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="debug"/>
<appender-ref ref="error"/>
<!--特定环境生效的appender-->
<springProfile name="test,product">
<appender-ref ref="FeiShuLogbackAppender" />
</springProfile>
</root>
</configuration>
先来讲解一下上面配置的logback信息:
configuration
- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。
- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
property
用来定义变量值的标签,property标签有两个属性,name和value;其中name的值是变量的名称,value的值时变量定义的值。通过property定义的值会被插入到logger上下文中。定义变量后,可以使“${name}”来使用变量。如上面的xml所示。
root
根logger,也是一种logger,且只有一个level属性
appender
负责写日志的组件,appender 有两个属性 name和class;name指定appender名称,class指定appender的全限定名。上面声明的是名为GLMAPPER-LOGGERONE,class为ch.qos.logback.core.rolling.RollingFileAppender的一个appender。
appender 的种类
- ConsoleAppender:把日志添加到控制台
- FileAppender:把日志添加到文件
- RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件。它是FileAppender的子类
appender 子标签
<append>true</append>
如果是 true,日志被追加到文件结尾,如果是false,清空现存文件,默认是true。
filter 子标签
在简介中提到了filter;作用就是上面说的。可以为appender 添加一个或多个过滤器,可以用任意条件对日志进行过滤。appender 有多个过滤器时,按照配置顺序执行。
ThresholdFilter
临界值过滤器,过滤掉低于指定临界值的日志。当日志级别等于或高于临界值时,过滤器返回NEUTRAL;当日志级别低于临界值时,日志会被拒绝。
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
LevelFilter
级别过滤器,根据日志级别进行过滤。如果日志级别等于配置级别,过滤器会根据onMath(用于配置符合过滤条件的操作) 和 onMismatch(用于配置不符合过滤条件的操作)接收或拒绝日志。
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
关于NEUTRAL、ACCEPT、DENY 见上文简介中关于filter的介绍。
file 子标签
file 标签用于指定被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。
<file>
${logging.path}/glmapper-spring-boot/glmapper-loggerone.log
</file>
这个表示当前appender将会将日志写入到${logging.path}/glmapper-spring-boot/glmapper-loggerone.log这个目录下。
rollingPolicy 子标签
这个子标签用来描述滚动策略的。这个只有appender的class是RollingFileAppender时才需要配置。这个也会涉及文件的移动和重命名(a.log->a.log.2018.07.22)。
TimeBasedRollingPolicy
最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。这个下面又包括了两个属性:
- FileNamePattern
- maxHistory
<rollingPolicy
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名:按天回滚 daily -->
<FileNamePattern>
${logging.path}/glmapper-spring-boot/glmapper-loggerone.log.%d{yyyy-MM-dd}
</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
上面的这段配置表明每天生成一个日志文件,保存30天的日志文件
FixedWindowRollingPolicy
根据固定窗口算法重命名文件的滚动策略。
encoder 子标签
对记录事件进行格式化。它干了两件事:
- 把日志信息转换成字节数组
- 把字节数组写入到输出流
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}
- %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
目前encoder只有PatternLayoutEncoder一种类型。
步骤3:自定义一个告警推送的appender
public class FeiShuLogbackAppender extends UnsynchronizedAppenderBase<ILoggingEvent> {
Layout<ILoggingEvent> layout;
/**
* 自定义配置
*/
String printString;
/**
* 飞书告警开关
*/
private Boolean feishuAlarm;
/**
* 飞书webhook
*/
private String feishuWebhook;
private String appName;
@Override
public void start() {
//这里可以做些初始化判断 比如layout不能为null ,
if (layout == null) {
addWarn("Layout was not defined");
}
//或者写入数据库 或者redis时 初始化连接等等
super.start();
}
@Override
public void stop() {
//释放相关资源,如数据库连接,redis线程池等等
System.out.println("logback-stop方法被调用");
if (!isStarted()) {
return;
}
super.stop();
}
@Override
public void append(ILoggingEvent event) {
if (event == null || !isStarted()) {
return;
}
if (feishuAlarm != null && feishuAlarm && event.getLevel() == Level.ERROR) {
IThrowableProxy iThrowableProxy = event.getThrowableProxy();
StringBuilder sb = new StringBuilder();
if (iThrowableProxy != null && iThrowableProxy instanceof ThrowableProxy) {
ThrowableProxy throwableProxy = (ThrowableProxy) iThrowableProxy;
Throwable throwable = throwableProxy.getThrowable();
String throwableMsg = throwable.getMessage();
StackTraceElementProxy[] stackTraceElementProxy = iThrowableProxy.getStackTraceElementProxyArray();
//sb.append(event.getMessage()).append("\n");
if (StringUtils.isNotEmpty(throwableMsg)) {
sb.append(throwableMsg).append("\n");
}
int i = 0;
for (StackTraceElementProxy proxy : stackTraceElementProxy) {
//只打印30行的堆栈
sb.append(proxy.getSTEAsString()).append("\n");
if (++i > 30) {
break;
}
}
} else {
sb.append(event.getMessage());
}
String msg = sb.toString();
if (StringUtils.isNotEmpty(msg)) {
sendFeishuMsg(event.getFormattedMessage() + "\n" + msg);
System.out.println();
}
}
}
void sendFeishuMsg(String msg) {
msg = parseMsg(msg);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Map map = new HashMap<String, Object>();
map.put("datetime", dateFormat.format(Calendar.getInstance().getTime()));
map.put("appName", appName);
map.put("errorInfo", msg);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
ResponseEntity<JSONObject> responseEntity = new RestTemplate().postForEntity(feishuWebhook, new HttpEntity<>(JSON.toJSON(map).toString(), headers), JSONObject.class);
}
private String parseMsg(String s) {
StringBuffer sb = new StringBuffer();
for (int i=0; i<s.length(); i++) {
char c = s.charAt(i);
switch (c){
case '"':
sb.append("\"");
break;
case '\':
sb.append("\\");
break;
case '/':
sb.append("\/");
break;
case '\b':
sb.append("\b");
break;
case '\f':
sb.append("\f");
break;
case '\n':
sb.append("\n");
break;
case '\r':
sb.append("\r");
break;
case '\t':
sb.append("\t");
break;
default:
sb.append(c);
}
}
return sb.toString();
}
public Layout<ILoggingEvent> getLayout() {
return layout;
}
public void setLayout(Layout<ILoggingEvent> layout) {
this.layout = layout;
}
public String getPrintString() {
return printString;
}
public void setPrintString(String printString) {
this.printString = printString;
}
public Boolean getFeishuAlarm() {
return feishuAlarm;
}
public void setFeishuAlarm(Boolean feishuAlarm) {
this.feishuAlarm = feishuAlarm;
}
public String getFeishuWebhook() {
return feishuWebhook;
}
public void setFeishuWebhook(String feishuWebhook) {
this.feishuWebhook = feishuWebhook;
}
public String getAppName() {
return appName;
}
public void setAppName(String appName) {
this.appName = appName;
}
}
自定义appender需要集成UnsynchronizedAppenderBase,并实现对应方法,在lobabck需要增加<appender-ref ref="FeiShuLogbackAppender" />才能生效。