[日志系统/Java] Java日志框架:最佳实践总结(Slf4j+Log4j)

1 概述:Java日志框架

1.1 常见Java日志框架及对比选择

commons-logging和slf4j(slf4j-api.jar)都是日志类库的接口,供客户端使用,而没有提供实现!
log4j,logback等等才是日志的真正实现。

  • 日志(类库)
    • 作用:可以调试程序,就像输出一样
  • Java语言的日志类库:
    • JUL(java.util.logging) :自 JDK 1.5 开始,java.util.logging(JUL) 包下就提供了内置的日志工具类,功能比较简单,一般没人使用。
    • Apache Commons-Logging : Apache Commons-logging 也被称作 Jakarta Commons-Logging(JCL) 最早提供了日志门面接口,采用 facade 模式更符合面向接口的抽象编程方式,使得用户可以自由选择不同的日志实现框架,而不必改动具体的日志语句。2014年后没再更新,JCL 的风光早已不在。
    • SLF4J : [Simple Logging Facade(外观/表面) For Java], 一个简单的日志门面,类似 Commons-logging,通过 Facade Pattern 提供一些 Java logging API。SLF4J 的 slf4j-api 包中提供了众多日志接口定义,它只服务于各种各样的日志框架而不负责具体的日志实现,只在编译时负责寻找合适的日志系统进行绑定。作者创建 SLF4J 的目的是为了替代 JCL。
      • 1个 日志标准/适配器
        • 通过调用slf4j的API统一打印日志,而可忽略其他日志的具体方法
    • Log4J : [Log For Java]
      • log4j 是Apache为java提供日志管理的工具
      • 真正实现日志功能的日志类库
    • Logback : Logback 是由 Log4j 创始人设计的又一个开源日记组件,是 Slf4j 的原生实现框架,相比 log4j,logback 拥有更快的执行速度,在 Log4j2 出来前的很长一段时间里都是 java 日志界的主流。
    • Log4J : [Log For Java 2] : Log4j 和 Log4j2 也都是 Apache 的开源日志框架,Log4j 2.0 以后的版本称为 Log4j2Log4 1.x 的升级版,Log4j 1.x 版在 2015.08.05 停止维护了。Log4j2 参考了 Logback 的一些优秀的设计,并修复了老版的问题,整体提升很大,特别是异步方面的性能提升。 Log4j2 与 Log4j 发生了很大的变化,log4j2 不兼容 Log4j。

JCLSLF4J 只是一种日志抽象门面、一种标准,不是负责具体实现的日志框架,若项目中只有slf4j的包,是没有办法实现日志功能的。
LogbackLog4j/Log4j2 是具体的日志实现框架。在选择一个日志框架时可考虑以下两点:

  • 具有日志缓冲区的框架可以减少频繁的文件 I/O 操作,对性能提升显著;
  • 支持异步日志功能的框架,不会阻塞其它应用线程,因而是首选;

LogbackLog4j2 都支持以上特性,在关注性能的地方,推荐使用: slf4j + log4j2slf4j + logback。如果不想任何额外的依赖,则使用 JUL容器框架已经提供的日志接口。

1.2 日志级别(从高到低)

  • OFF : 最高等级,用于关闭所有日志记录
  • FATAL : 最严重的日志级别/重大错误级(系统有问题),必须慎用。一般表示服务不可用,比如:程序崩溃、无法启动、OOM 等;
  • ERROR : 错误级(一个模块有问题)
  • WARN : 警告级
  • INFO : 信息级。可以查看程序执行的流程
  • DEBUG : 调试。用来调试程序的bug及显示
  • TRACE : 类似 DEBUG,但记录更详细的跟踪信息
  • ALL : 最低等级。用于打开所有日志记录

常用日志框架间的级别对应关系见下表:

SLF4J Log4j Log4j2 Logback JUL
FATAL FATAL FATAL
ERROR ERROR ERROR ERROR SEVERE
WARN WARN WARN WARN WARNING
INFO INFO INFO INFO INFO, CONFIG
DEBUG DEBUG DEBUG DEBUG FINE, FINER
TRACE TRACE TRACE TRACE FINEST

debug,info,error等日志级别的方法都可传入多个可变参数:(以debug为例)

2 Slf4J

组件定位:日志系统的门面框架、标准规约 => 不参与具体实现

  • SLF4J简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。
  • 它允许最终用户在部署其应用时使用其所希望的日志系统。
  • 实际上,SLF4J所提供的核心API是一些接口以及一个LoggerFactory的工厂类。

主要特点

  • 不同于其他日志类库,与其它日志类库有很大的不同
    • 不是1个真正的日志实现框架,而是1个抽象层
    • 它允许你在后台使用任意一个日志类库
  • 使代码独立于任一特定的日志API;
  • 无需忍受加载和维护新的日志框架的痛苦
  • 占位符(place holder):"{}"
    • 占位符非常类似于在String的format()方法中的%s
      • 因为它会在运行时被某个提供的实际字符串所替换
      • slf4j使用占位符,比字符串拼接更加高效。
      • 例如: logger.error("sql为 {} ", sql);
    • 不仅降低了代码中字符串连接次数,而且还节省了新建的String对象
    • 可以在运行时延迟字符串的建立。
      • 这意味着只有需要的String对象才被建立

日志级别

slf4j日志级别 | Slf4j日志级别,级别由低到高,设置的级别约低,打印的日志越多

①trace: 一般不会使用,在日志里边也不会打印出来,最低的一个日志级别。
②debug: 一般放于程序的某个关键点的地方,用于打印一个变量值或者一个方法返回的信息之类的信息
③info 一般处理业务逻辑的时候使用,就跟 system.err打印一样,用于说明此处是干什么的。
④warn:警告,不会影响程序的运行,但是值得注意。
⑤error: 用户程序报错,必须解决的时候使用此级别打印日志。

依赖引入

<!-- slf4j | 推荐版本: 1.7.30 / 1.7.36 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>${slf4j.version}</version>
</dependency>

<!-- lombok | 推荐版本: 1.18.22,可选择引入/非强制要求 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
</dependency>

基本使用

  • 定义 logger
方式1:lombok 的 lombok.extern.slf4j.Slf4j 注解
@Slf4j
public class Xxxx {
    ...
}
    

方式2:
public class Xxxx {
    private static final Logger logger = LoggerFactory.getLogger(Log4jKafkaAppenderDemoEntry.class);
}
  • 使用 logger
loggger.info("xxxx : {}", xxxx)
  • 使用 MDC API
import org.slf4j.MDC;
MDC.put(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM, kafkaProducerBootstrapServers);
log.info("MDC | {} : {}", Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM, MDC.get(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_BOOTSTRAP_SERVERS_PARAM) );

appender.layout.pattern : %X{xxxxVar}

3 LOG4J

3.0 简介

  • 日志体系是软件中不可缺少的部分,Apache的开源项目log4j 1.x/2.x是一个功能强大的日志组件,提供方便的日志记录。
  • Log4j(别名: log for java)是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。

最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

3.1 安装/依赖 [slf4j + log4j 2.x]

方式1:手动导入JAR包

slf4j-api-1.7.5.jar、slf4j-log4j-1.7.5.jar、log4j-1.2.16.jar (这其实是 log4j 1.x 的相关包,已不建议使用)
或配置如下Maven依赖:

  • slf4j-api:提供接口
  • slf4j-log4j[xx]:提供Log具体的实现

方式2:配置Maven依赖【推荐】

slf4j + log4j 2.x 【强烈推荐】

<!-- log [start] -->
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-api</artifactId>
	<version>${slf4j.version}</version>
</dependency>

<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-api</artifactId>
	<version>${log4j.version}</version>
</dependency>

<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-core</artifactId>
	<version>${log4j.version}</version>
</dependency>

<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-slf4j-impl</artifactId>
	<version>${log4j.version}</version>
	<exclusions>
		<exclusion>
			<artifactId>slf4j-api</artifactId>
			<groupId>org.slf4j</groupId>
		</exclusion>
	</exclusions>
</dependency>

<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
<!-- slf4j-log4j12 包 与 log4j-slf4j-impl 包:同一工程中不能同时使用,个人倾向于使用后者
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.2</version>
</dependency>
-->

<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-jul</artifactId>
	<!--<version>2.13.3</version>-->
	<version>${log4j.version}</version>
	<scope>compile</scope>
</dependency>
<!-- log [end] -->

slf4j.version : 1.7.30
log4j.version : 2.20.0

3.2 核心配置文件

  • 在Java应用程序的/resources/(源码目录)、/classes/(编译后的JAR包目录)下配置配置文件。
  • log4j2.properties
  • log4j2.xml 【推荐/配套网络文档更多,可参考的资料更丰富】

log4j/org.apache.logging.log4j:log4j2.0.0及以后,被拆分为 log4j-apilog4j-core,对应的配置文件为 log4j2.xml

其实您也可以完全不使用配置文件,而是在代码中配置Log4j环境。但是,使用配置文件将使您的应用程序更加灵活。Log4j支持两种配置文件格式

  • XML格式的文件[.xml]
  • Java特性文件[.properties](键=值)

下面我们介绍使用log4j.properties文件做为配置文件的方法:

1. 配置 Root Logger / Logger

log4j 1.x

log4j.rootLogger = [ level ] , appenderName, appenderName, …

其中:

  • level : 日志记录的优先级,分为OFFFATALERRORWARNINFODEBUGALL或您定义的级别。
    Log4j建议只使用四个级别,优先级从高到低分别是ERROR、WARN、INFO、DEBUG
    通过在这里定义的级别,您可以控制到应用程序中相应级别的日志信息的开关。比如在这里定 义了INFO级别,则应用程序中所有DEBUG级别的日志信息将不被打印出来。
  • appenderName: 就是指B日志信息输出到哪个地方。您可以同时指定多个输出目的地。

log4j 2.x

    <!-- 日志器-->
    <Loggers>
        <!-- 定义 RootLogger 等 全局性配置(不可随意修改) -->
        <!-- rootLogger, 根记录器,所有记录器的父辈 | 指定根日志的级别 | All < Trace < Debug < Info < Warn < Error < Fatal < OFF -->
        <Root level="${log.level}"> <!-- ${log.level} -->
            <!-- 2.17.2 版本以下通过这种方式将 root 和 Appender关联起来 / 2.17.2 版本以上有更简便的写法 -->
            <!-- rootLogger.appenderRef.stdout.ref=${log.consoleAppender} -->
            <AppenderRef ref="${log.consoleAppender}" level="INFO" />
        </Root>

        <!-- 指定个别 Class 的 Logger (可随意修改,建议在 nacos 上修改) -->

        <!-- KafkaAppender | org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender
            1. 确保不要让 org.apache.kafka Logger 的日志级别为 DEBUG,因为这将导致递归日志记录
            2. 请记住将配置 additivity 属性设置为false
         -->
        <Logger name="org.apache.kafka" level="WARN" additivity="false">
            <AppenderRef ref="${log.consoleAppender}"/>
        </Logger>

    </Loggers>

