您可以使用本指南为您的应用程序发现、理解和使用正确的 Java 日志库,例如 Log4j2、Logback 或 java.util.logging。
日志“似乎”是一个非常简单的主题,但在实践中可能相当棘手,并且没有在任何地方进行足够详细的介绍。阅读本指南以充分了解 Java 日志环境。
迟早,每个 Java 应用程序都需要日志记录。
可能您只是想将系统状态或用户操作记录到文件中,以便您的操作人员了解正在发生的事情。
logger.info("Application successfully started on port 8080");
可能需要在发生异常时记录错误消息,然后向人员发送电子邮件或文本消息以进行紧急干预。
logger.error("Database connection is down", exception);
或者,您的批处理作业之一可能希望在无法导入 csv 文件的某些记录时记录并向基于 GUI 的中央日志服务器发送警告。
logger.warn("Invalid bank account number for record=[{}]", 53);
不管你想做什么,你都需要确保有一个合适的日志库,然后正确配置和使用它。
不幸的是,Java 世界有大量可用的日志库(请参阅此视频以了解 Java 日志地狱的概述),开发人员应该大致了解为什么有这么多选项以及何时使用哪个选项。
让我们来了解一下。
要了解 Java 日志库的最新发展,有必要认识并了解恐龙 - Java 最古老的日志库,您今天仍然可以在某些生产环境中找到它们。
从 Java 1.4 (2002) 开始,JDK 捆绑了自己的日志记录“框架”,称为 java.util.logging,通常缩写为 JUL。以下是如何使用 JUL 记录事物的示例:
// java.util.logging
java.util.logging.Logger logger = java.util.logging.Logger.getLogger(this.getClass().getName());
logger.info("This is an info message");
logger.severe("This is an error message"); // == ERROR
logger.fine("Here is a debug message"); // == DEBUG
与每个日志库的情况一样,首先您可以获得特定类或包的 Logger,然后您可以记录语句。您可能认为日志级别 'severe' 和 'fine' 看起来很奇怪,但它们基本上对应于所有现代 Java 日志库的 'error' 和 'debug' 级别。
当然,您可以使用所谓的处理程序配置记录器,例如 FileHandler(将日志语句写入文件)或 ConsoleHandler(写入 System.err)。
FileHandler fileHandler = new FileHandler("status.log");
logger.addHandler(fileHandler);
所以,你可能会问,如果有 JUL,为什么还有人需要另一个日志框架?
虽然 JUL 完成了这项工作,但过去一直在讨论它的缺点,包括其不一致的 API、性能缓慢、缺乏(复杂的)配置选项、文档等——最终导致人们开发和使用其他日志框架。
很长一段时间以来,Java 领域最流行的日志选择是Log4j(版本 1),它最初于 2001 年发布并一直维护到 2015 年。 事实上,您仍然会发现它在相当多的企业项目中使用,在 2018 年。
使用 Log4j,上面的日志示例如下所示:
// Log4j V1
org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(MyClass.getClass().getName());
logger.info("This is an info message");
logger.error("This is an error message");
logger.debug("Here is a debug message");
Log4j 不仅具有合理的日志级别名称,例如“错误”和“调试”。它还带有大量不同且聪明的 appender,如 SMTPAppender(发送电子邮件日志事件)、SyslogAppenders(发送事件到远程系统日志守护进程)、JdbcAppenders(发送它们到数据库)等等。
在PatternLayouts的帮助下,它还可以让您对日志消息的确切外观进行相当多的控制。因此,Java 应用程序中的相同日志事件可以像这样打印在日志文件中,具体取决于布局:
# contents of status.log
[INFO] 2012-11-02 21:57:53,662 MyLoggingClass - Application succesfully started on port 8080
# or
2010.03.23-mainThread --INFO -MyLoggingClass:Application succesfully started on port 8080
# or other endless possibilities
Log4j 做的很好,但在过去几年被 Log4j2 取代,它与 Log4j1 不完全兼容,因此我们将在下一节中讨论它。
大约在同一时间,在 2002 年,另一个名为JCL 的库出现了,它有两个名字。Jakarta Commons Logging 或 Apache Commons Logging。
JCL 的有趣之处在于,它本身并不是一个日志框架实现。相反,它是其他日志记录实现的接口。这意味着什么?
正如您可能已经猜到的那样,日志记录代码本身仍然相当简单,只是您现在引用 JCL 类而不是引用 JUL 或 Log4j 类 - 前提是您的类路径中有 JCL 库。
// Apache Commons Logging
org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(MyApp.class);
log.info("This is an info message");
log.error("This is an error message");
log.debug("Here is a debug message");
因此,您的代码仅使用 JCL 特定的类。但是实际的日志记录是由另一个日志记录框架完成的,无论是 Log4j、JUL 还是(已不复存在的)Apache Avalon都无关紧要。
这意味着您需要在类路径上使用另一个库,例如 Log4j,并将这两个库配置为协同工作。
为什么会有人想要这样做?
为什么不想直接使用 Log4j 呢?有一个主要用例,即编写库:
在编写库时,您根本不知道库的用户想要在他自己的应用程序中使用哪个日志记录框架。因此,编写您的库以使用日志记录接口是有意义的 - 然后用户可以在部署自己的应用程序时插入他或她想要的任何日志记录实现。
问题在哪里?
JCL 的问题在于,它依赖于类加载器技巧来找出它应该在运行时使用哪个日志记录实现。这会导致很多痛苦。此外,您会发现 API 有点不灵活,它带有大量的杂物,而且现在有更好的替代品。
在某个时候,Log4j 的原始创建者 Ceki Gülcü 决定从 Log4j 项目中分离出来并创建一个继承者,它不叫 Log4j2,而是Logback。您可以在此处阅读他尝试使用 Logback 改进的内容。
可以说,Logback 是一个成熟而可靠的日志库,具有大量功能,其中开发人员似乎记得最多的功能是在生产中自动重新加载配置文件。
同时,他还开始为 Java 编写 Simple Logging Facade,也称为SLF4J,它与上面的 Apache Commons Logging 'bridging' 库非常相似,只是有更好的实现。让我们看看这意味着什么:
要开始使用 SLF4J,您只需要在类路径上有一个库,即 slf4j-api 依赖项(请参阅下一节的截屏视频)。如果您使用像Maven这样的依赖管理工具,那么您需要将以下依赖添加到您的依赖项部分:
org.slf4j
slf4j-api
1.7.30
在类路径上使用 API 将允许您编写如下日志语句:
// SLF4J
org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(MyClass.class);
logger.info("This is an info message");
logger.error("This is an error message");
logger.debug("Here is a debug message"); // you do not need 'logger.isDebugEnabled' checks anymore. SLF4J will handle that for you).
就像 JCL 一样,SLF4J 不能自己做日志记录。它需要一个日志库来进行实际的日志记录,例如 Log4j、JUL、Logback 等。因此,假设您想使用 Log4j v1,那么您将需要在类路径中使用 slf4j-log4j12 绑定库:
org.slf4j
slf4j-log4j12
1.7.30
该依赖项将为您传递 Log4j (v1),并确保 SLF4J 记录“通过”Log4j。如果您对其工作原理感兴趣,请阅读SLF4J 手册中有关绑定的部分。
其他的库,比如 Logback,不需要绑定库,因为它们原生实现了 SLF4J,所以你可以简单地使用 slf4j-api 依赖,也可以放入 logback-classic jar 中,你可以通过 Logback 登录。
ch.qos.logback
logback-classic
1.2.3
这种方法的美妙之处在于,您的代码只知道 SLF4J。没有对 Log4j、Logback 或 Jul 的引用。如果您正在编写库,那就更好了。因为如果您的库使用 SLF4J,那么您的库的最终用户可以决定使用 Log4j、Logback 或他想要的任何库进行日志记录。因为可以通过在类路径中添加或删除几个 jar 来简单地做出选择。
等等,我们是不是错过了什么?
当您使用 3rd 方库时,事情变得很有趣,这些库被硬编码为使用特定的日志库。假设您正在使用一个 PDF 生成器库,该库被硬编码为使用 Log4j。您还使用了一个使用 JUL 的电子邮件发送库。您自己的应用程序使用 SLF4J,但您不能仅仅更改这些库的源代码以也使用 SLF4J。
现在做什么?
值得庆幸的是,SLF4J 的创建者也考虑了这个用例(请参阅此处的截屏视频)。让我们先看看 Maven 依赖项,看看它是什么样子:
每当您引入使用 Log4j 的 3rd 方库时,显然它都会引入 Log4j 依赖项。Log4j 依赖项如下所示:
log4j
log4j
1.2.17
然后,您需要确保从您的项目中排除该依赖项,并使用以下直接替换:
org.slf4j
log4j-over-slf4j
1.7.30
诀窍是:在 log4j-over-slf4j.jar 中,您会发现像 org.apache.log4j.Logger 这样的类,但它们与 Log4j 无关!相反,这些是 SLF4J 特定的类,即您的代码“认为”它调用 Log4j,但所有内容都被路由到 SLF4J。(对于其他“over-slf4j”库也是如此,JUL 库除外,您可以在此处阅读有关内容)。
这反过来意味着,作为库的最终用户,您可以使用您想要的任何日志库,即使原始库创建者希望您专门使用 Log4j。
现实生活
因此,根据您正在构建的内容和正在使用的第三方库,您的类路径中可能最终会包含以下库:
SLF4J API
您的 SLF4J 实现,例如 Logback 或 Log4j 等。
一个或多个桥接库,如 log4j-over-slf4j、jul-to-slf4j、jcl-over-slf4j 等。
主要外卖
使用 SLF4J,您可以对 API 进行编码,并且可以在稍后(编译时)选择实现(Log4j、Logback 等)。此外,您可以使用桥接库让传统的 3rd 方库“说”SLF4J。
虽然所有这些对于初学者来说可能看起来很可怕,但只要有一点经验,这一切都是有道理的。
有人可能认为 SLF4J 以及所有周围的日志库,几乎可以满足所有日志需求。好像不是这样。2014 年,发布了 Log4j (v1) 库的继任者,称为Log4j2 - 完全重写,当然受到所有其他现有日志库的极大启发。
此外,就像 SLF4J、JCL 或 Commons Logging 一样,Log4j2 可以用作桥梁,因为它带有两个依赖项:
API 依赖项:
org.apache.logging.log4j
log4j-api
2.14.0
以及实际的日志实现:
org.apache.logging.log4j
log4j-core
2.14.0
API 依赖项适用于各种其他日志记录框架,就像 SLF4J 或 JCL 一样。您可以加入 Log4j2 自己的日志记录实现,使用 SLF4J 实现,或使用桥接/适配器库之一以您希望的任何方式设置日志记录。但是,您的代码只会像这样引用 Log4j2 类:
// Log4j (version 2)
org.apache.logging.log4j.Logger logger = org.apache.logging.log4j.LogManager.getLogger(MyApp.class);
logger.info("This is an info message");
logger.error("This is an error message");
logger.debug("Here is a debug message");
如果您阅读了前面的部分,您可能会得出结论,SLF4J 和 Log4j2 有很多共同点,但不清楚为什么要使用 Log4j2 而不是只坚持使用 SLF4J。
Log4j2 的创建者试图在这里自己回答这个问题,主要区别似乎是性能(AsyncLogger、垃圾收集)和稍微好一点的 API(记录对象的能力,而不仅仅是字符串、Lambda 支持等)。
虽然应该说,虽然这些原因可能会对复杂的高负载应用程序产生影响,但在“普通”应用程序上工作的开发人员可能不会注意到差异。
如果不提及JBoss-Logging,则谈论日志库是不完整的。它是另一个日志桥,与 SLF4J 或 JCL 非常相似,因此您必须将它与另一个日志实现甚至 SLF4J 本身一起使用。
与这些其他伐木桥相比,它的主要声望似乎是它的国际化特征。除此之外,似乎没有什么理由将您的项目完全基于 jboss-logging,尽管您会发现像Hibernate这样的项目使用它,因为这两个库都是在RedHat 保护伞下开发的。
一旦你决定了你最喜欢的日志框架,就该真正使用你的记录器了。这给我们带来了一个问题:你应该如何登录?
如果您查看组织中的不同 Java 项目,或者甚至只查看一个项目,您可能会看到人们尝试获取 Logger 实例的多种方式: 使他们能够首先登录的类 -地方。
这可以采用以下外观:
// in class 1
private Logger LOG = LoggerFactory.getLogger(...);
// in class 2
private static final Logger LOGGER = ....;
// in class 3
private static Logger log = Logger.getLogger(...);
// in class 4
private Logger LOG_INSTANCE = ...;
// etc. etc.
现在应该是什么样子呢?对此有一个简单的答案。如果您正在创建的类,以及您为创建该类而调用的方法,内部都包含“logger”一词,则调用变量“logger”。
不要太担心静态或非静态、最终或非最终,只需确保在整个项目中选择同质化即可。
最后,真的没有必要仅仅为了它而 UPPER_CASE 你的记录器,当然不是你的代码库中的唯一例外。
一个非常有趣的话题是:您应该实际登录到哪个日志级别?您可以选择 TRACE、DEBUG、INFO、WARN、ERROR、FATAL,而且相当多的开发人员不确定何时使用哪一个。
这是我在一些地方看到成功使用的一般方法,但请注意,这不是一成不变的(请参阅此处的截屏视频)。在适当的情况下对这些指南进行更改,但请确保您有一个可靠的用例和推理。最重要的是,确保您的开发人员和运营人员在同一页面上。
现在让我们先分别看看“错误组”日志级别,以及您可能将它们用于什么。
致命的
此级别的任何内容都意味着您的 Java 进程无法继续,现在将终止。
最不有趣的日志级别,因为您不太可能在您的应用程序中使用它,而且诸如 SLF4J 之类的 API甚至不直接支持它。
错误
请求已中止,根本原因需要尽快进行人工干预。
警告
请求未得到令人满意的服务,需要尽快进行干预,但不一定立即进行。
这在实际中意味着什么?
要根据 ERROR 和 WARN 评估条目,您可以提出问题“需要采取什么行动”,如果这听起来不像是“天哪!现在就采取行动!” 事件类型,它会因不符合标准而降级到较低级别。
考虑一下您将闪亮的金融科技(银行)应用程序的新功能推向生产,不幸的是,每当用户尝试显示其银行帐户的最近交易时,就会触发臭名昭著的 Hibernate LazyLoadingException。这听起来像是一个非常强大的 OMG 情况,您会希望将这些错误记录为“错误” - 并触发适当的反应措施。
2018-09-11 08:48:36.480 ERROR 10512 --- [ost-startStop-1] com.marcobehler.UserService : Retrieving transaction list for user[id={}] failed
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: User.transactionDetails, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:582)
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:201)
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:561)
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:132)
at org.hibernate.collection.internal.PersistentBag.iterator(PersistentBag.java:277)
at java.lang.Iterable.forEach(Iterable.java:74)
at LibraryTest.spring_test(LibraryTest.java:78)
...
然后考虑一个批处理作业,它每天或每周导入事务。通常情况下,某些记录可能格式不正确,因此无法导入系统。有人,一个人,需要手动查看这些记录并修复它们。但这可能不像错误情况那样具有时间敏感性和紧迫性,因此您将选择使用 WARN 级别记录这些项目。
2018-09-11 00:00:36.480 WARN 10512 --- [ost-startStop-1] com.marcobehler.BatchJob : Could not import record[id=25] from csv file[name=transactions.csv] because of malformed[firstName,lastName]
保持 ERROR 和 WARN 标签干净的主要原因是它使监控和对这些事件的反应变得更加简单。
或者简单地说:确保在凌晨 3 点唤醒您的操作人员,以发现正确的(某种)错误。
信息
Info 是开发人员可能觉得最“舒服”的使用日志级别,在实践中你会发现开发人员打印出大量具有 INFO 级别的语句,从客户端活动(webapps)、进度信息(批处理作业)到相当复杂的,内部流程细节。
同样,决定什么应该是 INFO 什么应该是 DEBUG 可能是一条模糊的界线,但一般来说,流程详细信息应该与调试级别一起记录,而不是在信息中完全复制用户通过您的应用程序的旅程。日志。
从历史上看,将几乎所有内容都作为 INFO 注销的主要原因是,很难动态更改应用程序的日志级别,而不必重新启动(退回)所述应用程序。有时,开发人员和运营人员之间的组织孤岛也太大,无法轻松快速地更改日志级别。因此,开发人员选择安全起见,向控制台打印“更多”而不是更少,以便能够通过系统跟踪整个调用。
足够的介绍。让我们看一些例子。
显然,您可以使用 INFO 级别来注销应用程序状态,如下所示:
2018-09-11 08:46:26.547 INFO 8844 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
但是另一种考虑 INFO 级别的有趣方式是作为一个额外的(弱)错误案例:请求没有得到令人满意的服务,但解析细节已传递给请求者,不需要主动支持。
示例信息是“用户登录失败,用户名或密码不正确”。
2018-09-11 08:46:26.547 INFO 8844 --- [ main] com.marcobehler.UserService : User with id[=45] tried to login with wrong username/password combination
您可能想要记录这些信息,因为用户(通过支持层)可能会向操作员询问他们为什么不能使用该应用程序的问题。操作人员将能够在日志文件中看到原因(即使用户已经通过应用程序前端获得了此信息)。
最后,还有两个日志级别,“调试”和“跟踪”。关于在线跟踪级别的必要性已经有很多激烈的讨论,并且 SLF4J 仅在其较晚(较新)版本之一中引入了跟踪日志级别 - 在许多社区请求之后。
再说一次,这两者之间的界限可能很模糊,但让我们快速浏览一下:
调试
内部流程的高级细节。这仅在调查特定问题期间打开,然后再次关闭。根据所使用的日志库,可能无法在不弹跳(重新启动)应用程序的情况下执行此操作,这可能是不可接受的。
2018-08-01 05:05:00,031 DEBUG - Checking uploaded XML files for valid structure [...]
2018-08-01 05:06:00,031 DEBUG - Checking uploaded XML files for valid content [...]
2018-08-01 05:07:00,031 DEBUG - Masking inputs for XML file[id=5] [...]
2018-08-01 05:08:00,031 DEBUG - Replacing [...] XML sections for file[id=5] with [...]
...
2018-08-01 05:09:00,142 DEBUG - Forwarding XML file to archiving service
TRACE - 比调试更多的细节或保留在特定环境中使用
您可以看到跟踪级别与调试级别一样详细,或者您可以决定将跟踪级别与某些环境(即 DEV 或 TEST 环境)结合起来,在那里开发人员可以发疯并随意注销他们想要的任何东西,知道“跟踪”在生产中将始终被禁用。(虽然这也可以通过不同的日志配置/配置文件轻松实现)
不过,如果您想看看一个勤奋使用 TRACE 日志记录框架的框架,那么只需看看Spring Framework。当使用 Spring 的事务管理时,您将只能看到真正的数据库事务边界,当您启用 TRACE 日志级别时:
2018-08-01 05:05:00,031 TRACE - Getting transaction for [com.marcobehler.BitcoinApp.mine]
... your own log statements./..
2018-08-01 05:05:00,142 TRACE - Completing transaction for [com.marcobehler.BitcoinApp.mine]
日志文件
在谈论日志文件时,一种常见的方法是为不同的用例使用单独的文件。这意味着应用程序通常会记录到多个日志文件。
您可能有一个error.log(文件名模式为
你可以有另一个日志文件名为info.log建立或status.log(与<应用程序名称>。<实例名称>的文件名模式.YYYYMMDD.ZZZ.status.log),其中包含有关应用进展或用户活动的上述信息,以及例如 trace.log 文件,只要您想对日志记录发疯。
当记录到单独的文件时,有一个命令行实用程序(如log-merger,或只是一个普通的旧 bash 脚本)来动态合并这些单独的日志文件以获得特定的时间戳是有意义的。
假设你有两个文件:
错误日志
2015-08-29 15:49:46,641 ERROR [org.jboss.msc.service.fail] (MSC service thread 1-4) MSC000001: Failed to start service jboss.undertow.listener.default: org.jboss.msc.service.StartException in service jboss.undertow.listener.default: Could not start http listener
at org.wildfly.extension.undertow.ListenerService.start(ListenerService.java:150)
at org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1948)
at org.jboss.msc.service.ServiceControllerImpl$StartTask.run(ServiceControllerImpl.java:1881)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.net.BindException: Die Adresse wird bereits verwendet
at sun.nio.ch.Net.bind0(Native Method)
at sun.nio.ch.Net.bind(Net.java:436)
at sun.nio.ch.Net.bind(Net.java:428)
at sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:214)
at sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:74)
at sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:67)
at org.xnio.nio.NioXnioWorker.createTcpConnectionServer(NioXnioWorker.java:182)
at org.xnio.XnioWorker.createStreamConnectionServer(XnioWorker.java:243)
at org.wildfly.extension.undertow.HttpListenerService.startListening(HttpListenerService.java:115)
at org.wildfly.extension.undertow.ListenerService.start(ListenerService.java:147)
... 5 more
和
状态日志
2015-08-29 15:49:46,033 INFO [org.xnio] (MSC service thread 1-3) XNIO version 3.3.1.Final
运行日志合并实用程序后,可以按如下方式即时查看它们:
[1] 2015-08-29 15:49:46,033 INFO [org.xnio] (MSC service thread 1-3) XNIO version 3.3.1.Final
[0] 2015-08-29 15:49:46,641 ERROR [org.jboss.msc.service.fail] (MSC service thread 1-4) MSC000001: Failed to start service jboss.undertow.listener.default: org.jboss.msc.service.StartException in service jboss.undertow.listener.default: Could not start http listener
[0] at org.wildfly.extension.undertow.ListenerService.start(ListenerService.java:150)
[0] at org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1948)
[0] at org.jboss.msc.service.ServiceControllerImpl$StartTask.run(ServiceControllerImpl.java:1881)
[0] at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
[0] at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
[0] at java.lang.Thread.run(Thread.java:745)
[0] Caused by: java.net.BindException: Die Adresse wird bereits verwendet
[0] at sun.nio.ch.Net.bind0(Native Method)
[0] at sun.nio.ch.Net.bind(Net.java:436)
[0] at sun.nio.ch.Net.bind(Net.java:428)
[0] at sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:214)
[0] at sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:74)
[0] at sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:67)
[0] at org.xnio.nio.NioXnioWorker.createTcpConnectionServer(NioXnioWorker.java:182)
[0] at org.xnio.XnioWorker.createStreamConnectionServer(XnioWorker.java:243)
[0] at org.wildfly.extension.undertow.HttpListenerService.startListening(HttpListenerService.java:115)
[0] at org.wildfly.extension.undertow.ListenerService.start(ListenerService.java:147)
[0] ... 5 more
您当然也可以选择从一开始就将所有内容都记录到一个文件中。
然而,有一个警告:经验表明开发人员经常错误地假设,仅仅因为日志语句具有时间/位置相关性 - 这可能会违反直觉,尤其是当我们刚刚谈到合并日志文件时。
下面是一个示例:假设您有一个使用 Hibernate 的应用程序。它启动到某个点然后挂起,您看不到更多日志消息。该应用程序根本无法启动。
您看到的最后一条日志消息如下:
2018-09-11 09:35:19.166 INFO 14620 --- [ost-startStop-1] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
很容易假设某些东西必须被 JPA 或 Hibernate 破坏,仅仅因为这是您的最后一条日志消息。事实上,它可能是 Hibernate,但您的应用程序也可能在尝试启动另一个部分/第三方框架时挂起,但尚未将内容注销。
因此,当您快速得出结论时要小心,这通常发生在高压情况下:当生产中出现严重错误时。通过位置/时间戳日志文件关联并不自动意味着它IS相关,只知道它CAN是。
为了加强日志语句的相关性,还有一个重要的概念需要了解,尤其是当您使用多个分布式进程(微服务)时:映射诊断上下文 (MDC)或线程上下文。
假设您有一个用户请求,该请求被路由到多个不同的微服务。当出现问题时,请求失败,您如何知道微服务的哪些日志行对应于该请求。简单:您需要一个生成的请求 ID,您希望在每条日志消息中注销该 ID 。
因为你很懒,所以你不想手动注销那个 id,它应该自动工作。这就是 MDC 的用武之地。
在您的代码中的某个地方,在 HTTP servlet 过滤器中,您将拥有如下内容:
MDC.put("requestId", "lknwelqk-12093alks-123nlkasn-5t234-lnakmwen");
就够了。只需调用一次静态方法。
稍后,在您的应用程序代码中,您将像往常一样继续记录:
logger.info("Hi, my name is: Slim Shady!");
您还需要配置您的日志库以在每个日志语句中注销 MDC 变量(请参阅此处)。这将为您提供如下所示的日志消息:
[lknwelqk-12093alks-123nlkasn-5t234-lnakmwen] - Hi, my name is: Slim Shady!
然后很容易关联所有相应的日志消息,您只需在所有日志文件或集中式日志服务器中指定或搜索相同的请求 ID。
不用说,您应该避免(阅读:不得)注销敏感信息:用户凭据(即密码)或财务信息(如信用卡号等)或类似的敏感用户详细信息。
根据您系统的复杂性,您可能不想担心修复系统中的每个单独的日志语句(尽管您可能被迫通过审计的方式),但有更多的通用解决方案来确保某些信息被屏蔽 - 部分或完全,取决于您需要遵守的安全标准。
例如,在 Log4j2 的情况下,这意味着编写自定义LogEventPatternConverter,根据您的规定屏蔽日志事件。
显然,完整的屏蔽解决方案超出了本指南的范围,但您可以在此处和此处获得一些指示。
另一个在任何地方都没有真正详细介绍的主题是在日志语句中到底要写什么。这让我们想到了主动帮助的概念。
一个很好的例子是Spring Boot框架。当您使用 Spring Boot 构建 Web 应用程序并启动它进行测试时,该应用程序将在端口 8080 下运行,因此您可以在浏览器中从http://localhost:8080访问它。
有时会发生,您有另一个 Spring Boot 应用程序或同一应用程序的旧版本已经在端口 8080 上运行。这意味着您无法启动您的应用程序,因为这会失败。在较旧的 Spring Boot 版本中,他们只是简单地注销了“原始”异常,如下所示:
2018-09-11 09:35:57.062 ERROR 15516 --- [ main] o.apache.catalina.core.StandardService : Failed to start connector [Connector[HTTP/1.1-8080]]
org.apache.catalina.LifecycleException: Failed to start component [Connector[HTTP/1.1-8080]]
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:167)
at org.apache.catalina.core.StandardService.addConnector(StandardService.java:225)
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.addPreviouslyRemovedConnectors(TomcatWebServer.java:256)
at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:198)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.startWebServer(ServletWebServerApplicationContext.java:300)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.finishRefresh(ServletWebServerApplicationContext.java:162)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:553)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:759)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:395)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:327)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1255)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1243)
at com.marcobehler.MarcobehlerRootApplication.main(MarcobehlerRootApplication.java:26)
Caused by: org.apache.catalina.LifecycleException: Protocol handler start failed
at org.apache.catalina.connector.Connector.startInternal(Connector.java:1020)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
... 13 common frames omitted
...
开发人员当然可以进行从“无法启动连接器”→“另一个 Spring Boot 实例正在运行”所需的心理转换,但更好的方法是 Spring Boot 在较新版本中提供的内容:
2018-09-11 17:44:49.179 ERROR 24745 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Embedded servlet container failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.
通过添加它可以是一个随机进程或只是需要手动关闭的应用程序的另一个 (IDE) 实例,可以进一步改进它。
然而,这里的主要内容是,不仅可以在任何地方注销错误消息,而且如果可以的话,还可以提示可能的修复/号召性用语。这将节省以后分析错误和相应日志文件的时间。
(如果您想查看更多主动帮助的示例,您还可以查看Apache Wicket框架,该框架附带了针对大量错误的“建议修复”。查看其源代码以了解更多详细信息。 )
迁移遗留应用程序
从正常情况到本指南中讨论的设置(即统一日志记录实践、日志库等)需要一些时间。特别是如果您正在谈论多个现有的遗留应用程序,您都希望将它们迁移到某种程度相同的日志记录策略。即使不考虑管理支持等所有问题,您也需要为多个版本的所有应用程序缓慢迁移。
一种方法可能是开始时将所有内容都放入同一个日志文件中,当重新访问代码和日志记录配置时,它会修改所有日志记录以满足新标准。不幸的是,这只是一个粗略的指针,因为完整详细的迁移指南超出了本指南的范围。
一旦你有一个以上的应用程序实例在运行,甚至多个应用程序的多个实例(想想更大的组织或微服务),“如何管理这些日志”的问题就变得有趣了。一个非常简单的解决方案可能是 OP 具有某种脚本,它将所有实例中的所有日志文件复制到某个共享网络文件夹,以便开发人员可以访问它。
更高级的解决方案是集中式日志服务器或堆栈,如Graylog或Splunk或Elk。
让我们来看看 Graylog。有适用于所有主要 Java 日志记录框架的Graylog 扩展日志格式 - GELF附加程序,这意味着您可以配置例如 Log4j2 将其日志事件直接发送到 Graylog。Graylog 将根据需要使用来自尽可能多的实例和应用程序的日志事件,并将它们显示在一个漂亮的小图形 UI 中——尽管搜索特定日志事件/日期范围的语法需要一些时间来适应。
以下是 Graylog 仪表板外观的概览:(链接自graylog 主页)。有关 Graylog 外观的更多信息,请参阅其文档。
要点:如果您大到需要一个集中式日志服务器,请对所有可用选项进行快速比较,然后确保以下各项,与您的最终选择无关:
您的开发人员和应用程序不仅可以写入您的日志服务器。但运维人员和开发人员实际上也知道如何正确处理日志服务器的 UI 并发出正确的搜索查询——尤其是在涉及多个应用程序的多个实例时。
到目前为止,这是一段相当长的旅程。我们讨论了大量不同的日志库和不同的日志记录方式。让我们总结一下一切,并且 - 一如既往 - 记住没有“唯一正确的方法”。
如果您正在开发一个新项目,请从SLF4J + Logback或Log4j2 开始。你会没事的。
如果您正在处理遗留项目,请尝试将所有内容迁移到日志门面 (SLF4J),并在您的所有团队中引入同构和统一的日志记录方法
练习正确的日志级别、消息内容、监控措施等需要时间,所以不要沮丧。这是一个迭代过程。
如果您足够大(多个应用程序,多个实例),您可能需要查看集中式日志服务器
最重要的是,享受日志记录!
这就是今天的内容。如果您有任何问题或发现一些错误(拼写、逻辑等),只需将它们发布到评论部分或给我发送电子邮件。