第 12 章:Groovy 配置

**成为一个不满意的人总比不满意的猪好;最好是成为一个不满意的苏格拉底,而不是一个傻子。如果傻瓜或猪对此有其他看法,那是因为他们没有更好的经验.

-约翰·斯托尔·米尔,功利主义**

特定领域的语言或 DSL 相当普遍。基于 XML 的 logback 配置可以视为 DSL 实例。由于 XML 的本质,基于 XML 的配置文件非常冗长且庞大。此外,logback 中相对大量的代码(即 Joran)专用于处理这些基于 XML 的配置文件。 Joran 支持漂亮的功能,例如变量替换,条件处理和动态扩展。但是,乔兰不仅是复杂的野兽,而且它提供的用户体验也可以描述为不令人满意或至少是不直观的。

本章中介绍的基于 Groovy 的 DSL 旨在保持一致,直观和强大。您可以在配置文件中使用 XML 进行的所有操作,也可以使用更短的语法在 Groovy 中进行。为了帮助您迁移到 Groovy 样式配置,我们开发了自动将您现有的 logback.xml 文件迁移到 logback.groovy 的工具

General philosophy

通常,* logback.groovy 文件是 Groovy 程序。而且,由于 Groovy 是 Java 的超集,因此您可以在 Java 中执行任何配置操作,您都可以在 logback.groovy 文件中进行相同的操作。但是,由于使用 Java 语法以编程方式配置日志备份很麻烦,因此我们添加了一些特定于日志备份的扩展,以使您的生活更轻松。我们尽力将特定于 Logback 的语法扩展的数量限制为绝对最小。如果您已经熟悉 Groovy,则应该能够轻松阅读,理解甚至编写自己的 logback.groovy 文件。那些不熟悉 Groovy 的人应该仍然觉得 logback.groovy 语法比 logback.xml *更舒适。

假设* logback.groovy 文件是具有最少 logback 特定 extensions 的 Groovy 程序,那么 all 常用的 groovy 构造,例如类导入,变量定义,字符串(GStrings)中包含的${..}表达式的求值,以及 if-else 语句可在 logback.groovy *文件中找到。

Automatic imports

从 1.0.10 开始,为了减少不必要的样板,自动导入了几种常见的类型和包。因此,只要您仅配置内置的附加程序,布局等,就不需要在脚本中添加相应的 import 语句。当然,对于默认导入未涵盖的类型,您将需要它们。

这是默认导入的列表:

  • import ch.qos.logback.core.*;

  • import ch.qos.logback.core.encoder.*;

  • import ch.qos.logback.core.read.*;

  • import ch.qos.logback.core.rolling.*;

  • import ch.qos.logback.core.status.*;

  • import ch.qos.logback.classic.net.*;

  • import ch.qos.logback.classic.encoder.PatternLayoutEncoder;

此外,ch.qos.logback.classic.Level 中的所有常量都按原样(大写)和小写别名静态导入。因此,您的脚本无需静态 import 语句即可引用* INFO info *。

不再支持 SiftingAppender

由于 groovy 配置文件不再支持版本 1.0.12 SiftingAppender。但是,如果有需求,则可以重新引入。

特定于 logback.groovy 的扩展

本质上,* Logback.groovy *语法由以下介绍的六种方法组成;按照其惯常外观的相反 Sequences。严格来说,这些方法的调用 Sequences 无关紧要,只有一个 exception:必须在将附加程序附加到 Logger 之前对其进行定义。

•根(级别,列表 appenderNames = [])

root方法可用于设置根 Logger 的级别。作为类型List<String>的可选第二个参数,可用于按名称附加先前定义的追加程序。如果未指定附加名称列表,则假定为空列表。在 Groovy 中,空列表由[]表示。

要将根记录程序的级别设置为 WARN,您应编写:

root(WARN)

要将根 Logger 的级别设置为 INFO,并将名为“ CONSOLE”和“ FILE”的附加程序附加到根,您应编写:

root(INFO, ["CONSOLE", "FILE"])

在前面的示例中,假定已经定义了名为“ CONSOLE”和“ FILE”的附加程序。定义追加程序将在稍后讨论。

•Logger(字符串名称,级别,列表 appenderNames = [],布尔可加性= null)

logger()方法采用四个参数,其中最后两个是可选的。第一个参数是要配置的 Logger 的名称。第二个参数是指定 Logger 的级别。将 Logger 的级别设置为null会将其从最近的具有指定级别的祖先变为继承其水平List<String>类型的第三个参数是可选的,如果省略则默认为空列表。列表中的附加程序名称将附加到指定的 Logger。类型Boolean的第四个参数也是可选的,它控制additivity flag。如果省略,则默认为null

例如,以下脚本将“ com.foo”Logger 的级别设置为 INFO。