2. 配置日志信息输出目的地Appender

log4j 1.x

log4j.appender.appenderName = fully.qualified.name.of.appender.class  
log4j.appender.appenderName.option1 = value1  
…  
log4j.appender.appenderName.option = valueN

各类日志管理工具或开源工具(Eg: Log4JLogbackSkywalking等)提供的Appender可见:

log4j 2.x

# console
# 指定输出源的类型与名称 (type 对应 Plugin 插件的 name 属性)
appender.console.type=Console
appender.console.name=CONSOLE_APPENDER
#appender.console.layout.type=PatternLayout
appender.console.layout.type=CustomPatternLayout
appender.console.layout.pattern=${log.layout.consolePattern}

appender.systemRollingFile.type=RollingFile
appender.systemRollingFile.name=MySystemFileAppender
appender.systemRollingFile.fileName=${log.dir}/system.log
appender.systemRollingFile.filePattern=${log.dir}/system-%d{MM-dd-yyyy}-%i.log.gz
appender.systemRollingFile.policies.type=Policies
appender.systemRollingFile.policies.time.type=TimeBasedTriggeringPolicy
appender.systemRollingFile.policies.size.type=SizeBasedTriggeringPolicy
appender.systemRollingFile.policies.size.size=128MB
appender.systemRollingFile.strategy.type=DefaultRolloverStrategy
#appender.systemRollingFile.layout.type=PatternLayout
appender.systemRollingFile.layout.type=CustomPatternLayout
# appender.systemRollingFile.layout.pattern=[%traceId] ${log.layout.mainPattern}
appender.systemRollingFile.layout.pattern=${log.layout.mainPattern}
#appender.systemRollingFile.layout.pattern=[%traceId] [${application.name}] [${instance.name}] [${env:HOST_IP}] [${env:CONTAINER_IP}] [%d{yyyy/MM/dd HH:mm:ss.SSS}] [%p] [%t] [%l] %m%n
<Console name="MyConsoleAppender" target="SYSTEM_OUT">
	<PatternLayout pattern="${log.layout.consolePattern}" />
	<!-- com.platform.sp.framework.log.layout.CustomPatternLayout -->
	<!--<CustomPatternLayout pattern="${log.layout.consolePattern}" />-->
</Console>

<Kafka name="MyKafkaAppender" topic="flink_monitor_log" key="$${web:contextName}" syncSend="true" ignoreExceptions="false">
    <!--<JsonTemplateLayout/>-->
    <PatternLayout pattern="${log.layout.mainPattern}"/>
    <Property name="bootstrap.servers" value="${log.appender.kafka.producer.bootstrap.servers}"/>
    <Property name="max.block.ms">2000</Property>
</Kafka>


<RollingFile name="MyFailoverAppender" fileName="../log/failoverKafka/request.log"
filePattern="../log/failover/request.%d{yyyy-MM-dd}.log">
    <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
    <PatternLayout>
        <Pattern>${log.layout.mainPattern}</Pattern>
    </PatternLayout>
    <Policies>
        <TimeBasedTriggeringPolicy />
    </Policies>
</RollingFile>

3. 配置日志信息的格式(布局)

log4j 1.x

log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class  
log4j.appender.appenderName.layout.option1 = value1  
…  
log4j.appender.appenderName.layout.option = valueN

其中,Log4j 1.x / 2.x 提供的layout有好几种,详情参见:本文档 Layout 章节

log4j 2.x

# 指定输出源的类型与名称 (type 对应 Plugin 插件的 name 属性)
appender.systemRollingFile.type=RollingFile
appender.systemRollingFile.name=MySystemFileAppender
appender.systemRollingFile.fileName=${log.dir}/system.log
appender.systemRollingFile.filePattern=${log.dir}/system-%d{MM-dd-yyyy}-%i.log.gz
appender.systemRollingFile.policies.type=Policies
appender.systemRollingFile.policies.time.type=TimeBasedTriggeringPolicy
appender.systemRollingFile.policies.size.type=SizeBasedTriggeringPolicy
appender.systemRollingFile.policies.size.size=128MB
appender.systemRollingFile.strategy.type=DefaultRolloverStrategy
#appender.systemRollingFile.layout.type=PatternLayout
appender.systemRollingFile.layout.type=CustomPatternLayout
# appender.systemRollingFile.layout.pattern=[%traceId] ${log.layout.mainPattern}
appender.systemRollingFile.layout.pattern=${log.layout.mainPattern}
#appender.systemRollingFile.layout.pattern=[%traceId] [${application.name}] [${instance.name}] [${env:HOST_IP}] [${env:CONTAINER_IP}] [%d{yyyy/MM/dd HH:mm:ss.SSS}] [%p] [%t] [%l] %m%n

4. 日志格式化问题

Log4J采用类似C语言中的printf函数的打印格式格式化日志信息,打印参数如下: %m 输出代码中指定的消息

%p 输出优先级,即DEBUG,INFO,WARN,ERROR,FATAL  
%r 输出自应用启动到输出该log信息耗费的毫秒数  
%c 输出所属的类目,通常就是所在类的全名  
%t 输出产生该日志事件的线程名  
%n 输出一个回车换行符,Windows平台为“rn”,Unix平台为“n”  
%d 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921  
%l 输出日志事件的发生位置,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)
log信息耗费的毫秒数  

3.3 Java应用程序代码中使用Log4j

Log4j 1.x

1. 定义/获取记录器/日志器(Logger)

使用Log4j,第一步就是获取日志记录器,这个记录器将负责控制日志信息。其语法为:

public static Logger getLogger( String name)

通过指定的名字获得记录器,如果必要的话,则为这个名字创建一个新的记录器。Name一般取本类的名字,比如:

static Logger logger = Logger.getLogger ( ServerWithLog4j.class.getName () )

2. 读取配置文件

当获得了日志记录器之后,第二步将配置Log4j环境,其语法为:

BasicConfigurator.configure (): 自动快速地使用缺省Log4j环境。  
PropertyConfigurator.configure ( String configFilename) :读取使用Java的特性文件编写的配置文件。  
DOMConfigurator.configure ( String filename ) :读取XML形式的配置文件。

3. 插入记录信息(格式化日志信息)

当上两个必要步骤执行完毕,您就可以轻松地使用不同优先级别的日志记录语句插入到您想记录日志的任何地方,其语法如下:

Logger.debug ( Object message ) ;  
Logger.info ( Object message ) ;  
Logger.warn ( Object message ) ;  
Logger.error ( Object message ) ;

[可选] 4. LogManager: 动态设置Logger的日志等级

import org.apache.log4j.LogManager;

Strig loggerName = "root";

org.apache.log4j.Logger logger = loggerName.equalsIgnoreCase("ROOT")?LogManager.getRootLogger() : LogManager.getLogger(loggerName);
logger.setLevel( Level.toLevel( loggersConfig.getLogLevel() ) );

3.3 Log4J的关键机制

自动加载配置文件

  • 如果没有调用BasicConfigurator.configure()PropertyConfigurator.configure()DOMConfigurator.configure()方法,Log4j会自动加载CLASSPATH下名为log4j.properties的配置文件。

如果把此配置文件改为其他名字,例如my.properties,程序虽然仍能运行,但会报出不能正确初始化Log4j系统的提示
这时可以在程序中加上:PropertyConfigurator.configure("classes/my.properties");

Slf4j + Log4j2 的启动流程(源码分析)

com.xxx.xxx.app.entry.LogTest : private static final Logger logger = LoggerFactory.getLogger(LogTest.class);
org.slf4j.LoggerFactory#getLogger(java.lang.Class<?>)
	org.slf4j.LoggerFactory#getLogger(java.lang.String) | slf4j-api
		org.slf4j.ILoggerFactory#getLogger  | slf4j-api
			org.apache.logging.log4j.spi.AbstractLoggerAdapter#getLogger | log4j-api
				org.apache.logging.log4j.spi.AbstractLoggerAdapter#getContext()
					org.apache.logging.slf4j.Log4jLoggerFactory#getContext

续: Log4jLoggerFactory#getContext
	org.apache.logging.log4j.spi.AbstractLoggerAdapter#getContext(java.lang.Class<?>)
		org.apache.logging.log4j.LogManager#getContext(java.lang.ClassLoader, boolean)
			org.apache.logging.log4j.LogManager#getContext(java.lang.ClassLoader, boolean)	log4j-api							
				org.apache.logging.log4j.spi.LoggerContextFactory#getContext(java.lang.String, java.lang.ClassLoader, java.lang.Object, boolean) | log4j-core


续: LoggerContextFactory#getContext
	org.apache.logging.log4j.core.selector.ContextSelector#getContext(java.lang.String, java.lang.ClassLoader, boolean)
		org.apache.logging.log4j.core.selector.ClassLoaderContextSelector#getContext(java.lang.String, java.lang.ClassLoader, boolean, java.net.URI) | log4j-core
			org.apache.logging.log4j.core.selector.ClassLoaderContextSelector#getContext(java.lang.String, java.lang.ClassLoader, java.util.Map.Entry<java.lang.String,java.lang.Object>, boolean, java.net.URI) | log4j-core
				org.apache.logging.log4j.core.selector.ClassLoaderContextSelector#locateContext | log4j-core
					org.apache.logging.log4j.core.selector.ClassLoaderContextSelector#createContext
						org.apache.logging.log4j.core.LoggerContext(name, externalContext=null, configLocation)
							org.apache.logging.log4j.core.LoggerContext#reconfigure(java.net.URI) | log4j-core
								代码: Object externalContext = externalMap.get(EXTERNAL_CONTEXT_KEY);

续: LoggerContext#reconfigure(java.net.URI)
	org.apache.logging.log4j.core.LoggerContext#setConfiguration
		org.apache.logging.log4j.core.LifeCycle#start
			org.apache.logging.log4j.core.config.AbstractConfiguration#initialize
				org.apache.logging.log4j.core.config.plugins.visitors.PluginElementVisitor#visit

续: PluginElementVisitor#visit
	org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender#start | log4j-core
		org.apache.logging.log4j.core.filter.AbstractFilterable#start | log4j-core
			org.apache.logging.log4j.core.appender.mom.kafka.KafkaManager#startup | log4j-core
				org.apache.logging.log4j.core.appender.mom.kafka.KafkaProducerFactory#newKafkaProducer | log4j-core
				org.apache.logging.log4j.core.appender.mom.kafka.DefaultKafkaProducerFactory#newKafkaProducer
					org.apache.kafka.clients.producer.KafkaProducer | kafka-clients
  • 【补充】调试日志框架/分析源码时,其他的需重点关注的类
org.slf4j.MDC

org.apache.logging.log4j.core.config.LoggerConfig#LoggerConfig()
	org.apache.logging.log4j.core.config.DefaultReliabilityStrategy
		org.apache.logging.log4j.core.config.LoggerConfig#addAppender

org.apache.logging.log4j.core.config.plugins.util.PluginBuilder#build | 重要

