Skip to content

Logback 手册 - 第九章:日志分离

🏷️ Logback 手册


来源:https://logback.qos.ch/manual/loggingSeparation.html
作者:Ceki Gülcü、Sébastien Pennec、Carl Harris
版权所有 © 2000-2022 QOS.ch Sarl

本文档采用 Creative Commons Attribution-​NonCommercial-SéShareAlike 2.5 License 许可。


学习的快乐不在于知识本身,而在于学习的过程;拥有的乐趣也不在于拥有本身,而在于获得它的过程。当我澄清并耗尽一门学科时,我就会放弃它,回到黑暗中去;这种永远不满足的人是如此奇怪,他如果完成了一个建筑物,那不是为了安静地在其中居住,而是为了开始另一个。我想世界征服者会感到如此,当他刚刚征服一个王国,就伸出手臂去征服其他王国。

——卡尔·弗里德里希·高斯,给博亚伊的信,1808 年。


Style, like sheer silk, too often hides eczema.

——阿尔贝·加缪,《坠落》


WARNING

为了运行本章中的示例,您需要确保一些 jar 文件存在于类路径上。请参阅 设置页面 以获取更多详细信息。

问题:日志分离

本章涉及为在同一 Web 或 EJB 容器上运行的多个应用程序提供单独的日志记录环境的相对困难的问题。在本章的其余部分中,“应用程序”这个术语将用于交替引用 Web 应用程序或 J2EE 应用程序。在分离的日志记录环境中,每个应用程序看到一个不同的 logback 环境,因此一个应用程序的 logback 配置不会干扰另一个应用程序的设置。更具体地说,每个 Web 应用程序都有一个专门为其自己使用保留的 LoggerContext 的副本。回想一下,在 logback 中,每个记录器对象都是由它所附属的 LoggerContext 制造的,只要记录器对象在内存中存在,它就会保持附属于该 LoggerContext。这个问题的一个变体是应用程序日志记录和容器本身的日志记录的分离。

最简单、最容易的方法

假设您的容器支持子优先级类加载,可以通过在每个应用程序中嵌入 slf4j 和 logback jar 文件的副本来实现日志记录的分离。对于 Web 应用程序,将 slf4j 和 logback jar 文件放置在 Web 应用程序的 WEB-INF/lib 目录下就足以赋予每个 Web 应用程序一个独立的日志记录环境。当 logback 被加载到内存中时,放置在 WEB-INF/classes 下的 logback.xml 配置文件的副本将被拾取。

由于容器提供的类加载器分离,每个 Web 应用程序将加载其自己的 LoggerContext 副本,该副本将拾取自己的 logback.xml 配置文件。

如探囊取物般容易。

好吧,不完全是这样。有时您会被迫将 SLF4J 和 logback 构件放置在所有应用程序都可以访问的地方,通常是因为共享库使用 SLF4J。在这种情况下,所有应用程序将共享相同的日志环境。还有各种其他场景,在这些场景中,SLF4J 和 logback 构件的副本必须被放置在可以被所有应用程序看到的位置,使得通过类加载器分离实现日志记录分离变得不可能。所有的希望并没有破灭。请继续阅读。

上下文选择器

Logback 提供了一个机制,使得加载到内存中的单个 SLF4J 和 logback 类实例可以提供多个记录器上下文。当您编写以下代码时:

java
Logger logger = LoggerFactory.getLogger(​"foo");

LoggerFactory 类中的 getLogger() 方法将要求 SLF4J 绑定提供一个 ILoggerFactory。当 SLF4J 被绑定到 logback 时,返回一个 ILoggerFactory 的任务将被委托给 ContextSelector 的一个实例。请注意,ContextSelector 实现始终返回 LoggerContext 的实例。这个类实现了 ILoggerFactory 接口。换句话说,上下文选择器有选择任何适合其自己标准的 LoggerContext 实例的选项。因此它的名称是上下文 选择器

默认情况下,logback 绑定使用 DefaultContextSelector,它总是返回同一个称为默认记录器上下文的 LoggerContext

您可以通过设置 logback.ContextSelector 系统属性来指定不同的上下文选择器。假设您想将上下文选择器指定为 myPackage.myContextSelector 类的一个实例,您可以添加以下系统属性:

java
-Dlogback.ContextSelector=​myPackage.myContextSelector

上下文选择器需要实现 ContextSelector 接口,并具有一个唯一参数为 LoggerContext 实例的构造方法。

ContextJNDISelector

Logback-classic 随附了一个称为 ContextJNDISelector 的选择器,它根据通过 JNDI 查找可用的数据来选择记录器上下文。这种方法利用 J2EE 规范所要求的 JNDI 数据分离。因此,同一个环境变量可以在不同的应用程序中设置不同的值。换句话说,从不同的应用程序调用 LoggerFactory.getLogger() 将返回附加到不同记录器上下文的记录器,即使所有应用程序都共享内存中加载的单个 LoggerFactory 类。这就是日志记录分离。

要启用 ContextJNDISelector,需要将 logback.ContextSelector 系统属性设置为 "JNDI",如下所示:

java
-Dlogback.ContextSelector=​JNDI

请注意,JNDI 值是 ch.qos.logback.classic.​selector.ContextJNDISelector 的便捷缩写。

在应用程序中设置 JNDI 变量

在每个应用程序中,您需要为该应用程序命名日志记录上下文。对于 Web 应用程序,JNDI 环境条目在 web.xml 文件中指定。如果 "kenobi" 是您的应用程序名称,则应向 kenobi 的 web.xml 文件添加以下 XML 元素:

xml
<env-entry>
  <env-entry-name>logback/context-name</​env-entry-name>
  <env-entry-type>java.lang.String</​env-entry-type>
  <env-entry-value>kenobi</env-entry-value>
</env-entry>

假设您已启用 ContextJNDISelector,那么 Kenobi 的日志记录将使用名为 “kenobi” 的记录器上下文完成。此外,“kenobi” 记录器上下文将通过使用线程上下文类加载器查找名为 logback-kenobi.xml 的配置文件作为资源来 惯例 初始化。因此,例如对于 kenobi Web 应用程序,logback-kenobi.xml 应放置在 WEB-INF/classes 文件夹下。

如果需要的话,您可以通过设置 "logback/configuration-resource" JNDI 变量来指定一个不同于惯例的配置文件。例如,对于 kenobi web 应用程序,如果您希望指定 aFolder/my_config.xml 而不是常规的 logback-kenobi.xml,您可以向 web.xml 添加以下 XML 元素:

xml
<env-entry>
  <env-entry-name>logback/configuration-resource</env-entry-name>
  <env-entry-type>java.lang.String</env-entry-type>
  <env-entry-value>aFolder/my_config.xml</env-entry-value>
</env-entry>

文件 my_config.xml 应放置在 WEB-INF/classes/aFolder/ 下。重要的一点是,使用当前线程的上下文类加载器查找配置。

为 ContextJNDISelector 配置 Tomcat

首先,将 logback jars(即 logback-classic-1.3.8.jar、logback-core-1.3.8.jar 和 slf4j-api-2.0.7.jar)放置在 Tomcat 的全局(共享)类文件夹中。在 Tomcat 6.x 中,该目录为 $TOMCAT_HOME/lib/

可以通过向 $TOMCAT_HOME/bin 文件夹下的 catalina.sh 脚本(在 Windows 中为 catalina.bat )添加以下行来设置 logback.ContextSelector 系统属性。

java
JAVA_OPTS="$JAVA_OPTS -Dlogback.ContextSelector=​JNDI"

热部署应用程序

当 Web 应用程序被回收或关闭时,我们强烈建议关闭现有的 LoggerContext,以便可以正确进行垃圾回收。Logback 提供了一个名为 ContextDetachingSCLServletContextListener,专门用于分离与旧 Web 应用程序实例相关联的 ContextSelector 实例。可以通过在 Web 应用程序的 web.xml 文件中添加以下行来安装它。

xml
<listener>
  <listener-class>ch.qos.logback.classic.​selector.servlet.​ContextDetachingSCL</​listener-class>
</listener>

