Pisces集成logback

对于应用程序,我们应该依赖slf4j这样的日志facade框架,具体的日志实现是不会引用的。
logback是目前市面上非常常用的日志框架,也是我们系统的首选。

通常的用法是,我们在boot工程里创建一个logback.xml文件,里面定义若干appender对象和logger对象。

但是这样的做法对于一个大型的、多模块的系统来说,管理不是特别好:

  1. 一个日志文件定义了所有的日志对象和输出,难以管理和维护;
  2. 如果团队引用其他团队提供的工程时,我们也希望提供方已经对日志进行了基础配置;
  3. 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);
	}

}
输出截图

在这里插入图片描述

在这里插入图片描述


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值