org.apache.logging.log4j.core.config.plugins.util.PluginBuilder#createBuilder
	org.apache.logging.log4j.core.util.Builder#build
	
			org.apache.logging.log4j.core.config.PropertiesPlugin#configureSubstitutor
				org.apache.logging.log4j.core.config.PropertiesPlugin#unescape(org.apache.logging.log4j.core.config.Property)
	org.apache.logging.log4j.core.config.LoggerConfig.Builder#build | 重要
		org.apache.logging.log4j.core.config.LoggerConfig#includeLocation(java.lang.String, org.apache.logging.log4j.core.config.Configuration)
			config.propertiesComponent / config.name = "PropertiesConfig" / config.propertyMap / config.componentMap
		org.apache.logging.log4j.core.config.LoggerConfig


org.apache.logging.log4j.core.config.LoggerConfig.Builder | 重要
	
org.apache.logging.log4j.core.appender.mom.kafka.KafkaManager#KafkaManager(org.apache.logging.log4j.core.LoggerContext, java.lang.String, java.lang.String, boolean, boolean, org.apache.logging.log4j.core.config.Property[], java.lang.String)

	config.setProperty(property.getName(), property.getValue());

org.apache.logging.log4j.core.appender.mom.kafka.KafkaManager#getManager(org.apache.logging.log4j.core.LoggerContext, java.lang.String, java.lang.String, boolean, boolean, org.apache.logging.log4j.core.config.Property[], java.lang.String)
	sb.append(" ")
		.append(topic)
		.append(" ")
		.append(syncSend)
		.append(" ")
		.append(sendTimestamp);
	for (Property prop : properties) {
		sb.append(" ").append(prop.getName()).append("=").append(prop.getValue());
	}

org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender.Builder#build
	org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender


org.apache.logging.log4j.core.filter.AbstractFilterable
	@PluginElement("Properties")
	private Property[] propertyArray;
	
org.apache.logging.log4j.core.config.PropertiesPlugin#unescape(org.apache.logging.log4j.core.config.Property)

propertiesComponent

org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration#BuiltConfiguration
org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration#convertToNode


config instanceof BuiltConfiguration

RootLogger : 影响所有 Logger

  • 配置Log4j环境就是指配置root Logger,包括把Logger为哪个级别,为它增加哪些Appender,以及为这些Appender设置Layout,等等。

因为所有其他的Logger都是root Logger的后代,所以它们都继承了root Logger的性质。
这些可以通过设置系统属性的方法来隐式地完成,也可以在程序中调用XXXConfigurator.configure()方法来显式地完成。

配置 Log4j 配置文件的方式

有以下几种方式来配置Log4j。

  • 方式0:配置放在配置文件(log4j2.properties/xml)里,通过org.apache.log4j.PropertyConfigurator.configure(args[])解析log4j.properties文件并配置Log4j

  • 方式1:通过加载其他资源配置文件${bundle}/环境变量${env}/JVM Options变量${sys}/系统属性${sys}/main启动入参${main}/,利用Log4j默认的初始化过程解析并配置。

Appender:Layoutpattern 中使用 ${env:HOST_IP:-127.0.0.1}
...
推荐文献: [日志框架] Log4j2 变量配置管理 - 博客园/千千寰宇

  • 方式2:配置放在远程应用服务(特定的Servlet/HTTP接口)、或配置中心(如:NACOS),应用程序启动时基于HTTP接口等方式获取远程配置,并通过Java代码更新 Log4J框架的Appender或其他配置。

【DEMO】 综合应用

【DEMO】:通过Java程序代码动态添加Appender

  • step1:用默认的方式创建PatternLayout对象p:
  • PatternLayout p = new PatternLayout("%-4r[%t]%-5p%c%x-%m%n");
  • step2: 用p创建ConsoleAppender对象a,目标是System.out,标准输出设备:
  • ConsoleAppender a = new CpnsoleAppender(p,ConsoleAppender.SYSTEM_OUT);
  • step3: 为root Logger增加一个ConsoleAppender p:
  • rootLogger.addAppender(p);
  • step4: 把rootLogger的log level设置为DUBUG级别
  • rootLogger.setLevel(Level.DEBUG);

【DEMO】通过Java程序代码动态添加Appender

import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.layout.PatternLayout;

private Properties applicationProperties;

//Java应用程序,启动时执行 execute 方法(获取远程的配置文件、亦或是数据库、或其他配置文件的配置转为 Properties:applicationProperties)
public void execute() throws Exception {
    log.debug("Initializing {} ...", this.getClass().getCanonicalName());

    LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
    Configuration config = ctx.getConfiguration();
    Appender kafkaAppender = createKafkaAppender(ctx, config, applicationProperties);
    kafkaAppender.start();//防止错误: Attempted to append to non-started appender testName

    Level level = getLevel(applicationProperties);
    config.getRootLogger().addAppender(kafkaAppender, level, null);// 添加 Appender 到配置中
    ctx.updateLoggers();

    log.debug("Initialized {} ...", this.getClass().getCanonicalName());
}


private static Appender createKafkaAppender(LoggerContext loggerContext,Configuration configuration, Properties applicationProperties) {
    KafkaAppender kafkaAppender = null;
    if(loggerContext == null){
        loggerContext = (LoggerContext) LogManager.getContext(false);
    }
    if(configuration == null){
        configuration = loggerContext.getConfiguration();
    }

    final PatternLayout layout = PatternLayout.newBuilder()
            .withCharset(Charset.forName("UTF-8"))
            .withConfiguration(configuration)
            .withPattern("%d %p %c{1.} [%t] %m%n").build();

    Filter filter = null;

    String topic = applicationProperties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_TOPIC_PARAM);
    String appenderName = applicationProperties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_TOPIC_PARAM) + "Log4J2KafkaAppender";

    Property [] propertyArray = propertiesToPropertyArray(applicationProperties);

    String messageKey = applicationProperties.getProperty( Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_KEY_PARAM );
    Boolean isIgnoreExceptions = Boolean.getBoolean(applicationProperties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_APPENDER_IGNORE_EXCEPTIONS_PARAM, Constants.Log4j2KafkaAppender.KAFKA_APPENDER_IGNORE_EXCEPTIONS_DEFAULT));
    Boolean syncSend = Boolean.getBoolean(applicationProperties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_SYNC_SEND_PARAM, Constants.Log4j2KafkaAppender.KAFKA_PRODUCER_SYNC_SEND_DEFAULT));
    Boolean sendEventTimestamp = Boolean.getBoolean(applicationProperties.getProperty(Constants.Log4j2KafkaAppender.KAFKA_APPENDER_SEND_EVENT_TIMESTAMP_PARAM, Constants.Log4j2KafkaAppender.KAFKA_APPENDER_SEND_EVENT_TIMESTAMP_DEFAULT));

    //kafkaAppender = KafkaAppender.createAppender(layout, filter, appenderName, isIgnoreExceptions, topic, propertyArray, configuration, key );//此方式不支持传入 syncSend 参数
    //kafkaAppender = new KafkaAppender(name, layout, filter, isIgnoreExceptions, kafkaManager, getPropertyArray(), getRetryCount());//此方式,因构造器是 private,不支持

    kafkaAppender = KafkaAppender.newBuilder()//此方式 √
            .setName(appenderName)
            .setConfiguration(configuration)
            .setPropertyArray(propertyArray)
            .setFilter(filter)
            .setLayout(layout)
            .setIgnoreExceptions(isIgnoreExceptions)
            .setTopic(topic)
            .setKey(messageKey)
            .setSendEventTimestamp(sendEventTimestamp)
            .setSyncSend(syncSend)
            .setRetryCount(3)
            .build();

    return kafkaAppender; // 需要替换为实际的 Appender 创建代码
}

/**
 * @note
 *  1. required properties:
 *      1. {@link Constants.Log4j2KafkaAppender#LEVEL_PARAM}
 * @param applicationProperties
 * @return
 */
private static Level getLevel(Properties applicationProperties) {
    Level level = null;
    String levelStr = applicationProperties == null ? Constants.Log4j2KafkaAppender.LEVEL_DEFAULT : applicationProperties.getProperty( Constants.Log4j2KafkaAppender.LEVEL_PARAM );
    levelStr = (levelStr == null || levelStr.equals("") ) ? Constants.Log4j2KafkaAppender.LEVEL_DEFAULT : levelStr.toUpperCase();
    level = Level.getLevel(levelStr);
    log.info("user config's `{}`'s log level: {}", KafkaAppender.class.getCanonicalName(), levelStr);
    return level;
}
  • 其他
  • 配置放在文件里,在程序中直接调用org.apache.log4j.BasicConfigor.configure()方法
  • org.apache.log4j.DOMConfigurator.configure(String filename);

3.4 核心概念、核心组件

  • 推荐文献
  • org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration#BuiltConfiguration

  • Log4j有3大最重要的核心组件: Logger 、Appender 、Layout,分别定义了:

    • 日志信息的作用的对象、优先级

      • 日志信息的优先级从高到低有: ERRORWARNINFODEBUG,分别用来指定这条日志信息的重要程度;
    • 日志信息的输出目的地

      • 日志信息的输出目的地指定了日志将打印到控制台还是文件中;
    • 日志信息的输出格式

      • 而输出格式则控制了日志信息的显 示内容

核心概念:日志信息的优先级

核心组件: Logger

  • 【日志器(Logger)】:用来输出消息的类,可以输出不同级别的,比如错误消息,警告消息等
    • 创建日志器

      • Logger log = Logger.getLogger(calssNameX.class); log.info(); log.debug(); [log4J方式]
      • Logger logger = LoggerFactory.getLogger(calssNameX.class); log.info(); log.debug();[slf方式(推荐)]
    • 根日志器(rootLogger)

      • 若需要输出到多个位置的时候可用逗号隔开: log4j.rootLogger=info, A, B (log4j 1.x 方式) / rootLogger=info, A, B (log4j 2.x 方式)

log4j 1.x :

在配置文件里,需要为log4j.properties配置一个根日志器:
log4j.rootLogger=DEBUG,AA
log4j.rootLogger=WARN
log4j.APPENDER.AA=org.apache.log4j.ConsoleAppender

log4j 2.x :

在配置文件里,需要为log4j2.properties配置一个根日志器:

# ------------------- 定义 RootLogger 等 全局性配置(不可随意修改) ------------------- #
## rootLogger, 根记录器,所有记录器的父辈
## 指定根日志的级别 | All < Trace < Debug < Info < Warn < Error < Fatal < OFF
rootLogger.level=${log.level}
## 指定输出的appender引用
## 2.17.2 版本以下通过这种方式将 root 和 Appender关联起来
## 2.17.2 版本以上有更简便的写法
rootLogger.appenderRef.stdout.ref=${log.consoleAppender}
rootLogger.appenderRef.rolling.ref=${log.systemFileAppender}
rootLogger.appenderRef.linkTrace.ref=${log.linkTraceClientTargetAppender}