logger("com.foo", INFO)

下一个脚本将“ com.foo”Logger 的级别设置为 DEBUG,并向其附加名为“ CONSOLE”的附加程序。

logger("com.foo", DEBUG, ["CONSOLE"])

下一个脚本与上一个脚本类似,不同之处在于,它还将“ com.foo”Logger 的可加性标志设置为 false。

logger("com.foo", DEBUG, ["CONSOLE"], false)

•附加程序(字符串名称,类 clazz,闭包闭包= null)

appender 方法采用被配置为第一个参数的 appender 的名称。第二个强制参数是要实例化的附加程序的类。第三个参数是一个包含其他配置指令的闭包。如果省略,则默认为 null。

大多数附加程序需要设置属性,并且需要注入子组件才能正常运行。使用“ =”运算符(分配)设置属性。通过调用以该属性命名的方法并将该方法传递给该类以实例化为参数来注入子组件。可以递归应用此约定,以配置属性以及任何追加程序子组件的子组件。这种方法是* logback.groovy *脚本的核心,并且可能是唯一需要学习的约定。

例如,以下脚本实例化名为“ FILE”的FileAppender,将其文件属性设置为“ testFile.log”,并将其 append 属性设置为 false。类型PatternLayoutEncoder的编码器被注入到附加器中。编码器的 pattern 属性设置为“%level%logger-%msg%n”。然后将附加程序附加到根 Logger。

appender("FILE", FileAppender) {
  file = "testFile.log"
  append = true
  encoder(PatternLayoutEncoder) {
    pattern = "%level %logger - %msg%n"
  }
}

root(DEBUG, ["FILE"])

•时间戳记(字符串 datePattern,long timeReference = -1)

timestamp()方法方法返回与根据datePattern参数格式化的timeReference参数相对应的字符串。 datePattern参数应遵循SimpleDateFormat定义的约定。如果未指定timeReference值,则默认为-1,在这种情况下,将当前时间(即解析配置文件的时间)用作时间参考。视情况而定,您可能希望使用context.birthTime作为时间参考。

在下一个示例中,以 yyyyMMdd'T'HHmmss 格式为当前时间分配bySecond变量。然后使用“ bySecond”变量来定义文件属性的值。

def bySecond = timestamp("yyyyMMdd'T'HHmmss")

appender("FILE", FileAppender) {
  file = "log-${bySecond}.txt"
  encoder(PatternLayoutEncoder) {
    pattern = "%logger{35} - %msg%n"
  }
}
root(DEBUG, ["FILE"])

•conversionRule(字符串 conversionWord,converterClass 类)

创建自己的conversion specifier后,您需要通知 logback 它的存在。这是一个示例 logback.groovy 文件,该文件指示 logback 在遇到%sample转换字时使用 MySampleConverter。

import chapters.layouts.MySampleConverter

conversionRule("sample", MySampleConverter)
appender("STDOUT", ConsoleAppender) {
  encoder(PatternLayoutEncoder) {
    pattern = "%-4relative [%thread] %sample - %msg%n"
  }
}
root(DEBUG, ["STDOUT"])

•scan(字符串 scanPeriod = null)

调用 scan()方法将指示 logback 定期扫描 logback.groovy 文件以进行更改。每当检测到更改时,都会重新加载* logback.groovy *文件。

scan()

默认情况下,每分钟扫描一次配置文件是否有更改。您可以通过传递“ scanPeriod”字符串值来指定其他扫描周期。可以以毫秒,秒,分钟或小时为单位指定值。这是一个例子:

scan("30 seconds")

如果未指定时间单位,则将时间单位假定为毫秒,这通常是不合适的。如果更改默认扫描周期,请不要忘记指定时间单位。有关扫描如何工作的其他详细信息,请参阅自动重装部分

•statusListener(Class listenerClass)

您可以通过调用statusListener方法并将侦听器类作为参数来添加状态侦听器。这是一个例子:

import chapters.layouts.MySampleConverter

// We highly recommended that you always add a status listener just
// after the last import statement and before all other statements
statusListener(OnConsoleStatusListener)

Status listeners在前面的章节中进行了介绍。

•jmxConfigurator(字符串名称)

您可以使用此方法注册JMXConfigurator MBean。无需任何参数即可调用它,以对注册的 MBean 使用 Logback 的默认 ObjectName(ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator):

jmxConfigurator()

要将Name键的值更改为“默认”以外的其他值,只需传入另一个名称作为jmxConfigurator方法的参数即可:

jmxConfigurator('MyName')

如果要完全定义 ObjectName,请使用相同的语法,但将有效的 ObjectName 字符串表示形式作为参数传递:

jmxConfigurator('myApp:type=LoggerManager')

该方法将首先尝试将该参数用作 ObjectName,如果该参数不能表示有效的 ObjectName,则将其视为“ Name”键的值。

