Chapter 3: Logback configuration & Joran
In symbols one observes an advantage in discovery which is greatest when they express the exact nature of a thing briefly and, as it were, picture it; then indeed the labor of thought is wonderfully diminished.
—GOTTFRIED WILHELM LEIBNIZ
Joran stands for a cold north-west wind which, every now and then, blows forcefully on Lake Geneva. Located right in the middle of Europe, the Geneva lake happens to be the continent's largest sweet water reserve.
In the first part, we start by presenting ways for configuring logback, with many example configuration scripts. In the second part, we present Joran, a generic configuration framework, which you can put into use in order to configure your own applications.
Configuration in logback
Inserting log requests into the application code requires a fair amount of planning and effort. Observation shows that approximately four percent of code is dedicated to logging. Consequently, even moderately sized applications will contain thousands of logging statements embedded within its code. Given their number, we need tools to manage these log statements.
Logback can be configured either programmatically or with configuration a script (expressed in XML format). By the way, existing log4j users can convert their log4j.properties files to logback.xml using our PropertiesTranslator web-application.
Let us begin by discussing the initialization steps that logback follows to try to configure itself:
-
Logback tries to find a file called logback-test.xml in the classpath.
If no such file is found, it checks for the file logback.xml.
In case neither file is found, logback configures itself automatically using the
BasicConfigurator
which will cause logging output to be directed on the console.
The third and last step is meant to provide a default (but very basic) logging functionnality in the absence of a configuration file.
Assuming the logback-test.xml file is placed under src/test/resources folder, Maven will ensure that it won't be included in the artifact produced. Thus, you can use a different configuration file, namely logback-test.xml during testing, and another file, namely, logback.xml, in production. The same principle applies by analogy for Ant.
Automatically configuring logback
The simplest way to configure logback is by letting logback
fallback to its default configuration. Let us give a taste of how
this is done in an imaginary application called
MyApp1
.
BasicConfigurator
usage (logback-examples/src/main/java/chapter3/MyApp1.java)
package chapter3; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyApp1 { final static Logger logger = LoggerFactory.getLogger(MyApp1.class); public static void main(String[] args) { logger.info("Entering application."); Foo foo = new Foo(); foo.doIt(); logger.info("Exiting application."); } }
This class defines a static logger variable. It then instantiates a Foo object. The Foo class is listed below:
Example 3.: Small class doing logging (logback-examples/src/main/java/chapter3/Foo.java)package chapter3; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Foo { static final Logger logger = LoggerFactory.getLogger(Foo.class); public void doIt() { logger.debug("Did it again!"); } }
In order to run the examples in this chapter, you need to make sure that certain jar files are present on the classpath. Please refer to the setup page for further details.
Assuming the configuration files logback-test.xml or
logback.xml could not be found, logback will default to a
minimal configuration mentioned earlier. This configuration is
hardwired to attaching a ConsoleAppender
to the root
logger. The output is formatted using a
PatternLayout
set to the pattern %d{HH:mm:ss.SSS}
[%thread] %-5level %logger{36} - %msg%n. Moreover, by default
the root logger is assigned to the DEBUG
level.
Thus, the output of the command java chapter3.MyApp1 should be similar to:
16:06:09.031 [main] INFO chapter3.MyApp1 - Entering application. 16:06:09.046 [main] DEBUG chapter3.Foo - Did it again! 16:06:09.046 [main] INFO chapter3.MyApp1 - Exiting application.
The MyApp1
application links to logback via
calls org.slf4j.LoggerFactory
and
org.slf4j.Logger
classes, retrieve the loggers it
wishes to use, and chugs on. Note that the only dependence of the
Foo
class on logback are through
org.slf4j.LoggerFactory
and
org.slf4j.Logger
imports. Except code that configures
logback (if such code exists) client code does not need to depend
on logback. Given that SLF4J permits the use of any implementation
under its abstraction layer, it is rather easy to migrate large
bodies of code from one logging system to another.
Automatic configuration with logback-test.xml or logback.xml
As mentioned earlier, logback will try to configure itself using
the files logback-test.xml or logback.xml if
found on the classpath. Here is a configuration file equivalent to
the one established by BasicConfigrator
we've just
seen.
Example 3.: Basic configuration file (logback-examples/src/main/java/chapter3/sample0.xml)
<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern> </layout> </appender> <root level="debug"> <appender-ref ref="STDOUT" /> </root> </configuration>
After you have renamed sample0.xml as logback.xml (or logback-test.xml) place it into a directory accesible from the classpath. Running the MyApp1 application should give identical results to its previous run.
If and only if there are errors during the parsing of the
configuration file, logback will automatically print status data on
the console. In the absence of errors, if you still wish to inspect
logback's internal status, then you can instruct logback to print
status data by invoking the print()
of the
StatusPrinter
class. The MyApp2 application
shown below is identical to MyApp1 except the addition of
two lines of code for printing internal status data.
public static void main(String[] args) { logger.info("Entering application."); // print logback's internal status LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); StatusPrinter.print(lc); ... }
If everything goes well, you should see the following output on the console
17:44:58,578 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback-test.xml] 17:44:58,671 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set 17:44:58,671 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender] 17:44:58,687 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT] 17:44:58,812 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Popping appender named [STDOUT] from the object stack 17:44:58,812 |-INFO in ch.qos.logback.classic.joran.action.LevelAction - root level set to DEBUG 17:44:58,812 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[root] 17:44:58.828 [main] INFO chapter3.MyApp2 - Entering application. 17:44:58.828 [main] DEBUG chapter3.Foo - Did it again! 17:44:58.828 [main] INFO chapter3.MyApp2 - Exiting application.
At the end of this output, you can recognize the lines that were
printed in the previous example. You should also notice the
logback's internal messages, a.k.a. Status
objects,
which allow convient access to logback's internal state.
Instead of invoking StatusPrinter
programmatically
from your code, you can instruct the configuration file to dump
status data, even in the absence of errors. To achieve this, you
need to set the debug attribute of the
configuration element, i.e. the top-most element in the
configuration file, as shown below. Please note that this debug attribute relates only to the status
data. It does not affect logback's configuration
otherwise, in particuler with respect to logger levels. (Put
differently, no, the root logger will not be set to
DEBUG
.)
<configuration debug="true"> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern> </layout> </appender> <root level="debug"> <appender-ref ref="STDOUT" /> </root> </configuration>
Setting the debug
attribute within the
<configuration> element will output status information, under
the assumption that
- the configuration file is found
- the configuration file is well-formed XML.
If any of these two conditions is not fulfilled, the Joran
cannot interpret debug
attribute since the
configuration file cannot be read. If the configuration file is
found but is ill-formed, then logback will detect the error
condition and automatically print its internal status on the
console. However, if the configration file cannot be found, since
this is not necessarily an error condition, logback will not
automatically print its status data. Programmatically invoking
StatusPrinter.print()
, as as in MyApp2
application above, ensures that status information is always
printed.
Invoking JoranConfigurator
directly
Logback relies on a configuration library called Joran which is part of logback-core. Logback's default configuration mechanism invokes JoranConfigurator on the default configuration files it finds on the claspath. For whatever reason if you wish to override logback's default configuration meachanism, you can do so by invoking JoranConfigurator directly. The next application, MyApp3, invokes JoranConfirator on a configuration file passed as parameter.
Example 3.: Invoking
JoranConfigurator
directly (logback-examples/src/main/java/chapter3/MyApp3.java)
package chapter3; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.util.StatusPrinter; public class MyApp3 { final static Logger logger = LoggerFactory.getLogger(MyApp3.class); public static void main(String[] args) { // assume logback is in use LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); try { JoranConfigurator configurator = new JoranConfigurator(); configurator.setContext(lc); // the context was probably already configured by default configuration // rules lc.shutdownAndReset(); configurator.doConfigure(args[0]); } catch (JoranException je) { je.printStackTrace(); } StatusPrinter.printIfErrorsOccured(lc); logger.info("Entering application."); Foo foo = new Foo(); foo.doIt(); logger.info("Exiting application."); } }
This application fetches the LoggerContext
currently
in effect, creates a new JoranConfigurator
, sets the
context on which it will operate, resets the logger context, and
then finally asks the configurator to configure the context using
configuration file passed as parameter to the application. Internal
status data is printed in case errors occur.
Configuration file Syntax
To obtain these different logging behaviors we do not need to recompile code. You can easily configure logback so as to disable logging for certain parts of your application, or direct output to a UNIX Syslog daemon, to a database, to a log visualizer, or forward logging events to a remote logback server, which would log according to local server policy, for example by forwarding the log event to a second logback server.
The remainder of this section presents the syntax of configuration files.
As shall become clear, the syntax of logback configuration files is extremely flexible. As such, it is not possible specify the allowed syntax with a DTD file or an XML Schema. Nevertheles, the very basic structure of configration can be desribed as, <configuration> element, followed by zero or more <appender> elements, followed by by zero or more <logger> elements, followed by at most one <root> element. The following diagram illustrates this basic structure.
Configuring Loggers, or the <logger>
element
A logger is configured using the logger
element. A
logger element takes exactly one mandatory name atttribute, an optional level attribute, and an optional aditivity attribute, which admits the values
true or false. The value of the level attribute can be one of the
case-insensitive strings TRACE, DEBUG, INFO, WARN, ERROR, ALL or
OFF. The special case-insensitive value INHERITED, or its
synonym NULL, will force the level of the logger to be
inherited from higher up in the hierarchy. This comes in handy in
case you set the level of a logger and later decide that it should
inherit its level.
The logger element may contain zero or more appender-ref elements; each appender thus referenced is added to the named logger. It is important to keep mind that each named logger that is declared with a <logger element first has all its appenders removed and only then are the referenced appenders attached to it. In particular, if there are no appender references, then the named logger will lose all its appenders.
Configuring the roor logger, or the <root>
element
The <root> element configures the root logger. It admits a single attribute, namely the level attribute. It does not admit any other attributes because the additivity flag does not apply to the root logger. Moreover, since the root logger is already named, it does not admit a name attribute either. The value of the level attribute can be set to one of the case-insensitive strings TRACE, DEBUG, INFO, WARN, ERROR, ALL or OFF. Note that the level of the root logger cannot be inherited.
The <root> element admits zero or more <appender-ref> elements. Similar to the <logger element, declaring a <root element will have the effect of first closing and then detaching all its current appenders and only subsequently will referenced appenders, if any, will be added. In particular, if it has no appender references, then the root logger will lose all its appenders.
Example
Setting the level of a logger or root logger is as simple as declaring it and setting its level, as the next example illustrates. Suppose we are no longer interested in seeing any DEBUG messages from any component belonging to the "chapter3" package. The following configuration file shows how to achieve that.
Example 3.: Setting the level of a logger (logback-examples/src/main/java/chapter3/sample2.xml)<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern> </layout> </appender> <logger name="chapter3" level="INFO"/> <!-- Strictly speaking, the level attribute is not necessary since --> <!-- the level of the root level is set to DEBUG by default. --> <root level="DEBUG"> <appender-ref ref="STDOUT" /> </root> </configuration>
This new configuration will yield the following output, when invoked with the MyApp3 application.
17:34:07.578 [main] INFO chapter3.MyApp3 - Entering application. 17:34:07.578 [main] INFO chapter3.MyApp3 - Exiting application.
You can configure the levels of as many loggers as you wish. In
the next configuration file, we set the level of the
chapter3 logger to INFO but at the same time set the level
of the chapter3.Foo logger to DEBUG
.
<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern> %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n </Pattern> </layout> </appender> <logger name="chapter3" level="INFO" /> <logger name="chapter3.Foo" level="DEBUG" /> <root level="DEBUG"> <appender-ref ref="STDOUT" /> </root> </configuration>
Running MyApp3
with this configuration file will
result in the following output on the console:
17:39:27.593 [main] INFO chapter3.MyApp3 - Entering application. 17:39:27.593 [main] DEBUG chapter3.Foo - Did it again! 17:39:27.593 [main] INFO chapter3.MyApp3 - Exiting application.
The table below list the loggers and their levels, after
JoranConfigurator
has configured logback with the
sample3.xml configuration file.
Logger name | Assigned Level | Effective Level |
---|---|---|
root | DEBUG |
DEBUG |
chapter3 | INFO |
INFO |
chapter3.MyApp3 | null |
INFO |
chapter3.Foo | DEBUG |
DEBUG |
It follows that the two logging statements of level
INFO
in the MyApp3
class as well as the
DEBUG messages in Foo.doIt()
are all enabled. Note that
the level of the root logger is always set to a non-null value,
which is DEBUG by default.
Let us note that the basic-selection rule depends on the effective level of the logger being invoked, not the level of the logger where appenders are attached. Loback will first determine wheteher a logging statement is enabled or not, and if enabled, it will invoke the appenders found in the logger hierarchy, regardless of their level. The configuration file sample4.xml is a case in point:
Example 3.: Logger level sample (logback-examples/src/main/java/chapter3/sample4.xml)<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern> %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n </Pattern> </layout> </appender> <logger name="chapter3" level="INFO" /> <!-- turn OFF all logging (children can override) --> <root level="OFF"> <appender-ref ref="STDOUT" /> </root> </configuration>
The following table lists the loggers and their levels after applying the sample4.xml configuration file.
Logger name | Assigned Level | Effective Level |
---|---|---|
root | OFF |
OFF |
chapter3 | INFO |
INFO |
chapter3.MyApp3 | null |
INFO |
chapter3.Foo | null |
INFO |
The ConsoleAppender named STDOUT, the only configured
appender in sample4.xml, is attached to the root logger
whose level is set to OFF
. However, running
MyApp3 with configuration script sample4.xml will
yield:
17:52:23.609 [main] INFO chapter3.MyApp3 - Entering application. 17:52:23.609 [main] INFO chapter3.MyApp3 - Exiting application.
Thus, the level of the root logger has no apparent effect because
the loggers in chapter3.MyApp3
and
chapter3.Foo
classes are all enabled for the
INFO
level. As a side note, the chapter3
logger exists by virtue of its declaration in the configuration file
- even if the Java source code does not directly refer to it.
Configuring Appenders
Appenders are configured using <appender>
elements, taking two attributes name and
class, both of which are mandatory. The
name attribute specifies the name of the
appender whereas the class attribute
specifies the fully qualified name of the class of which the named
appender will be an instance. The <appender>
element may contain zero or one <layout>
elements
and zero or more <filter>
elements. Appart from
these two common elements, <appender>
elements may
contain any number of element corresponding to javabean properties
of the appender class. Seamlessly supporting any property of a given
logback component is one of the major strengths of Joran. The
following diagram illustrates the common structure. Note that
support for properties is not visible.
The <layout>
element takes a mandatory class
attribute specifying the fully qualified name of the class of which
the associated layout should be an instance. Like the
<appender>
element, it may contain other elements
corresponding to properties of the layout class.
Logging to multiple appenders is as easy as defining the various appenders and referencing them in a logger, as the next configuration file illustrates:
Example 3.: Multiple loggers (logback-examples/src/main/java/chapter3/multiple.xml)<configuration> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>myApp.log</file> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</Pattern> </layout> </appender> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%msg%n</Pattern> </layout> </appender> <root> <level value="debug" /> <appender-ref ref="FILE" /> <appender-ref ref="STDOUT" /> </root> </configuration>
This configuration scripts defines two appenders called
FILE and STDOUT. The FILE appender logs
to a file called myApp.log. The layout for this appender is
a PatternLayout
that outputs the date, level, thread
name, logger name, file name and line number where the log request
is located, the message and line separator character(s). The second
appender called STDOUT
outputs to the console. The
layout for this appender outputs only the message string followed by
a line separator.
The appenders are attached to the root logger by referencing them by name within an appender-ref element. Note that each appender has its own layout. Layouts are usually not designed to be shared by multiple appenders. As such, logback configuration files do not provide any syntactical means for sharing layouts.
By default, appenders are cumulative: a logger will log to the appenders attached to itself (if any) as well as all the appenders attached to its ancestors. Thus, attaching the same appender to multiple loggers will cause logging output to be duplicated.
Example 3.: Duplicate appender (logback-examples/src/main/java/chapter3/duplicate.xml)<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern> </layout> </appender> <logger name="chapter3"> <appender-ref ref="STDOUT" /> </logger> <root> <level value="debug" /> <appender-ref ref="STDOUT" /> </root> </configuration>
Running MyApp3
with duplicate.xml will
yield the following output:
14:25:36.343 [main] INFO chapter3.MyApp3 - Entering application. 14:25:36.343 [main] INFO chapter3.MyApp3 - Entering application. 14:25:36.359 [main] DEBUG chapter3.Foo - Did it again! 14:25:36.359 [main] DEBUG chapter3.Foo - Did it again! 14:25:36.359 [main] INFO chapter3.MyApp3 - Exiting application. 14:25:36.359 [main] INFO chapter3.MyApp3 - Exiting application.
Notice the duplicated output. The appender named STDOUT is attached to two loggers, to root and to chapter3. Since the root logger is the ancestor of all loggers and chapter3 is the parent of chapter3.MyApp3 and chapter3.Foo, logging request made with these two loggers will be output twice, once because STDOUT is attached to chapter3 and once because it is attached to root.
Appender additivity is not intended as a trap for new users. It is a quite convenient logback feature. For instance, you can configure logging such that log messages appear on the console (for all loggers in the system) while messages only from some specific set of loggers flow into a specific appender.
Example 3.: Multiple appender (logback-examples/src/main/java/chapter3/restricted.xml)<configuration> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>myApp.log</file> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</Pattern> </layout> </appender> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%msg%n</Pattern> </layout> </appender> <logger name="chapter3"> <appender-ref ref="FILE" /> </logger> <root> <level value="debug" /> <appender-ref ref="STDOUT" /> </root> </configuration>
In this example, the console appender will log all the messages (for all loggers in the system) whereas only logging request originating from loggers chapter3 and below go into the myApp.log file.
Overriding the default cumulative behaviour
In case the default cumulative behavior turns out to be unsuitable for your needs, you can override it by setting the additivity flag to false. Thus, a branch in your logger tree may direct output to a set of appenders different than those of the rest of the tree.
Example 3.: Additivity flag (logback-examples/src/main/java/chapter3/additivityFlag.xml)<configuration> <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>foo.log</file> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%date %level [%thread] %logger{10} [%file : %line] %msg%n</Pattern> </layout> </appender> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>%msg%n</Pattern> </layout> </appender> <logger name="chapter3.Foo" additivity="false"> <appender-ref ref="FILE" /> </logger> <root> <level value="debug" /> <appender-ref ref="STDOUT" /> </root> </configuration>
This example, the appender named FILE is attached to the
chapter3.Foo logger. Moreover, the chapter3.Foo
logger has its additivity flag set to false such that its logging
output will be sent to the appender named FILE but not to
any appender attached higher in the hierarchy. Other loggers remain
oblivious to the additivity setting of the chapter3.Foo
logger. Running the MyApp3
application with the
additivityFlag.xml configuration file will output results
on the console from the chapter3.MyApp3 logger. However,
output from the chapter3.Foo logger will appear in the
foo.log file and only in that file.
Variable substitution
All option values admit variable substitution. The syntax of variable substitution is similar to that of Unix shells. The string between an opening ${ and closing } is interpreted as a key. The value of the substituted variable can be defined as a system property in the configuration file itself or in a separate file linked to the configuration file. The value of the key is first searched in configuration file or linked properties file, and if not found there, it is then searched in system properties. The corresponding value replaces ${aKey} sequence. For example, if java.home.dir system property is set to /home/xyz, then every occurrence of the sequence ${java.home.dir} will be interpreted as /home/xyz.
The first example shows a declared property at the beginning of the configuration file. It is then used further down the file to specify the place to create the output file.
Example 3.: Simple Variable substitution (logback-examples/src/main/java/chapter3/variableSubstitution1.xml)<configuration> <substitutionProperty name="user.home.dir" value="/Users/seb" /> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${user.home.dir}/myApp.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern> myApp-%d{yyyy-MM-dd-HH}.log </FileNamePattern> </rollingPolicy> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern> %date %level [%thread] %logger{10} [%file : %line] %msg%n </Pattern> </layout> </appender> <root> <level value="debug" /> <appender-ref ref="FILE" /> </root> </configuration>
The next example shows the use of a System property to achieve the same result. The property is not declared anywhere, thus logback will look for it in the System properties.
Example 3.: System Variable substitution (logback-examples/src/main/java/chapter3/variableSubstitution2.xml)<configuration> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${user.home.dir}/myApp.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern> myApp-%d{yyyy-MM-dd-HH}.log </FileNamePattern> </rollingPolicy> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern> %date %level [%thread] %logger{10} [%file : %line] %msg%n </Pattern> </layout> </appender> <root> <level value="debug" /> <appender-ref ref="FILE" /> </root> </configuration>
When many variables are used, it is often more convenient to create a separate file that will contain all the variables. Here is how one can do such a setup.
Example 3.: Variable substitution using a separate file (logback-examples/src/main/java/chapter3/variableSubstitution3.xml)<configuration> <substitutionProperty file="variables1.properties" /> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${user.home.dir}/myApp.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern> myApp-%d{yyyy-MM-dd-HH}.log </FileNamePattern> </rollingPolicy> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern> %date %level [%thread] %logger{10} [%file : %line] %msg%n </Pattern> </layout> </appender> <root> <level value="debug" /> <appender-ref ref="FILE" /> </root> </configuration>
This configuration file contains a link to another file called variables1.properties. The variables contained in that other file will be read and will thus be available to the logback configuration file. Here is what the variable.properties file looks like.
Example 3.: Variable file (logback-examples/src/main/java/chapter3/variables1.properties)user.home.dir=/Users/seb
Nothing more is needed to declare the variable.
Recursive subsitution is also available. If the user wants to use variables to specify not only the destination directory but also the file name, here is what she would write in her variables1.properties file.
Example 3.: Recursive use of variables (logback-examples/src/main/java/chapter3/variables2.properties)user.home.dir=/Users/seb fileName=myApp.log destination=${user.home.dir}/${fileName}
In the configuration file, only the last variable, ${destination} will be used, as shown below:
Example 3.: Variable substitution using a separate file (logback-examples/src/main/java/chapter3/variableSubstitution4.xml)<configuration> <substitutionProperty file="variables1.properties" /> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${destination}</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern> myApp-%d{yyyy-MM-dd-HH}.log </FileNamePattern> </rollingPolicy> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern> %date %level [%thread] %logger{10} [%file : %line] %msg%n </Pattern> </layout> </appender> <root> <level value="debug" /> <appender-ref ref="FILE" /> </root> </configuration>
File inclusion
It is possible to include configuration elements into a logback.xml configuration file. This is done by using a include element, as shown in the example below:
Example 3.: File include (logback-examples/src/main/java/chapter3/redirectConfig.xml)<configuration> <include file="path/to/configuration/file"/> </configuration>
The target file must have its elements nested inside an
included element. For example, a
ConsoleAppender
would be declared this way:
<included> <appender name="redirectConsole" class="ch.qos.logback.core.ConsoleAppender"> <layout class="ch.qos.logback.classic.PatternLayout"> <param name="Pattern" value="%d - %m%n" /> </layout> </appender> <root> <level value="DEBUG" /> <appender-ref ref="redirectConsole" /> </root> </included>
The file to be included can be referenced as a URL or as a
resource. To reference a URL use the url
attribute
instead of the file attribute in the previous example. To reference
a resource, use the resource
attribute instead of the
file attribute in the previous example.
Using Joran in your own application
As we've seen, logback relies on Joran, a mature, flexible and powerful configuration framework. Many of the capabilities offered by logback modules are possible with the help of Joran.
Joran is actually a generic configuration system which can be used independently of logging. To emphaises this point, we should mention that the logback-core module does not have a notion of loggers. In that spirit, many of the examples related to this tutorial, have nothing to do with loggers, appenders or layouts.
The examples for this chapter can be found under LOGBACK_HOME/logback-examples/src/main/java/chapter3.
To install joran, simply download logback and add logback-core-VERSION.jar to your classpath.
Historical perspective
One of the most powerful features of the Java language is reflection. Reflection makes it possible to configure software systems declaratively. For example, many important properties of an EJB are configured with the ejb.xml file. While EJBs are written in Java, many of their properties are specified within the ejb.xml file. Similarly, logback settings can be specified in a configuration file, expressed in XML format.
In log4j, logback's predecessor, DOMConfigurator
that shipped with log4j version 1.2.x can parse configuration files
written in XML. The DOMConfigurator
was written in a
way that forced to tweak it each time the structure of the
configuration file changed. The modified code had to be recompiled
and redeployed. Just as importantly, the code of the DOMConfigurator
consists of loops dealing with children elements containing many
interspersed if/else statements. One could not help but notice that
that particular code reeked of redundancy. The digester
project has shown that it is possible to parse XML files using
pattern matching rules. At parse time, digester will apply the rules
that match previously stated patterns. Rule classes are usually
quite small and specialized. Consequently, they are relatively easy
to understand and to maintain.
Joran is heavily inspired by the commons-digester project but
uses a slightly different terminology. In commons-digester, a rule
can be seen as consisting of a pattern and a rule, as shown by the
Digester.addRule(String pattern, Rule rule)
method. We
find it unnecessarily confusing to have a rule to consist of itself,
not recursively but with a different meaning. In Joran, a rule
consists of a pattern and an action. An action is invoked when a
match occurs for the corresponding pattern. This relation between
patterns and actions lies at the core of Joran. Quite remarkably,
one can deal with quite complex requirements by using simple
patterns, or more precisely with exact matches and wildcard
matches. For example, the pattern a/b will match a
<b>
element nested within an <a>
element but not a <c>
element, even if nested within
a <b>
element. It is also possible to match a
particular XML element, regardless of its nesting level, by using
the * wildcard character. For example, the pattern
*/a will match an <a>
element at any
nesting position within the document. Other types of patterns, for
example a/*, are not currently supported by Joran.
SAX or DOM?
Due to the event-based architecture of the SAX API, a tool based on SAX cannot easily deal with forward references, that is, references to elements which are defined later than the current element being processed. Elements with cyclical references are equally problematic. More generally, the DOM API allows the user to perform searches on all the elements and make forward jumps.
This extra flexibility initially led us to choose the DOM API as the underlying parsing API for Joran. After some experimentation, it quickly became clear that dealing with jumps to distant elements while parsing the DOM tree did not make sense when the interpretation rules were expressed in the form of patterns and actions. Joran only needs to be given the elements in the XML document in a sequential, depth-first order.
Joran was first implemented in DOM. However, the author migrated
to SAX in order to benefit form the location information provided to
the user, that is, to an
org.w3.sax.ContentHandler
. With the help of location
information, it becomes possible to display essential error reports
to the user which include exact line and column. This extra
information turns out to be handy in hunting down problems.
Actions
Actions extend the
ch.qos.logback.core.joran.action.Action
class which
consists of the following abstract methods.
package ch.qos.logback.core.joran.action; import org.xml.sax.Attributes; import ch.qos.logback.core.joran.spi.ExecutionContext; public abstract class Action { /** * Called when the parser first encounters an element. */ public abstract void begin(ExecutionContext ec, String name, Attributes attributes); /** * Called when the parser encounters the element end. At * this stage, we can assume that child elements, if any, * have been processed. */ public abstract void end(ExecutionContext ec, String name); }
Thus, every action must implement the begin and end methods.
Execution context
To allow various actions to collaborate, the invocation of begin
and end methods include an execution context as the first
parameter. The execution context includes an object stack, an object
map, an error list and a reference to the Joran interpreter invoking
the action. Please see the
ch.qos.logback.core.joran.spi.ExecutionContext
class
for the exact list of fields contained in the execution context.
Actions can collaborate together by fetching, pushing or popping
objects from the common object stack, or by putting and fetching
keyed objects on the common object map. Actions can report any error
conditions by adding error items on the execution context's
StatusManager
.
A hello world example
The logback-examples/src/main/java/chapter3/helloWorld/ directory includes a trivial action and Joran interpreter setup which just displays Hello World when a <hello-world> element is encountered in an XML file. It also includes the basic steps which are necessary to set up and invoke a Joran interpreter.
The hello.xml file contains only one element, without
any other nested elements. The
HelloWorldAction
class is a trivial implementation:
it only prints "Hello World" in the console when it's
begin()
method is called.
HelloWorld
is a class that sets up the Joran interpreter, with the minimal
steps necessary:
- It creates a
RuleStore
and aContext
- It adds the hello-world pattern, with it's corresponding action
- It creates a Joran interpreter, and passes the
RuleStore
- It creates a SAX parser and parses the given file, specifying the newly created
Joran interpreter as the
ContentHandler
It's last step is to print the content of the
Context
. Since Joran uses logback's powerfull
Status
objects for error reporting, one can have a good
feedback on what happened during the parsing.
In this example, the parsing is rather simple. The
hello-world element will activate
HelloWorldAction
's begin()
and
end()
methods. In the first method, a simple call to
System.out.println()
will be issued, displaying
Hello World in the console.
Collaborating actions
The logback-examples/src/main/java/joran/calculator/ directory includes several actions which collaborate together through the common object stack in order to accomplish simple computations.
The calculator1.xml file contains a computation
element, with a nested literal
element.
In the
Calculator1
class, we declare various patterns and
actions, that will collaborate and calculate a result based on the
xml file. The simple calculator1.xml file only creates a
computation and declares a literal value. The resulting parsing is
pretty simple:
- The
ComputationAction1
class'begin()
method is called - The
LiteralAction
class'begin()
andend()
methods are called - The
ComputationAction1
class'end()
method is called
What is interesting here is the way that the Actions collaborate.
The LiteralAction
reads a literal value and pushes it
in the object stack maintained by the
ExecutionContext
. Once done, any other action can pop
the value to read or modify it. Here, the end()
method
of the ComputationAction1
class pops the value from the
stack and prints it.
The calculator2.xml file is a bit more complex, but much more interesting.
It contains the following elements:
Example 3.: Calculator configuration file (logback-examples/src/main/java/chapter3/calculator/calculator2.xml)<computation name="toto"> <literal value="7"/> <literal value="3"/> <add/> <literal value="3"/> <multiply/> </computation>
Here, there are obviously more actions that will be part of the computation.
When called, the
AddAction
class will remove the two integers at the
bottom of the stack, add them and push the resulting integer at the
top of the stack, for further use.
Later in the computation, the
MultiplyAction
class will be called. It will take
the last two integers from the stack, multiply them and push the
result in the stack.
We have here two examples of action whose begin()
method behaves in a certain, predictable way, but whose
end()
methods are empty.
Finally, a calculator3.xml is also provided, to demonstrate the possibility elements that contain instances of the same element. Here's the content of calculator3.xml:
Example 3.: Calculator configuration file (logback-examples/src/main/java/chapter3/calculator/calculator3.xml)<computation name="toto"> <computation> <literal value="7"/> <literal value="3"/> <add/> </computation> <literal value="3"/> <multiply/> </computation>
Much like the use of parentheses in an algebrical equation, the
presence of a computation
element nested in another is
managed by the
ComputationAction2
class using an internal
stack. The well-formedness of XML will guarantee that a value saved
by one begin()
will be consumed only by the matching
end()
method.
New-rule action
Joran includes an action which allows the Joran interpreter to lean new rules on the fly while interpreting the XML file containing the new rules. See the logback-examples/src/main/java/joran/newRule/ directory for sample code.
In this package, the
NewRuleCalculator
class contains the same setup as
we have seen so far, but for one line:
ruleStore.addRule(new Pattern("/computation/new-rule"), new NewRuleAction());
By adding this line, we ask Joran to allow new rules to be learnt
at parsing time. It works pretty much like the other rules: it has a
begin()
and end()
method, and is called each time
the parser finds a new-rule element.
When called, the begin()
method looks for a
pattern and a actionClass attribute. The action
class is then instanciated and added to the RuleStore
,
along with its corresponding pattern.
Here is how new rules can be declared in an xml file:
<new-rule pattern="*/computation/literal" actionClass="chapter3.calculator.LiteralAction"/>
Using new rule declarations, the preceding example, involving the calculation, could be expressed this way:
Example 3.: Configuration file using new rules on the fly (logback-examples/src/main/java/chapter3/newrule/new-rule.xml)<computation name="toto"> <new-rule pattern="*/computation/literal" actionClass="chapter3.calculator.LiteralAction"/> <new-rule pattern="*/computation/add" actionClass="chapter3.calculator.AddAction"/> <new-rule pattern="*/computation/multiply" actionClass="chapter3.calculator.MultiplyAction"/> <computation> <literal value="7"/> <literal value="3"/> <add/> </computation> <literal value="3"/> <multiply/> </computation>
Implicit actions
The rules defined thus far are called explicit rules because they require an explicit pattern, hence fixing the tag name of the elements for which they apply.
In highly extensible systems, the number and type of components to handle are innumerable so that it would become very tedious or even impossible to list all the applicable patterns by name.
At the same time, even in highly extensible systems one can
observe well-defined patterns linking the various parts
together. Implicit rules come in very handy when processing
components composed of sub-components unknown ahead of time. For
example, Apache Ant is capable of handling tasks which contain tags
unknown at compile time by looking at methods whose names start with
add, as in addFile
, or
addClassPath
. When Ant encounters an embedded tag
within a task, it simply instantiates an object that matches the
signature of the task class' add method and attaches the resulting
object to the parent.
Joran includes similar capability in the form of implicit actions. Joran keeps a list of implicit actions which can be applied if no explicit pattern matches the current XML element. However, applying an implicit action may not be always appropriate. Before executing the implicit action, Joran asks an implicit action whether it is appropriate in the current context. Only if the action replies affirmatively does Joran interpreter invoke the (implicit) action. This extra step makes it possible to support multiple implicit actions or obviously none, if no implicit action is appropriate for a given situation.
For example, the
NestedComponentIA
extending
ImplicitAction
, will instantiate the class
specified in a nested component and attach it to the parent
component by using setter method of the parent component and the
nested element's name. Under certain circumstances, a nested action
needs to be applied to an element say <a> and also to another
element <b> nested within <a>. The current implementation of
NestedComponentIA
is capable of handling multiply
nested elements requiring intervention by the same implicit action.
Both ImplicitAction
and
NestedComponentIA
are located in the
ch.qos.logback.core.joran.action
package.
Refer to the logback-examples/src/main/java/joran/implicit directory for an example of an implicit action.
In that directory, you will find two actions classes, one xml file and one class containing the setup of Joran.
The
NOPAction
class does nothing. It is used to set the
context of the foo element, using this line:
ruleStore.addRule(new Pattern("*/foo"), new NOPAction());
After that, the implicit action, namely
PrintMeImplicitAction
, is added to the
RuleStore
. This is done by simply adding a new instance
of the action to the Joran interpreter
ji.addImplicitAction(new PrintMeImplicitAction());
When called, the isApplicable()
method of
PrintMeImplicitAction
checks the value of the
printme attribute. If the value is true
, the
implicit action is applicable: its begin()
method will
be called.
The implicit1.xml file contains the following lines:
Example 3.: Usage of implicit rules (logback-examples/src/main/java/chapter3/implicit/implicit1.xml)<foo> <xyz printme="true"> <abc printme="true"/> </xyz> <xyz/> <foo printme="true"/> </foo>
As you can see, the first element will be printed, since it has a
printme attribute, which bears the value
true
.
The second element will not be printed, because no printme attibute is present.
The last element will not be printed, although the required
attribute is present. This is because implicit rules are called
only if no explicit rules are defined. Since we added a
NOPAction
with the */foo pattern, it will be
used instead of the PrintMeImplicitAction
.
Running the example yields the following output:
Element <xyz> asked to be printed. Element <abc> asked to be printed. ERROR in ch.qos.logback.core.joran.spi.ExecutionContext@1c5c1 - no applicable action \ for <xyz>, current pattern is [/foo/xyz]
The last line was printed because of a call to
StatusPrinter
at the end of the main class.
Non goals
The Joran API is not intended to be used to parse documents with thousands of elements.