logback官方文档中文翻译第十二章:Groovy 配置

第十二章:Groovy 配置

领域特定语言或者 DSL 更加普遍。logback 基于 XML 的配置可以看做 DSL 的实例。由于 XML 的本质,基于 XML 的配置文件变得非常的啰嗦以及臃肿。另外,logback 中的 Joran 有一个相对庞大的代码,用来专门处理基于 XML 的配置文件。Joran 支持一些非常好的特性,例如变量替换,条件处理,以及动态扩展。但是,不但 Joran 非常复杂,而且给用户的体验非常的不好,或者至少不直观。

本章叙述基于 Groovy 的 DSL 致力于一致性,直观性,以及非常强大。任何你可以使用 XML 配置的文件,你都可以用更加简短的符号使用 Groovy 来实现。

常规建议

一般来说,logback.groovy 文件是 Groovy 程序。因为 Groovy 是 Java 的超集,所以无论你在 Java 执行什么配置操作,你都可以在 logback.groovy 文件中做同样的事情。但是,在 Java 中,使用变成的方式配置 logback 有点笨重,所以我们增加了一些 logback 特有的扩展来减轻你的负担。我们尝试限制 logback 特有的拓展符号尽量的少。如果你已经熟悉了 Groovy,那么你应该更加容易去读,去理解甚至去写你自己的 logback.groovy 文件。那么不熟悉 Groovy 的人依然会发现 logback.groovy 中的语法比 logback.xml 中的语法更加容易使用。

logback.groovy 文件是 Groovy 程序,具有最小的 logback 特定的拓展。所有常用的 groovy 结构,例如类的导入,变量定义,字符串 (GString) 中包含 ${..} 评估表达式,以及 if-else 语句在 logback.grooby 文件中都是可用的。

自动导入

1.0.10 版本以后 为了减少不必要的引用,一些共同的类以及包会被自动导入。因此,只要你只是配置了内置的 appender,layout 等等,你不需要在你的脚本中添加相对应的导入语句。当然,对于默认导入不会涉及到类,你需要自己导入。

下面是默认导入的列表:

  • 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 中的所有常量 (大写) 都会被静态导入,以及小写的别名。也就是说在你的脚本中可以引用 INFO 以及 info,而不需要使用静态导入语句。

不再支持 SiftingAppender

1.0.12 版本以后 在 groovy 配置文件中不再支持 SiftingAppender。但是,如果有需要,可以重新引进。

logback.groovy 特定的拓展

本质上,logback.groovy 语法包含以下所说的六个方法;按照它们习惯上相反的顺序出现。严格来说,这些方法的调用顺序并重要,但是有一个例外:appender 附加到 logger 之前必须**被定义。

  • root(Level level, List<String> appenderNames = [])

root 方法可以用来设置 root logger 的日志级别。第二个可选参数的类型为 List<String>,可以用来添加之前定义的 appender 的名字。如果你不想指定 appenderNames,那么就是一个空 (empty) 的列表。在 Groovy 中,用 [] 表示一个空的列表。

设置 root logger 的级别为 WARN,你可以这样写:

root(WARN)

设置 root logger 的级别为 INFO,并且将名为 "CONSOLE" 与 "FILE" 的 appender 附加到 root 上,你可以这样写:

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

在前面的例子中,假设名为 "CONSOLE" 与 "FILE" 的 appender 已经被定义好了。很快将会讨论有关 appender 的定义。

  • logger(String name, Level level, List<String> appenderNames = [], Boolean additivity = null)

logger() 方法接收四个参数,最后两个是可选的。第一个参数表示配置 logger 的名字。第二参数表示指定 logger 的级别。设置 logger 的级别为 null 将强制它从它最近的祖先那里继承级别。第三个参数的类型为 List<String>,是可选的,默认为空列表。列表中 appender 会被附加到指定的 logger 上去。第四个参数的类型为 Boolean,也是可选的,用来控制叠加性。如果忽略,默认值为 null

例如,下面这个脚本设置 "com.foo" 这个 logger 的级别为 INFO:

logger("com.foo", INFO)

下个脚本设置 "com.foo" 这个 logger 的级别为 DEBUG,并且将名为 "CONSOLE" 的 appender 附加到其上:

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

下个脚本跟上一个类似,只是这个还设置了 "com.foo" 这个 logger 的叠加性为 false:

logger("com.foo", DEBUG, ["CONSOLE"],false)
  • appender(String name, Class clazz, Closure closure = null)

appender 方法的第一个参数接收 appender 的名字进行配置。第二个参数是强制的,表示 appender 实例化的类。第三个参数包含更多的配置信息。如果忽略,默认为 null。

大部分 appender 都需要设置属性,并且注入子组件才能正常工作。属性通过 '=' 进行设置。子组件的注入通过调用以属性命名的方法,并且将实例化的类作为参数传递给该方法。这个约定可以被递归的应用到配置的属性以及任何 appender 子组件的子组件中。这个方法是 logback.groovy 的核心,可能是唯一需要去学习的约定。

例如,接下来的脚本实例化一个 FileAppender 命名为 "FILE",设置它的 file 属性为 "testFile.log",以及它的 append 属性设置为 false。类型为 PatternLayoutEncoder 的 encoder 被注入到这个 appender 中。encoder 的模式属性设置为 "%level %logger - %msg%n"。然后将这个 appender 附加到 root logger 上。

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

root(DEBUG, ["FILE"])
  • timestamp(String datePattern, long timeReference = -1)

