埃里克·迪特里希
2018 年 9 月 5 日
软件开发中最烦人的方面之一绝对是日志记录。如果一个非平凡的应用程序缺少日志记录,那么维护它的人都会遇到困难,事后调试将主要是一个猜谜游戏。今天,我们搞个 log4j2 配置的教程,为解决这种情况出点儿力。
有很多方法可以为 Java 程序记录日志。您可以使用手动的方法,但推荐的方法是采用专用的日志记录框架。这就是为什么我们要介绍 Apache log4j2,它是 log4j 的改进版本,是行业标准的日志框架之一。
我们将首先快速回顾一下我们之前关于 Java 日志的文章,并介绍一些关于 log4j2 的事。然后我们编写一个示例应用程序以展示 log4j2的使用。
下面,我们进入文章的重点。您将学习如何配置 log4j2,从基础开始,并逐步学习更高级的主题,例如日志格式、附加程序、日志级别和日志层次结构。
让我们开始吧。
使用 Log4j2 进行日志记录:又不是头一回了
本教程中,我们使用了 log4j 版本 2,这是来自 Apache 项目的日志框架。
让我们进一步了解 Java 应用程序日志并查看 log4j2 配置。今天我们将介绍 log4j2 配置的基本方面,以帮助您入门。
Log4j 的功能使其成为 Java 最流行的日志框架之一。它可以配置为多个日志记录目的地和各种日志文件格式。
可以在单个类级别过滤和定向日志消息,从而使开发人员和运维人员能够对应用程序消息进行精细控制。
让我们通过使用命令行 Java 应用程序配置 log4j 来检查这些机制。
示例应用程序
让我们使用 log4j 进行日志记录的应用程序。
package com.company;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class Main {
private static final Logger logger = LogManager.getLogger(Main.class);
public static void main(String[] args) {
String message = "Hello there!";
logger.trace(message);
logger.debug(message);
logger.info(message);
logger.warn(message);
logger.error(message);
logger.fatal(message);
}
}
我们在每个 log4j 预定义的日志级别记录相同的消息:跟踪、调试、信息、警告、错误和致命。
我们将使用 log4j 的 YAML 文件格式,因此您需要向 pom.xml(或build.gradle)添加一些额外的依赖项。
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.12.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.12.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.10.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.0</version>
</dependency>
</dependencies>
设置此代码,以便您可以使用您喜欢的 Java 工具构建和运行它。
基本的 Log4j2 配置
默认配置
让我们在没有 log4j 配置文件的情况下运行我们的应用程序。如果您已经有一个,请将其删除或将其移动到另一个文件名,以便 log4j 将忽略它。
当我们运行应用程序时,我们会在控制台上看到:
09:38:14.114 [main] ERROR com.company.Main - Hello there!
09:38:14.119 [main] FATAL com.company.Main - Hello there!
六个日志消息中的两个,指定为“错误”和“致命”的消息被发送到控制台。
Log4j 有一个默认配置。它将记录到控制台,显示归类为“错误”或更高级别的消息。
了解 log4j 在没有配置文件的情况下如何运行很有用,但让我们看看如何根据我们的需要设置它。
配置文件位置
我们可以通过log4j.configurationFile系统属性在特定位置为 log4j 提供配置文件。这是它将查找配置文件的第一个位置。
如果 log4j 找不到系统属性,它会在类路径中查找文件。由于 log4j 版本 2 支持四种不同的文件格式和两种不同的文件命名约定,因此定位文件的规则很复杂。在我们介绍了不同的选项后,我们将讨论它们。
配置文件格式
Log4j 将加载 Java 属性和 YAML、JSON 和 XML 配置文件。它通过检查文件扩展名来识别文件格式。
- Java properties — .properties
- YAML — .yaml or .yml
- JSON — .json or .jsn
- XML — .xml
log4j.configurationFile
系统属性指定的文件必须具有这些文件扩展名之一,但可以具有任何基本名称。Log4j 将根据扩展名指示的格式对其进行解析。
当 log4j 扫描文件的类路径时,它会按照上面列出的顺序扫描每种格式,并在找到匹配项时停止。
例如,如果它找到一个 YAML 配置,它将停止搜索并加载它。如果没有 YAML 文件但它找到了 JSON,它将停止搜索并使用它。
配置文件名
当 log4j 扫描类路径时,它会查找两个文件名之一:log4j2-test.[extension] 或 log4j2.[extension]。
它首先加载测试文件,为开发人员提供了一种方便的机制,可以在不改变标准配置的情况下强制应用程序在调试或跟踪级别进行记录。
扫描配置
当我们将文件格式和名称的规则放在一起时,我们可以看到log4j的自我配置算法。
如果以下任何步骤成功,log4j 将停止并加载生成的配置文件。
- 检查 log4j.configurationFile系统属性并在找到时加载指定的文件。
- 在类路径中搜索 log4j2-test.properties。
- 扫描 log4j2-test.yaml 或 log4j2-test.yml 的类路径
- 检查 log4j2-test.json 或 log4j2-test.jsn
- 搜索 log4j2-test.xml
- 寻找 log4j2.properties
- 搜索 log4j2.yaml 或 log4j2.yml
- 扫描 log4j2.json 或 log4j2.jsn 的类路径
- 检查 log4j2.xml
- 使用默认配置。
实践正确的配置文件卫生
log4j 有 12 个潜在的配置文件名。如果应用程序在生产环境中记录了不必要的消息,则加载错误的信息可能会导致日志信息丢失或性能下降。
在部署代码之前,请确保您的应用程序只有一个配置文件,并且您知道它在哪里。如果您坚持从类路径加载配置,请在发布代码之前扫描虚假文件。
基本配置
现在我们知道如何为 log4j 提供配置,让我们创建一个并使用它来自定义我们的应用程序。
重新审视 Log4j 的默认配置
让我们从默认配置开始,然后从那里修改我们应用程序的行为。我们将从 log4j 的配置规则中获取提示并使用 YAML。
默认配置如下所示:
Configuration:
status: warn
Appenders:
Console:
name: Console
target: SYSTEM_OUT
PatternLayout:
Pattern: "%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"
Loggers:
Root:
level: error
AppenderRef:
ref: Console
使用这些内容创建一个文件名log4j2.yaml并将log4j.configurationFile设置 为指向其位置。
接下来,运行应用程序。您将看到与以前相同的输出。
09:38:14.114 [main] ERROR com.company.Main - Hello there!
09:38:14.119 [main] FATAL com.company.Main - Hello there!
我们已经控制了应用程序的日志记录配置。现在让我们改进它。
日志文件位置
第一步是将我们的日志从控制台中取出并放入一个文件中。为此,我们需要了解appender。
Appenders 将日志消息放在它们所属的地方。默认配置提供一个控制台附加程序。顾名思义,它将消息附加到控制台。
Appenders:
Console:
name: Console
target: SYSTEM_OUT
PatternLayout:
Pattern: "%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"
我们想要一个文件附加程序。让我们替换我们的控制台 appender。
Appenders:
File:
name: File_Appender
fileName: logfile.log
PatternLayout:
Pattern: "%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"
文件附加器有一个name,就像控制台附加器一样。但是他们有一个fileName而不是target。
与控制台 appender 类似,它们也有一个PatternLayout,我们将在下面介绍。
这个名字不仅仅是为了展示。如果我们想用文件附加器替换控制台附加器,我们需要让我们的记录器 知道在哪里放置我们的日志消息。
因此,将记录器中的ref值更改为文件附加程序的名称。
Loggers:
Root:
level: error
AppenderRef:
ref: File_Appender
现在,重新运行应用程序。它没有记录到控制台,而是将消息放在工作目录中名为logfile.log 的文件中。我们已将日志移至文件中!
日志级别
在我们的日志文件中,我们仍然只看到了六个日志消息中的两个。让我们谈谈记录器以及它们如何管理日志消息。
我们的基本配置定义了一个记录器。
Loggers:
Root:
level: error
AppenderRef:
ref: File_Appender
它有一个“错误”级别,所以它只打印错误或致命的消息。
当记录器收到日志消息时,它会根据其配置的级别传递或过滤它。此表显示了记录器配置和日志消息级别之间的关系。
因此,如果我们更改记录器的级别,我们将看到更多消息。将其设置为“调试”。
Loggers:
Root:
level: debug
AppenderRef:
ref: File_Appender
接下来,重新运行程序。应用程序记录所有调试级别或更高级别的消息。
记录器层次结构
Log4j 按层次结构排列记录器。这使得为各个类指定不同的配置成为可能。
让我们更改我们的应用程序,看看它的实际效果。
public class Main {
private static final Logger logger = LogManager.getLogger(Main.class);
public static void main(String[] args) {
String message = "Hello there!";
System.out.println(message);
logger.debug(message);
logger.info(message);
logger.error(message);
LoggerChild.log();
}
private static class LoggerChild {
private static final Logger childLogger = LogManager.getLogger(LoggerChild.class);
static void log() {
childLogger.debug("Hi Mom!");
}
}
}
我们添加了一个内部类来创建一个记录器并用它记录一条消息。
之后主要做了记录,它调用LoggerChild 。
如果我们使用当前配置运行它,我们会看到新消息,并且它是从不同的类记录的。
12:29:23.325 [main] DEBUG com.company.Main - Hello there!
12:29:23.331 [main] INFO com.company.Main - Hello there!
12:29:23.332 [main] ERROR com.company.Main - Hello there!
12:29:23.333 [main] DEBUG com.company.Main.LoggerChild - Hi Mom!
记录器具有类似于 Java 的类层次结构。所有记录器都是迄今为止我们一直在使用的根记录器的后代。
缺少任何特定配置的记录器继承根配置。
所以当 Main 和 LoggerChild 使用它们的类名创建记录器时,这些记录器继承了 Root 的配置,即向 File_Appender发送调试级别和更高级别的消息 。
我们可以覆盖这两个记录器的指定配置。
Loggers:
logger:
-
name: com.company.Main
level: error
additivity: false
AppenderRef:
ref: File_Appender
-
name: com.company.Main.LoggerChild
level: debug
additivity: false
AppenderRef:
ref: File_Appender
Root:
level: debug
AppenderRef:
ref: File_Appender
记录器在记录器部分命名 。由于我们列出了两个,因此我们使用 YAML 数组语法。
我们设置 com.company.Main的 记录为“错误”,并 com.company.Main.LoggerChild的 为“调试”。
该加设置控制的log4j是否会从记录仪的祖先将消息发送给后代。
如果设置为 true,两个记录器将处理相同的消息。某些系统希望将相同的消息添加到两个不同的日志中。我们不希望出现这种行为,因此我们覆盖了默认值并指定了 false。
现在再次运行程序:
12:33:11.062 [main] ERROR com.company.Main - Hello there!
12:33:11.073 [main] DEBUG com.company.Main.LoggerChild - Hi Mom!
我们只看到来自Main的错误消息, 但仍然看到来自LoggerChild的调试消息!
不止一个 Appender
就像我们可以拥有多个 logger 一样,我们也可以拥有多个 appender。
让我们对我们的配置进行一些更改。
添加第二个文件附加程序。为此,请使用原始 appender 创建一个列表,并使用不同的名称和文件创建第二个列表。您的Appenders部分应如下所示:
Appenders:
File:
-
name: File_Appender
fileName: logfile.log
PatternLayout:
Pattern: "%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"
-
name: Child_Appender
fileName: childlogfile.log
PatternLayout:
Pattern: "%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"
接下来,将 LoggerChild 记录器指向新的 appender。您的记录器部分将如下所示。
Loggers:
logger:
-
name: com.company.Main
level: error
additivity: false
AppenderRef:
ref: File_Appender
-
name: com.company.Main.LoggerChild
level: debug
additivity: false
AppenderRef:
ref: Child_Appender
Root:
level: debug
AppenderRef:
ref: File_Appender
现在运行该应用程序,您将看到两个不同的日志文件,每个文件都包含来自其关联类的消息。
日志消息格式
我们的每个 appender 都有一个 PatternLayout。
PatternLayout:
Pattern: “%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} – %msg%n”
PatternLayout 是 Log4j 布局类的一个实例。Log4j 具有用于以 CSV、JSON、Syslog 和各种不同格式记录消息的内置布局。
PatternLayout 有一组用于格式化消息的运算符,其操作类似于 C 的sprintf函数。通过指定模式,我们可以控制由 appender 写入的日志消息的格式。
我们的布局字符串如下所示:
"%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"
每个 % 对应于日志消息中的一个字段。
PatternLayout有许多额外的操作符。
变量替换
随着 appender 和 loggers 的增加,配置文件可能会变得重复。Log4j 支持变量替换以帮助减少重复并使其更易于维护。让我们使用Properties 来优化我们的配置。
Configuration:
status: warn
Properties:
property:
-
name: "LogDir"
value: "logs"
-
name: "DefaultPattern"
value: "%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"
Appenders:
File:
-
name: File_Appender
fileName: ${LogDir}/logfile.log
PatternLayout:
Pattern: ${DefaultPattern}
-
name: Child_Appender
fileName: ${LogDir}/childlogfile.log
PatternLayout:
Pattern: ${DefaultPattern}
Loggers:
logger:
-
name: com.company.Main
level: error
additivity: false
AppenderRef:
ref: File_Appender
-
name: com.company.Main.LoggerChild
level: debug
additivity: false
AppenderRef:
ref: Child_Appender
Root:
level: debug
AppenderRef:
ref: File_Appender
在文件的顶部,我们声明了两个属性,一个名为LogDir,另一个名为DefaultPattern。
声明属性后,可以使用大括号和美元符号在配置中使用它: ${LogDir} 或${DefaultPattern}
LogDir 是我们添加到两个日志文件名称中的子目录名称。当我们运行应用程序时,log4j 将创建这个目录并将日志文件放在那里。
我们指定DefaultPattern作为我们两个日志文件的模式布局,将定义移到一个地方。如果我们想修改我们的日志文件格式,我们现在只需担心更改一次。
Log4j 还可以从环境中导入属性。您可以在此处找到详细信息 。
例如,如果我们想从 Java 系统属性导入日志文件目录,我们在 log4j 配置中将其指定为${sys : LogDir}并将 LogDir 系统属性设置为所需的目录。
自动重新配置
Log4j 可以定期重新加载其配置,使我们能够在不重新启动应用程序的情况下更改应用程序的日志记录配置。
将monitorInterval设置添加到文件的Configuration部分,log4j 将按指定的时间间隔扫描文件。
Configuration:
monitorInterval: 30
间隔以秒为单位指定。
结论
Log4j 是一个强大的日志框架,它允许我们将应用程序配置为以各种不同的方式登录,并对不同组件如何使用日志文件进行精细控制。
本教程涵盖了配置 log4j 的基本方面,但还有很多东西需要学习。您可以在项目网站上了解 log4j 配置 。
这篇文章是由 Eric Goebelbecker 撰写的。Eric在纽约市的金融市场工作了 25 年,为市场数据和金融信息交换 (FIX) 协议网络开发基础设施。他喜欢谈论什么使团队有效(或不那么有效!)