对于应用程序,我们应该依赖slf4j这样的日志facade框架,具体的日志实现是不会引用的。
logback是目前市面上非常常用的日志框架,也是我们系统的首选。
通常的用法是,我们在boot工程里创建一个logback.xml文件,里面定义若干appender对象和logger对象。
但是这样的做法对于一个大型的、多模块的系统来说,管理不是特别好:
- 一个日志文件定义了所有的日志对象和输出,难以管理和维护;
- 如果团队引用其他团队提供的工程时,我们也希望提供方已经对日志进行了基础配置;
- fileappender元素往往比较重,对于一个文件输出,我们可能只是更改文件名,其他部分需要复制粘贴,一旦文件输出多了,单个配置文件会显得非常臃肿;
针对这些问题,pisces-logger工程提供了解决方案,旨在帮助大家简化日志配置,方便配置管理。
支持多个logback.xml配置
logback框架默认会在classpath路径寻找logback.xml文件,如果存在则会进行加载,它本身并不支持自动加载多个配置文件。所以这个功能我们需要自行设计和实现。
我们规定:符合classpath*?*/logback/*.xml的所有文件,都可以被找到,并加载。
示意代码如下:
Resource[] resources = resolver.getResources("classpath*:**/logback/*.xml");
if (resources.length > 0) {
for (Resource resource : resources) {
LogBackXmlConfigLoader.load(resource);
}
}
......
public class LogBackXmlConfigLoader{}
public static void load(Resource resource) {
LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(lc);
configurator.doConfigure(resource.getURL());
StatusPrinter.printInCaseOfErrorsOrWarnings(lc);
}
}
上述代码是logback加载配置文件的api。
需要注意的是,我们不要在针对工程的配置文件里定义rootlogger,因为整个应用应该只有一个rootlogger,它应该由当前宿主工程定义。
springboot日志配置的启发
springboot也集成了logback,它提供了一种更加简单配置定义日志。我们只需要在application.properties中配置若干springboot定义的属性,springboot的logging工程就会读取它们,调用logback的api创建日志对象。
springboot提供的日志配置仅仅满足基本需要,它为rootlogger提供了基本的console和file输出,如果我们要定义多个输出,springboot是不支持的。
但是springboot这种思想对我们指明了一个方向,我们可以用同样的方式自己定义配置、解析配置、创建日志对象。
自定义的logback.properties
我们规定:符合classpath*?*/logback/*.properties"的所有文件,都可以被找到,并加载。
一个logback的属性文件是一个对象的定义,我们可以在一个工程里定义多个属性文件,最终所有日志对象都会被加载和创建。
属性文件参数如下:
- logging.name:日志对象的名称,必填;
- logging.level:日志对象的等级,高于此等级的日志都会被记录;
- logging.additive:是否集成父日志对象的输出。默认true;
- logging.charset:日志字符集,默认utf-8;
- logging.console.enable:是否启用console输出,考虑到springboot默认提供了console输出,每个具体的日志对象不需要再单独设置,所以默认false;
- logging.console.pattern:如果logging.console.enable为true,通过此属性设置控制台输出的表达式;
- logging.file.enable:是否启用file输出,系统默认提供基于RollingFileAppender的输出,历史日志会以天为单位生成一个独立的文件,文件名后追加日期;
- logging.file.path:日志的文件路径,默认为${user.home}/logs/${spring.application.name}/${logging.name}.log
- logging.file.pattern:通过此属性设置文件输出的表达式
- logging.file.rolling.maxSize:单个日志文件容量的最大,超过此大小后,文件会被分割成多个,文件名后追加索引,默认10MB;
- logging.file.splitByLevel:文件是否需要根据日志等级分割成多个文件,如果为true,当前日志会生成4个文件,分别记录debug、info、warn、error的日志,如果为false,则所有等级的信息都会记录在一个文件中
例:
logging.name=org.springframework
logging.level=debug
logging.additive=true
logging.charset=utf-8
logging.console.enable=false
logging.console.pattern=
logging.file.enable=true
logging.file.path=${user.home}/logs/${spring.application.name}/spring.log
logging.file.pattern=
logging.file.splitByLevel=true
logging.file.rolling.maxSize=10MB
系统默认提供的控制台和文件pattern分别是:
-
CONSOLE:"%date{yyyy-MM-dd HH:mm:ss.SSS} %5level ${PID:- } — [%15.15thread] %-50.50logger{49} : %msg%n";
-
FILE:"%date{yyyy-MM-dd HH:mm:ss.SSS} %5level ${PID:- } — [thread] %-50.50logger{49} : %msg%n";
与加载xml的方式类似,不同是,我们需要自行编写代码创建logback的日志对象、appender对象、encoder对象等。
代码如下:
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.filter.LevelFilter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
import ch.qos.logback.core.spi.FilterReply;
import ch.qos.logback.core.util.FileSize;
import ch.qos.logback.core.util.OptionHelper;
/**
* 加载属性文件,创建logback的logger对象
*
* @author hangwen
*
*/
public class LogBackPropertyConfigLoader {
private static final String LOGGING_NAME_KEY = "logging.name";
private static final String LOGGING_LEVEL_KEY = "logging.level";
private static final String LOGGING_ADDITIVE_KEY = "logging.additive";
private static final String LOGGING_CHARSET_KEY = "logging.charset";
private static final String LOGGING_CONSOLE_ENABLE_KEY = "logging.console.enable";
private static final String LOGGING_CONSOLE_PATTERN_KEY = "logging.console.pattern";
private static final String LOGGING_FILE_ENABLE_KEY = "logging.file.enable";
private static final String LOGGING_FILE_PATH_KEY = "logging.file.path";
private static final String LOGGING_FILE_PATTERN_KEY = "logging.file.pattern";
private static final String LOGGING_FILE_SPLITBYLEVEL_KEY = "logging.file.splitByLevel";
private static final String LOGGING_FILE_ROLLING_MAXSIZE_KEY = "logging.file.rolling.maxSize";
private static final String SPRINGBOOT_LOGGING_PATTERN_CONSOLE = "logging.pattern.console";
private static final String SPRINGBOOT_LOGGING_PATTERN_FILE = "logging.pattern.file";
private static final String DEFAULT_CONSOLE_PATTERN = "%date{yyyy-MM-dd HH:mm:ss.SSS} %5level ${PID:- } --- [%15.15thread] %-50.50logger{49} : %msg%n";
private static final String DEFAULT_FILE_PATTERN = "%date{yyyy-MM-dd HH:mm:ss.SSS} %5level ${PID:- } --- [thread] %-50.50logger{49} : %msg%n";
public static void load(Resource resouce) throws IOException {
Properties properties = PropertiesLoaderUtils.loadProperties(resouce);
String loggerName = properties.getProperty(LOGGING_NAME_KEY);
if (StringUtils.isEmpty(loggerName)) {
return;
}
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger logger = loggerContext.getLogger(loggerName);
boolean additive = BooleanUtils
.toBoolean(properties.getProperty(LOGGING_ADDITIVE_KEY, "true"));
logger.setAdditive(additive);
// 设置日志等级,默认info
Level level = getLoggerLevel(properties);
logger.setLevel(level);
boolean consoleEnable = BooleanUtils
.toBoolean(properties.getProperty(LOGGING_CONSOLE_ENABLE_KEY, "false"));
if (consoleEnable) {
// 设置console输出
ConsoleAppender<ILoggingEvent> consoleAppender = createConsoleAppender(properties,
loggerName, loggerContext);
logger.addAppender(consoleAppender);
}
// 设置文件输出
boolean fileEnable = BooleanUtils
.toBoolean(properties.getProperty(LOGGING_FILE_ENABLE_KEY, "true"));
if (fileEnable) {
List<RollingFileAppender<ILoggingEvent>> fileAppenders = createFileAppenders(properties,
loggerName, level, loggerContext);
for (RollingFileAppender<ILoggingEvent> fileAppender : fileAppenders) {
logger.addAppender(fileAppender);
}
}
}
private static List<RollingFileAppender<ILoggingEvent>> createFileAppenders(
Properties properties, String name, Level loggerLevel, LoggerContext loggerContext) {
boolean splitByLevel = BooleanUtils
.toBoolean(properties.getProperty(LOGGING_FILE_SPLITBYLEVEL_KEY, "true"));
List<RollingFileAppender<ILoggingEvent>> appenders = new ArrayList<>();
if (splitByLevel) {
//按级别分割文件
Level[] levels = new Level[] { Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR };
for (Level level : levels) {
RollingFileAppender<ILoggingEvent> fileAppender = createFileAppender(properties,
name, level, loggerContext);
if (fileAppender != null) {
appenders.add(fileAppender);
}
}
} else {
RollingFileAppender<ILoggingEvent> fileAppender = createFileAppender(properties, name,
null, loggerContext);
if (fileAppender != null) {
appenders.add(fileAppender);
}
}
return appenders;
}
private static final String FILE_SUFFIX = ".log";
private static RollingFileAppender<ILoggingEvent> createFileAppender(Properties properties,
String loggerName, Level level, LoggerContext loggerContext) {
RollingFileAppender<ILoggingEvent> fileAppender = new RollingFileAppender<>();
String levelStr = level != null ? level.toString().toLowerCase() : "";
// 设置context和名称
fileAppender.setName(loggerName + "'s " + levelStr + " file appender");
fileAppender.setContext(loggerContext);
// 设置输出文件
String fileName = getFile(properties, loggerName, levelStr);
String file2Use = OptionHelper.substVars(fileName + FILE_SUFFIX, loggerContext);
fileAppender.setFile(file2Use);
// 设置pattern
String filePattern = properties.getProperty(LOGGING_FILE_PATTERN_KEY);
if (StringUtils.isEmpty(filePattern)) {
filePattern = loggerContext.getProperty(SPRINGBOOT_LOGGING_PATTERN_FILE);
if (StringUtils.isEmpty(filePattern)) {
filePattern = DEFAULT_FILE_PATTERN;
}
}
PatternLayoutEncoder encoder = createPatternLayoutEncoder(properties, filePattern,
loggerContext);
fileAppender.setEncoder(encoder);
// 设置level过滤器
if (level != null) {
LevelFilter levelFilter = createLevelFilter(level);
fileAppender.addFilter(levelFilter);
}
// 设置文件时间和大小的分片策略
SizeAndTimeBasedRollingPolicy<ILoggingEvent> policy = createSizeAndTimeBasedRollingPolicy(
properties, loggerContext, fileName, fileAppender);
fileAppender.setRollingPolicy(policy);
// 启动
fileAppender.start();
return fileAppender;
}
private static SizeAndTimeBasedRollingPolicy<ILoggingEvent> createSizeAndTimeBasedRollingPolicy(
Properties properties, LoggerContext loggerContext, String fileName,
FileAppender<?> fileAppender) {
SizeAndTimeBasedRollingPolicy<ILoggingEvent> policy = new SizeAndTimeBasedRollingPolicy<>();
String fileNamePattern = OptionHelper
.substVars(fileName + "-%d{yyyy-MM-dd}.%i" + FILE_SUFFIX, loggerContext);
policy.setFileNamePattern(fileNamePattern);
String fileSize = properties.getProperty(LOGGING_FILE_ROLLING_MAXSIZE_KEY, "10MB");
policy.setMaxFileSize(FileSize.valueOf(fileSize));
policy.setContext(loggerContext);
policy.setParent(fileAppender);
policy.start();
return policy;
}
private static LevelFilter createLevelFilter(Level level) {
LevelFilter levelFilter = new LevelFilter();
levelFilter.setLevel(level);
levelFilter.setOnMatch(FilterReply.ACCEPT);
levelFilter.setOnMismatch(FilterReply.DENY);
levelFilter.start();
return levelFilter;
}
private static String getFile(Properties properties, String loggerName, String levelStr) {
// ${user.home}/logs/example/myLog.log
String file = properties.getProperty(LOGGING_FILE_PATH_KEY);
if (StringUtils.isEmpty(file)) {
file = "${user.home}/logs/${spring.application.name}/" + loggerName;
}
if (!file.endsWith(FILE_SUFFIX)) {
file += FILE_SUFFIX;
}
// ${user.home}/logs/example/myLog
String fileNameWithoutSuffix = file.substring(0, file.length() - FILE_SUFFIX.length());
if (!StringUtils.isEmpty(levelStr)) {
// ${user.home}/logs/example/myLog-info
fileNameWithoutSuffix += ("-" + levelStr);
}
return fileNameWithoutSuffix;
}
private static ConsoleAppender<ILoggingEvent> createConsoleAppender(Properties properties,
String name, LoggerContext loggerContext) {
ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>();
// 设置context和名称
consoleAppender.setContext(loggerContext);
consoleAppender.setName(name + "'s console appender");
String cosolePattern = properties.getProperty(LOGGING_CONSOLE_PATTERN_KEY);
if (StringUtils.isEmpty(cosolePattern)) {
cosolePattern = loggerContext.getProperty(SPRINGBOOT_LOGGING_PATTERN_CONSOLE);
if (StringUtils.isEmpty(cosolePattern)) {
cosolePattern = DEFAULT_CONSOLE_PATTERN;
}
}
PatternLayoutEncoder encoder = createPatternLayoutEncoder(properties, cosolePattern,
loggerContext);
consoleAppender.setEncoder(encoder);
// 启动
consoleAppender.start();
return consoleAppender;
}
private static PatternLayoutEncoder createPatternLayoutEncoder(Properties properties,
String pattern, LoggerContext loggerContext) {
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
Charset charSet = getCharSet(properties);
encoder.setCharset(charSet);
encoder.setPattern(OptionHelper.substVars(pattern, loggerContext));
encoder.setContext(loggerContext);
encoder.start();
return encoder;
}
private static Charset getCharSet(Properties properties) {
String charsetName = properties.getProperty(LOGGING_CHARSET_KEY, "UTF-8");
try {
return Charset.forName(charsetName);
} catch (UnsupportedCharsetException e) {
return Charset.forName("UTF-8");
}
}
private static Level getLoggerLevel(Properties properties) {
String level = properties.getProperty(LOGGING_LEVEL_KEY, "INFO");
return Level.toLevel(level, Level.INFO);
}
}