内部 DSL,也就是说,这一切都很古怪!

  • logback.groovy 是内部 DSL,这意味着其内容作为 Groovy 脚本执行。因此,所有常规 Groovy 构造(例如类导入,GString,变量定义,字符串(GStrings)中包含的${..}表达式的求值,if-else 语句)都可以在 logback.groovy 文件中使用。在下面的讨论中,我们将在 logback.groovy *文件中介绍这些 Groovy 构造的典型用法。

变量定义和 GString

您可以在* logback.groovy *文件中的任何位置定义变量,然后在 GString 中使用该变量。这是一个例子。

// define the USER_HOME variable setting its value 
// to that of the "user.home" system property
def USER_HOME = System.getProperty("user.home")

appender("FILE", FileAppender) {
  // make use of the USER_HOME variable
  file = "${USER_HOME}/myApp.log"
  encoder(PatternLayoutEncoder) {
    pattern = "%msg%n"
  }
}
root(DEBUG, ["FILE"])

在控制台上打印

您可以调用 Groovy 的println方法在控制台上进行打印。这是一个例子。

def USER_HOME = System.getProperty("user.home");
println "USER_HOME=${USER_HOME}"

appender("FILE", FileAppender) {
  println "Setting [file] property to [${USER_HOME}/myApp.log]"
  file = "${USER_HOME}/myApp.log"  
  encoder(PatternLayoutEncoder) {
    pattern = "%msg%n"
  }
}
root(DEBUG, ["FILE"])

自动导出的字段

'hostname' variable

“主机名”变量包含当前主机的名称。但是,由于作者无法完全解释的范围规则,'hostname'变量仅在最高范围内可用,而在嵌套范围内不可用。下一个例子应该说明这一点。

// will print "hostname is x" where x is the current host's name
println "Hostname is ${hostname}"

appender("STDOUT", ConsoleAppender) {
  // will print "hostname is null"
  println "Hostname is ${hostname}" 
}

如果希望在所有作用域中都能看到 hostname 变量,则需要定义另一个变量,并为其分配'hostname'值,如下所示。

// define HOSTNAME by assigning it hostname
def HOSTNAME=hostname
// will print "hostname is x" where x is the current host's name
println "Hostname is ${HOSTNAME}"

appender("STDOUT", ConsoleAppender) {
  // will print "hostname is x" where x is the current host's name
  println "Hostname is ${HOSTNAME}" 
}

通过参考当前上下文,一切都可以识别上下文

  • logback.groovy *脚本的执行是在ContextAware对象的范围内完成的。因此,始终可以使用“ context”变量访问当前上下文,并且可以调用addInfo(),addWarn()和addError()方法将状态消息发送到上下文的StatusManager
// always a good idea to add an on console status listener
statusListener(OnConsoleStatusListener)

// set the context's name to wombat
context.name = "wombat"
// add a status message regarding context's name
addInfo("Context name has been set to ${context.name}")

def USER_HOME = System.getProperty("user.home");
// add a status message regarding USER_HOME
addInfo("USER_HOME=${USER_HOME}")

appender("FILE", FileAppender) {
  // add a status message regarding the file property
  addInfo("Setting [file] property to [${USER_HOME}/myApp.log]")
  file = "${USER_HOME}/myApp.log"  
  encoder(PatternLayoutEncoder) {
    pattern = "%msg%n"
  }
}
root(DEBUG, ["FILE"])

Conditional configuration

鉴于 Groovy 是一种成熟的编程语言,条件语句允许单个* logback.groovy *文件适应各种环境,例如开发,测试或生产。

在下一个脚本中,在我们的生产机器 pixie 或 orion 以外的主机上激活了控制台附加程序。请注意,滚动文件附加程序的输出目录也取决于主机。

// always a good idea to add an on console status listener
statusListener(OnConsoleStatusListener)

def appenderList = ["ROLLING"]
def WEBAPP_DIR = "."
def consoleAppender = true;

// does hostname match pixie or orion?
if (hostname =~ /pixie|orion/) {
  WEBAPP_DIR = "/opt/myapp"     
  consoleAppender = false   
} else {
  appenderList.add("CONSOLE")
}

if (consoleAppender) {
  appender("CONSOLE", ConsoleAppender) {
    encoder(PatternLayoutEncoder) {
      pattern = "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
    }
  }
}

appender("ROLLING", RollingFileAppender) {
  encoder(PatternLayoutEncoder) {
    Pattern = "%d %level %thread %mdc %logger - %m%n"
  }
  rollingPolicy(TimeBasedRollingPolicy) {
    FileNamePattern = "${WEBAPP_DIR}/log/translator-%d{yyyy-MM}.zip"
  }
}

root(INFO, appenderList)