第 9 章:日志记录分离

**这不是知识,而是学习的行为,不是占有的行为,而是到达那里的行为,它赋予最大的享受。当我弄清并用尽了一个主题之后,我便转身离开,以再次陷入黑暗。永不满足的人如果完成了一个结构,就会感到很奇怪,那不是为了和平地居住在其中,而是为了开始另一个。我想世界征服者一定会感到这样,在几乎征服了一个王国之后,谁会为其他国家伸手.

卡尔·弗里德里希·高斯(KARL FRIEDRICH GAUSS),写给 Bolyai 的信,1808 年。

*风格,如纯粹的丝绸,往往会掩盖湿疹。

-阿尔伯特·卡缪斯,秋天**

问题:记录分隔

本章涉及一个相对困难的问题,即为在同一 Web 或 EJB 容器上运行的多个应用程序提供单独的日志记录环境。在本章的其余部分,术语“应用程序”将用于互换地指代 Web 应用程序或 J2EE 应用程序。在单独的日志记录环境中,每个应用程序都会看到一个独特的日志备份环境,因此一个应用程序的日志备份配置不会干扰另一个应用程序的设置。用更多的技术术语来说,每个 Web 应用程序都有一个单独的LoggerContext副本,供其自己使用。回想一下,在 logback 中,每个 Logger 对象都是由LoggerContext制造的,只要 Logger 对象存在于内存中,它就会一直保持连接状态。此问题的一个变体是将应用程序日志记录与容器本身的日志记录分离。

最简单,最简单的方法

假设您的容器支持子优先级加载,则可以通过在每个应用程序中嵌入 slf4j 和 logback jar 文件的副本来完成日志记录的分离。对于 Web 应用程序,将 slf4j 和 logback jar 文件放在 Web 应用程序的* WEB-INF/lib 目录下足以为每个 Web 应用程序提供单独的日志记录环境。当将 logback 加载到内存中时,将获取放置在 WEB-INF/classes 下的 logback.xml *配置文件的副本。

通过容器提供的类加载器分隔,每个 Web 应用程序将加载自己的LoggerContext副本,该副本将获取自己的* logback.xml *副本。

非常简单。

好吧,不完全是。有时,您会被迫将 SLF4J 和注销构件放置在可从所有应用程序访问的位置,通常是因为共享库使用了 SLF4J。在这种情况下,所有应用程序将共享相同的日志记录环境。在其他各种情况下,必须将 SLF4J 和 logback 工件的副本放在所有应用程序都可以看到的位置,从而无法通过类加载器进行日志记录分离。所有希望都不会丢失。请 continue 阅读。

Context Selectors

Logback 为 SLF4J 的单个实例提供了一种机制,并且将 logback 类加载到内存中以提供多个 Logger 上下文。当您写:

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

LoggerFactory类中的getLogger()方法将向 SLF4J 绑定请求ILoggerFactory。当 SLF4J 绑定到 logback 时,将返回ILoggerFactory的任务委派给ContextSelector的实例。请注意,ContextSelector实现始终返回实例LoggerContext。此类实现ILoggerFactory接口。换句话说,上下文 selectors 可以选择根据自己的条件返回它认为合适的任何LoggerContext实例。因此,名称为 context * selector *。

默认情况下,logback 绑定使用DefaultContextSelector,始终返回相同的LoggerContext,称为默认 Logger 上下文。

您可以通过设置* logback.ContextSelector *系统属性来指定其他上下文 selectors。假设您想为myPackage.myContextSelector类的实例指定上下文 selectors,则将添加以下系统属性:

-Dlogback.ContextSelector=myPackage.myContextSelector

上下文 selectors 需要实现ContextSelector接口并具有一个构造器方法,该方法接受LoggerContext实例作为其唯一参数。

ContextJNDISelector

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

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

-Dlogback.ContextSelector=JNDI

请注意,值JNDIch.qos.logback.classic.selector.ContextJNDISelector的便捷缩写。

在应用程序中设置 JNDI 变量

在每个应用程序中,您都需要为应用程序命名日志上下文。对于 Web 应用程序,在* web.xml *文件中指定了 JNDI 环境条目。如果“ kenobi”是应用程序的名称,则应将以下 XML 元素添加到 kenobi 的 web.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”的 Logger 上下文来记录 Kenobi。此外,将使用线程上下文类加载器通过将名为* logback-kenobi.xml 的配置文件作为 resource 查找来通过“常规”初始化“ kenobi”Logger 上下文。因此,例如对于 kenobi Web 应用程序,应将 logback-kenobi.xml 放在 WEB-INF/classes *文件夹下。