注意:大多数容器按照声明的顺序调用监听器的 contextInitialized() 方法,但按照相反的顺序调用它们的 contextDestroyed() 方法。因此,如果在 web.xml 中有多个 ServletContextListener 声明,那么 ContextDetachingSCL 应该被声明为 第一个,这样在应用程序关闭时其 contextDestroyed() 方法才会 最后 被调用。

更好的性能

ContextJNDISelector 处于活动状态时,每次检索日志记录器时都必须执行 JNDI 查找。这可能会对性能产生负面影响,特别是如果您在使用非静态(也称为实例)日志记录器引用。Logback 提供了一个名为 LoggerContextFilter 的 servlet 过滤器,专门设计用于避免 JNDI 查找成本。可以通过在应用程序的 web.xml 文件中添加以下行来安装它。

xml
<filter>
  <filter-name>LoggerContextFilter</​filter-name>
  <filter-class>ch.qos.logback.classic.​selector.servlet.​LoggerContextFilter</​filter-class>
</filter>
<filter-mapping>
  <filter-name>LoggerContextFilter</​filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

在每个 HTTP 请求开始时,LoggerContextFilter 将获取与应用程序关联的日志记录器上下文,然后将其放置在 ThreadLocal 变量中。ContextJNDISelector 首先检查 ThreadLocal 变量是否已设置。如果已设置,则将跳过 JNDI 查找。请注意,在 HTTP 请求结束时,ThreadLocal 变量将被清空。安装 LoggerContextFilter 可以显著改善日志记录器检索性能。

清空 ThreadLocal 变量允许在停止或回收 Web 应用程序时进行垃圾回收。

驯服共享库中的静态引用

ContextJNDISelector 在所有应用程序共享 SLF4J 和 logback artifacts 时非常好用。当 ContextJNDISelector 处于活动状态时,每次调用 LoggerFactory.getLogger() 都将返回一个属于调用 / 当前应用程序的日志记录器上下文的日志记录器。

引用日志记录器的常见习惯是通过静态引用。例如,

java
public class Foo {
  static Logger logger = LoggerFactory.getLogger(​Foo.class);
  ...
}

静态日志记录器引用既节省内存又高效。所有类实例只使用一个日志记录器引用。此外,只有在加载类到内存时才会检索日志记录器实例。如果主机类属于某个应用程序,比如 kenobi,那么静态日志记录器将通过 ContextJNDISelector 与 kenobi 的日志记录器上下文相关联。同样地,如果主机类属于其他应用程序,比如 yoda,那么它的静态日志记录器引用将通过 ContextJNDISelector 与 yoda 的日志记录器上下文相关联。

如果一个类,比如 Mustafar,属于被 kenobiyoda 共享的库,只要 Mustafar 有非静态的日志记录器,每次调用 LoggerFactory.getLogger() 都将返回一个属于调用 / 当前应用程序的日志记录器上下文的日志记录器。但如果 Mustafar 有一个静态日志记录器引用,那么它的日志记录器将附加到首次调用它的应用程序的日志记录器上下文。因此,在使用静态日志记录器引用的共享类的情况下,ContextJNDISelector 无法提供日志记录器分离。这种情况一直未能找到解决方案。

要透明且完美地解决这个问题的唯一方法是在日志记录器内部引入另一个级别的间接性,使得每个日志记录器外壳以某种方式将工作委托给与适当上下文相关联的内部日志记录器。这种方法实现起来非常困难,并且会产生大量的计算开销。这不是我们打算追求的方法。

不用说,通过将共享类移入 Web 应用程序(取消共享)可以轻松解决“共享类静态日志记录器”问题。如果无法取消共享,那么可以利用 SiftingAppender 的神奇力量,以 JNDI 数据作为分离标准来分离日志记录。

Logback 提供了一个称为 JNDIBasedContextDiscriminator 的判别器,它返回由 ContextJNDISelector 计算出的当前日志记录器上下文的名称。SiftingAppenderJNDIBasedContextDiscriminator 的组合将为每个 Web 应用程序创建单独的附加器。

