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
/*** 自定义日志过滤器*/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修饰*/public static LogLimitFilter createFilter( final String entranceName,final Result match,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);}public Result filter(LogEvent event) {//如果是命中的rhino的限流规则,继续判断是否限流,如果限流了,直接返回onMatchString loggerName = event.getLoggerName();if("".equals(loggerName)){loggerName = "root";}if (loggerName.equals(entranceName) && logLimiter.run(entranceName).isReject()) {return this.onMatch;}return this.onMisMatch;}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*/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/
