需求
系统报ERROR错误时,能实时做到消息通知。
思路
当前项目比较小,不想过多的依赖额外的第三方组件。
项目在ERROR时,都会打印ERROR日志,所以可以在log4j接收到ERROR日志请求时,发送通知消息。
实践
Filter
是log4j2的扩展点,从图中(图片来自如何编写Log4j2脱敏插件)流程可以看到,Filter
分别可以在全局
、Logger
、Appender
三个地方做过滤。
三个地方对应的log4j.xml
配置地方如下:
<Configuration>
<MyFilter /><!-- 全局 -->
<Appenders>
<RollingFile name="ErrorRollingFile">
<Filters>
<ThresholdFilter level="ERROR" onMatch="NEUTRAL" />
<MyFilter /> <!-- Logger -->
</Filters>
</RollingFile>
</Appenders>
<Loggers>
<Root level="INFO">
<MyFilter /> <!-- Appender -->
<AppenderRef ref="ErrorRollingFile" />
</Root>
</Loggers>
</Configuration>
log4j提供了过滤器的基类AbstractFilter
:
-
全局过滤器
入口方法是filter(Logger logger, Level level, Marker marker, String msg, Object... params)
,msg
是填充参数之前的内容,params
是参数列表,包含Throwable
对象。 -
Logger
和Appender
入口方式是filter(final LogEvent event)
,通过event.getMessage().getFormattedMessage()
取到填充参数之后的内容,通过event.getThrown()
获取异常对象。
代码
@Plugin(name = "ErrorNotifyFilter", category = Node.CATEGORY, elementType = Filter.ELEMENT_TYPE, printObject = true)
public class ErrorNotifyLog4j2Filter extends AbstractFilter {
private String projectName;
private List<String> rtxReceivers;
private ErrorNotifyLog4j2Filter(String projectName, String rtxReceivers) {
super();
this.projectName = projectName;
this.rtxReceivers = Lists.newArrayList(rtxReceivers.split(","));
}
@Override
public Result filter(LogEvent event) {
notify(event.getLevel(), event.getMessage().getFormattedMessage(), event.getThrown());
return super.filter(event);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Message msg, Throwable t) {
notify(level, msg.getFormattedMessage(), t);
return super.filter(logger, level, marker, msg, t);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) {
notify(level, msg == null ? "" : msg.toString(), t);
return super.filter(logger, level, marker, msg, t);
}
@Override
public Result filter(Logger logger, Level level, Marker marker, String msg, Object... params) {
notify(level, msg, getExceptionParam(params));
return super.filter(logger, level, marker, msg, params);
}
/**
* @param level
* @param msg
* @param t
* @author
* @date
*/
private void notify(Level level, String msg, Throwable t) {
try {
if (level == null || level.intLevel() != Level.ERROR.intLevel()) {
return;
}
if (StringUtils.isBlank(msg) && t == null) {
return;
}
Log4j2AsyncExecutor.executorService.submit(() -> {
try {
String notifyMsg = getNotifyMsg(msg, t);
String actualActiveProfiles = getActiveProfiles();
MessageUtil.postMessage(Lists.newArrayList(MessageTypeEnum.RTX),
rtxReceivers,
(StringUtils.isBlank(actualActiveProfiles) ? "" : "【" + actualActiveProfiles + "】") + "【" + projectName + "】项目异常告警 - " + DateUtils.formatDateTime(new Date()),
notifyMsg);
} catch (Exception ignoreException) {
ignoreException.printStackTrace();
}
});
} catch (Throwable ignoreException) {
ignoreException.printStackTrace();
}
}
/**
* @param params
* @return java.lang.Throwable
* @author
* @date
*/
private Throwable getExceptionParam(Object... params) {
if (params == null || params.length == 0) {
return null;
}
for (Object param : params) {
if (param instanceof Throwable) {
return (Throwable) param;
}
}
return null;
}
/**
* 为了能让告警更清晰,这里获取了两行堆栈,但同样的也就降低了性能
*
* @param msg
* @param t
* @return java.lang.String
* @author
* @date
*/
private String getNotifyMsg(String msg, Throwable t) {
String errorMsg = "信息:" + (msg == null ? "" : msg);
String exceptionMsg = "";
if (t != null) {
exceptionMsg += "\n异常:" + t.toString();
StackTraceElement[] stackTraceElements = t.getStackTrace();
if(stackTraceElements != null) {
if(stackTraceElements.length > 0) {
exceptionMsg += "\n" + stackTraceElements[0];
}
if(stackTraceElements.length > 1) {
exceptionMsg += "\n" + stackTraceElements[1];
}
}
}
return errorMsg + exceptionMsg;
}
/**
* @return java.lang.String
* @author
* @date
*/
private String getActiveProfiles() {
String[] activeProfiles = SpringContextUtil.getApplicationContext() == null ? null : SpringContextUtil.getActiveProfile();
if (activeProfiles == null || activeProfiles.length == 0) {
return "";
}
List<String> actualActiveProfiles = Arrays.stream(activeProfiles)
// 部分项目的profile中有用到include,include中含有"-",过滤掉include的数据
.filter(str -> !str.contains("-"))
.collect(Collectors.toList());
return StringUtils.join(actualActiveProfiles, ",");
}
/**
* @return com.tencent.tscm.purchase.interfacer.log4j2.ErrorNotifyFilter
* @author
* @date
*/
@PluginFactory
public static ErrorNotifyLog4j2Filter createFilter(@PluginAttribute("projectName") final String projectName,
@PluginAttribute("rtxReceivers") final String rtxReceivers) {
return new ErrorNotifyLog4j2Filter(projectName, rtxReceivers);
}
}
@Component
public class Log4j2AsyncExecutor {
// 根据自己项目的情况配置线程池信息
public static ExecutorService executorService = new ThreadPoolExecutor(1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder().setNameFormat("log4j2-async-executor-pool-%d").build());
/**
* @author
* @date
*/
@PreDestroy
public synchronized void shutdown() {
if (executorService != null) {
ThreadUtils.shutdown(executorService, 5, TimeUnit.SECONDS);
executorService = null;
}
}
}
这里要特别说明:为什么要有Log4j2AsyncExecutor
这个类?
因为发送告警是异步的,用了线程池,那在进程停止时,肯定要销毁线程池;而AbstractFilter
虽然实现了LifeCycle
接口,有stop
方法,但是实际上stop
方法并不会被调用到;所以这里依赖了Spring的PreDestroy
来销毁。
配置
告警肯定是要取填充参数之后的内容
,所以Filter
放在Logger
或Appender
里会更合适;但代码里同时也兼容放在全局
配置里的过滤器。
<Configuration>
......
<Appenders>
<RollingFile name="ErrorRollingFile">
<Filters>
<ThresholdFilter level="ERROR" onMatch="NEUTRAL" />
<ErrorNotifyFilter projectName="xxxx" rtxReceivers="xxxx" />
</Filters>
......
</RollingFile>
</Appenders>
<Loggers>
<Root level="INFO">
......
<AppenderRef ref="ErrorRollingFile" />
</Root>
</Loggers>
</Configuration>
避坑
NEUTRAL
和ACCEPT
的区别是:
- 如果配置的是
ACCEPT
,则表示过滤通过且不走后续的Filter。 - 如果配置的是
NEUTRAL
,也通过并继续后续的Filter。
备注
本篇文章的做法,是告警跟项目在同一进程内,这样有不少缺点,比如:
- 如果告警功能有问题,比如造成内存泄漏,可能会影响到项目正常运行。
- 升级不方便:功能改进/BUG修复时,业务方需要升级的jar包,这样会给业务方造成困扰;而且无法统一各个业务方的版本,后续版本升级时,还需要考虑各个版本的兼容情况。
综上,本篇文章的做法,比较适合小系统。
发表回复