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的限流规则,继续判断是否限流,如果限流了,直接返回onMatch
String 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/