基于log4j2简易实现日志告警

需求

系统报ERROR错误时,能实时做到消息通知。

思路

当前项目比较小,不想过多的依赖额外的第三方组件。
项目在ERROR时,都会打印ERROR日志,所以可以在log4j接收到ERROR日志请求时,发送通知消息。

实践

Filter是log4j2的扩展点,从图中(图片来自如何编写Log4j2脱敏插件)流程可以看到,Filter分别可以在全局LoggerAppender三个地方做过滤。

三个地方对应的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对象。
  • LoggerAppender入口方式是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放在LoggerAppender里会更合适;但代码里同时也兼容放在全局配置里的过滤器。

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

避坑

NEUTRALACCEPT的区别是:

  1. 如果配置的是ACCEPT,则表示过滤通过且不走后续的Filter。
  2. 如果配置的是NEUTRAL,也通过并继续后续的Filter。

备注

本篇文章的做法,是告警跟项目在同一进程内,这样有不少缺点,比如:

  1. 如果告警功能有问题,比如造成内存泄漏,可能会影响到项目正常运行。
  2. 升级不方便:功能改进/BUG修复时,业务方需要升级的jar包,这样会给业务方造成困扰;而且无法统一各个业务方的版本,后续版本升级时,还需要考虑各个版本的兼容情况。

综上,本篇文章的做法,比较适合小系统。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理