核心组件: Appender

  • 【输出源(Appender)】:日志输出的目标。日志输出到哪里去(文件、控制台)
  • org.apache.log4j.ConsoleAppender: 向控制台输出日志
  • org.apache.log4j.FileAppender: 向文件输出日志
  • org.apache.log4j.DailyRollingFileAppender
  • org.apache.log4j.RollingFileAppender
  • org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender : 向Kafka输出日志
  • 自定义 Appender

log4j 1.x

log4j.appender.AA.File=../log.txt
log4j.appender.AA.LAYOUT=org.apacskhe.log4j.SimpleLayout
log4j.appender.AA.DatePatten='yyyy-MM-dd'

log4j 2.x

    <Console name="MyConsoleAppender" target="SYSTEM_OUT">
        <PatternLayout pattern="${log.layout.consolePattern}" />
        <!--<CustomPatternLayout pattern="${log.layout.consolePattern}" />-->
    </Console>

核心组件:Filter

核心组件:Properties

核心子组件: Layout

  • 【格式化器(Layout)】:对输出消息进行格式化,比如添加日期
  • org.apache.log4j.PatternLayout
  • org.apache.log4j.SimpleLayout
  • org.apache.log4j.HTMLLayout
  • org.apache.log4j.TTCCLayout

3.5 日志器 [Logger]

  • 特点

    • Logger(log4j)记录的是当前类的日志,不是每个实例的日志
      • 使用static修饰的属性
      • 只要有一个记录就可以了
    • private static final Logger logger = LoggerFactory.getLogger(Slf4jTest.class.getName());// slf4j 日志记录器
    • 每个Logger都配有1个日志级别(Log Level):用来控制日志信息的输出。
  • rootLogger(根日志器)

    • root Logger(根Logger)是所有Logger的祖先
    • 它总是存在的
    • 它不可以通过名字获得
      • public static Logger Logger.getRootLogger();
      • public static Logger Logger.getLogger(Class clazz)

3.6 输出源[Appender]

A) Log4J提供的Appender

ConsoleAppender | log4j 1.x/2.x

  • org.apache.log4j.ConsoleAppender | log4j 1.x (控制台)
Threshold=WARN:指定日志信息的最低输出级别,默认DEBUG
ImmediateFlush=true:表示所有消息都会被立即输出,设为false则不输出,默认值是true
Target=System.err:默认值是System.out
  • org.apache.logging.log4j.core.appender.ConsoleAppender | log4j 2.x
<Console name="MyConsoleAppender" target="SYSTEM_OUT">
    <PatternLayout pattern="${log.layout.consolePattern}" />
</Console>

FileAppender | log4j 1.x/2.x

  • org.apache.log4j.FileAppender | log4j 1.x (文件)
Threshold=WARN:指定日志信息的最低输出级别,默认DEBUG
ImmediateFlush=true:表示所有消息都会被立即输出,设为false则不输出,默认true
Append=false:true表示消息增加到指定文件中,false则将消息覆盖指定的文件内容,默认true
File=D:/logs/logging.log4j:指定消息输出到logging.log4j文件
  • org.apache.logging.log4j.core.appender.FileAppender | log4j 2.x

RollingFileAppender | log4j 1.x/2.x

  • org.apache.log4j.RollingFileAppender (文件大小到达指定尺寸的时候产生一个新的文件)
Threshold=WARN:指定日志信息的最低输出级别,默认DEBUG
ImmediateFlush=true:表示所有消息都会被立即输出,设为false则不输出,默认true
Append=false:true表示消息增加到指定文件中,false则将消息覆盖指定的文件内容,默认true
File=D:/logs/logging.log4j:指定消息输出到logging.log4j文件
MaxFileSize=100KB:后缀可以是KB,MB或者GB。在日志文件到达该大小时,将会自动滚动,即将原来的内容移到logging.log4j.1文件
MaxBackupIndex=2:指定可以产生的滚动文件的最大数,例如,设为2则可以产生logging.log4j.1,logging.log4j.2两个滚动文件和一个logging.log4j文件
  • org.apache.logging.log4j.core.appender.RollingFileAppender | log4j 2.x

RollingRandomAccessFileAppender | log4j 2.x

  • org.apache.logging.log4j.core.appender.RollingRandomAccessFileAppender | log4j 2.x

DailyRollingFileAppender | log4j 1.x

  • org.apache.log4j.DailyRollingFileAppender | log4j 1.x (每天产生一个日志文件, Log4j 1.x 常用,但Log4j2中已消失)
Threshold=WARN:指定日志信息的最低输出级别,默认DEBUG
ImmediateFlush=true:表示所有消息都会被立即输出,设为false则不输出,默认true
Append=false:true表示消息增加到指定文件中,false则将消息覆盖指定的文件内容,默认true
File=D:/logs/logging.log4j:指定当前消息输出到logging.log4j文件
DatePattern='.'yyyy-MM:每月滚动一次日志文件,即每月产生一个新的日志文件。
                        当前月的日志文件名为logging.log4j,前一个月的日志文件名为logging.log4j.yyyy-MM
另外,也可以指定按周、天、时、分等来滚动日志文件,对应的格式如下:
1)'.'yyyy-MM:每月
2)'.'yyyy-ww:每周
3)'.'yyyy-MM-dd:每天
4)'.'yyyy-MM-dd-a:每天两次
5)'.'yyyy-MM-dd-HH:每小时
6)'.'yyyy-MM-dd-HH-mm:每分钟

WriterAppender | log4j 1.x

  • org.apache.log4j.WriterAppender | log4j 1.x (将日志信息以流格式发送到任意指定的地方)
  • org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender
        <!--
            @warn
                1. 此 KafkaAppender 不建议在 生产环境 的 log4j2.xml/properties 中启用,因 无法从外部动态注入 kafka broker servers
                2. 针对第1点,需通过 自定义的 {@link org.example.app.hooks.startup.impl.Log4j2KafkaAppenderInitializer } ,实现程序启动时动态注册 KafkaAppender
            @Appender : KafkaAppender | org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender | log4j-core:2.20.0
            @note
                1. 计划在下一个主要版本中删除此附加程序!如果您正在使用此库,请使用官方支持渠道 与 Log4j 维护人员联系。
                    from https://logging.apache.org/log4j/2.x/manual/appenders/message-queue.html#KafkaAppender
                2. 使用 Kafka Appender 需要额外的运行时依赖项 : org.apache.kafka:kafka-clients:{version}
                    from https://logging.apache.org/log4j/2.x/manual/appenders/message-queue.html#KafkaAppender
                3. Kafka appender ignoreExceptions 必须设置为false,否则无法触发 FailOver Appender
                4. 确保不要让 `org.apache.kafka`Logger 日志记录的日志级别为 DEBUG,因为这将导致`KafkaAppender`递归日志记录
                    from https://logging.apache.org/log4j/2.x/manual/appenders/message-queue.html#KafkaAppender
            @property
                //配置属性
                * name: Log Framework's Appender 's Name
                * topic : Kafka Topic Name

                key:String : Kafka Message(`ProducerRecord`) 的 key。 支持 运行时属性替换,并在全局上下文 中进行评估。
                    参考: https://logging.apache.org/log4j/2.x/manual/appenders/message-queue.html#KafkaAppender
                    参考 : https://logging.apache.org/log4j/2.x/manual/lookups.html#global-context
                    推荐值: key="$${web:contextName}" | contextName 是 log4j2 内置的变量
                ignoreExceptions:boolean[DefaultValue:true] : 如果false,日志记录异常将被转发给日志记录语句的调用者。否则,它们将被忽略。
                syncSend:boolean[DefaultValue:true] : 如果true,附加器将阻塞,直到 Kafka 服务器确认该记录为止。否则,附加器将立即返回,从而实现更低的延迟和更高的吞吐量。

                //嵌套属性
                Filter
                Layout
                Property[0..n] : 这些属性会直接转发给 Kafka 生产者。 有关更多详细信息,请参阅 Kafka 生产者属性。
                    参考: https://kafka.apache.org/documentation.html#producerconfigs
                    bootstrap.servers : 此属性是必需的
                    key.serializer : 不应使用这些属性
                    value.serializer : 不应使用这些属性
        -->
        <Kafka name="MyKafkaAppender" topic="flink_monitor_log" key="$${web:contextName}" syncSend="true" ignoreExceptions="false">
            <!--<JsonTemplateLayout/>-->
            <PatternLayout pattern="${log.layout.mainPattern}"/>
            <Property name="bootstrap.servers" value="${log.appender.kafka.producer.bootstrap.servers}"/>
            <Property name="max.block.ms">2000</Property>
        </Kafka>

第三方 | GRPCLogClientAppender

B) Apache Skywalking 或用户自己实现的第三方的Appender

  • org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender

[注意事项] Skywalking 的 Layout 所支持的ConversionPattern核心参数在apm-toolkit-log4j-1.x插件中是T(即 traceId, 区分大小写[T≠t]),在apm-toolkit-logback-1.x插件中是%X{tid},需打印到日志中,才能被 Skywalking OAP 服务的Collertor收集并识别请求链路中的日志信息,否则日志收集失败。
[注意事项] Skywalking for log4j 的JAR包依赖信息:

<dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-log4j-1.x</artifactId>
    <version>8.5.0</version>
</dependency>
<dependency>
    <groupId>org.apache.skywalking</groupId>
    <artifactId>apm-toolkit-trace</artifactId>
    <version>8.5.0</version>
</dependency>
Threshold=INFO
layout=org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLayout
layout.ConversionPattern=%d %-5p | %X{tid} | %t | (%C{1}.java:%L %M) | %m%n
  • org.apache.skywalking.apm.toolkit.log.log4j.v1.x.log.GRPCLogClientAppender
Threshold=INFO
layout=org.apache.skywalking.apm.toolkit.log.log4j.v1.x.TraceIdPatternLayout
layout.ConversionPattern=%d %-5p | %T | %t | (%C{1}.java:%L %M) | %m%n

上述 log4j 的 parttern 的样例日志记录:

2023-02-08 11:07:55,501 INFO  | TID:fee46004e86942d9a13a1ca8be0b88b1.44.16758256563660001 | http-nio-8081-exec-2 | (SkywalkingController.java:83 callThridPartyService$original$MTe6LVxm) | thridPartyServiceUrl: http://127.0.0.1:8081/study-springmvc/hello.do?caller=StudySpringMVCService&token=helloToken, statusCode: <200 OK,200>, headers: [Set-Cookie:"JSESSIONID=AB568DF47926DB97BDA634E0E348FF0D; Path=/study-springmvc; HttpOnly", Content-Type:"text/html;charset=UTF-8", Content-Language:"zh-CN", Content-Length:"342", Date:"Wed, 08 Feb 2023 03:07:55 GMT", Keep-Alive:"timeout=20", Connection:"keep-alive"] 
2023-02-08 11:07:55,501 INFO  | TID:fee46004e86942d9a13a1ca8be0b88b1.44.16758256563660001 | http-nio-8081-exec-2 | (SkywalkingController.java:90 callThridPartyService$original$MTe6LVxm) | Time-consuming in total: 2155ms