timestamp() 方法根据 datePatterntimeReference 参数格式化,返回一个对应的字符串。datePattern 参数应该尊村SimpleDateFormat中定义的约定。如果 timeReference 没有指定,那么默认为 -1。在这种情况下,当解析配置文件时,当前时间作为 timeReference 参数的值。

在下个例子中,bySecond 变量表示被 "yyyyMMdd'T'HHmmss" 格式化之后的当前时间。之后,"bySecond" 变量被用于 file 属性的定义中。

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

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

root(DEBUG, ["FILE"])
  • conversionRule(String conversionWord, Class converterClass)

在创建了你自己的转换说明符之后,你需要通知 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(String scanPeriod = null)

调用 scan() 方法告诉 logback 周期性的扫描 logback.groovy 文件的变化。当检测到变化时,logback.groovy 文件会被重新加载。

scan()

默认情况下,一分钟扫描一次配置文件。你可以通过 "scanPeriod" 来指定一个不同的扫描周期。它的值可以被指定以 milliseconds, seconds, minutes 或者 hours 位单位。例如:

scan("30 seconds")

如果没有指定时间单位,那么默认的时间单位为 milliseconds,但是通常来说是不合适的 (既然不合适,为什么默认还是毫秒,费解🤔)。如果你更改了默认的扫描周期,记得要指定时间单位。更多关于扫描工作的细节,请查看自动加载部分。

  • statusListener(Class listenerClass)

你可以通过调用 statusListener 方法,并给该方法传递一个监听器类,来添加一个状态监听器。例:

import chapters.layouts.MySampleConverter

// 强烈建议在最后一个导入语句之后,其它所有语句之前添加状态监听器
statusListener(OnConsoleStatusListener)

关于状态监听器请查看之前的章节。

  • jmxConfigurator(String name)

你可以通过该方法注册一个 JMXConfigurator MBean。无参调用将会使用 logback 默认的对象名 (ch.qos.logback.classic:Name=default,Type=ch.qos.logback.classic.jmx.JMXConfigurator) 去注册 MBean。

jmxConfigurator()

要改变 Name 键的值,而不是 "default",仅仅只需要给 jmxConfigurator 方法传递一个不同的名字参数就可以了。

jmxConfigurator('MyName')

如果你想要完整的定义对象名,可以使用同样的语法,但是需要传递一个有效的对象名字符串作为参数:

jmxConfigurator('myApp:type=LoggerManager')

该方法首先会去尝试将该参数作为对象名,如果它不表示一个有效的对象名,则会把它当作 "Name" 键的值。

内置 DSL

logback.groovy 是一个内置 DSL 的意思是,它的内容可以作为 Groovy 脚本执行。因此,所有常用的 Groovy 指令,例如类的导入,GString,变量的定义,包含字符串 (GString) 的 ${..} 评估表达式,if-else 语句这些在 logback.groovy 文件中都是可用的。在接下来的讨论中,我们将会展示 Groovy 指令在 logback.groovy 文件中的典型用法。

变量定义与 GString

你可以在 logback.groovy 文件中的任何地方定义变量,然后在 GString 中使用该变量。例如:

def USER_HOME = System.getProperty("user.home")

appender("FILE", FileAppender) {
    // 使用 USER_HOME 变量
    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' 变量

'hostname' 变量包含当前 host 的名字。但是由于作用域规则,所以作者不能完全解释清楚 (😓)。'hostname' 变量只在最上层的作用域中有效,但是在内部的作用域中无效。下面的例子应该可以解释这一点:

// 如果当前 host 的名字为 x,那么将会输出 "hostname is x"
println "hostname is ${hostname}"

appender("STDOUT", ConsoleAppender) {
    // 将会输出 "hostname is null"
    println "hostname is ${hostname}"
}

如果你想要在所有的作用域中使用 hostname 变量。那么你需要定义一个变量,并将 'hostname' 的值赋给它。如下:

// 将 hostname 的值赋给 HOSTNAME
def HOSTNAME = hostname

// 如果当前 host 的名字为 x,那么将会输出 "hostname is x"
println "hostname is ${HOSTNAME}"

appender("STDOUT", ConsoleAppender) {
    // 如果当前 host 的名字为 x,那么将会输出 "hostname is x"
    println "hostname is ${HOSTNAME}"
}

任何对于当前上下文的引用都是上下文感知的

logback.groovy 脚本是在 ContextAware 对象的范围内执行完成的。因此,在当前上下文的范围内,你可以使用 'context',并且可以通过 addInfo()addWarn()、与 addError() 方法将状态信息发送给上下文的 StatusManager

// 添加一个控制台转态监听器总是没错的
statusListener(OnConsoleStatusListener)

// 设置上下文的名字为 wombat
context.name = 'wombat'

// 添加一个关于上下文名字的状态信息
addInfo("Context name has been set to ${context_name}")

def USER_HOME = System.getProperty("user.home")

// 添加关于 USRE_HOME 的状态信息
addInfo("USER_HOME=${USER_HOME}")

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

条件配置

由于 Groovy 是一种完全成熟的编程语言,条件语句允许单一的 logback.groovy 文件用来适用不同的环境,例如开发,测试以及生产。

在下个脚本中,console appender 根据 host 来激活,而不是我们的生产环境 pixie 或 orion。rolling file appender 的输出目录也是根据 host 来确定。

statusListener(OnConsoleStatusListener)

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

// hostname 是否匹配 pixie 或 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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容