开发过程中,业务日志是非常重要且有必要的,往往能帮我们排查问题,如异常原因、入参出参、是否有脏数据、执行到了哪一步等;但是日志也不是越多越好,对于大流量接口,打日志需要慎重,大量的日志会导致频繁的IO、影响机器负载;
" 根据经验,4核8G的机器,1个小时日志量不要超过20W "
——经历过暴涨日志导致机器负载达到80的某模块SE给出的经验
此外,在中间件出异常时,如ZK连接不上、MQ链接异常等情况发生时,重启机器时也会产生疯长的日志量,可能导致服务依旧无法重启成功;这时候,如果有个全局的日志开关,能屏蔽某些指定Class打印的日志,就可以临时解决日志暴涨问题;
这里分别给出Log4j和Logback日志框架下的工具类实现,原理是拿到指定类的Logger对象,设置其日志等级为屏蔽,这一属性是在打印日志时实时判断的,因此无须重启机器生效;触发器是配置中心变更时间消息,可以是ZK、Apollo等;
1. Log4J日志开关
package com.AA.service.helper.logger;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import com.AA.client.ConfigManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author AA
* @description 全局日志开关工具
* @date 2022/3/25
*/
@Slf4j
@Component
public class LoggerSwitchHelper implements InitializingBean {
/**
* 仅对指定类屏蔽日志
*/
private static final String LOGGER_BLACKLIST = "logger.switch.blacklist";
/**
* 临时保存上一次关闭的日志对象开关,用于修改配置后恢复原有类下的日志
*/
private static Map<String, Level> LAST_LOGGER_BLACKLIST_MAP = new ConcurrentHashMap<>();
@Override
public void afterPropertiesSet() throws Exception {
String conf = ConfigManager.getString(LOGGER_BLACKLIST, "");
refreshLoggerSwitch(conf);
ConfigManager.addListener((item, type) -> {
if (StringUtils.equalsIgnoreCase(item.getName(), LOGGER_BLACKLIST)) {
refreshLoggerSwitch(item.getValue());
}
});
}
/**
* 刷新配置
*/
private synchronized void refreshLoggerSwitch(String conf) {
try {
log.info("refreshLoggerSwitch. [conf={}]", conf);
// 临时记录当前的黑名单
Map<String, Level> current_logger_blacklist = Maps.newConcurrentMap();
if (StringUtils.isNotEmpty(conf)) {
List<String> loggerBlackList = Arrays.asList(StringUtils.split(conf, ","));
if (CollectionUtils.isNotEmpty(loggerBlackList)) {
for (String loggerName : loggerBlackList) {
org.apache.log4j.Logger logger = LogManager.getLogger(loggerName);
if (null != logger) {
if (null != logger.getLevel()) {
current_logger_blacklist.put(loggerName, logger.getLevel());
}
// 当前日志对象的等级为空 且有父日志 则将其的当前等级初始化成父日志对象的等级
else if (logger.getAdditivity()) {
current_logger_blacklist.put(loggerName, logger.getParent().getLevel());
}
logger.setLevel(Level.OFF);
log.info("logger_turn_off. [loggerName={}]", loggerName);
} else {
log.info("logger_not_found. [loggerName={}]", loggerName);
}
}
}
}
// 针对上次关闭的类,这次打开后的日志需要进行恢复
if (MapUtils.isNotEmpty(LAST_LOGGER_BLACKLIST_MAP)) {
for (Map.Entry<String, Level> entry : LAST_LOGGER_BLACKLIST_MAP.entrySet()) {
String loggerName = entry.getKey();
if (!current_logger_blacklist.containsKey(loggerName)) {
// 从快照移除
LAST_LOGGER_BLACKLIST_MAP.remove(entry.getKey());
org.apache.log4j.Logger logger = LogManager.getLogger(loggerName);
if (null != logger) {
// 根据快照 设置成关闭前的日志等级
logger.setLevel(entry.getValue());
log.info("logger_remove_fr_blackList_reopen. [loggerName={} level={}]", loggerName, entry.getValue());
}
}
}
}
// 保存快照
current_logger_blacklist.forEach((loggerName, level) -> {
LAST_LOGGER_BLACKLIST_MAP.putIfAbsent(loggerName, level);
});
log.info("save_last_loggerCfg. [last_loggerCfg={}]", JSON.toJSONString(LAST_LOGGER_BLACKLIST_MAP));
log.info("refreshLoggerSwitch_suc.");
} catch (Exception e) {
log.error("refreshLoggerSwitch_error. [conf={}]", conf);
}
}
}
2. Logback日志开关
再补充下基于logback的实现;
package com.AA.service.config;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import com.AA.ConfigManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author AA
* @description logback日志开关工具
* @date 2022/1/19
*/
@Slf4j
@Component
public class LoggerSwitchHelper implements InitializingBean {
/**
* 仅对指定类屏蔽日志
*/
private static final String LOGGER_BLACKLIST = "logger.switch.blacklist";
/**
* 临时保存上一次关闭的日志对象开关,用于修改配置后恢复原有类下的日志
*/
private static Map<String, Level> LAST_LOGGER_BLACKLIST_MAP = new ConcurrentHashMap<>();
@Override
public void afterPropertiesSet() throws Exception {
String conf = ConfigManager.getString(LOGGER_BLACKLIST, "");
refreshLoggerSwitch(conf);
ConfigManager.addListener((item, type) -> {
if (StringUtils.equalsIgnoreCase(item.getName(), LOGGER_BLACKLIST)) {
refreshLoggerSwitch(item.getValue());
}
});
}
/**
* 刷新配置
*/
private synchronized void refreshLoggerSwitch(String conf) {
try {
log.info("refreshLoggerSwitch. [conf={}]", conf);
// 临时记录当前的黑名单
Map<String, ch.qos.logback.classic.Level> current_logger_blacklist = Maps.newConcurrentMap();
// 日志配置上下文
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
if (StringUtils.isNotEmpty(conf)) {
List<String> loggerBlackList = Arrays.asList(StringUtils.split(conf, ","));
if (CollectionUtils.isNotEmpty(loggerBlackList)) {
for (String loggerName : loggerBlackList) {
ch.qos.logback.classic.Logger logger = lc.getLogger(loggerName);
if (null != logger) {
if (null != logger.getLevel()) {
current_logger_blacklist.put(loggerName, logger.getLevel());
}
// 等级为空取父类等级 异常时取INFO等级
else if (logger.isAdditive()) {
try {
Field field = logger.getClass().getDeclaredField("parent");
field.setAccessible(true);
final Logger parentLogger = (Logger) field.get(logger);
current_logger_blacklist.put(loggerName, parentLogger.getLevel());
} catch (Exception e) {
current_logger_blacklist.put(loggerName, ch.qos.logback.classic.Level.INFO);
}
}
logger.setLevel(Level.OFF);
log.info("logger_turn_off. [loggerName={}]", loggerName);
} else {
log.info("logger_not_found. [loggerName={}]", loggerName);
}
}
}
}
// 针对上次关闭的类,这次打开后的日志需要进行恢复
if (MapUtils.isNotEmpty(LAST_LOGGER_BLACKLIST_MAP)) {
for (Map.Entry<String, ch.qos.logback.classic.Level> entry : LAST_LOGGER_BLACKLIST_MAP.entrySet()) {
String loggerName = entry.getKey();
if (!current_logger_blacklist.containsKey(loggerName)) {
// 从快照移除
LAST_LOGGER_BLACKLIST_MAP.remove(entry.getKey());
ch.qos.logback.classic.Logger logger = lc.getLogger(loggerName);
if (null != logger) {
// 根据快照 设置成关闭前的日志等级
logger.setLevel(entry.getValue());
log.info("logger_remove_fr_blackList_reopen. [loggerName={} level={}]", loggerName, entry.getValue());
}
}
}
}
// 保存快照
current_logger_blacklist.forEach((loggerName, level) -> {
LAST_LOGGER_BLACKLIST_MAP.putIfAbsent(loggerName, level);
});
log.info("save_last_loggerCfg. [last_loggerCfg={}]", JSON.toJSONString(LAST_LOGGER_BLACKLIST_MAP));
log.info("refreshLoggerSwitch_suc.");
} catch (Exception e) {
log.error("refreshLoggerSwitch_error. [conf={}]", conf);
}
}
}
主要区别于Logger对象的获取;也可以使用策略模式同时兼容两种日志框架;