3.7 日志格式化器[Layout]

log4j 1.x 指定logger输出内容及格式: log4j.appender.appenderName.layout=className

log4j 2.x 指定logger输出内容及格式:

<Root level="${log.level}"> <!-- ${log.level} -->
    <!-- 2.17.2 版本以下通过这种方式将 root 和 Appender关联起来 / 2.17.2 版本以上有更简便的写法 -->

    <!-- rootLogger.appenderRef.stdout.ref=${log.consoleAppender} -->
    <AppenderRef ref="${log.consoleAppender}" level="INFO" />

    <!-- rootLogger.appenderRef.kafka.ref=${log.loggingSystemMessageQueueAppender} -->
    <AppenderRef ref="${log.loggingSystemMessageQueueAppender}"/> <!-- MyKafkaAppender -->
</Root>

<Logger name="org.apache.kafka" level="WARN" additivity="false">
    <AppenderRef ref="${log.consoleAppender}"/>
</Logger>

PatternLayout

  • org.apache.log4j.PatternLayout | log4j 1.x(最常用,可以灵活地指定布局模式)根据指定的转换模式格式化日志输出,或若未指定任何转换模式,就用默认的转化模式格式

    • ConversionPattern=%m%n:设定以怎样的格式显示消息
  • org.apache.logging.log4j.core.layout.PatternLayout | log4j 2.x

HTMLLayout

  • org.apache.log4j.HTMLLayout:(以HTML表格形式布局)格式化日志输出为HTML表格形式
    • LocationInfo=true:输出java文件名称和行号,默认false
    • Title=My Logging: 默认值是Log4J Log Messages

SimpleLayout

  • org.apache.log4j.SimpleLayout:(包含日志信息的级别和信息字符串)以一种非常简单的方式格式化日志输出,它打印三项内容:级别-信息

TTCCLayout

  • org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)
【patterm参数】
%p: 日志级别<从高到低>(OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL)
%c: 输出日志信息所属的类目,通常就是所在类的全名。可写为%c{num},表示取完整类名的层数,从后向前取,比如%c{2}取 "cn.qlq.exam"类为"qlq.exam"。
%r: 输出自应用启动到输出该日志耗费的毫秒数
%d: 输出日志时间点的日期或时间。默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyyy-MM-dd HH:mm:ss,SSS},SSS为毫秒数(也可以写为SS,只不过SSS如果不足三位会补0),输出类似:2011-10-18 22:10:28,021
%m: 输出代码中指定的消息,产生的日志具体信息
%n: 输出一个回车换行符,Windows平台为"\r\n",Unix平台为"\n"输出日志信息换行
%t: 输出当前产生日志的线程名称
%F: 输出日志消息产生时所在的文件名称 
%x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中 
%%: 输出1个"%"字符
%l: 输出日志事件的位置,相当于%c.%M(%F:L)的组合,包括类全名、方法、文件名以及在代码中行数。例如:cn.xm.test.PlainTest.main(PlanTest.java:12)

4 最佳实践

1. 日志的基本格式

  • 基本的日志格式需要输出:时间、级别、线程名称、logger 名称、日志内容。

  • 如果能拿到调用链 ID 的话,输出到日志中对问题的定位帮助很大。

    出现异常时,将异常堆栈输出到日志也是非常有必要的。日志产生的日期和时间非常重要,一般精确到毫秒,推荐格式 yyyy-MM-dd HH:mm:ss.SSS。

    通常线上日志配置了按天滚动,日志文件名带有日期,此时使用 HH:mm:ss.SSS 格式即可。

2. 使用门面模式的日志框架(SLF4J),而不要直接使用具体日志实现框架

  • 一方面面向接口编程更优雅;
  • 另一方面便于统一使用方式和日后的维护。
    • 比如,老的项目中直接使用了 log4j,但 log4j 已经停止维护不再更新,现在想升级到 log4j2 后换成 logback,因 API 的不兼容所以替换的代价就高了。

3. 一个类中通常只使用一个 Logger 对象,Logger 应该是 private static final

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static final Logger logger = LoggerFactory.getLogger(Demo.class); 

4. 使用占位符,不要使用字符串拼接

字符串拼接一方面不便于阅读,另一方面字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符 仅是替换动作,可以有效提升性能。

// 推荐
logger.info("用户 {} 借走图书 <<{}>> ",  userName, bookTitle);

// 不推荐
logger.info("用户 " + userName + " 借走图书 <<" + bookTitle + ">>");

5. 输出异常的全部信息,不要使用 logger.error(msg) 和 logger.error(msg,e.getMessage()),因为它们会丢失掉最重要的 StackTrace 信息

void foo() {
    try {
        //  do something ...
    } catch ( Exception e ) {
        logger.error("Bad things : ", e); // 正确

        logger.error(e.getMessage());  // 错误
        logger.error("Bad things : ", e.getMessage()); // 错误
    }
}

6. catch 分支不要使用 System print(包括 System.out.println 和 System.error.println)和 printStackTrace 语句,代替 log 打印异常信息。

void foo() {
    try{
        // do something...
    } catch ( Exception e ) {
        logger.error("Bad things : ",e );  // 正确

        System.out.println(e.getMessage());  // 错误
        System.err.println(e.getMessage());  // 错误
        e.printStackTrace();  // 错误
    }
}

7. 出于性能的考虑,建议:执行频率非常高的、或需消耗大量计算资源的核心代码的 trace/debug 低级别的日志增加开关判断

  • 虽然日志框架内部有级别开关判断,比如 logger.debug(…) 在 Slf4j 内部调用的是 ExtendedLogger.logIfEnabled(String var1, Level var2 …),但是参数可能会进行字符串拼接运算,此时直接在外层进行开关判断可以省去无畏的方法调用,以及可能的字符串拼接开销。
if (logger.isDebugEnabled ()) {
    logger.debug("return content:" + content);
}

8. 可用 warn 级别日志记录【不在功能范围内】的操作,或错误的请求参数等场景,避免用户投诉时不知所措。但此类场景不要使用 error 级别日志,避免不必要的告警。

9. 严格控制生产日志

  • 禁止输出 debug 日志,对于支持动态调整日志级别的,不要将打开 debug 及以下级别的开关;
  • 有选择地输出 info 日志,避免打印大的对象,应该选择性的将关键的业务信息打印出来;
  • 如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。

10. Logback / Log4j / Log4j2 中的 additivity 属性设为 false

  • 通过 additivity 属性可以控制子 Logger 是否继承父 Logger 的 appender,Logback / Log4j / Log4j2 都支持该属性。
  • 属性 additivity 默认为 true 表示子 Logger 会继承父 Logger 的 appender,子 Logger 会在父 Logger 的 appender 里输出。
  • 将 additivity 设为 false,则子 Logger 只会在自己的 appender 里输出,可避免重复日志打印。
<!--Logback 中关闭 additivity-->
<Configuration>
  ...
  <Loggers>
    <Logger name="demo.wood" level="debug" additivity="false">
      <AppenderRef ref="applog" />
    </Logger>
    ...
  </Loggers>
</Configuration>

<!--Log4j2 中关闭 additivity-->
<Configuration>
    ...
    <Loggers>
        <Logger name="org.springframework" level="INFO" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        ...
    </Loggers>
</Configuration>

11. 谨慎打印日志

  • 要明确不同日志的用途,对日志内容进行分类,比如框架日志和应用日志分离。
  • 绝不要打印没用的日志,防止无用日志淹没重要信息;日志信息要精准,努力做到仅凭日志就可以定位问题。没有限制的日志输出,会带来无畏的性能和资源浪费,严重的会导致 OOM,应用直接崩溃。
  • 查询类接口不要将查询出来的数据全部打印,存在 DB 中的数据,查询出来后再打印一遍意义不带还消耗资源。
  • 写接口需要写日志,尽可能将有用的业务信息记录下来,但也要避免 controller、service 等多层重复打印。

12. 建议提供动态日志输出功能

动态日志输出可以实时调整系统日志级别,常见的做法是配置中心提供了服务的日志级别动态下发功能,在需要时实时调整(一般都要审批)日志级别以便获取重要信息。虽然不建议在生产中打开低级别的日志,但在遇到重大疑难问题时,可临时选择一个生产实例动态调低日志级别,以便问题排查。所以动态日志在实际中还是很有用的。关键动态日志的实现,可参考下节说明。

5 动态日志输出


Log4j2 和 Logback 都支持在线动态修改日志级别,除了提供相应的 API,还提供了 JMX 支持。下面通过例子演示下两种使用方式,其中 logback 的版本为 1.2.3,log4j2 的版本为 2.13.3(不同版本的 API 可能有所不同,在使用前先去官方文档 double check 下)。

5.1. 通过 API 动态修改日志级别

Log4j2 和 Logback 都提供了修改日志级别的方法 setLevel,其中 Logback 的 setLevel 方法位于 classch.qos.logback.classic.Logger里, setLevel 方法,Log4j 的 setLevel 方法位于org.apache.logging.log4j.core.config.Configurator下。使用方式上都是先获取到需要设置的 logger,然后在对 logger 设置级别,下面的示例代码演示了两者 API 的使用方式。

// Example 1: 设置 Logback 日志级别
Logger logger = oggerContext.getLogger("需要设置级别的 logger 名");
logger.setLevel(Level.DEBUG);

// Example 2: 设置 Log4j2 日志级别
Logger logger4j = LogManager.getLogger("需要设置级别的 logger 名");
Configurator.setLevel(logger4j.getName(), Level.ERROR);

由于我们通常使用 SL4J 而不是直接使用具体的日志框架,因而在获取 logger 的过程中注意类型转换。下面通过 DynamicLogbackLevelDemo 演示下在 SLF4J + Logback 使用方式下,如何通过 API 动态修改日志级别。Log4j / Log4j2 的使用方式类似,这里不再演示。

  • Logback 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss}] -- [%-5p]: [%c] -- %m%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>
  • Demo Code
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import com.google.common.collect.ImmutableMap;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
import java.util.Scanner;

public class DynamicLogbackLevelDemo {
    private static final Logger logger = LoggerFactory.getLogger(DynamicLogbackLevelDemo.class);

    private static final Map<Integer, String> LOG_LEVEL_MAP = new ImmutableMap.Builder<Integer, String>()
            .put(1, "TRACE").put(2, "DEBUG").put(3, "INFO").put(4, "WARN").put(5, "ERROR").build();

    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        String userInput = null;
        do {
            System.out.printf("\nPlease Enter Logback Logger Level (q to quit): 1 - TRACE, 2 - DEBUG, 3 - INFO, 4 - WARN, 5 - ERROR\n");

            userInput = scanner.nextLine().trim();
            if (!StringUtils.isNumeric(userInput)) {
                continue;
            }

            String logLevel = LOG_LEVEL_MAP.get(Integer.parseInt(userInput));
            if (null != logLevel) {

                // 获取 loggert
                LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
                ch.qos.logback.classic.Logger tmplogger = loggerContext.getLogger(DynamicLogbackLevelDemo.class);

                // 修改日志级别
                System.out.printf("[current logger level]: %s ---> [new logger level]: %s\n", tmplogger.getLevel(), logLevel);
                tmplogger.setLevel(Level.toLevel(logLevel));

                System.out.println("verifying new logger level:");
                logger.trace("sample trace log");
                logger.debug("sample debug log");
                logger.info("sample info log");
                logger.warn("sample warn log");
                logger.error("sample error log");
            }
        } while (!"q".equalsIgnoreCase(userInput));
        scanner.close();
    }
}

