在对 Java 应用程序性能进行故障排除时,JVM 指标已不再足够。要完全了解环境,您还需要 Java 日志和跟踪。今天,我们将关注您的 Java 应用程序日志。
Java 中的日志记录可以通过轻松地将数据写入文件来完成,但是,这不是最简单或最方便的日志记录方式。有一些 Java 框架提供对象、方法和统一配置方法,帮助设置日志记录、存储日志,在某些情况下甚至将它们发送到日志集中解决方案。最重要的是,还有一些抽象层可以轻松切换底层框架,而无需更改实现。如果您出于任何原因需要更换日志库,这会派上用场——例如性能、配置甚至简单性。
听起来很复杂?不必如此。
在这篇博文中,我们将重点介绍如何为您的代码正确设置日志记录,以避免我们已经犯过的所有错误。我们将涵盖:
- Java 的日志抽象层
- 开箱即用的 Java 日志记录功能
- Java 日志库
- 记录重要信息
- 日志集中解决方案。
让我们开始吧!
Java 日志框架
SLF4J
在简单的日志门面的Java或者干脆SLF4J的,而不是在开发时的各种日志框架,使我们的抽象层,作为用户,可以选择在部署时的日志框架。这可以在需要时快速轻松地更改选择的日志记录框架。
如果您是 SLF4J 的新手,我们只为您准备了这篇博文。在我们的SLF4J 教程中,我们解释了有关抽象层的所有知识,以便您可以更轻松、更智能地进行日志记录。
Java.util.logging
java.util.logging 包为 Java 核心日志记录工具提供了类和接口。它与 Java 开发工具包捆绑在一起,可供自 Java 1.4 以来使用 Java 的每个开发人员使用。
Logback
logback如预期的继任者的Log4j项目的第一个版本声称更快的实现,更好的测试,原生集成SLF4J,并开始更多。它由三个主要模块构建而成,并与 Tomcat 或 Jetty 等 Servlet 容器集成以提供 HTTP 访问日志功能。如果这是您选择的框架,请查看我们的Logback 教程,我们将介绍它的工作原理以及如何配置它以记录 Java 应用程序。
Log4j
Log4j是最广为人知的 Java 日志库之一,也是 Logback 或 Log4j 2 等项目的前身。Log4j 已于 2015 年 8 月 5 日结束,建议用户使用 Log4j 2。如果您仍在使用此框架,查看我们的Log4j 教程,了解其功能。
Log4j 2
Log4j 2是所有提到的 Java 日志框架列表中的最新版本。它结合了许多对其前身的改进,并承诺在修复一些架构问题的同时提供 Logback 改进。如果您正在开始一个新项目并且正在寻找一个可供选择的库,那么 Log4j 2 应该是主要关注点。您可以从我们的Log4j 2 教程中阅读有关如何使用它记录 Java 应用程序的更多信息。
为您的应用程序选择正确的日志记录解决方案
Java 应用程序的最佳日志记录解决方案是……嗯,这很复杂。您必须考虑并查看您当前的环境和组织的需求。如果您已经有一个可供大多数应用程序使用的选择框架——那就去吧。您很可能已经为您的日志确定了格式。这也意味着您可能已经有了一种将日志传送到您选择的日志集中解决方案的简单方法,您只需要遵循现有模式。
但是,如果您的组织没有任何类型的通用日志记录框架,请查看您正在使用的应用程序中使用的日志记录类型。你在使用Elasticsearch吗?它使用Log4j 2。大多数第三方应用程序也使用Log4j 2吗?如果是这样,请考虑使用它。为什么?因为您可能希望将日志放在一个地方,而且您可以更轻松地使用单个配置或模式。然而,这里有一个陷阱。如果这意味着在已经存在更新和成熟的替代方案时使用过时的技术,请不要走这条路。
最后,如果您选择一个新的日志框架,我建议使用抽象层和日志框架。这种方法使您可以在需要时灵活地切换到不同的日志记录框架,最重要的是,无需更改代码。您只需要更新依赖项和配置,其余的将保持不变。
在大多数情况下,绑定到您选择的日志记录框架的SLF4J将是一个好主意。Log4j 2是许多开源和闭源项目的首选框架。
在 Java 中进行日志记录:如何使用 Java 日志记录 API 进行日志记录
Java 包含 Java 日志 API,它允许您配置写入的日志消息类型。API 附带了一组标准的关键元素,我们应该了解这些元素,以便能够继续进行讨论,而不仅仅是基本的日志记录。java.util.logging 包提供了 Logger 类来记录应用程序消息。
记录器
该记录器是主要的实体应用程序使用,使记录来电,来捕获的LogRecords到相应的处理。该的LogRecord(或记录事件)是用来传递一个用于记录的框架和负责日志传送的处理程序之间的记录请求的实体。
Logger 对象通常用于单个类或单个组件,以提供与特定用例的上下文绑定。
向我们的 Java 应用程序添加日志记录通常是关于配置选择的库并包括 Logger。这允许我们将日志添加到我们想要了解的应用程序部分。
处理程序
该处理器是用来出口的LogRecord实体到给定的目标。这些目的地可以是内存、控制台、文件和通过套接字和各种 API 的远程位置。有不同的标准处理程序。以下是此类处理程序的一些示例:
- 控制台处理程序
- 文件处理器
- 系统日志处理程序
该ConsoleHandler被设计为您的日志消息发送到标准输出。该文件处理器写入日志消息到一个专用的文件和SyslogHandler的数据发送到系统日志兼容的守护进程。
打开和关闭任何处理程序只是将它包含在您选择的日志库的配置文件中。你不必再做任何事情了。
您还可以通过扩展Handler类来创建自己的处理程序。这是一个非常简单的例子:
public class ExampleHandler extends Handler {
@Override
public void publish(LogRecord logRecord) {
System.out.println(String.format("Log level: %s, message: %s",
logRecord.getLevel().toString(), logRecord.getMessage()));
}
@Override
public void flush() {
}
@Override
public void close() throws SecurityException {
}
}
日志级别
Java 日志级别是日志消息的一组标准严重性。说明日志记录的重要性。如果您不熟悉该主题,请务必查看我们的日志级别指南,以深入了解该主题。
例如,以下日志级别从最不重要到最重要,并解释了我如何看待它们:
TRACE – 非常细粒度的信息,仅在您需要完全了解正在发生的事情的极少数情况下使用。在大多数情况下,TRACE 级别会非常冗长,但您也可以期待有关应用程序的大量信息。用于注释算法中与日常使用无关的步骤。
LOGGER.trace("This is a TRACE log level message");
DEBUG – 比TRACE级别更细化,但仍比正常日常使用中所需的更细化。DEBUG 级别应该用于对故障排除有用的信息,而不是查看日常应用程序状态所需要的信息。
LOGGER.debug("This is a DEBUG log level message");
INFO – 指示正常应用程序操作的日志信息的标准级别 – 例如“Created a user {} with id {}”是 INFO 级别的日志消息的示例,它为您提供有关某个进程的信息,该进程以成功。在大多数情况下,如果您不研究应用程序的执行方式,则可以忽略大部分(如果不是全部)INFO 级别日志。
LOGGER.info("This is a INFO log level message");
WARN – 日志级别,通常指示可能有问题的应用程序状态或检测到异常执行。可能有问题,但这并不意味着应用程序失败。例如,一条消息没有被正确解析,因为它不正确。代码执行仍在继续,但我们可以将其记录为 WARN 级别,以通知我们和其他人潜在的问题正在发生。
LOGGER.warn("This is a WARN log level message");
ERROR – 日志级别,表明系统存在阻止某些功能工作的问题。例如,如果您通过社交媒体提供登录作为登录系统的一种方式,则此类模块的故障肯定是错误级别的日志。
LOGGER.error("This is a ERROR log level message");
FATAL – 日志级别,表明您的应用程序遇到了阻止其工作或其关键部分工作的事件。一个FATAL日志级别,例如,无法连接到数据库,你的系统依赖于或者是需要在您的电子商务系统,检查筐而出外部支付系统。该致命错误不会在SLF4J表示。
格式化程序
格式化程序支持LogRecord对象的格式化。默认情况下,有两种格式化程序可用:
- SimpleFormatter使用一两行以人类可读的形式打印LogRecord对象
- XMLFormatter以标准 XML 格式写入消息。
您还可以构建自己的自定义格式化程序。下面是一个例子:
public class TestFormatter extends Formatter {
@Override
public String format(LogRecord logRecord) {
return String.format("Level: %s, message: %s",
logRecord.getLevel(), logRecord.getMessage());
}
}
抽象层
当然,您的应用程序可以使用 Java 通过java.util.logging包提供的开箱即用的基本日志记录 API 。这没有任何问题,但请注意,这会限制您可以对日志执行的操作。某些库提供易于配置的格式化程序、开箱即用的行业标准目标和一流的性能。如果您希望使用这些框架之一,并且您还希望能够在将来切换该框架,您应该查看日志 API 之上的抽象层。
SLF4J – Java 的简单日志外观就是这样一种抽象层。它为常见的日志框架提供绑定,例如Log4j、Logback和开箱即用的java.util.logging包。您可以想象以以下简化方式编写日志消息的过程:
但从代码的角度来看,这会如何呢?嗯,这是一个很好的问题。让我们先看看开箱即用的java.util.logging代码。例如,如果我们只想启动我们的应用程序并在日志中打印一些内容,它将如下所示:
package com.sematext.blog.logging;
import java.util.logging.Level;
import java.util.logging.Logger;
public class JavaUtilsLogging {
private static Logger LOGGER = Logger.getLogger(JavaUtilsLogging.class.getName());
public static void main(String[] args) {
LOGGER.log(Level.INFO, "Hello world!");
}
}
可以看到我们通过类名初始化了静态Logger类。这样我们就可以清楚地识别日志消息的来源,并且我们可以为给定类生成的所有日志消息重用单个 Logger。
执行上述代码的输出将如下所示:
Jun 26, 2020 2:58:40 PM com.sematext.blog.logging.JavaUtilsLogging main
INFO: Hello world!
现在让我们使用 SLF4J 抽象层做同样的事情:
package com.sematext.blog.logging;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JavaSLF4JLogging {
private static Logger LOGGER = LoggerFactory.getLogger(JavaSLF4JLogging.class);
public static void main(String[] args) {
LOGGER.info("Hello world!");
}
}
这次的输出略有不同,如下所示:
[main] INFO com.sematext.blog.logging.JavaSLF4JLogging - Hello world!
这一次,情况有点不同。我们使用 LoggerFactory 类来检索我们类的记录器,并使用 Logger 对象的专用 info 方法使用 INFO 级别写入日志消息。
从SLF4J 2.0开始,引入了fluent logging API,但在撰写本博客时,它仍处于 alpha 阶段并且被认为是实验性的,因此我们现在将跳过它,因为 API 可能会发生变化。
因此,完成抽象层 - 与使用java.util.logging相比,它为我们提供了一些主要优势:
- 用于所有应用程序日志记录的通用 API
- 使用所需日志记录框架的简单方法
- 一种简单的方法来交换日志框架,并且在想要切换时不必遍历整个代码
希望这能说明日志级别是什么以及如何在应用程序中使用它们。
如何启用日志记录:java.util.logging 示例
使用标准java.util.logging包时,我们不需要任何类型的外部依赖项。我们需要的所有东西都已经存在于 JDK 发行版中,因此我们可以直接跳到它并开始将日志记录到我们很棒的应用程序中。
我们可以通过两种方式使用java.util.logging包来包含和配置日志记录——使用配置文件或以编程方式。出于演示目的,我们假设我们希望在一行中查看以日期和时间、日志消息的严重性以及日志消息本身开头的日志消息。
通过配置文件配置 java.util.logging
为了向您展示如何使用java.util.logging包,我们创建了一个简单的 Java 项目并在我们的Github存储库中共享它。生成日志和设置日志的代码如下所示:
package com.sematext.blog.loggging;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
public class UtilLoggingConfiguration {
private static Logger LOGGER = Logger.getLogger(UtilLoggingConfiguration.class.getName());
static {
try {
InputStream stream = UtilLoggingConfiguration.class.getClassLoader()
.getResourceAsStream("logging.properties");
LogManager.getLogManager().readConfiguration(stream);
} catch (Exception ex) {
ex.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
LOGGER.log(Level.INFO, "An INFO level log!");
}
}
我们首先使用类的名称为 UtilLoggingConfiguration 类初始化 Logger。我们还需要提供日志配置的名称。默认情况下,默认配置位于 JAVA_HOME/jre/lib/logging.properties 文件中,我们不想调整默认配置。因此,我创建了一个名为 logging.properties 的新文件,在静态块中,我们只是将其初始化并使用其 readConfiguration 方法将其提供给 LogManager 类。这足以初始化日志记录配置并产生以下输出:
[2020-06-29 15:34:49] [INFO ] An INFO level log!
当然,你只会初始化一次,而不是在每个类中。记在脑子里。
现在这与我们之前在博客文章中看到的不同,唯一改变的是我们包含的配置。现在让我们看看我们的 logging.properties 文件内容:
handlers=java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%4$-7s] %5$s %n
您可以看到我们已经设置了用于设置日志记录目标的处理程序。在我们的例子中,这是将日志打印到控制台的java.util.logging.ConsoleHandler。
接下来,我们说我们想为我们的处理程序使用什么格式,我们已经说过我们选择的格式化程序是java.util.logging.SimpleFormatter。这允许我们提供日志记录的格式。
最后通过使用 format 属性,我们将格式设置为[%1$tF %1$tT] [%4$-7s] %5$s %n。这是一个非常好的模式,但如果您第一次看到这样的东西,它可能会令人困惑。让我们讨论一下。所述SimpleFormatter使用的String.format方法用下面的列表或参数:(格式,日期,源,记录器,电平,消息,甩)。
所以[%1$tF %1$tT]部分告诉格式化程序采用第二个参数并提供日期和时间部分。然后[%4$-7s]部分读取日志级别并使用 7 个空格对其进行格式化。因此,对于 INFO 级别,它将添加 3 个额外的空格。在%5 $ S告诉格式化采取日志记录的消息,并将其打印为一个字符串,最后,%n是新行打印。现在应该更清楚了。
以编程方式配置 java.util.logging
现在让我们看看如何通过从 Java 代码级别配置格式化程序来进行类似的格式化。这次我们不会使用任何类型的属性文件,但我们将使用 Java 设置我们的 Logger。
执行此操作的代码如下所示:
package com.sematext.blog.loggging;
import java.util.Date;
import java.util.logging.*;
public class UtilLoggingConfigurationProgramatically {
private static Logger LOGGER = Logger.getLogger(UtilLoggingConfigurationProgramatically.class.getName());
static {
ConsoleHandler handler = new ConsoleHandler();
handler.setFormatter(new SimpleFormatter() {
private static final String format = "[%1$tF %1$tT] [%2$-7s] %3$s %n";
@Override
public String formatMessage(LogRecord record) {
return String.format(format,
new Date(record.getMillis()),
record.getLevel().getLocalizedName(),
record.getMessage()
);
}
});
LOGGER.setUseParentHandlers(false);
LOGGER.addHandler(handler);
}
public static void main(String[] args) throws Exception {
LOGGER.log(Level.INFO, "An INFO level log!");
}
}
这种方法与使用配置文件的方法之间的区别在于这个静态块。我们没有提供配置文件的位置,而是创建了 ConsoleHandler 的一个新实例,并覆盖了将 LogRecord 对象作为其参数的 formatMessage 方法。我们提供了格式,但我们没有将相同数量的参数传递给 String.format 方法,因此我们也修改了格式。我们还说我们不想使用父处理程序,这意味着我们只想使用我们自己的处理程序,最后我们通过调用 LOGGER.addHandler 方法添加我们的处理程序。就是这样 - 我们完成了。上述代码的输出如下所示:
[2020-06-29 16:25:34] [INFO ] An INFO level log!
我想表明在 Java 中设置日志记录也可以通过编程方式完成,而不仅仅是通过配置文件。但是,在大多数情况下,您最终会得到一个配置应用程序日志记录部分的文件。它使用起来更方便,调整、修改和使用更简单。
使用日志管理解决方案进行高效的 Java 日志记录
现在您了解了有关如何在我们的 Java 应用程序中打开日志记录的基础知识,但是随着应用程序的复杂性,日志量会增加。您可能会避免记录到文件并仅在需要故障排除时使用它们,但是处理大量数据很快变得无法管理,您最终应该使用日志管理解决方案进行日志监控和集中化。您可以选择基于开源软件的内部解决方案,也可以使用市场上可用的产品。
完全托管的日志集中解决方案将使您无需再管理基础架构的另一个通常非常复杂的部分。它将允许您管理大量日志源。
您可能希望在托管日志解决方案中包含诸如 JVM垃圾收集日志之类的日志。在为运行在 JVM 上的应用程序和系统打开它们之后,您将希望将它们放在一个地方进行关联、分析,并帮助您调整JVM 实例中的垃圾收集。对日志发出警报、汇总数据、保存并重新运行查询、连接您最喜欢的事件管理软件。
从我们关于Java 日志记录最佳实践的博客文章中阅读有关如何高效编写 Java 日志的更多提示。
结论
在对 Java 应用程序进行故障排除时,日志记录是非常宝贵的。事实上,无论是 Java 应用程序、硬件交换机还是防火墙,日志记录对于一般的故障排除都是无价的。软件和硬件都让我们了解它们是如何以富含上下文信息的参数化日志的形式工作的。
在本文中,我们从登录 Java 应用程序的基础知识开始。我们已经了解了 Java 日志记录有哪些选项,如何向应用程序添加日志记录,以及如何配置它。
我希望本文能让您了解如何处理 Java 应用程序日志,以及为什么您应该立即开始使用它们(如果您还没有的话)。并且不要忘记,高效的应用程序日志记录的关键是使用一个好的日志管理工具。