基于log4j2简易实现日志告警

4次阅读

共计 5412 个字符,预计需要花费 14 分钟才能阅读完成。

需求

系统报 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 包,这样会给业务方造成困扰;而且无法统一各个业务方的版本,后续版本升级时,还需要考虑各个版本的兼容情况。

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

正文完
 0