log4j2.xml
如侵权,请联系,无心侵权~
如有错误,也请指正。
1、log4j2.xml使用
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<properties>
<property name="LOG_HOME">./logs</property>
</properties>
<Appenders>
<!--*********************控制台日志***********************-->
<Console name="consoleAppender" target="SYSTEM_OUT">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
<!--设置日志格式及颜色-->
<PatternLayout
pattern="%style{%d{ISO8601}}{bright,green} %highlight{%-5level} [%style{%t}{bright,blue}] %style{%C{}}{bright,yellow}: %msg%n%style{%throwable}{red}"
disableAnsi="false" noConsoleNoAnsi="false" charset="UTF-8"/>
</Console>
<!--*********************文件日志***********************-->
<!--all级别日志-->
<RollingFile name="allFileAppender"
fileName="${LOG_HOME}/all.log"
<!--日志打印路径-->
filePattern="${LOG_HOME}/$${date:yyyy-MM}/all-%d{yyyy-MM-dd}-%i.log">
<!--设置日志格式-->
<PatternLayout>
<pattern>%d %p %C{} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<!-- 设置日志文件切分参数 -->
<!--<OnStartupTriggeringPolicy/>-->
<!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
<SizeBasedTriggeringPolicy size="100MB"/>
<!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
<TimeBasedTriggeringPolicy/>
</Policies>
<!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
<DefaultRolloverStrategy max="20"/>
</RollingFile>
<!--debug级别日志-->
<RollingFile name="debugFileAppender"
fileName="${LOG_HOME}/debug.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/debug-%d{yyyy-MM-dd}-%i.log">
<Filters>
<!--过滤掉info及更高级别日志-->
<ThresholdFilter level="info" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<!--设置日志格式-->
<PatternLayout>
<pattern>%d %p %C{} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<!-- 设置日志文件切分参数 -->
<!--<OnStartupTriggeringPolicy/>-->
<!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
<SizeBasedTriggeringPolicy size="100MB"/>
<!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
<TimeBasedTriggeringPolicy/>
</Policies>
<!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
<DefaultRolloverStrategy max="20"/>
</RollingFile>
<!--info级别日志-->
<RollingFile name="infoFileAppender"
fileName="${LOG_HOME}/info.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<!--过滤掉warn及更高级别日志-->
<ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<!--设置日志格式-->
<PatternLayout>
<pattern>%d %p %C{} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<!-- 设置日志文件切分参数 -->
<!--<OnStartupTriggeringPolicy/>-->
<!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
<SizeBasedTriggeringPolicy size="100MB"/>
<!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
<!--<DefaultRolloverStrategy max="20"/>-->
</RollingFile>
<!--warn级别日志-->
<RollingFile name="warnFileAppender"
fileName="${LOG_HOME}/warn.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<!--过滤掉error及更高级别日志-->
<ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<!--设置日志格式-->
<PatternLayout>
<pattern>%d %p %C{} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<!-- 设置日志文件切分参数 -->
<!--<OnStartupTriggeringPolicy/>-->
<!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
<SizeBasedTriggeringPolicy size="100MB"/>
<!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
<TimeBasedTriggeringPolicy/>
</Policies>
<!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
<DefaultRolloverStrategy max="20"/>
</RollingFile>
<!--error及更高级别日志-->
<RollingFile name="errorFileAppender"
fileName="${LOG_HOME}/error.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log.gz">
<!--设置日志格式-->
<PatternLayout>
<pattern>%d %p %C{} [%t] %m%n</pattern>
</PatternLayout>
<Policies>
<!-- 设置日志文件切分参数 -->
<!--<OnStartupTriggeringPolicy/>-->
<!--设置日志基础文件大小,超过该大小就触发日志文件滚动更新-->
<SizeBasedTriggeringPolicy size="100MB"/>
<!--设置日志文件滚动更新的时间,依赖于文件命名filePattern的设置-->
<TimeBasedTriggeringPolicy/>
</Policies>
<!--设置日志的文件个数上限,不设置默认为7个,超过大小后会被覆盖;依赖于filePattern中的%i-->
<DefaultRolloverStrategy max="20"/>
</RollingFile>
<!--json格式error级别日志-->
<RollingFile name="errorJsonAppender"
fileName="${LOG_HOME}/error-json.log"
filePattern="${LOG_HOME}/error-json-%d{yyyy-MM-dd}-%i.log.gz">
<JSONLayout compact="true" eventEol="true" locationInfo="true"/>
<Policies>
<SizeBasedTriggeringPolicy size="100MB"/>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
</RollingFile>
<!--日志远程上报-->
<Scribe name="ScribeAppender">
<!--表示远程日志,打到远程。原理:日志先保存到kafka中,其他日志中心再消费Kafka消息。-->
<!--远程日志默认使用appkey作为日志名(app.properties文件中的app.name字段),也可自定义scribeCategory属性,scribeCategory优先级高于appkey-->
<Property name="scribeCategory">123</Property>
<LcLayout/>
</Scribe>
<!--将ScribeAppender和remoteErrorLog两个远程日志变成异步的-->
<Async name="ScribeAsyncAppender" blocking="false">
<AppenderRef ref="ScribeAppender"/>
</Async>
</Appenders>
<Loggers>
<logger name="org.springframework" level="info"/>
<logger name="org.mybatis" level="info"/>
<logger name="org.apache" level="info"/>
<logger name="scribeLogger" level="info" additivity="false">
<!-- 如果代码中使用scribeLogger,表示这个日志往ScribeAsyncAppender这个远程日志上打 -->
<appender-ref ref="ScribeAsyncAppender" />
</logger>
<!-- 根日志设置 -->
<Root level="INFO">
<AppenderRef ref="allFileAppender" level="all"/>
<AppenderRef ref="consoleAppender" level="debug"/>
<AppenderRef ref="debugFileAppender" level="debug"/>
<AppenderRef ref="infoFileAppender" level="info"/>
<AppenderRef ref="warnFileAppender" level="warn"/>
<AppenderRef ref="errorFileAppender" level="error"/>
<AppenderRef ref="errorJsonAppender" level="error"/>
</Root>
</Loggers>
</Configuration>
说明:
- additivity:非必填,默认true。该Logger是否附加给Root(此参数详细介绍看)
子Logger 是否继承 父Logger 的 输出源(appender)的标志位。具体说,默认情况下子Logger会继承父Logger的appender,也就是说子Logger会在父Logger的appender里输出。(总之一句话,additivity=true时候,不仅会在该日志器打印一遍,而且还会在root中的所有appender中打印一遍)
以下配置了<Root>并将其打印级别设置为info,因此上面的name为"test"的<Logger>的additivity属性必须设置为false,就不会反馈到 <Root> 中;否则"apiLog"的Appender的info级别上的日志,将分别在<Logger name=“test”>和<Root>中被打印两次。
<Loggers>
<Logger name="test" level="trace" additivity="false">
<AppenderRef ref="apiLog"/>
</Logger>
<Root level="info">
<AppenderRef ref="apiLog"/>
<AppenderRef ref="errLog"/>
</Root>
</Loggers>
- 不要往监控平台打印过多的数据,例如美团的Raptor。日志中心是用来记录和展示业务日志的,什么日志都能往里放。
- 在日志器中会有level,表示这个日志器只会打印大于等于该level的日志,而appender中也可以配置<ThresholdFilter>level=xxx</ThresholdFilter>,表示该appender只会打印大于等于xxx的日志。因此这两个是要同时满足。举例:
配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="info">
<appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %t %-5level %logger{30}.%method - %ex %msg%n" />
<Filters>
<ThresholdFilter level="error" onMatch="DENY" onMismatch="ACCEPT"/>
</Filters>
</Console>
<loggers>
<root level="info">
<appender-ref ref="Console" />
</root>
</loggers>
</configuration>
Java程序:
@Slf4j
public class Log4j2Test {
@Test
public void testLevel(){
log.trace("trace level");
log.debug("debug level");
log.info("info level");
log.warn("warn level");
log.error("error level");
}
}
结果:日志器root可以输出info级别及以上的,但是console appender不输出error级别及以上的,所以只输出了info warn级别的日志。因此两者取交集。
2022-12-28 19:47:34.436 main INFO com.sankuai.mdp.fanjunfu.Log4j2Test.testLevel - info level
2022-12-28 19:47:34.442 main WARN com.sankuai.mdp.fanjunfu.Log4j2Test.testLevel - warn level
- RollingFile标签是日志打印到本地机器。
2、日志器的流程解析
2.1、几个重要的类
日志的门面类:
接口:org.slf4j的Logger(slfj4j:The Simple Logging Facade for Java)
package org.slf4j;
public interface Logger {
// 获取日志器的名字
public String getName();
// 日志器是否开启了trace级别的日志打印
public boolean isTraceEnabled();
// 以trace级别进行打印日志,该方法多种重载
public void trace(String msg);
// 下面和trace含义一样,只是日志打印级别不一样
public boolean isDebugEnabled();
public void debug(String msg);
public boolean isInfoEnabled();
public void info(String msg);
public boolean isWarnEnabled();
public void warn(String msg);
public boolean isErrorEnabled();
public void error(String msg);
}
实现:org.apache.logging.slf4j.Log4jLogger
日志的实现:
org.apache.logging.log4j的Logger
public interface Logger {
boolean isTraceEnabled();
void trace(Message message);
boolean isInfoEnabled();
void info(Message message);
boolean isDebugEnabled();
void debug(Message message);
boolean isWarnEnabled();
void warn(Message message);
boolean isErrorEnabled();
void error(Message message);
boolean isFatalEnabled();
void fatal(Message message);
// 以指定的级别进行打印
void log(Level level, Message message);
}
org.apache.logging.log4j.spi的ExtendedLogger
package org.apache.logging.log4j.spi;
import org.apache.logging.log4j.Logger;
public interface ExtendedLogger extends Logger {
// 日志日否开启
boolean isEnabled(Level level, Marker marker, Message message, Throwable t);
// 如果日志开启,打印日志
void logIfEnabled(String fqcn, Level level, Marker marker, CharSequence message, Throwable t);
}
org.apache.logging.log4j.spi的AbstractLogger是ExtendedLogger的实现类
继承图:
调用图:
2.2、整体流程图
主线程或者工作线程只是调用到各种appender,如AsyncAppender/consoleAppender,会把日志数据存储到BlockingQueue队列中,BlockingQueue队列大小为1024。如果超过1024(日志打印过多导致同步日志器消费赶不上生产速度),这个阻塞队列就放不下,就会放到StatusLogger的BoundedQueue(这个作用为了日志的有序性?)并且类似于StatusConsoleListener的监听器进行打印输出(这里是同步的,大量使用了synchronized)。
在AsyncAppender的BlockingQueue之后仍然使用主线程/工作线程执行后面的逻辑DefaultErrorHandler的逻辑,而这里会有大量的同步代码块synchronized,从而导致主线程/工作线程阻塞。
这些出现大量同步的地方都是可以优化的地方,提供系统的吞吐量。
PrintStream会把信息打印到控制台Console.
代码简述逻辑:
- 通过name获得日志器Logger
- 调用日志器的callAppender()方法
- callAppender()方法会调用这个日志器下面所有的appender-ref的appender.append()
- 具体就看这个append()的实现:
- 同步日志器ScribeLogger: 进行socket.connect()远程连接,然后发送数据
- 异步步日志器AsyncScribeLogger: 存放到阻塞队列,然后开启一个子线程,将阻塞队列中的日志数据分发给异步日志器所有的appender-ref
- 本地日志器XMDScribeLogger: 存放到阻塞队列,然后开启一个子线程,将阻塞队列中的日志数据分发给新创建的一个AppendRollingAppender(存储到本地机器)
3、部分源码
3.1、通过简单例子看源码
log4j2.xml配置:
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="info">
<appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<!--<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %t %-5level %logger{30}.%method - %msg%n" />-->
<PatternLayout pattern="%5p %d{yyyy-MM-dd HH:mm:ss} %c %L Line - %m %n" />
</Console>
<!--默认按天&按512M文件大小切分日志,默认最多保留30个日志文件,非阻塞模式-->
<XMDFile name="infoAppender" fileName="info.log" sizeBasedTriggeringSize="512M"
rolloverMax="30">
<Filters>
<ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</XMDFile>
<XMDFile name="warnAppender" fileName="warn.log" sizeBasedTriggeringSize="512M"
rolloverMax="30">
<Filters>
<ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</XMDFile>
<XMDFile name="errorAppender" fileName="error.log" sizeBasedTriggeringSize="512M"
rolloverMax="30">
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} - - [%p] %t %c{1.} %XMDT (%F:%L) %msg%n</pattern>
</PatternLayout>
</XMDFile>
<!--日志远程上报-->
<Scribe name="ScribeAppender">
<!--远程日志默认使用appkey作为日志名(app.properties文件中的app.name字段),也可自定义scribeCategory属性,scribeCategory优先级高于appkey-->
<Property name="scribeCategory">wpt_aggroupapi_tracelog</Property>
<LcLayout/>
</Scribe>
<Scribe name="remoteErrorLog">
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<LcLayout/>
</Scribe>
<Async name="ScribeAsyncAppender" blocking="false">
<AppenderRef ref="ScribeAppender"/>
<AppenderRef ref="remoteErrorLog"/>
</Async>
<CatAppender name="catAppender"/>
</appenders>
<loggers>
<!--远程日志,详细使用说明参见 MDP 文档中日志中心部分 https://docs.sankuai.com/dp/hbar/mdp-docs/master/log/#2 -->
<logger name="scribeLogger" level="info" additivity="false">
<appender-ref ref="ScribeAsyncAppender" />
</logger>
<logger name="org.springframework" level="info" additivity="false">
<appender-ref ref="Console" />
</logger>
<root level="info">
<appender-ref ref="infoAppender"/>
<appender-ref ref="warnAppender"/>
<appender-ref ref="errorAppender"/>
<appender-ref ref="Console" />
<appender-ref ref="catAppender"/>
</root>
</loggers>
</configuration>
代码:
public class Test {
private final static Logger log = LoggerFactory.getLogger("scribeLogger");
@Test
public void test(){
System.out.println("start~");
log.error("hello {} ! ", "word");
System.out.println("end!");
}
}
源码:
LoggerConfig:
public class LoggerConfig extends AbstractFilterable implements LocationAware {
private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) {
event.setIncludeLocation(isIncludeLocation());
if (predicate.allow(this)) {
callAppenders(event); // 调用所有的appender
}
logParent(event, predicate);
}
protected void callAppenders(final LogEvent event) {
final AppenderControl[] controls = appenders.get();
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < controls.length; i++) {
controls[i].callAppender(event); // 调用这个日志器下面的appender-ref(appender)
}
}
}
AppendControl
public class AppenderControl extends AbstractFilterable {
private void tryCallAppender(final LogEvent event) {
try {
appender.append(event); // 无论是AsynAppender还是XMDFileAppender,该方法都是存储到他们内部的阻塞队列中BlockingQueue
} catch (final RuntimeException error) {
handleAppenderError(event, error);
} catch (final Throwable throwable) {
handleAppenderError(event, new AppenderLoggingException(throwable));
}
}
}
AsynAppender
public final class AsyncAppender extends AbstractAppender {
// 在启动的时候,会调用该方法
public void start() {
final Map<String, Appender> map = config.getAppenders();
final List<AppenderControl> appenders = new ArrayList<>();
for (final AppenderRef appenderRef : appenderRefs) {
final Appender appender = map.get(appenderRef.getRef());
if (appender != null) {
appenders.add(new AppenderControl(appender, appenderRef.getLevel(), appenderRef.getFilter()));
} else {
LOGGER.error("No appender named {} was configured", appenderRef);
}
}
if (errorRef != null) {
final Appender appender = map.get(errorRef);
if (appender != null) {
errorAppender = new AppenderControl(appender, null, null);
} else {
LOGGER.error("Unable to set up error Appender. No appender named {} was configured", errorRef);
}
}
if (appenders.size() > 0) {
dispatcher = new AsyncAppenderEventDispatcher( // 这个类是一个线程继承Thread,这个类会从AsyncAppender的阻塞队列中获取数据,分发dispatch给appenders(这个是这个异步日志器下的appender(一般为同步日志器,同步日志器会通过socket.connect与远程日志器连接、发送))
getName(), errorAppender, appenders, queue); // 异步日志器的所有同步日志器会消费这个阻塞队列queue(该线程进行分配)
} else if (errorRef == null) {
throw new ConfigurationException("No appenders are available for AsyncAppender " + getName());
}
asyncQueueFullPolicy = AsyncQueueFullPolicyFactory.create();
dispatcher.start(); // 启动该线程
super.start();
}
}
AsyncAppenderEventDispatcher
class AsyncAppenderEventDispatcher extends Log4jThread {
@Override
public void run() {
LOGGER.trace("{} has started.", getName());
dispatchAll();
dispatchRemaining();
}
private void dispatchAll() {
while (!stoppedRef.get()) {
LogEvent event;
try {
event = queue.take(); //
} catch (final InterruptedException ignored) {
// Restore the interrupted flag cleared when the exception is caught.
interrupt();
break;
}
if (event == STOP_EVENT) {
break;
}
event.setEndOfBatch(queue.isEmpty());
dispatch(event); // 分发给这个日志的appender进行执行
}
LOGGER.trace("{} has stopped.", getName());
}
void dispatch(final LogEvent event) {
// Dispatch the event to all registered appenders.
boolean succeeded = false;
// noinspection ForLoopReplaceableByForEach (avoid iterator instantion)
for (int appenderIndex = 0; appenderIndex < appenders.size(); appenderIndex++) {
final AppenderControl control = appenders.get(appenderIndex);
try {
control.callAppender(event);
succeeded = true;
} catch (final Throwable error) {
// If no appender is successful, the error appender will get it.
// It is okay to simply log it here.
LOGGER.trace(
"{} has failed to call appender {}",
getName(), control.getAppenderName(), error);
}
}
// Fallback to the error appender if none has succeeded so far.
if (!succeeded && errorAppender != null) {
try {
errorAppender.callAppender(event);
} catch (final Throwable error) {
// If the error appender also fails, there is nothing further
// we can do about it.
LOGGER.trace(
"{} has failed to call the error appender {}",
getName(), errorAppender.getAppenderName(), error);
}
}
}
}
3.2、log4j2.xml配置指导
- 建议日志配置文件中对所有Appender的PatternLayout都增加%ex配置,因为如果没有显式配置%ex,则异常格式化输出的默认配置是%xEx,此时会打印异常的扩展信息(jar名称和版本),可能导致业务线程Block。
- 不建议日志配置文件中使用AsyncAppender,建议自定义Appender实现,因为AsyncAppender是日志框架默认提供的,目前最新版本中仍然存在日志事件入队前就加载异常堆栈类的问题,可能导致业务线程Block。
- 不建议生产环境使用ConsoleAppender,因为输出日志到Console时有synchronized同步操作,高并发场景下非常容易导致业务线程Block。
- 不建议在配置文件中使用<AsyncLogger>标签,因为日志事件元素在入队前就会触发加载异常堆栈类,可能导致业务线程Block。如果希望使用Log4j2提供的异步日志AsyncLogger,建议配置Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector参数,开启异步日志。