下图是运行 DynamicLogbackLevelDemo 后的截图,可以看到,随着用户输入不同的日志级别,logback 就会按级别进行日志过滤,并在控制台输出不同的日志信息。

img

5.2. 使用 JMX 动态修改日志级别

Log4j2 和 Logback 都支持 JMX 设置日志级别,其中 Log4j2 不需要配置默认就打开了 JMX 功能,而 Logback 需要在配置文件中添加 <jmxConfigurator /> 才能开启 JMX 功能。下面通过 DemoApp 演示下 Logback 的 JMX 功能,Log4j2 / Log4j 类似,这里不再赘述。

DemoApp 是在 spring initializr 网站 https://start.spring.io 上自动生成的一个应用,由于 springboot 默认使用 logback,因而 DemoApp 没有额外配置,只是增加了下面的 logback 配置文件。

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
    <!-- 开启 JMX -->
    <jmxConfigurator />

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%logger{36}]: %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/demoApp.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>demoApp-%d-%i.log</fileNamePattern>
            <MaxHistory>20</MaxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>500MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] [%-5level] [%logger{36}]: %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <logger name="org.springframework" additivity="false">
        <level value="ERROR" />
        <appender-ref ref="STDOUT" />
        <appender-ref ref="fileAppender" />
    </logger>

    <logger name="org.apache.tomcat.util" additivity="false">
        <level value="ERROR"/>
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="fileAppender"/>
    </logger>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="fileAppender"/>
    </root>
</configuration>

启动 DemoApp 后,就可以验证 Logback JMX 的功能了。这里选择 jconsole 测试下 Logback 的动态日志输出功能。使用 jconsole 连接上 DemoApp 后,在左边的窗口找到 *ch.qos.logback.classic* 打开 operations 就可以看到 setLoggerLevel 操作,填上 logger 名称和日志级别后回车,就能及时修改日志级别。下图展示了在 jconsole 中将 org.springframework 包下的日志级别调整为 debug 后,控制台立即输出 debug 及以上级别的日志效果。

img

下面为使用 jconsole 动态调整 Log4j2 的截图,操作起来和 logback 类似。由于 Springboot 默认使用 logback,同时也提供了对 log4j2 的集成,因而在 springboot 项目中使用 log4j2 时需要在 pom 中排除spring-boot-starter-logging的依赖,同时添加spring-boot-starter-log4j2的依赖即可。

img

5.2 程序启动时动态从远程配置中心获取配置,并动态配置 日志框架的 Appender

6 问题集/FAQ

Q:问题:log4j:WARN No appenders could be found for logger...log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

1: log4j.properties 放置在/src/log4j.properties

2: org.apache.log4j.BasicConfigurator.configure(); //自动快速地使用缺省Log4j环境

7 实际项目开发中的日志应用

案例0 Slf4j + log4j 1.x [2020.06.04]

step1 新建Java工程,并引入 Log4j JAR包

新建一个 JAVA 工程,导入包log4j-*.jar,整个工程最终目录如下:

以 Mave 方式引入JAR包为例:

step2 Maven(pom.xml):JAR包依赖

<!-- 日志 -->
<!-- Slf<Simple Logging Facade For Java> + Log4J -->
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
<!-- slf4j与log4j的整合jar包 : 其将自动引入其log4j-1.2.17.jar -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.25</version>
</dependency>

除了上面这种方式外,也可以:(不推荐 / 非基于 Slf4j 的方式)

<!-- 加入log4j支持 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

step3 Maven(pom.xml):build(自动构建)

<build>
    <!-- ↓解决问题↓:IDEA的Maven不会编译src/java目录的xml文件 在Mybatis的配置文件中找不到xml文件 -->
    <!-- 在pom文件中新加<build>提示编译器:src/java下存在配置文件-->
    <!-- 资源目录 -->
    <resources>
        <resource>
            <!-- 设定主资源目录  -->
            <directory>src/main/java</directory>
            <!-- maven default生命周期,process-resources阶段执行maven-resources-plugin插件的resources目标处理主资源目下的资源文件时,只处理如下配置中包含的资源类型 -->
            <includes>
                <include>**/*.xml</include>
                <include>**/*.properties</include>
            </includes>
            <!-- maven default生命周期,process-resources阶段执行maven-resources-plugin插件的resources目标处理主资源目下的资源文件时,不处理如下配置中包含的资源类型(剔除下如下配置中包含的资源类型)-->
            <excludes>
                <exclude>**/*.yaml</exclude>
            </excludes>

            <!-- maven default生命周期,process-resources阶段执行maven-resources-plugin插件的resources目标处理主资源目下的资源文件时,指定处理后的资源文件输出目录,默认是${build.outputDirectory}指定的目录-->
            <!--<targetPath>${build.outputDirectory}</targetPath> -->

            <!-- maven default生命周期,process-resources阶段执行maven-resources-plugin插件的resources目标处理主资源目下的资源文件时,是否对主资源目录开启资源过滤 -->
            <filtering>true</filtering>
        </resource>
    </resources>
</build>

step4 配置文件: log4j.properties

###########################################################################################
# 注意事项:
#  1 出现此类异常的可能原因
#       [异常信息]↓
#           log4j:WARN No appenders could be found for logger (org.example.XXXXTest).
#           log4j:WARN Please initialize the log4j system properly.
#           log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
#       [原因/方案]
#           1)【根前缀】必须是 log4j,而非log。
#           2)构建/编译后的target/classes目录下是否存在 【log4j[2].properties/xml】文件
###########################################################################################

### Global logging configuration ###
####################################
### 根日志器(rootLogger) : 新的使用名称,对应Logger类 ###
#       配置: (继承的)子日志器的默认日志级别 + 自定义的日志输出源(Appender)
#       日志级别: OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL
#       自定义的输出源: CONSOLE, stdout, logfile, 其他自定义名字
#       示例:
#           log4j.rootLogger=ERROR,file,stdout 表示,日志级别为ERROR的日志输出到控制台(stdout)和file中
log4j.rootLogger=ALL, stdout, ERROR_FILE

### 根目录(rootCategory) : 旧的使用名称,对应原来的Category类 ###
#log4j.rootCategory=ERROR, LOGFILE
### log4j.category.* : 对自定义类的设置,可对类、包和工程单独设置
#log4j.category.org.springframework=error
#log4j.category.org.apache=error

### 日志器继承状态 additivity
#   意义: 子Logger是否继承父Logger的输出源[appender]的标志位
#   默认: [true]子Logger会继承父Logger的appender (即 子Logger会在父Logger的appender里输出)
log4j.additivity=false
#log4j.additivity.com.example.user.service=false

### 配置: 具体Java包的日志级别 ###
################################
log4j.logger.org.apache=INFO
log4j.logger.org.springframework=warn
log4j.logger.org.hibernate=ERROR
# mybatis
log4j.logger.com.ibatis=DEBUG
log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=DEBUG
log4j.logger.com.ibatis.common.jdbc.ScriptRunner=DEBUG
log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate=DEBUG
# sql
log4j.logger.java.sql.ResultSet=INFO
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG

### 配置: 输出源 ###
###################
### 控制台(ConsoleAppender) ###
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
# ↓Threshold=WARN:指定日志信息的最低输出级别,默认DEBUG
log4j.appender.stdout.Threshold=DEBUG
# ↓Target=System.err:默认值是System.out,可选值:System.err / System.out / ...
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
# Pattern to output the caller's file name and line number.
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] [%l] - %m%n

### 文件(org.apache.log4j.FileAppender) ###
# org.apache.log4j.DailyRollingFileAppender(特有属性:datePattern / 可实现每天单独产生1个日志文件)
#       [extends FileAppender]
# org.apache.log4j.RollingFileAppender(特有属性:MaxBackupIndex / MaxFileSize)
#       [extends FileAppender]
log4j.appender.ERROR_FILE=org.apache.log4j.DailyRollingFileAppender
log4j.appender.ERROR_FILE.name=LogDemo_File
# ↓Threshold=WARN:指定日志信息的最低输出级别,默认DEBUG
log4j.appender.ERROR_FILE.Threshold=ERROR
# ↓ D:/project.log | ../logs/logs.log | /example.log[项目根目录] | example.log[classes目录下]
log4j.appender.ERROR_FILE.File=logs/error.log
log4j.appender.ERROR_FILE.Encoding=UTF-8
log4j.appender.ERROR_FILE.Append=true
log4j.appender.ERROR_FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.ERROR_FILE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] [%l] - %m%n
# ↓Keep one backup file
#log4j.appender.FILE.MaxBackupIndex=5 [org.apache.log4j.RollingFileAppender 特有属性]
#log4j.appender.FILE.MaxFileSize=100KB [org.apache.log4j.RollingFileAppender 特有属性]
log4j.appender.ERROR_FILE.DatePattern='_'yyyy-MM-dd-HH'.log'

step4 测试:开发Java类,并在执行方法的过程中打印各级别的日志

  • /test/java/example/AppTest.java
package org.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// import org.apache.log4j.Logger;

/**
 * @project: LogDemo20200604
 * @author: 千千寰宇
 * @date: 2020/6/4  13:55:47
 * @description: ...
 */
public class AppTest {
    private final static Logger LOGGER = LoggerFactory.getLogger(AppTest.class);

    public static void main(String[] args) {
        LOGGER.debug("Hello log! - start");
        System.out.println("执行ing! - [AppTest.testLog]");
        LOGGER.info("Hello log! - end");
    }

    @Test
    public void testOpenFile(){
//        BasicConfigurator.configure(); //自动快速地使用缺省Log4j环境
        openFile("356326");
    }

    public static void openFile(String filePath) {
        File file = new File(filePath);
        try {
            InputStream in = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            //e要放在{}参数之外,而且只能放在最后一个。如果放在中间也不会被打印错误信息:
            LOGGER.error("can found file [{}]", filePath, e);
        }
    }
}
①main测试
②Test测试(项目中更为实用)

1 一般是将捕捉到的Exception对象(e)作为日志记录的最后一个参数(会显示具体的出错信息以及出错位置),而且要放在{}可以格式化的参数之外。防止被{}转为e.toString()
2 可使用e.toString(),而尽量不使用e.getMessage()。因为有的异常不一定有message,可以使用e.toString只会显示信息,不会显示出错的位置信息(不建议这种)

