vlambda博客
学习文章列表

log4j日志级别调整&日志限流

背景

一方面,我们希望通过完善的日志帮助我们在排查线上问题时,能够提供一些有用的线索,便于快速定位;另一方面,在一些高流量的场景,如果日志打的太多,一旦业务出现异常,会集中产生大量的错误日志,从而导致磁盘IO急剧提高,耗费大量CPU,进而导致整个服务瘫痪。

一方面要快速响应业务,另一方面要兼顾系统性能,能不能两方面兼顾?在不影响系统性能的同时,快速响应业务。本文介绍两种方案。

重要概念

LogManager

log管理器,提供静态方法getContext,getFactory,getLogger,可以方便的获取相关信息。

LoggerContext

The LoggerContext is the anchor for the logging system。简单来说,就是日志上下文,包含已配置的Configuration、Appender,Logger,Filter等信息。

Configuration

每一个LoggerContext都有一个有效的Configuration。Configuration包含了所有的Appenders、上下文范围内的过滤器、LoggerConfigs。在重配置期间,新与旧的Configuration将同时存在。当所有的Logger对象都被重定向到新的Configuration对象后,旧的Configuration对象将被停用和丢弃。

1.动态日志级别调整

level是定义的输出级别,低于该级别的将不会输出,主要级别有OFF、ALL、FATAL、ERROR、WARN、INFO、DEBUG或自定义级别,其中OFF设定的话将不输出任何信息,ALL设定的话将输出所有信息;另外5个级别从高到底分别是:FATAL>ERROR>WARN>INFO>DEBUG,如果level设定为INFO,那么不能输出DEBUG信息;

实现原理:通过log4j提供的logManager等工具,动态调整Logger的日志级别,从而屏蔽掉一些级别较低的日志,不打印。

/** * 调整日志级别 * @param level * @return */ public static void changeAllLevel(Level level) { Configurator.setAllLevels(LogManager.getRootLogger().getName(), level); }
/** * 调整某一个logName的日志级别 * @param logName * @param level * @return */ public static Level changeLevelByName(String logName, Level level) { Configurator.setLevel(logName, level); }
public static void setLevel(final String loggerName, final Level level) { final LoggerContext loggerContext = LoggerContext.getContext(false); if (Strings.isEmpty(loggerName)) { setRootLevel(level); } else { if (setLevel(loggerName, level, loggerContext.getConfiguration())) { loggerContext.updateLoggers(); } } }
private static boolean setLevel(final String loggerName, final Level level, final Configuration config) { boolean set; LoggerConfig loggerConfig = config.getLoggerConfig(loggerName); if (!loggerName.equals(loggerConfig.getName())) { // TODO Should additivity be inherited? loggerConfig = new LoggerConfig(loggerName, level, true); config.addLogger(loggerName, loggerConfig); loggerConfig.setLevel(level); set = true; } else { set = setLevel(loggerConfig, level); } return set; } /** * 调整root日志级别 * @param logName * @param level * @return */ public static Level changeLevelByName(String logName, Level level) { Configurator.setRootLevel(Level.DEBUG); }
public static void setRootLevel(final Level level) { final LoggerContext loggerContext = LoggerContext.getContext(false); final LoggerConfig loggerConfig = loggerContext.getConfiguration().getRootLogger(); if (!loggerConfig.getLevel().equals(level)) { loggerConfig.setLevel(level); loggerContext.updateLoggers(); } }
该方案更加倾向于当问题发生时(异常日志已经很多了),快速解决问题场景,属于一种被动操作。

2.日志限流

我们知道,日志的输出路径大致为:业务代码——>logger——>appender——>目的地,其中在logger,appender两个环节,log4j都提供了可扩展的Filter,开发人员可以自定义一些限流的Filter,例如根据loggerNanme,appenderName,日志关键信息等去限流,当日志打印的qps超过某个阈值时,直接丢弃。该方案更倾向是主动保护系统的主动防御操作。

自定义一个用于根据logger名称限流的Filter:LogLimitFilter

/** * 自定义日志过滤器 */@Plugin(name = "LogLimitFilter", category = Node.CATEGORY, elementType = Filter.ELEMENT_TYPE, printObject = true)public class LogLimitFilter extends AbstractFilter {
public static final OneLimiter logLimiter = Rhino.newOneLimiter("log");
private String entranceName; private Result onMatch; private Result onMisMatch;
public LogLimitFilter(String entranceName, Result onMatch, Result onMisMatch) { super(); this.entranceName = entranceName; this.onMatch = onMatch; this.onMisMatch = onMisMatch; }
/** * Create a LogLimitFilter instance. * * @param entranceName The entrance name * @param match The action to take on a match. * @param mismatch The action to take on a mismatch. * @param level log level * @return The created MyCustomFilter. */ /** * @PluginFactory注解对应的必须使用一个静态的方法,传入的参数用@PluginAttribute修饰 */ @PluginFactory public static LogLimitFilter createFilter(@PluginAttribute("entrance") final String entranceName, @PluginAttribute("onMatch") final Result match, @PluginAttribute("onMismatch") final Result mismatch) { assert StringUtils.isNotBlank(entranceName); final Result onMatch = match == null ? Result.NEUTRAL : match; final Result onMismatch = mismatch == null ? Result.DENY : mismatch; return new LogLimitFilter(entranceName, onMatch, onMismatch); }
@Override public Result filter(LogEvent event) { //如果是命中的rhino的限流规则,继续判断是否限流,如果限流了,直接返回onMatch String loggerName = event.getLoggerName(); if("".equals(loggerName)){ loggerName = "root"; } if (loggerName.equals(entranceName) && logLimiter.run(entranceName).isReject()) { return this.onMatch; } return this.onMisMatch; }
@Override public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) { return super.filter(logger, level, marker, msg, t); }}

为需要加限流的logger添加上自定义Filter。

public class ApplicationStartupListener implements ApplicationListener<ContextRefreshedEvent> {
/** * 服务启动后,添加限流配置 * * @param contextRefreshedEvent */ @Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { addLogLimit(); }
/** * 需要加限流的logger集合 */ static final List<String> limitLoggers = Lists.newArrayList("com.meituan.mafka");
/** * 根节点限流 */ public void addRootLogLimit() { final LoggerContext loggerContext = LoggerContext.getContext(false); final Configuration config = loggerContext.getConfiguration(); LoggerConfig loggerConfig = config.getRootLogger(); setLogLimit("root", loggerConfig); loggerContext.updateLoggers(); }
/** * 指定节点限流 */ public void addLogLimit() { final LoggerContext loggerContext = LoggerContext.getContext(false); final Configuration config = loggerContext.getConfiguration(); for (String loggerName : limitLoggers) { LoggerConfig loggerConfig = config.getLoggerConfig(loggerName); if (!loggerName.equals(loggerConfig.getName())) { continue; } else { setLogLimit(loggerName, loggerConfig); } } loggerContext.updateLoggers(); }
public void setLogLimit(String loggerName, final LoggerConfig loggerConfig) { loggerConfig.addFilter(new LogLimitFilter(loggerName, Filter.Result.ACCEPT, Filter.Result.DENY)); }}

参考文档

https://tech.meituan.com/2017/02/17/change-log-level.html

http://vicviz.com/re-xiu-gai-ri-zhi-deng-ji/