Chapter 12: Groovy Configuration
It is better to be a human being dissatisfied than a pig satisfied; better to be a Socrates dissatisfied than a fool satisfied. And if the fool or the pig thinks otherwise, it is because they have no experience of the better part.
—JOHN STUART MILL, Utilitarianism
Domains-specific languages or DSLs are rather pervasive. The XML-based logback configuration can be viewed as a DSL instance. By the very nature of XML, XML-based configuration files are quite verbose and rather bulky. Moreover, a relatively large body of code in logback, namely Joran, is dedicated to processes these XML-based configuration files. Joran supports nifty features such as variable substitution, conditional processing and on-the-fly extensibility. However, not only Joran is a complex beast, the user-experience it provides can be described as unsatisfactory or at the very least unintuitive.
The Groovy-based DSL described in this chapter aims to be consistent, intuitive, and powerful. Everything you can do XML in configuration files, you can do in Groovy with a much shorter syntax. To help you migrate to Groovy style configuration, we have developped a tool to automatically migrate your logback.xml files to logback.groovy.
General philosophy
As a general rule, logback.groovy files are groovy programs. And since groovy is a super-set of Java, whatever configuration actions you can perform in Java, you can do the same within a logback.groovy file. However, since configuring logback progammatically using Java syntax can be cumbersome, we added a few logback-specific extensions to make your life easier. We try hard to limit the number of logback-specific syntactic extensions to an absolute minimum. If you are already familiar with groovy, you should be able to read, understand and even write your own logback.groovy files with great ease. Those unfamiliar with Groovy should still find logback.groovy syntax much more comfortable to use than logback.xml.
Given that logback.groovy files are groovy programs with minimal logback-specific extensins, all the usual groovy constructs such as class imports, variable definitions, if-else statements are avaiable in logback.groovy files.
Logback.groovy syntax consists of half a dozen methods described next in the reverse order of their customary appearance. Strictly speaking, the order of invocation of these methods does NOT matter, with one exception: appenders MUST be defined before they can be attached to a logger.
• root(Level level, List<String> appenderNames = [])
The root
method can be used to set the level of
the root logger. As an optional second argument of type
List<String>
, can be used to attach previously
defined appenders by name. If you do not specify the list of
appender names, then an empty list is assumed. In groovy, an empty
list is denoted by []
.
To set the level of the root logger to WARN, you would write:
import static ch.qos.logback.classic.Level.WARN root(WARN)
To set the level of the root logger to DEBUG, and attach appenders named "CONSOLE" and "FILE" to root, you would write:
import static ch.qos.logback.classic.Level.INFO root(INFO, ["CONSOLE", "FILE"])
In the previous example, it is assumed that the appenders named "CONSOLE" and "FILE" were already defined. Defining appenders will be discussed shortly.
• logger(String name, Level level, List<String> appenderNames = [],
Boolean additivity = null)
The logger()
method takes four arguments, of which
the last two are optional. The first argument is the name of the
logger to configure. The second argument is the level of the
designated logger. Setting the level of a logger to
null
forces it to inherit its level from
its nearest ancestor with an assigned level. The third argument of
type List<String>
is optional and defaults to an
emtpy list if omitted. The appender names in the list are attached
to the designated logger. The forth argument of type
Boolean
is also optional and controls the additivity flag. If
omitted, it defaults to null
.
For example, the following script set the level of the "com.foo" logger to INFO.
import static ch.qos.logback.classic.Level.INFO logger("com.foo", INFO)
The next script sets the level of the "com.foo" logger to DEBUG, and attaches the appender named "CONSOLE" to it.
import static ch.qos.logback.classic.Level.DEBUG logger("com.foo", DEBUG, ["CONSOLE"])
The next script is similiar to the previous one, except that it also sets the the additivity flag of the "com.foo" logger to false.
import static ch.qos.logback.classic.Level.DEBUG logger("com.foo", DEBUG, ["CONSOLE"], false)
• appender(String name, Class clazz, Closure closure = null)
The appender method takes the name of the appender being configured as its first argument. The second mandatory argument is the class of the appender to instantiate. The third argument is a closure containing further configuration instructions. If omitted, it defaults to null.
Most appenders require properties to be set and sub-components to be injected to function properly. Properties are set using the '=' operator (assignment). Sub-components are injected by invoking a method named after the property and passing that method the class to instantiate as an argument. This convention can be applied recursively to configure properties as well as sub-components of any appender sub-component.
For example, the following script instantiates a
FileAppender
named "FILE", setting its file property to "testFile.log" and its
append property to false. An encoder
of type PatternLayoutEncoder
is injected into the
appender. The pattern property of the encoder is set to "%level
%logger - %msg%n". The appender is then attached to the root
logger.
import ch.qos.logback.classic.encoder.PatternLayoutEncoder import ch.qos.logback.core.FileAppender import static ch.qos.logback.classic.Level.DEBUG appender("FILE", FileAppender) { file = "testFile.log" append = true encoder(PatternLayoutEncoder) { pattern = "%level %logger - %msg%n" } } root(DEBUG, ["FILE"])
• conversionRule(String conversionWord, Class converterClass)
After creating your own conversion
specifier, you need to inform logback of its existence. Here
is a sample logback.groovy file which instructs logback to use
MySampleConverter whenever the %sample
conversion
word is encountered.
import ch.qos.logback.classic.encoder.PatternLayoutEncoder import ch.qos.logback.core.ConsoleAppender import chapters.layouts.MySampleConverter import static ch.qos.logback.classic.Level.DEBUG conversionRule("sample", MySampleConverter) appender("STDOUT", ConsoleAppender) { encoder(PatternLayoutEncoder) { pattern = "%-4relative [%thread] %sample - %msg%n" } } root(DEBUG, ["STDOUT"])
• scan(String scanPeriod = null)
Invoking the scan() method instructs logback to periodically scan the logback.groovy file for changes. Whenever a change is detected, the logback.groovy file is reloaded.
scan()
By default, the configuration file will be scanned for changes once every minute. You can specify a different scanning period by passing a "scanPeriod" string value. Values can be specified in units of milliseconds, seconds, minutes or hours. Here is an example:
scan("30 seconds")
If no unit of time is specified, then the unit of time is assumed to be milliseconds, which is usually inappropriate. If you change the default scanning period, do not forget to specify a time unit. For additional details on how scanning works, please refer to the section on automatic reloading.
• statusListener(Class listenerClass)
You can add a status listener by invoking the
statusListener
method and passing a listener class as
an argument. Here is an example:
import ch.qos.logback.core.status.OnConsoleStatusListener statusListener(OnConsoleStatusListener)
Status listeners were described in an earlier chapter.