//link - blog: https://www.cnblogs.com/qlqwjy/p/9275415.html
package cn.johnnyzen.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4jTest {

    private static Logger log = LoggerFactory.getLogger(Slf4jTest.class);

    public static void main(String[] args) {
        openFile("xxxxxx");
    }

    public static void openFile(String filePath) {
        File file = new File(filePath);
        try {
            InputStream in = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            //Exception对象(e)作为日志记录的最后一个参数(会显示具体的出错信息以及出错位置),而且要放在{}可以格式化的参数之外
            log.error("can found file [{}]", filePath, e);
        }
    }

}
2020-06-04 00:30:03 [cn.johnnyzen.test.Slf4jTest]-[ERROR] can found file [xxxxxx]
java.io.FileNotFoundException: xxxxxx (系统找不到指定的文件。)
    at java.io.FileInputStream.open(Native Method)
    at java.io.FileInputStream.<init>(FileInputStream.java:146)
    at cn.xm.exam.test.Slf4jTest.openFile(Slf4jTest.java:22)
    at cn.xm.exam.test.Slf4jTest.main(Slf4jTest.java:16)

案例2 resources/log4j.properties

# +======================================================================+#
log4j.rootLogger=${log4j.log.level},${log4j.log.target}
log4j.addivity.org.apache=false # 将 logger 中的 additivity 属性配置为 false,则 这个logger不会将日志流反馈到root中,防止日志中同一记录反复打印N次(N>1)
log4j.addivity.cn.johnnyzen.bd.gatewayservice.biz=false


# +======================================================================+#
# | [target] - Console / A1
# +----------------------------------------------------------------------+#
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Threshold=${log4j.log.level}
log4j.appender.CONSOLE.Encoding=${log4j.log.encoding}
log4j.appender.CONSOLE.Target=System.out
log4j.appender.CONSOLE.layout=${log4j.log.layout}
# log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=${log4j.log.layout.pattern}

log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.Threshold=DEBUG
log4j.appender.A1.Encoding=${log4j.log.encoding}
log4j.appender.A1.Target=System.out
log4j.appender.A1.layout=${log4j.log.layout}
log4j.appender.A1.layout.ConversionPattern=${log4j.log.layout.pattern}

# +----------------------------------------------------------------------+#
# 若想某个【包】的打印级别和别的文件不一样,则自己定义appender,比如 A1
## 方式1
# log4j.logger.cn.seres.bd.gatewayservice.biz.common.filter.gatewayfilter=DEBUG,A1
## 方式2
log4j.category.org.springframework: WARN,A1
log4j.category.org.springframework.cloud.gateway: WARN,A1
log4j.category.org.springframework.web.reactive: WARN,A1
log4j.category.com.alibaba.nacos.client.naming: ERROR,A1
log4j.category.com.alibaba.nacos.shaded.io.grpc.netty: WARN,A1
log4j.category.reactor.netty: WARN,A1
log4j.category.reactor.ipc.netty: WARN,A1
log4j.category.reactor.util.Loggers: WARN,A1
log4j.category.io.netty.buffer.AbstractByteBuf: WARN,A1
log4j.category.com.zaxxer.hikari.pool.HikariPool: WARN,A1
log4j.category.cn.johnnyzen.bd.gatewayservice.biz.common.filter.gatewayfilter=DEBUG,A1

# 若想某个【类】的打印级别和别的文件不一样,则 自己定义appender(且必须指定),比如A1
log4j.logger.org.springframework.jdbc.datasource.DataSourceTransactionManager=DEBUG,A1
# log4j.logger.com.alibaba.nacos.shaded.io.grpc.netty.shaded.io.netty.util.internal.logging.AbstractInternalLogger=WARN,A1
# log4j.logger.com.alibaba.nacos.client.naming=ERROR,A1
# log4j.logger.cn.johnnyzen.bd.gatewayservice.biz.common.filter.gatewayfilter.PrefixPathGatewayFilter=DEBUG,A1
# log4j.logger.cn.johnnyzen.bd.gatewayservice.biz.common.filter.gatewayfilter.StripPrefixGatewayFilter=DEBUG,A1

# 日志输出的地址: 可任意配置, logs/存储在当前项目中   e:/logs
log4j.log.dir=logs/
#日志的等级:
#log4j.log.level=ALL,TRACE,DEBUG,INFO,WARN,ERROR,FATAL,OFF
log4j.log.level=WARN
#log4j.log.target=CONSOLE,FILE,DATABASE,EMAIL,SOCKET
log4j.log.target=CONSOLE
log4j.log.encoding=UTF-8
log4j.log.layout=org.apache.log4j.PatternLayout
log4j.log.layout.pattern=[%d %r] [%-5p] [%t] [%l] [%m]%n 

案例3 resources/log4j.properties

##################### [0] 自定义配置(可灵活修改) #####################
## 日志的等级(自定义配置项)
##log4j.log.level=ALL,TRACE,DEBUG,INFO,WARN,ERROR,FATAL,OFF
log4j.log.level=WARN
log4j.log.threshold=${log4j.log.level}

## 2 个 target(自定义配置项)
# log4j.log.target=CONSOLE,FILE,DATABASE,EMAIL,SOCKET
log4j.log.target=CONSOLE
log4j.log.fileTarget=MyFileAppender

log4j.log.encoding=UTF-8

## 日志输出的地址(自定义配置项): 可任意配置, logs/存储在当前项目中   e:/logs
log4j.log.dir=./logs

## org.apache.log4j.HTMLLayout (以HTML表格形式布局),
## org.apache.log4j.PatternLayout (可以灵活地指定布局模式),
## org.apache.log4j.SimpleLayout (包含日志信息的级别和信息字符串),
## org.apache.log4j.TTCCLayout (包含日志产生的时间、线程、类别等等信息)
log4j.log.layout=org.apache.log4j.PatternLayout
log4j.log.layout.pattern=[%d %r] [%-5p] [%t] [%l] [%m]%n


##################### [1] 定义 Logger #####################
# ------------------- [1.1] 定义 RootLogger 等 全局性配置(不可随意修改) ------------------- #
## log4j.rootLogger = [ level ] , appenderName, appenderName, ...
#log4j.rootLogger=${log4j.log.level},${log4j.log.target},${log4j.log.fileTarget}
log4j.rootLogger=${log4j.log.level},${log4j.log.target},${log4j.log.fileTarget}
log4j.additivity.org.apache=false

# ------------------- [1.2] 指定个别 Class 的 Logger (可随意修改,建议在 nacos 上修改) ------------------- #
log4j.logger.cn.johnnyzen.bd.dataservice.common.xx.XXClass=DEBUG,MyFileAppender
log4j.logger.cn.johnnyzen.bd.dataservice.common.utils.XXUtil=DEBUG,CONSOLE,MyFileAppender

##################### [2] 定义 Appender #####################

# ------------------- [2.1] CONSOLE Appender ------------------- #
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
# Threshold: 当前 Appender 输出日志的最低日志级别。例如 rootLogger=DEBUG, 当前 Appender 的 Threshold=ERROR, 则: 当前 Appender 输出日志仅输出 ERROR 信息
#log4j.appender.CONSOLE.Threshold=${log4j.log.threshold}
log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd HH\:mm\:ss} %-5p[%t] : %m%n
log4j.appender.CONSOLE.Target=System.out
log4j.appender.CONSOLE.Encoding=${log4j.log.encoding}
log4j.appender.CONSOLE.layout=${log4j.log.layout}

# ------------------- [2.1] MyFileAppender Appender ------------------- #
log4j.appender.MyFileAppender=org.apache.log4j.DailyRollingFileAppender
log4j.appender.MyFileAppender.File=${log4j.log.dir}/bdp_data_service.log
log4j.appender.MyFileAppender.DatePattern='_'yyyy-MM-dd
log4j.appender.MyFileAppender.encoding=${log4j.log.encoding}
#log4j.appender.MyFileAppender.Threshold=${log4j.log.threshold}
log4j.appender.MyFileAppender.layout=${log4j.log.layout}
log4j.appender.MyFileAppender.layout.ConversionPattern=%d{yyyy-MM-dd HH\:mm\:ss} %-5p[%t] : %m%n

