A guide to logging in Java - JAVA日志简史
from:https://www.marcobehler.com/guides/a-guide-to-logging-in-java
本指南指导您发现,理解和正确地使用Java日志库(如Log4j2,Logback或java.util.logging)。
日志“似乎”像是一个非常简单的主题,但在实践中可能会非常棘手,且没有任何详细介绍。阅读本指南以充分了解Java日志历史背景。
介绍
或多或少的,每个Java应用程序都需要日志功能。
可能您只想将系统的状态或用户的操作记录到文件中,因此了解程序使用者做了什么。
logger.info("Application successfully started on port 8080");
可能每当发生异常时您都需要记录错误,然后通过电子邮件或文本发送给运维以紧急救援。
logger.error("Database connection is down", exception);
或者您的一个批处理作业可能想要记录警告并将警告发送到基于GUI的中央日志服务器中,然而它无法导入csv文件中的某些记录。
logger.warn("Invalid bank account number for record=[{}]", 53);
无论您要什么,都需要确保有适当的日志记录库,然后正确配置和使用它。
不幸的是,Java世界有很多可用的日志库(有关Java日志库地狱的详情,请参见此视频),因此开发人员应该对为什么有这么多选择以及何时使用哪个选择有一个大概了解。
让我们试试看。
传统过时的旧记录库
要了解Java日志记录库的最新发展,有必要认识并了解史前恐龙纪的Java最古老的日志记录库,那些今天任在某些生产环境中能找到的古董们。
java.util.logging(JUL)
从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,然后可以记录语句。您可能会觉得“错误SEVERS”和“还好FINE”这种日志级别看起来很奇怪,但它们基本上对应于所有现代Java日志记录库的“错误”和“调试”级别。
当然,您可以使用所谓的处理程序来配置记录程序,例如FileHandler(将日志语句写入到文件)或ConsoleHandler(写入System.err,控制台)。
FileHandler fileHandler = new FileHandler("status.log");
logger.addHandler(fileHandler);
因此,您可能会问,如果有JUL,为什么有人需要另一个日志记录框架?
尽管JUL够用了,但过去一直在讨论其缺点:如API不一致,性能下降,缺少(复杂的)配置选项,文档等-最终导致人们开发和使用其他日志框架。
Log4j(v1)
长期以来,登录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不仅具有健全的日志级别名称,例如“错误error”和“调试debug”。它还带有大量不同而巧妙的附加器appenders,例如SMTPAppender(用于发送电子邮件日志事件),SyslogAppenders(用于将事件发送到远程syslog守护程序),JdbcAppenders(用于将事件发送到数据库)等等。
此外借助PatternLayouts,您可以对日志消息外观进行更多控制。因此,日志可以以这样的布局打印在文件中:
# 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并不完全兼容,因此我们将在下一节中讨论它。
Apache Commons Logging(JCL)
大约在同一时间(2002年),弹出了另一个名为JCL的库,它有两个名称。Jakarta Commons Logging或Apache Commons Logging。
关于JCL,有趣的是它本身不是日志记录框架的实现。而是其他日志的接口。那是什么意思?
您可能已经猜到了,有了JCL库,日志代码可以相当简单,只要引用JCL替代JUL或Log4j即可。
// 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,JULApache Avalon(废弃)无关。
这意味着您需要在类路径上使用另一个库,如Log4j,并将这两个库配置在一起使用。
为什么这样做?
为什么不直接使用Log4j?有一个例子:
在编写三方库提供给他人使用时,您根本不知道 您写的三方库 的 使用者 希望在自己(使用者)的应用程序中使用哪种日志框架。因此,您在编写三方库时,使用日志接口就很有意义,用户只要部署自己的应用程序,就可以插入所需的任何日志实现。
还有啥问题没?
JCL的问题在于它依赖类加载器,以在运行时查找具体的日志实现。这会导致很多问题,该API不灵活,附带的不足之处,如今有更好的替代方法。
现代logging库(2020年时髦的日志库)
SLF4J和登录
可以说Logback是一个成熟,可靠,具有大量功能的日志库,其中一个功能:在生产环境中自动重载配置,最让开发人员铭记于心。
同时,他还开始编写Java的简单日志记录门面,也称为SLF4J,它与上面的Apache Commons Logging“桥接”库非常相似,只是实现了更好的实现。让我们看看这意味着什么:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
implementation 'org.slf4j
:slf4j-api:1.7.25'
将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绑定库:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
该依赖关系将为您传递Log4j(v1),并确保SLF4J记录“通过” Log4j。如果您对它的工作方式感兴趣,请阅读SLF4J手册中有关绑定的部分。
Logback不需要绑定,因为它自实现SLF4J,所以可以直接使用slf4j-api,即只要加入logback-classic jar依赖,就可以通过Logback记录日志,不要桥接门面绑定。
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
这种方法的优点在于,您的代码只需要知道SLF4J。并没有对Log4j,Logback或Jul的引用。如果您正在编写库(三方库),那就更好了。因为如果您的库使用SLF4J,则库的最终用户可以决定使用Log4j还是Logback或是他想要的其他任何日志实现库。用户可以简单地通过在类路径中添加或删除几个jar做出选择。
等等,我们没错过别的吗?
每当您使用第3方库时,事情都会变得有趣,这些库被硬编码为使用特定的日志库。假设您使用的是PDF生成器库,该库被硬编码为使用Log4j。您还使用了使用JUL的电子邮件发送库。您自己的应用程序使用SLF4J,但是您不能只是去更改这些库的源代码以也使用SLF4J。
现在要做什么?
幸运的是,SLF4J的创建者也考虑了该用例(请参见此处的截屏视频)。让我们先看一下Maven依赖项,看看它是什么样的:
每当您引入使用Log4j的第3方库时,显然,它将引入Log4j依赖项。Log4j依赖性如下所示:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
然后,您需要确保从项目中排除该依赖项,并使用以下直接替换代替:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.25</version>
</dependency>
这就是窍门:在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等)。此外,您可以使用桥接库使旧的第三方库“说” SLF4J。
虽然所有这些对于初学者来说似乎都很恐怖,但是通过一点经验,这一切都是有意义的。
Log4j2
有人可能会认为SLF4J及其周围的所有日志记录库将满足几乎所有日志记录需求。似乎并非如此。2014年,Log4j(v1)库的后继版本被发布,称为Log4j2-是完全重写,当然受到所有其他现有日志记录库的启发。
此外,就像SLF4J或JCL或Commons Logging一样,Log4j2可以用作桥接,因为它具有两个依赖项:
API依赖项:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.1</version>
</dependency>
以及实际的日志记录实现:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
</dependency>
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日志记录
如果不提及JBoss-Logging,那么谈论日志库将是不完整的。它是另一个日志记录桥,与SLF4J或JCL非常相似,因此您必须将其与其他日志记录实现甚至SLF4J本身一起使用。
如何登录
一旦确定了自己喜欢的日志记录框架,就该开始实际使用记录器了。这就引出了一个问题:您应该如何登录?
小巧的技术选择
如果您查看组织中不同的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”。
不要为静态或非静态,最终或非最终而烦恼,只需确保在整个项目中选择一致即可。
最后,确实没有必要仅仅为了它而对logger进行大写修改,当然也不是您的代码库中唯一的例外。
日志级别和文件
一个非常有趣的话题是:您实际上应该登录到哪个日志级别?您可以选择TRACE,DEBUG,INFO,WARN,ERROR,FATAL和相当数量的开发人员,他们不确定何时使用哪个。
这是我在一些地方成功使用过的通用方法,但是请注意,这并不是一成不变的(请参见此处的截屏视频)。在适当的地方对这些准则进行更改,但要确保您有一个可靠的用例及其理由。最重要的是,请确保您的开发人员和操作人员在同一页面上。
现在,让我们首先来看一下“错误组”日志级别,以及它们的用途。
错误
请求被中止,根本原因需要尽快进行人工干预。
警告
未能令人满意地满足请求,需要尽快但不一定立即进行干预。
实际上意味着什么?
为了评估错误和警告条目,您可以提出问题“需要采取什么措施”,如果听起来不像是“ OMG!立即采取措施!”。事件类型,由于不符合条件而被降级为较低级别。
考虑一下您将闪亮的金融科技(银行)应用程序的新功能推向生产,不幸的是,每当用户尝试显示其银行帐户的最近交易时,该功能就会触发臭名昭著的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级别打印了大量语句,从客户端活动(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
跟踪 -比调试更多或保留供特定环境使用的详细信息
您可能会看到跟踪级别甚至比调试级别更详细,或者您可以决定将跟踪级别与某些环境(例如DEV或TEST环境)结合使用,在这些环境中,开发人员可能会发疯并随意注销他们想要的任何东西,并且知道“ trace”将始终在生产中禁用。(尽管使用不同的日志配置/配置文件也可以轻松实现)
但是,如果您想看一看勤奋使用TRACE日志记录框架的框架,那么只需看一下Spring框架即可。使用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(文件名模式为<appname>。<instance-name> .YYYYMMDD.ZZZ.error.log),该文件由监视和警报系统以及操作人员使用。显然,您只希望在该日志文件中要提醒的条目,即ERROR或WARN语句。
你可以有另一个日志文件名为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
和
status.log
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
为了增强日志语句的关联性,还有一个重要的概念要知道,尤其是在使用多个分布式进程(微服务)时:映射诊断上下文(MDC)或线程上下文。
假设您有一个用户请求进入,该请求被路由到多个不同的微服务。当出现问题时,请求将失败,您如何知道来自哪些微服务的哪些日志行与该请求相对应。简便:您需要一个生成的请求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在浏览器中对其进行访问。
有时,您可能已经在端口8080上运行了另一个Spring Boot应用程序或同一应用程序的旧版本。这意味着您无法启动应用程序,因为这将失败。在较早的Spring Boot版本中,他们只是注销了“ raw”异常,如下所示:
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。所有主要Java日志记录框架都有Graylog扩展日志格式-GELF附加程序,这意味着您可以配置Log4j2等直接将其日志事件发送到Graylog。Graylog将根据需要从许多实例和应用程序中消耗日志事件,并以漂亮的,很少的图形用户界面显示它们-尽管搜索特定日志事件/日期范围的语法需要一些时间才能习惯。
以下是Graylog仪表板的外观:(从graylog主页链接)。有关Graylog外观的更多信息,请查阅其文档。
总结:如果您足够大,需要一个集中式日志服务器,请对所有可用选项进行快速比较,然后确定以下内容,与最终选择无关:
您的开发人员和应用程序不仅可以写入日志服务器。但是,运维人员和开发人员实际上也知道如何正确处理日志服务器的UI并发出正确的搜索查询-特别是在涉及多个应用程序的多个实例的情况下。
确定正确的日志记录方式
到目前为止,已经相当不错了。我们讨论了许多不同的日志记录库以及如何记录日志的不同方法。让我们总结所有内容,并且一如既往地记住,没有“唯一正确的方法”。
今天就这样。如果您有任何疑问,或者发现一些错误(拼写,逻辑等),只需将其发布到评论部分或给我发送电子邮件。
谢谢阅读
如果您想看到本指南的“实际应用”
我制作了一个有关Java日志记录的简短视频课程,其中涵盖了本指南的大部分概念以及实际代码,示例配置等。查看Java Logging:Masterclass。
致谢
3amopsguy于2012年成为伐木业的真正导师。