龙空技术网

Java 故障诊断 - 实现日志记录

粥屋与包屋 95

前言:

如今看官们对“java systemerr”大致比较看重,我们都需要了解一些“java systemerr”的相关内容。那么小编同时在网摘上汇集了一些对于“java systemerr””的相关文章,希望小伙伴们能喜欢,你们一起来学习一下吧!

本文我们将讨论在应用程序中实现日志功能的最佳实践。 为了使你的应用程序的日志信息可以用于调查,并避免给应用程序的执行带来麻烦,你需要注意一些实现细节。

我们将在第 1 小节中开始讨论应用程序如何持久化日志,特别是这些做法的优点和缺点。 在第 2 小节中,你将学习如何通过根据严重程度对日志信息进行分类来更有效地使用它们,从而使应用程序的性能更好。 在第 3 小节中,我们将讨论日志信息可能导致的问题以及如何避免它们。

篇幅较长,请耐心阅读。[呲牙]

1 持久化日志

持久化是日志信息的基本特征之一。正如前面文章所讨论的,日志与其他调查技术不同,因为它更关注过去而不是现在。 我们阅读日志是为了了解所发生的事情,所以应用程序需要存储它们,以便我们以后可以阅读它们。 如何存储日志信息会影响日志的可用性和应用程序的性能。 我曾与许多应用程序合作,并有机会看到开发人员实现日志信息持久化的各种方式:

在非关系型数据库中存储日志在文件中存储日志在关系型数据库中存储日志

这些都可以是很好的选择,取决于你的应用程序是做什么的。让我们看看你需要考虑的一些主要事项,以做出正确的决定。

在非关系型数据库中存储日志

非关系型(NoSQL)数据库帮助你在性能和一致性之间做出妥协。 你可以使用 NoSQL 数据库以更高的性能来存储日志,这让数据库有可能会错失一些日志消息, 或者不按照应用程序写日志的确切时间顺序来存储它们。 但是,一条日志消息应该总是包含消息被存储的时间戳,最好是在消息的开头。

在 NoSQL 数据库中存储日志消息是很常见的。在大多数情况下,应用程序使用一个完整的引擎来存储日志,并具有检索、搜索和分析日志消息的能力。 今天,两个最常用的引擎是 ELK 栈( .co/what-is/elk-stack)和 Splunk()。

在文件中存储日志

在过去,应用程序将日志存储在文件中。 你可能仍然会发现老的应用程序直接在文件中写入日志信息,但这种方法在今天已经不那么常见了,因为它通常比较慢, 而且搜索日志数据也比较困难。 我提请你注意这一点,因为你会发现许多教程和例子中,应用程序将日志存储在文件中, 但对于更多的当前应用程序,你应该避免这样做。

在关系型数据库中存储日志

我们很少使用关系型数据库来存储日志信息。 关系型数据库主要是保证数据的一致性,从而确保日志信息不会丢失。 一旦它们被存储,你就可以检索到它们。但是,一致性是以性能的妥协为代价的。

在大多数应用程序中,丢失一条日志信息并不是什么大问题,而且性能通常比一致性更重要。 但是,和以往一样,在现实世界的应用程序中,也有例外。 例如,世界各国政府对金融应用,特别是支付功能,都有日志信息规定。 这样的功能通常应该有特定的日志信息,应用程序不允许丢失。如果不遵守这些规定,可能会导致制裁和罚款。

2 定义日志级别和使用日志框架

现在我们将讨论日志级别和使用日志框架在应用程序中正确实现日志。 我们将首先研究为什么日志级别是必不可少的,然后实现一个例子。

日志级别,也称为严重程度,是一种根据日志信息对调查的重要性进行分类的方法。 一个应用程序在运行时通常会产生大量的日志信息。 然而,你往往不需要所有日志信息中的所有细节。 有些信息对你的调查来说比其他信息更重要:有些信息代表了关键事件,总是需要注意。

最常见的日志级别(严重程度)如下:

Error — 一个关键问题。应用程序应始终记录此类事件。通常情况下,Java应用程序中未处理的异常被记录为错误。Warn — 可能是一个错误的事件,但应用程序处理它。例如,如果与第三方系统的连接最初失败,但应用程序在第二次尝试时设法发送了调用,这个问题应该被记录为警告。Info — “Common” 日志信息。这些信息代表了主要的应用程序执行事件,帮助你了解应用程序在大多数情况下的行为。Debug — 仅当信息消息不够时才应启用的细粒度详细信息。

请注意,不同的库可能使用不止这四个严重级别,或者使用不同的名称。 例如,在某些情况下,应用程序或框架可能会使用严重级别 fatal(比 error 更严重)和t race(比 debug 不严重)。 在这我只关注真实世界的应用程序中最常遇到的严重性和术语。

根据严重程度对日志信息进行分类,可以使你的应用程序存储的日志信息数量最小化。 你应该只让你的应用程序记录最相关的细节,只有在你需要更多细节时才启用更多的日志记录。

请看图 1,它展示了日志严重程度的金字塔:

一个应用程序会记录少量的关键问题,但这些问题具有很高的重要性,所以它们总是需要被记录下来。越接近金字塔的底部,应用程序写的日志信息越多,但它们变得不那么关键,在调查中也不那么经常需要。

图 1

对于大多数调查研究案例,你不需要被归类为调试的信息。 此外,由于它们的数量很大,它们使你的调查研究更具挑战性。 出于这个原因,调试信息通常是被禁用的,只有当你面临一个需要更多细节的问题时,你才应该启用它们。

当你开始学习Java时,有人教你如何使用 System.outSystem.err 在控制台中打印东西。 最终,你学会了使用 printStackTrace() 来记录异常信息,正如我在《Java 故障诊断 - 使用日志调查问题》中使用的那样。 但这些在Java应用程序中处理日志的方式并没有给配置带来足够的灵活性。 因此,我建议你使用一个日志框架,而不是在现实世界的应用程序中使用它们。实现日志级别很简单。 如今,Java 生态系统提供了各种日志框架选项,如 Logback、Log4j 和 Java Logging API。 这些框架都是类似的,使用它们也很简单。

让我们举个例子,用 Log4j 实现日志记录。 这个例子在项目 da-ch5-ex2中。 要用 Log4j 实现日志功能,首先需要添加 Log4j 依赖项。 在我们的 Maven 项目中,你必须修改 pom.xml 并添加 Log4j 依赖项。

清单1 为了使用Log4j,你需要在 pom.xml 文件中添加的依赖项

<dependencies>   <dependency>      <groupId>org.apache.logging.log4j</groupId>      <artifactId>log4j-api</artifactId>      <version>2.14.1</version>    </dependency>    <dependency>      <groupId>org.apache.logging.log4j</groupId>      <artifactId>log4j-core</artifactId>      <version>2.14.1</version>    </dependency>  </dependencies>

一旦你在项目中建立了依赖关系,你可以在任何你想写日志信息的类中声明一个 Logger 实例。 在 Log4j 中,创建 Logger 实例的最简单方法是使用 LogManager.getLogger() 方法,如清单 2 所示。 这个方法允许你写日志信息,这些信息的名字与它们所代表的事件的严重程度相同。 例如,如果你想记录一条严重程度为 info 的消息,你将使用 info() 方法。 如果你想记录一条严重程度为 debug 的消息,你将使用 debug() 方法,以此类推。

清单 2 编写不同严重程度的日志信息