xml
<configuration>

  <statusListener class="ch.qos.logback.​core.status.OnConsoleStatusListener" />

  <appender name="SIFT" class="ch.qos.logback.​classic.sift.SiftingAppender">
    <discriminator class="ch.qos.logback.​classic.sift.JNDIBasedContextDiscriminator">
      <defaultValue>unknown</defaultValue>
    </discriminator>
    <sift>
      <appender name="FILE-${contextName}" class="ch.qos.logback.core.FileAppender">
        <file>${contextName}.log</file>
        <encoder>
          <pattern>%-50(%level %logger{35}) cn=%contextName - %msg%n</pattern>
         </encoder>
      </appender>
     </sift>
    </appender>

  <root level="DEBUG">
    <appender-ref ref="SIFT" />
  </root>
</configuration>

如果 kenobi 和 yoda 是 Web 应用程序,那么上述配置将会将 yoda 的日志输出到 yoda.log,将 kenobi 的日志输出到 kenobi.log;甚至对位于共享类中的静态日志记录器引用生成的日志也同样适用。

您可以尝试使用 logback-starwars 项目来尝试上述描述的技术。

上述方法解决了日志记录器分离问题,但相当复杂。它要求正确安装 ContextJNDISelector,并要求附加器被 SiftingAppender 包装,而 SiftingAppender 本身就是一个非平凡的东西。

请注意,可以使用相同文件或不同文件配置每个日志记录器上下文。选择权在您手中。指示所有上下文使用相同的配置文件较为简单,因为只需维护一个文件。为每个应用程序维护一个不同的配置文件更难以维护,但允许更灵活的配置。

所以我们完成了吗?我们可以宣布胜利然后回家了吗?嗯,并没有那么简单。

假设 Web 应用程序 yodakenobi 之前初始化。要初始化 yoda,请访问 http://localhost:port/yoda/servlet,这将调用 YodaServlet。这个 Servlet 只是打个招呼并在调用 Mustafar 中的 foo 方法之前记录消息。

调用 YodaServlet 后,yoda.log 文件的内容应该包含

txt
DEBUG ch.qos.starwars.​yoda.YodaServlet             cn=yoda - in doGet()
DEBUG ch.qos.starwars.​shared.Mustafar              cn=yoda - in foo()

请注意,两条日志条目都与 "yoda" 上下文名称相关联。在这个阶段直到服务器停止,ch.qos.starwars.​shared.Mustafar 日志记录器都附加到 'yoda' 上下文,并将一直保持这样,直到服务器停止。

访问 http://localhost:port/kenobi/servlet 将在 kenobi.log 中输出以下内容。

txt
DEBUG ch.qos.starwars.​kenobi.KenobiServlet          cn=kenobi - in doGet()
DEBUG ch.qos.starwars.​shared.Mustafar               cn=yoda - in foo()

请注意,即使 ch.qos.starwars.​shared.Mustafar 日志记录器输出到 kenobi.log,它仍然附加到 'yoda'。因此,在这种情况下,我们有两个不同的日志记录器上下文记录到同一个文件,即 kenobi.log。这些上下文都引用 FileAppender 实例,这些实例位于不同的 SiftingAppender 实例中,它们记录到同一个文件。尽管看起来日志记录分离似乎符合我们的意愿,FileAppender 实例除非启用谨慎模式,否则不能安全地写入同一个文件。否则,目标文件将会损坏。

以下是启用谨慎模式的配置文件:

xml
<configuration>

  <statusListener class="ch.qos.logback.​core.status.OnConsoleStatusListener" />

  <appender name="SIFT" class="ch.qos.logback.​classic.sift.SiftingAppender">
    <discriminator class="ch.qos.logback.​classic.sift.JNDIBasedContextDiscriminator">
      <defaultValue>unknown</defaultValue>
    </discriminator>
    <sift>
      <appender name="FILE-${contextName}" class="ch.qos.logback.core.FileAppender">
        <file>${contextName}.log</file>
        <prudent>true</prudent>
        <encoder>
          <pattern>%-50(%level %logger{35}) cn=%contextName - %msg%n</pattern>
         </encoder>
      </appender>
     </sift>
    </appender>

  <root level="DEBUG">
    <appender-ref ref="SIFT" />
  </root>
</configuration>

如果您能够跟上到目前为止的讨论并实际尝试了 logback-starwars 的示例,那么您一定是对日志记录着迷。您应该考虑寻求 专业帮助