案例4 resources/log4j.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration status="TRACE" monitorInterval="1800">
    <Properties>
        <!-- ==================== 公共配置 ==================== -->
        <!-- 设置日志文件的目录名称 -->
        <property name="serviceName">johnnyzen-data-service</property>

        <!-- 日志默认存放的位置,可以设置为项目根路径下,也可指定绝对路径 -->
        <property name="basePath">../logs/${serviceName}</property>

        <!-- 日志文件输出格式;%d{yyyy/MM/dd HH:mm:ss.SSS}:时间; ${serviceName}:服务名; %X{localIp}:ip; %traceId:traceId; %-5p:日志等级,5位左对齐; %t:线程名; %c{length}:类名及限定名;%M:方法名;%m:错误信息;%n:换行 -->
        <property name="log_pattern">[%d{yyyy/MM/dd HH:mm:ss.SSS}] [${serviceName}] [%X{localIp}] [%traceId] [%-5p] [%t] [%c{8}] [%M] %m%n</property>

        <!-- 日志默认切割的最小单位 -->
        <property name="every_file_size">20MB</property>
        <!-- ==================== 日志级别控制 ==================== -->
        <!-- 日志级别 -->
        <property name="log_level">${env:LOG_LEVEL:-WARN}</property>
        <property name="johnnyzen_log_level">${env:JOHNNYZEN_LOG_LEVEL:-DEBUG}</property>
        <property name="middleware_log_level">${env:MIDDLEWARE_LOG_LEVEL:-WARN}</property>

        <!-- ==================== 所有级别日志配置 ==================== -->
        <!-- 日志默认存放路径(所有级别日志) -->
        <property name="rolling_fileName">${basePath}/${serviceName}-all.log</property>
        <!-- 日志默认压缩路径,将超过指定文件大小的日志,自动存入按"年月"建立的文件夹下面并进行压缩,作为存档 -->
        <property name="rolling_filePattern">${basePath}/%d{yyyy-MM}/all-%d{yyyy-MM-dd}-%i.log.gz</property>
        <!-- 日志默认同类型日志,同一文件夹下可以存放的数量,不设置此属性则默认为7个,filePattern最后要带%i才会生效 -->
        <property name="rolling_max">500</property>
        <!-- 日志默认同类型日志,多久生成一个新的日志文件,这个配置需要和filePattern结合使用;
                如果设置为1,filePattern是%d{yyyy-MM-dd}到天的格式,则间隔一天生成一个文件
                如果设置为12,filePattern是%d{yyyy-MM-dd-HH}到小时的格式,则间隔12小时生成一个文件 -->
        <property name="rolling_timeInterval">1</property>
        <!-- 日志默认同类型日志,是否对封存时间进行调制,若为true,则封存时间将以0点为边界进行调整,
                如:现在是早上3am,interval是4,那么第一次滚动是在4am,接着是8am,12am...而不是7am -->
        <property name="rolling_timeModulate">true</property>

        <!-- ==================== Warn ERROR级别日志 ==================== -->
        <!-- 日志默认存放路径(Warn Error级别日志) -->
        <property name="warn_fileName">${basePath}/${serviceName}-warn-error.log</property>
        <!-- 日志默认压缩路径,将超过指定文件大小的日志,自动存入按"年月"建立的文件夹下面并进行压缩,作为存档 -->
        <property name="warn_filePattern">${basePath}/%d{yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log.gz</property>
        <!-- 日志默认同一文件夹下可以存放的数量,不设置此属性则默认为7个 -->
        <property name="warn_max">100</property>
        <!-- 日志默认同类型日志,多久生成一个新的日志文件,这个配置需要和filePattern结合使用;
                如果设置为1,filePattern是%d{yyyy-MM-dd}到天的格式,则间隔一天生成一个文件
                如果设置为12,filePattern是%d{yyyy-MM-dd-HH}到小时的格式,则间隔12小时生成一个文件 -->
        <property name="warn_timeInterval">1</property>
        <!-- 日志默认同类型日志,是否对封存时间进行调制,若为true,则封存时间将以0点为边界进行调整,
                如:现在是早上3am,interval是4,那么第一次滚动是在4am,接着是8am,12am...而不是7am -->
        <property name="warn_timeModulate">true</property>

    </Properties>

    <!--定义appender -->
    <appenders>
        <!-- ==================== 用来定义输出到控制台的配置 ==================== -->
        <Console name="Console" target="SYSTEM_OUT">
            <!-- 设置输出格式,不设置默认为:%m%n -->
            <PatternLayout charset="UTF-8" pattern="${log_pattern}"/>
        </Console>

        <!--kafka的配置-->
<!--        <Kafka name="kafkaLog" topic="backend_log">-->
<!--            <PatternLayout charset="UTF-8" pattern="${log_pattern}"/>-->
<!--            &lt;!&ndash;kakfa集群的各个节点:host:port,以逗号分隔&ndash;&gt;-->
<!--            <Property name="bootstrap.servers">${env:KAFKA_LOG_SERVER:-172.20.64.13:9092}</Property>-->
<!--        </Kafka>-->

        <!-- ==================== 打印root中指定的level级别以上的日志到文件 ==================== -->
        <RollingFile name="RollingFile" fileName="${rolling_fileName}" filePattern="${rolling_filePattern}">
            <PatternLayout pattern="${log_pattern}"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="${rolling_timeInterval}" modulate="${warn_timeModulate}"/>
                <SizeBasedTriggeringPolicy size="${every_file_size}"/>
            </Policies>
            <!-- 设置同类型日志,同一文件夹下可以存放的数量,如果不设置此属性则默认存放7个文件 -->
            <DefaultRolloverStrategy max="${rolling_max}">
                <Delete basePath="${basePath}" maxDepth="2">
                    <IfFileName glob="*.log">
                        <IfLastModified age="60d">
                            <IfAny>
                                <IfAccumulatedFileSize exceeds="10 GB" />
                            </IfAny>
                        </IfLastModified>
                    </IfFileName>
                </Delete>
            </DefaultRolloverStrategy>
        </RollingFile>

        <!-- ==================== 打印ERROR WARN级别的日志到文件 ==================== -->
        <RollingFile name="WarnFile" fileName="${warn_fileName}" filePattern="${warn_filePattern}">
            <PatternLayout pattern="${log_pattern}"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="${warn_timeInterval}" modulate="${warn_timeModulate}"/>
                <SizeBasedTriggeringPolicy size="${every_file_size}"/>
            </Policies>
            <DefaultRolloverStrategy max="${warn_max}"/>
            <Filters>
                <ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
                <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
        </RollingFile>
    </appenders>

    <!--定义logger,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!--建立一个默认的root的logger-->
        <root level="${log_level}">
            <appender-ref ref="Console"/>
<!--            <appender-ref ref="kafkaLog"/>-->
            <appender-ref ref="RollingFile"/>
            <appender-ref ref="WarnFile"/>
        </root>
        <logger name="cn.seres" level="${seres_log_level}"
                additivity="false">
            <appender-ref ref="Console"/>
<!--            <appender-ref ref="kafkaLog"/>-->
            <appender-ref ref="RollingFile"/>
            <appender-ref ref="WarnFile"/>
        </logger>
        <!-- 设置spring包下的日志只打印WARN及以上级别的日志 -->
        <logger name="org.springframework" level="${middleware_log_level}" additivity="false">
            <appender-ref ref="Console"/>
<!--            <appender-ref ref="kafkaLog"/>-->
            <appender-ref ref="RollingFile"/>
            <appender-ref ref="WarnFile"/>
        </logger>
        <!-- 设置kafka下的日志只打印WARN及以上级别的日志 -->
        <logger name="org.apache.kafka" level="${middleware_log_level}" additivity="false">
            <appender-ref ref="Console"/>
<!--            <appender-ref ref="kafkaLog"/>-->
            <appender-ref ref="RollingFile"/>
            <appender-ref ref="WarnFile"/>
        </logger>
    </loggers>

</configuration>

案例3 resources/log4j.properties [2023-02-08]

##################### [0] 自定义配置(可灵活修改) #####################
## 日志的等级(自定义配置项)
##log4j.log.level=ALL,TRACE,DEBUG,INFO,WARN,ERROR,FATAL,OFF
log4j.log.level=DEBUG
log4j.log.threshold=${log4j.log.level}

## 2 个 target(自定义配置项)
# log4j.log.target=CONSOLE,FILE,DATABASE,EMAIL,SOCKET
# 当前聚焦的 Target
log4j.log.target=CONSOLE
# 文件输出的 Target
log4j.log.fileTarget=MyFileAppender
# 链路追踪客户端的 Target
log4j.log.linkTraceClientTarget=MySkyWalkingClientAppender

log4j.log.encoding=UTF-8

## 日志输出的地址(自定义配置项): 可任意配置, logs/存储在当前项目中   e:/logs
log4j.log.dir=./logs

## org.apache.log4j.HTMLLayout (以HTML表格形式布局),
## org.apache.log4j.PatternLayout (可以灵活地指定布局模式),
## org.apache.log4j.SimpleLayout (包含日志信息的级别和信息字符串),
## org.apache.log4j.TTCCLayout (包含日志产生的时间、线程、类别等等信息)
# log4j.log.layout=org.apache.log4j.PatternLayout
log4j.log.layout=org.apache.skywalking.apm.toolkit.log.log4j.v1.x.TraceIdPatternLayout
## log4j.appender.*.layout=org.apache.skywalking.apm.toolkit.log.log4j.v1.x.TraceIdPatternLayout
log4j.log.layout.pattern=%d %-5p | %T | %t | (%C{1}.java:%L %M) | %m%n
## log4j.log.layout.pattern=%d %-5p %t (%C{1}.java:%L %M) %m%n
## log4j.log.layout.pattern=[%d %r] [%-5p] [%t] [%l] [%m]%n
## log4j.log.layout.pattern=%d{yyyy-MM-dd HH\:mm\:ss} %-5p[%t] : %m%n

##################### [1] 定义 Logger #####################
# ------------------- [1.1] 定义 RootLogger 等 全局性配置(不可随意修改) ------------------- #
## log4j.rootLogger = [ level ] , appenderName, appenderName, ...
# log4j.rootLogger=${log4j.log.level},${log4j.log.target},${log4j.log.fileTarget}
log4j.rootLogger=${log4j.log.level},${log4j.log.target},${log4j.log.fileTarget},${log4j.log.linkTraceClientTarget}
log4j.additivity.org.apache=false

# ------------------- [1.2] 指定个别 Class 的 Logger (可随意修改,建议在 nacos 上修改) ------------------- #
log4j.logger.org.springframework=WARN
# log4j.logger.org.springframework.context=WARN
# log4j.logger.org.springframework.core=WARN
# log4j.logger.org.springframework.beans.factory=WARN
log4j.logger.org.mybatis.logging=WARN
log4j.logger.com.baomidou.mybatisplus=WARN
log4j.logger.com.alibaba.nacos.shaded=WARN

##################### [2] 定义 Appender #####################

# ------------------- [2.1] CONSOLE Appender ------------------- #
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
## Threshold: 当前 Appender 输出日志的最低日志级别。例如 rootLogger=DEBUG, 当前 Appender 的 Threshold=ERROR, 则: 当前 Appender 输出日志仅输出 ERROR 信息
# log4j.appender.CONSOLE.Threshold=${log4j.log.threshold}
log4j.appender.CONSOLE.layout.ConversionPattern=${log4j.log.layout.pattern}
log4j.appender.CONSOLE.Target=System.out
log4j.appender.CONSOLE.Encoding=${log4j.log.encoding}
log4j.appender.CONSOLE.layout=${log4j.log.layout}

# ------------------- [2.2] MyFileAppender Appender ------------------- #
log4j.appender.MyFileAppender=org.apache.log4j.DailyRollingFileAppender
log4j.appender.MyFileAppender.file=${log4j.log.dir}/bdp_data_service.log
log4j.appender.MyFileAppender.datePattern='_'yyyy-MM-dd
log4j.appender.MyFileAppender.encoding=${log4j.log.encoding}
#log4j.appender.MyFileAppender.Threshold=${log4j.log.threshold}
log4j.appender.MyFileAppender.layout=${log4j.log.layout}
log4j.appender.MyFileAppender.layout.ConversionPattern=${log4j.log.layout.pattern}

# ------------------- [2.3] MySkyWalkingClientAppender Appender ------------------- #
## 依赖 JAR 包 : org.apache.skywalking:apm-toolkit-log4j-1.x:8.5.0 , org.apache.skywalking:apm-toolkit-trace:8.5.0
## Skywalking 的 Layout 所支持的`ConversionPattern`核心参数在`apm-toolkit-log4j-1.x`插件中是`T`(即 `traceId`, 区分大小写[T≠t]),在`apm-toolkit-logback-1.x`插件中是`%X{tid}`,需打印到日志中,才能被 Skywalking OAP 服务的`Collertor`收集并识别请求链路中的日志信息,否则日志收集失败。
log4j.appender.MySkyWalkingClientAppender=org.apache.skywalking.apm.toolkit.log.log4j.v1.x.log.GRPCLogClientAppender
log4j.appender.MySkyWalkingClientAppender.Threshold=${log4j.log.threshold}
log4j.appender.MySkyWalkingClientAppender.layout=${log4j.log.layout}
log4j.appender.MySkyWalkingClientAppender.layout.ConversionPattern=${log4j.log.layout.pattern}

Y 推荐文献

  • Apache Log4j 2.x - Kafka Appender 【推荐】

{@link org.apache.logging.log4j.core.appender.mom.kafka.KafkaAppender }

  • Apache Log4j 2.x - 变量
  • 优质第三方文档

X 参考文献

posted @ 2020-06-03 22:05  千千寰宇  阅读(813)  评论(0)    收藏  举报