public class StringDigitExtractor {  // 为当前类声明一个 logger 实例以写入日志消息。  private static Logger log = LogManager.getLogger();  private final String input;  public StringDigitExtractor(String input) {    this.input = input;  }  public List<Integer> extractDigits() {    log.info("Extracting digits for input {}", input);  // 写入级别为 info 的消息    List<Integer> list = new ArrayList<>();    for (int i = 0; i < input.length(); i++) {      log.debug("Parsing character {} of input {}", // 写入一条级别为 debug 的消息          input.charAt(i), input);      if (input.charAt(i) >= '0' && input.charAt(i) <= '9') {        list.add(Integer.parseInt(String.valueOf(input.charAt(i))));      }    }    log.info("Extract digits result for input {} is {}", input, list);    return list;  }}

一旦你决定了要记录哪些信息并使用 Logger 实例来写这些信息, 你就需要配置 Log4j 来告诉应用程序如何以及在哪里写这些信息。 我们将使用一个 XML 文件,命名为 log4j2.xml 来配置 Log4j。 这个 XML 文件必须在应用程序的类路径中,所以我们将其添加到 Maven 项目的 resources 文件夹中。 我们需要定义三样东西(图 2):

logger — 告诉 Log4j 哪些信息将被写入哪个 appender。appender — 告诉 Log4j 在哪里写日志信息。formatter — 告诉 Log4j 如何打印信息

图 2

logger 定义了应用程序所记录的信息。在这个例子中,我们使用 Root 来写来自应用程序任何部分的消息。 它的属性级别,其值为 info,意味着只有严重程度为 info 以上的消息才会被记录下来。 logger 也可以决定只记录来自特定应用部分的消息。 例如,当使用一个框架时,你很少对框架打印的日志信息感兴趣, 但你经常对你的应用程序的日志信息感兴趣,所以你可以定义一个 logger,排除框架的日志信息, 只打印那些来自你的应用程序的信息。记住,你只想写必要的日志信息。 否则,调查会变得不必要的更有挑战性,因为你必须过滤掉非必要的日志信息。

在现实世界的应用程序中,你可以定义多个 appender,这些 appender 很可能被配置为将消息存储在不同的来源, 如数据库或文件系统中的文件。 在本文的前面,我们讨论了应用程序保留日志信息的多种方式。 appender 只是负责以特定方式存储日志消息的实现。

Appender 还使用一个定义消息格式的 formatter。 在这个例子中,formatter 指定了消息应该包括时间戳和严重程度,所以应用程序只需要发送描述。

清单 3 显示了同时定义了 appender 和 logger 的配置。 在这个例子中,我们只定义了一个 appender,它告诉 Log4j 在系统的标准输出流(控制台)中记录消息。

清单 3 在 log4j2.xml 文件中配置 appender 和 logger。

<?xml version="1.0" encoding="UTF-8"?><Configuration status="WARN">    <Appenders> <!-- Defining an appender. -->      <Console name="Console" target="SYSTEM_OUT">            <PatternLayout pattern="%d{yy-MM-dd HH:mm:ss.SSS} [%t]              %-5level %logger{36} - %msg%n"/>      </Console>    </Appenders>    <Loggers>  <!-- Defining a logger configuration. -->      <Root level="info">        <AppenderRef ref="Console"/>      </Root>    </Loggers></Configuration>

图 3 直观地显示了清单 3 中的XML配置与它所定义的三个组件之间的联系:logger、appender 和 formatter。

图 3

下一个片段显示了该例子运行时打印的日志的一部分。 注意,调试信息没有被记录下来,因为它们的严重程度比信息低(清单 3 中的第10行)。

23-03-28 13:17:39.915 [main] INFO  main.StringDigitExtractor - Extracting digits for input ab1c23-03-28 13:17:39.932 [main] INFO  main.StringDigitExtractor - Extract digits result for input ab1c is [1]23-03-28 13:17:39.943 [main] INFO  main.StringDigitExtractor - Extracting digits for input a112c23-03-28 13:17:39.944 [main] INFO  main.StringDigitExtractor - Extract digits result for input a112c is [1, 1, 2]…

如果我们想让应用程序也记录具有调试严重性的信息,我们就必须改变 logger 的定义。

清单 4 使用不同的严重程度配置

<?xml version="1.0" encoding="UTF-8"?><Configuration status="WARN">  <!-- Setting the logging level for internal Log4J events -->  <Appenders>        <Console name="Console" target="SYSTEM_OUT">            <PatternLayout pattern="%d{yy-MM-dd HH:mm:ss.SSS} [%t]              %-5level %logger{36} - %msg%n"/>        </Console>    </Appenders>    <Loggers>       <Root level="debug">  <!-- Changing the logging level to debug. -->         <AppenderRef ref="Console"/>       </Root>  </Loggers></Configuration>

在清单 4 中,你可以看到一个状态和一个日志级别。这通常会造成混淆。 大多数时候,你关心的是 level 属性,它显示了哪些消息将根据严重程度被记录下来。 <Configuration> 标签中的 status 属性是 Log4J 事件的严重程度,即库遇到的问题。 也就是说,status 属性是日志库的日志配置。

我们可以改变清单 4 中的 logger,使其也能写入具有优先级的消息:

23-03-28 13:18:36.164 [main ] INFO  main.StringDigitExtractor - Extracting digits for input ab1c23-03-28 13:18:36.175 [main] DEBUG main.StringDigitExtractor - Parsing character a of input ab1c23-03-28 13:18:36.176 [main] DEBUG main.StringDigitExtractor - Parsing character b of input ab1c23-03-28 13:18:36.176 [main] DEBUG main.StringDigitExtractor - Parsing character 1 of input ab1c23-03-28 13:18:36.176 [main] DEBUG main.StringDigitExtractor - Parsing character c of input ab1c23-03-28 13:18:36.177 [main] INFO  main.StringDigitExtractor - Extract digits result for input ab1c is [1]23-03-28 13:18:36.181 [main] INFO  main.StringDigitExtractor - Extracting digits for input a112c…

一个日志库让你可以灵活地只记录你需要的东西。 编写调查某个问题所需的最低数量的日志信息是很好的做法,因为它可以帮助你更容易地理解日志, 并保持应用程序的良好性能和可维护性。 一个日志库还能让你在不需要重新编译应用程序的情况下配置日志。

3 日志造成的问题以及如何避免这些问题

我们存储日志信息,以便我们可以利用它们来了解一个应用程序在某个时间点或一段时间内的行为方式。 在许多情况下,日志是必要的,而且非常有帮助,但如果处理不当,它们也可能成为恶意的。 在本小节中,我们将讨论日志可能导致的三个主要问题以及如何避免它们(图 4):

安全和隐私问题 — 由暴露私人数据的日志信息引起的性能问题 — 由应用程序存储过多或过大的日志信息引起的可维护性问题 — 由日志指令引起的,使源代码更难阅读

图 4

安全和隐私问题

安全是我最喜欢的话题之一,也是开发者在实现一个应用时需要考虑的最重要的课题之一。 我写的书中有一本是关于安全的,如果你使用 Spring 框架实现应用程序,并想了解更多关于安全的知识, 我推荐你阅读这本书: Spring Security in Action(曼宁,2020)。

令人惊讶的是,日志有时会导致应用程序的漏洞,在大多数情况下,这些问题的发生是因为开发人员没有注意到他们所暴露的细节。 请记住,日志会让任何能够访问它们的人看到特定的细节。 你总是需要考虑你所记录的数据是否应该让那些可以访问日志的人看到(图 5)。

图 5

下面的片段显示了一些暴露敏感细节和导致漏洞的日志信息的例子:

登录成功。用户 bob 以密码 RwjBaWIs66 登录。认证失败。令牌没有签名。该令牌应该有一个 IVL4KiKMfz 的签名。一个新的通知被发送到以下电话号码 +1233…

这里呈现的日志有什么问题?前两条日志信息暴露了私人细节。 你不应该记录密码或用于签署令牌的私钥,或任何其他交换的信息。 密码是只有其所有者才应该知道的东西。 出于这个原因,任何应用程序都不应该以明文形式存储任何密码(无论是在日志还是在数据库中)。 私钥和类似的秘密细节应该存储在秘密库中,以保护它们不被盗用。 如果有人得到这样一个密钥的值,他们就可以冒充一个应用程序或用户。

第三个日志信息的例子暴露了一个电话号码。 一个电话号码被认为是一个个人细节,在世界各地,特定的法规限制使用这些细节。 例如,欧盟在2018年5月实施了《通用数据保护条例》(GDPR)。 在任何欧盟国家有用户的应用程序必须遵守这些法规,以避免严重的制裁。 该法规允许任何用户要求应用程序使用他们的所有个人数据,并要求立即删除这些数据。 在日志中存储电话号码等信息会暴露这些私人细节,并使检索和删除它们更加困难。

性能问题

编写日志需要通过应用程序外部的某个 I/O 流来发送细节(通常为字符串)。 我们可以简单地将这些信息发送到应用程序的控制台(终端),也可以将其存储在文件甚至是数据库中, 正如我们在本文的前面中所讨论的。 无论哪种方式,你都需要记住,记录信息也是一个需要时间的指令;添加过多的日志信息会极大地降低应用程序的性能。

我记得几年前我的团队调查过一个问题。 一个客户报告了我们在工厂实现的用于库存的应用程序的问题。 这个问题并没有造成很大的麻烦,但我们发现要找到根本原因是很困难的,所以我们决定增加更多的日志信息。 在交付了一个带有小改动的补丁后,系统变得非常慢,有时几乎没有反应,这最终导致了生产停滞, 我们不得不迅速恢复我们的改动。我们不知不觉地把一只蚊子变成了一只大象。

但是,一些简单的日志信息怎么会引起这么大的麻烦? 日志被配置为将信息发送到网络中的一个单独的服务器,在那里它们被持久化。 在那个工厂里,不仅网络速度极慢,而且日志信息还加入了一个循环,在相当多的项目上迭代,使应用程序变得极慢。

最后,我们学到了一些东西,帮助我们更加小心,避免重蹈覆辙:

确保你了解应用程序是如何记录这些信息的。记住,即使是同一个应用程序,不同的部署也会有不同的配置。避免记录太多的消息。不要在大量元素的循环中记录消息。记录太多的消息也会使阅读日志变得复杂。 如果你需要在一个大的循环中记录消息,使用一个条件来缩小消息被记录的迭代次数。确保应用程序只在真正需要的时候才存储某个特定的日志消息。 你可以通过使用日志级别来限制你所存储的日志消息的数量。实现日志机制的方式是,你可以在不需要重新启动服务的情况下启用和禁用它。 这将允许你改变到一个细粒度的日志级别,获得你需要的细节,然后再使你的日志不那么敏感。可维护性

日志信息也会对应用程序的可维护性产生负面影响。 如果你过于频繁地添加日志信息,它们会使应用程序的逻辑更加难以理解。 让我们看一个例子:试着阅读清单5和6。哪段代码更容易理解?

清单 5 一个实现简单逻辑的方法

public List<Integer> extractDigits() {  List<Integer> list = new ArrayList<>();  for (int i = 0; i < input.length(); i++) {    if (input.charAt(i) >= '0' && input.charAt(i) <= '9') {      list.add(Integer.parseInt(String.valueOf(input.charAt(i))));    }  }  return list;}

清单 6 一个实现简单的逻辑片段的方法,被日志信息所拥挤

public List<Integer> extractDigits() {  log.info(“Creating a new list to store the result.”);  List<Integer> list = new ArrayList<>();  log.info(“Iterating through the input string ” + input);  for (int i = 0; i < input.length(); i++) {    log.info(“Processing character ” + i + “ of the string”);    if (input.charAt(i) >= '0' && input.charAt(i) <= '9') {      log.info(“Character ” + i +               “ is digit. Character: ” +               input.charAt(i))      log.info(“Adding character” + input.charAt(i) + “ to the list”);      list.add(Integer.parseInt(String.valueOf(input.charAt(i))));    }  }  Log.info(“Returning the result ” + list);  return list;}

两者都显示了同样的一段实现逻辑。但在清单 6 中,我添加了许多日志信息,这使得方法的逻辑更难阅读。

我们如何避免影响一个应用程序的可维护性?

你不一定需要为代码中的每条指令添加日志信息。 找出那些提供最相关细节的指令。 记住,如果现有的日志信息不够用,你可以在以后添加额外的日志信息。保持方法足够小,这样你只需要记录参数的值和方法执行后返回的值。一些框架允许你将部分代码与方法解耦。 例如,在 Spring 中,你可以使用自定义切面来记录方法的执行结果(包括参数值和方法执行后返回的值)。日志 vs. 远程调试

在第四章中,我们讨论了远程调试,你了解到你可以将 debugger 连接到一个在外部环境中执行的应用程序。 我开始这个讨论是因为我的学生经常问我,既然我们可以连接并直接调试一个给定的问题,为什么还要使用日志。 但正如我在本章前面以及之前的章节中提到的,这些调试技术并不互相排斥。 有时一个比另一个更好;在其他情况下,你需要把它们一起使用。

让我们来分析一下,用日志与远程调试能做什么,不能做什么,以弄清你如何有效地使用这两种技术。 表 1 显示了日志和远程调试的并列比较。

Capability

Logs

Remote debugging

可用于了解一个远程执行的应用程序的行为

Y

Y

需要特殊的网络权限或配置

X

Y

持久地存储执行线索

Y

X

允许你暂停在某一行代码上的执行,以了解应用程序的工作。

X

Y

可用于了解一个应用程序的行为,而不干扰执行的逻辑。

Y

Y

建议用于生产环境

Y

X

你可以使用日志和远程调试来了解一个远程执行的应用程序的行为。 但这两种方法都有各自的困难。日志意味着应用程序会写下调查所需的事件和数据。 如果不是这样,你需要添加这些指令并重新部署应用。这就是开发人员通常所说的 "添加额外的日志"。 远程调试允许你的 debugger 连接到远程执行的应用程序,但需要授予特定的网络配置和权限。

一个很大的区别是每种技术所暗示的理念。 调试的重点是现在。你暂停执行,观察应用程序的当前状态。 记录更多的是关于过去。你得到一堆日志信息并分析执行情况,关注时间线。 同时使用调试和日志来了解更复杂的问题是很常见的,我可以根据经验告诉你, 有时使用日志与调试相比,取决于开发者的偏好。 我有时会看到开发者使用一种技术,仅仅是因为他们对一种技术比另一种技术更适应。

结语

当你开始调查任何问题时,总是检查应用程序的日志。日志可能会指出问题所在,或者至少给你一个调查的起点。

所有的日志信息都应该包括一个时间戳。 记住,在大多数情况下,系统并不保证日志的存储顺序。 时间戳将帮助你按时间顺序排列日志信息。

避免保存太多的日志信息。 并非每个细节都与调查潜在问题有关或有帮助,而且存储过多的日志信息会影响应用程序的性能,并使代码更难阅读。

你应该只在需要时实现更多的日志记录。一个正在运行的应用程序应该只记录必要的信息。 如果你需要更多的细节,你可以随时在短时间内启用更多的日志记录。

日志中的异常不一定就是问题的根源。它可能是一个问题的后果。在本地处理异常之前,先研究是什么原因造成的。

你可以使用异常stack追踪来弄清楚是什么在调用某个方法。 在大型的、混乱的、难以理解的代码库中,这种方法会很有帮助,可以节省你的时间。

永远不要在日志信息中写敏感的细节(例如,密码、私钥或个人信息)。 记录密码或私钥会引入安全漏洞,因为任何能够访问日志的人都可以看到并使用它们。 写入个人细节,如姓名、地址或电话号码,也可能不符合各种政府法规。

标签: #java systemerr