如果需要,可以通过设置“ logback/configuration-resource” JNDI 变量来指定不同于约定的其他配置文件。例如,对于 kenobi Web 应用程序,如果您希望指定* aFolder/my_config.xml 而不是常规的 logback-kenobi.xml *,则可以在 web.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/*下。要记住的重要一点是,使用当前线程的上下文类加载器将配置作为 Java 资源查找。

为 ContextJNDISelector 配置 Tomcat

首先,将 logback jar(即 logback-classic-1.3.0-alpha5.jar,logback-core-1.3.0-alpha5.jar 和 slf4j-api-2.0.0-alpha1.jar)放置在 Tomcat 的全局(共享)中。 )类文件夹。在 Tomcat 6.x 中,此目录为* $ TOMCAT_HOME/lib/*。

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

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

热部署应用程序

当 Web 应用程序被回收或关闭时,我们强烈建议关闭现有的LoggerContext以便可以正确地对其进行垃圾回收。 Logback 附带一个名为_的ServletContextListener,专门用于分离与较旧的 Web 应用程序实例关联的ContextSelector实例。可以通过将以下行添加到 Web 应用程序* web.xml *文件中来进行安装。

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

注意:大多数容器以声明的 Sequences 调用侦听器的contextInitialized()方法,但以相反的 Sequences 调用其contextDestroyed()方法。因此,如果您在* web.xml 中具有多个ServletContextListener声明,则应 first 声明ContextDetachingSCL,以便在应用程序关闭期间 last *调用其contextDestroyed()方法。

Better performance

ContextJNDISelector处于活动状态时,每次检索 Logger 时,都必须执行 JNDI 查找。这可能会对性能产生负面影响,尤其是在您使用非静态(也称为实例)Logger 引用的情况下。 Logback 附带了一个名为LoggerContextFilter的 servlet 过滤器,该过滤器专门用于避免 JNDI 查找成本。可以通过将以下行添加到应用程序的 web.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将获取与应用程序关联的 Logger 上下文,然后将其放在ThreadLocal变量中。 ContextJNDISelector首先检查ThreadLocal变量是否已设置。如果已设置,则将跳过 JNDI 查找。请注意,在 http 请求结束时,ThreadLocal变量将为空。安装LoggerContextFilter可以大大提高 Logger 的检索性能。

禁用ThreadLocal变量可以在停止或回收 Web 应用程序时对其进行垃圾回收。

在共享库中驯服静态引用

当 SLF4J 和 logback 工件被所有应用程序共享时,ContextJNDISelector可以很好地创建日志分隔。当ContextJNDISelector处于活动状态时,对LoggerFactory.getLogger()的每次调用将返回一个 Logger,该 Logger 属于与调用方/当前应用程序关联的 Logger 上下文。

引用 Logger 的常用习惯是通过静态引用。例如,

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

静态 Logger 引用同时具有内存和 CPU 效率。该类的所有实例仅使用一个 Logger 引用。此外,当类加载到内存中时,Logger 实例仅被检索一次。如果主机类属于某个应用程序,例如 kenobi,则静态 Logger 将借助ContextJNDISelector附加到 kenobi 的 Logger 上下文。同样,如果主机类属于其他应用程序,例如 yoda,则其静态 Logger 引用将再次通过ContextJNDISelector附加到 yoda 的 Logger 上下文中。

如果某个类(例如Mustafar)属于* kenobi yoda *共享的库,只要Mustafar具有非静态 Logger,则每次LoggerFactory.getLogger()的调用都将返回一个 Logger,该 Logger 与调用/当前相关联应用。但是,如果Mustafar具有静态 Logger 引用,则其 Logger 将附加到首先调用它的应用程序的 Logger 上下文中。因此,在使用静态日志 Logger 引用的共享类的情况下,ContextJNDISelector不提供日志记录分隔。这种极端的情况使人们无法找到解决方案。

透明,完美地解决此问题的唯一方法是在 Logger 内部引入另一种间接方式,以便每个 LoggerShell 以某种方式将工作委派给附加到适当上下文的内部 Logger。这种方法将很难实施,并且会产生大量的计算开销。这不是我们计划追求的方法。

不用说,可以通过在 Web 应用程序内部移动共享类(取消共享)来简单地解决“共享类静态 Logger”问题。如果无法共享,则可以使用SiftingAppender的神奇力量,以便使用 JNDI 数据作为分离标准来分离日志记录。

Logback 附带了一个名为JNDIBasedContextDiscriminator的鉴别符,该鉴别符返回由ContextJNDISelector计算得出的当前 Logger 上下文的名称。 SiftingAppenderJNDIBasedContextDiscriminator组合将为每个 Web 应用程序创建单独的附加程序。

<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 *;这甚至适用于位于共享类中的静态 Logger 引用生成的日志。

您可以在logback-starwars项目的帮助下尝试上述技术。

上述方法解决了测井分离问题,但是相当复杂。它要求正确安装ContextJNDISelector,并要求附加器由SiftingAppender包裹,而SiftingAppender本身是不平凡的野兽。

请注意,可以使用相同文件或不同文件来配置每个日志记录上下文。这个选择由你。指示所有上下文使用同一配置文件更为简单,因为仅需维护一个文件。为每个应用程序维护不同的配置文件较难维护,但可以提供更大的灵 Active。

那我们完成了吗?我们可以宣布胜利并回家吗?好吧,不完全是。

假设 Web 应用程序yodakenobi之前初始化。要初始化yoda,请访问http://localhost:port/yoda/servlet,它将调用YodaServlet。该 Servlet 会打个招呼并记录消息,然后在Mustafar中调用foo方法,这并不奇怪地会记录一条简单消息并返回。

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

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.MustafarLogger 已附加到“ yoda”上下文,并且将保持不变,直到服务器停止运行为止。

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

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

请注意,即使ch.qos.starwars.shared.MustafarLogger 输出到* kenobi.log ,它也仍附加到“ yoda”。因此,我们有两个截然不同的日志记录上下文记录到同一文件,在这种情况下为 kenobi.log *。这些上下文中的每一个都引用嵌套在不同SiftingAppender实例中的FileAppender实例,这些实例正在记录到同一文件中。尽管日志分离似乎按照我们的意愿起作用,但是 FileAppender 实例除非启用审慎模式,否则无法安全地写入同一文件。否则,目标文件将被破坏。

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

<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 示例,那么您必须 true 沉迷于日志记录。您应该考虑寻找professional help