1. Introduction
1.1. Why log?
Logging has many uses within an application — spanning:
-
auditing actions
-
reporting errors
-
providing debug information to assist in locating a problem
With much of our code located in libraries — logging is not just for our application code. We will want to know audit, error, and debug information in our library calls as well:
-
did that timer fire?
-
which calls failed?
-
what HTTP headers were input or returned from a REST call?
1.2. Why use a Logger over System.out?
Use of Loggers allow statements to exist within the code that will either:
-
be disabled
-
log output uninhibited
-
log output with additional properties (e.g., timestamp, thread, caller, etc.)
Logs commonly are written to the console and/or files by default — but that is not always the case. Logs can also be exported into centralized servers or database(s) so they can form an integrated picture of a distributed system and provide search and alarm capabilities.
However simple or robust your end logs become, logging starts with the code and is a very important thing to include from the beginning (even if we waited a few modules to cover it). |
1.3. Goals
The student will learn:
-
to understand the value in using logging over simple System.out.println calls
-
to understand the interface and implementation separation of a modern logging framework
-
the relationship between the different logger interfaces and implementations
-
to use log levels and verbosity to properly monitor the application under different circumstances
-
to express valuable context information in logged messages
-
to manage logging verbosity
-
to configure the output of logs to provide useful information
1.4. Objectives
At the conclusion of this lecture and related exercises, the student will be able to:
-
obtain access to an SLF4J Logger
-
issue log events at different severity levels
-
filter log events based on source and severity thresholds
-
efficiently bypass log statements that do not meet criteria
-
format log events for regular and exception parameters
-
customize log patterns
-
customize appenders
-
add contextual information to log events using Mapped Diagnostic Context
-
trigger additional logging events using Markers
-
use Spring Profiles to conditionally configure logging
2. Starting References
There are many resources on the Internet that cover logging, the individual logging implementations, and the Spring Boot opinionated support for logging. You may want to keep a browser window open to one or more of the following starting links while we cover this material. You will not need to go thru all of them, but know there is a starting point to where detailed examples and explanations can be found if not covered in this lesson.
-
Spring Boot Logging Feature provides documentation from a top-down perspective of how it supplies a common logging abstraction over potentially different logging implementations.
-
SLF4J Web Site provides documentation, articles, and presentations on SLF4J — the chosen logging interface for Spring Boot and much of the Java community.
-
Logback Web Site provides a wealth of documentation, articles, and presentations on Logback — the default logging implementation for Spring Boot.
-
Log4J2 Web Site provides core documentation on Log4J2 — a top-tier Spring Boot alternative logging implementation.
-
Java Util Logging (JUL) Documentation Web Site provides an overview of JUL — a lesser supported Spring Boot alternative implementation for logging.
3. Logging Dependencies
Most of what we need to perform logging is supplied
through our dependency on the spring-boot-starter
and its dependency on
spring-boot-starter-logging
. The only time we need to supply additional dependencies
is when we want to change the default logging implementation or make use of optional,
specialized extensions provided by that logging implementation.
Take a look at the transitive dependencies brought in by a straight forward dependency on
spring-boot-starter
.
$ mvn dependency:tree
...
[INFO] info.ejava.examples.app:appconfig-logging-example:jar:6.0.1-SNAPSHOT
[INFO] \- org.springframework.boot:spring-boot-starter:jar:2.7.0:compile
[INFO] +- org.springframework.boot:spring-boot:jar:2.7.0:compile
...
[INFO] +- org.springframework.boot:spring-boot-autoconfigure:jar:2.7.0:compile
[INFO] +- org.springframework.boot:spring-boot-starter-logging:jar:2.7.0:compile (1)
[INFO] | +- ch.qos.logback:logback-classic:jar:1.2.11:compile
[INFO] | | +- ch.qos.logback:logback-core:jar:1.2.11:compile
[INFO] | | \- org.slf4j:slf4j-api:jar:1.7.36:compile
[INFO] | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.2:compile
[INFO] | | \- org.apache.logging.log4j:log4j-api:jar:2.17.2:compile
[INFO] | \- org.slf4j:jul-to-slf4j:jar:1.7.36:compile
...
1 | dependency on spring-boot-starter brings in spring-boot-starter-logging |
3.1. Logging Libraries
Notice that:
-
spring-core
dependency brings in its own repackaging and optimizations of Commons Logging withinspring-jcl
-
spring-jcl
provides a thin wrapper that looks for logging APIs and self-bootstraps itself to use them — with a preference for the SLF4J interface, then Log4J2 directly, and then JUL as a fallback -
spring-jcl
looks to have replaced the need for jcl-over-slf4j
-
-
spring-boot-starter-logging
provides dependencies for the SLF4J API, adapters and three optional implementations-
implementations — these will perform the work behind the SLF4J interface calls
-
Logback (the default)
-
-
adapters — these will bridge the SLF4J calls to the implementations
-
Logback
implements SLF4J natively - no adapter necessary -
log4j-to-slf4j
bridges Log4j to SLF4J -
jul-to-slf4j
- bridges Java Util Logging (JUL) to SLF4J
-
-
If we use Spring Boot with spring-boot-starter right out of the box, we will
be using the SLF4J API and Logback implementation configured to work correctly for most cases.
|
3.2. Spring and Spring Boot Internal Logging
Spring and Spring Boot use an internal version of the
Apache Commons Logging API
(Git Repo)
(that was previously known as the Jakarta Commons Logging or JCL (
Ref: Wikipedia, Apache Commons Logging))
that is rehosted within the
spring-jcl
module to serve as a bridge to different logging implementations (Ref:
Spring Boot Logging).
4. Getting Started
OK. We get the libraries we need to implement logging right out of the box with the
basic spring-boot-starter
. How do we get started generating log messages?
Lets begin with a comparison with System.out
so we can see how they are similar
and different.
4.1. System.out
System.out was built into Java from day 1
-
no extra imports are required
-
no extra libraries are required
System.out writes to wherever System.out
references. The default is stdout. You have
seen many earlier examples just like the following.
@Component
@Profile("system-out") (1)
public class SystemOutCommand implements CommandLineRunner {
public void run(String... args) throws Exception {
System.out.println("System.out message");
}
}
1 | restricting component to profile to allow us to turn off unwanted output after this demo |
4.2. System.out Output
The example SystemOutCommand
component above outputs the following statement when called with the
system-out
profile active (using spring.profiles.active
property).
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=system-out (1)
System.out message (2)
1 | activating profile that turns on our component and turns off all logging |
2 | System.out is not impacted by logging configuration and printed to stdout |
4.3. Turning Off Spring Boot Logging
Where did all the built-in logging (e.g., Spring Boot banner, startup messages, etc.) go in the last example?
The system-out
profile specified a logging.level.root
property that
effectively turned off all logging.
spring.main.banner-mode=off (1)
logging.level.root=OFF (2)
1 | turns off printing of verbose Spring Boot startup banner |
2 | turns off all logging (inheriting from the root configuration) |
Technically the logging was only turned off for loggers inheriting the root configuration — but we will ignore that detail for right now and just say "all logging". |
4.4. Getting a Logger
Logging frameworks make use of the fundamental design idiom — separate interface from implementation. We want our calling code to have simple access to a simple interface to express information to be logged and the severity of that information. We want the implementation to have limitless capability to produce and manage the logs, but want to only pay for what we likely will use. Logging frameworks allow that to occur and provide primary access thru a logging interface and a means to create an instance of that logger. The following diagram shows the basic stereotype roles played by the factory and logger.
-
Factory creates Logger
Lets take a look at several ways to obtain a Logger using different APIs and techniques.
4.6. JUL Example Output
The following output shows that even code using the JUL interface will be integrated into our standard Spring Boot logs.
java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=factory
...
20:40:54,136 INFO info.ejava.examples.app.config.logging.factory.JULLogger - Java Util logger message
...
However, JUL is not widely used as an API or implementation. I won’t detail it here, but it has been reported to be much slower and missing robust features of modern alternatives. That does not mean JUL cannot be used as an API for your code (and the libraries your code relies on) and an implementation for your packaged application. It just means using it as an implementation is uncommon and won’t be the default in Spring Boot and other frameworks. |
4.8. SLF4J Example Output
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=factory (1)
...
20:40:55,156 INFO info.ejava.examples.app.config.logging.factory.DeclaredLogger - declared SLF4J logger message
...
1 | supplying custom profile to filter output to include only the factory examples |
4.9. Lombok SLF4J Declaration Example
Naming loggers after the fully qualified classname is so common that the Lombok library was able to successfully take advantage of that fact to automate the tasks for adding the imports and declaring the Logger during Java compilation.
package info.ejava.examples.app.config.logging.factory;
import lombok.extern.slf4j.Slf4j;
...
@Component
@Slf4j (1)
public class LombokDeclaredLogger implements CommandLineRunner {
(2)
@Override
public void run(String... args) throws Exception {
log.info("lombok declared SLF4J logger"); (3)
}
}
1 | @Slf4j annotation automates the import statements and Logger declaration |
2 | Lombok will declare a static log property using LoggerFactory during compilation |
3 | normal log statement provided by calling class — no different from earlier example |
4.10. Lombok Example Output
Since Lombok primarily automates code generation at compile time, the produced output is identical to the previous manual declaration example.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=factory
...
20:40:55,155 INFO info.ejava.examples.app.config.logging.factory.LombokDeclaredLogger - lombok declared SLF4J logger message
...
4.11. Lombok Dependency
Of course, we need to add the following dependency to the project pom.xml
to enable
Lombok annotation processing.
<!-- used to declare logger -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
6. Discarded Message Expense
The designers of logger frameworks are well aware that excess logging — even statements that are disabled — can increase the execution time of a library call or overall application. We have already seen how severity level thresholds can turn off output and that gives us substantial savings within the logging framework itself. However, we must be aware that building a message to be logged can carry its own expense and be aware of the tools to mitigate the problem.
Assume we have a class that is relatively expensive to obtain a String representation.
class ExpensiveToLog {
public String toString() { (1)
try { Thread.sleep(1000); } catch (Exception ignored) {}
return "hello";
}
}
1 | calling toString() on instances of this class will incur noticeable delay |
6.1. Blind String Concatenation
Now lets say we create a message to log through straight, eager String concatenation. What is wong here?
ExpensiveToLog obj=new ExpensiveToLog();
//...
log.debug("debug for expensiveToLog: " + obj + "!");
-
The log message will get formed by eagerly concatenating several Strings together
-
One of those Strings is produced by a relatively expensive
toString()
method -
Problem: The work of eagerly forming the String is wasted if
DEBUG
is not enabled
6.2. Verbosity Check
Assuming the information from the toString()
call is valuable and needed when we
have DEBUG
enabled — a verbosity check is one common solution we can use to determine
if the end result is worth the work. There are two very similar ways we can do this.
The first way is to dynamically check the current threshold level of the logger
within the code and only execute if the requested severity level is enabled.
We are still going to build the relatively expensive String when DEBUG
is enabled
but we are going to save all that processing time when it is not enabled. This overall
approach of using a code block works best when creating the message requires multiple
lines of code. This specific technique of dynamically checking is suitable when there
are very few checks within a class.
6.2.1. Dynamic Verbosity Check
The first way is to dynamically check the current threshold level of the logger
within the code and only execute if the requested severity level is enabled.
We are still going to build the relatively expensive String when DEBUG
is enabled
but we are going to save all that processing time when it is not enabled. This overall
approach of using a code block works best when creating the message requires multiple
lines of code. This specific technique of dynamically checking is suitable when there
are very few checks within a class.
if (log.isDebugEnabled()) { (1)
log.debug("debug for expensiveToLog: " + obj +"!");
}
1 | code block with expensive toString() call is bypassed when DEBUG disabled |
6.2.2. Static Final Verbosity Check
A variant of the first approach is to define a static final boolean
variable
at the start of the class, equal to the result of the enabled test.
This variant allows the JVM to know that the value of the if
predicate will never change allowing the code block and further checks to be eliminated when disabled.
This alternative is better when there are multiple blocks of code that you want to make conditional on the threshold level of the logger.
This solution assumes the logger threshold will never be changed or that the JVM will be restarted to use the changed value.
I have seen this technique commonly used in
libraries
where they anticipate many calls and they are commonly judged on their method throughput performance.
private static final boolean DEBUG_ENABLED = log.isDebugEnabled(); (1)
...
if (DEBUG_ENABLED) { (2)
log.debug("debug for expensiveToLog: " + obj + "!");
}
...
1 | logger’s verbosity level tested when class loaded and stored in static final variable |
2 | code block with expensive toString() |
6.3. SLF4J Parameterized Logging
SLF4J API offers another solution that removes the need for the if
clause — thus
cleaning your code of those extra conditional blocks. The SLF4J Logger
interface has a
format
and args
variant for each verbosity level call that permits the threshold to
be consulted prior to converting any of the parameters to a String.
The format specification uses a set of curly braces ("{}") to express an insertion
point for an ordered set of arguments. There are no format options. It is strictly a
way to lazily call toString()
on each argument and insert the result.
log.debug("debug for expensiveToLog: {}!", obj); (1) (2)
1 | {} is a placeholder for the result of obj.toString() if called |
2 | obj.toString() only called and overall message concatenated if logger threshold set to ⇐ DEBUG |
6.5. Simple Performance Results: Enabled
The second set of results are from logging threshold set to DEBUG
. You can see that causes the
relatively expensive toString()
to be called for each of the four techniques shown with somewhat
equal results. I would not put too much weight on a few milliseconds difference between the calls
here except to know that neither provide a noticeable processing delay over the other when the
logging threshold has been met.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=expense \
--logging.level.info.ejava.examples.app.config.logging.expense=DEBUG
11:44:43.560 INFO info.ejava.examples.app.config.logging.expense.DisabledOptimization - warmup logger
11:44:43.561 DEBUG info.ejava.examples.app.config.logging.expense.DisabledOptimization - warmup logger
11:44:44.572 DEBUG info.ejava.examples.app.config.logging.expense.DisabledOptimization - debug for expensiveToLog: hello!
11:44:45.575 DEBUG info.ejava.examples.app.config.logging.expense.DisabledOptimization - debug for expensiveToLog: hello!
11:44:46.579 DEBUG info.ejava.examples.app.config.logging.expense.DisabledOptimization - debug for expensiveToLog: hello!
11:44:46.579 DEBUG info.ejava.examples.app.config.logging.expense.DisabledOptimization - debug for expensiveToLog: hello!
11:44:47.582 INFO info.ejava.examples.app.config.logging.expense.DisabledOptimization - \
concat: 1010, ifDebug=1003, DEBUG_ENABLED=1004, param=1003 (1)
1 | all four methods paying the cost of the relatively expensive obj.toString() call |
7. Exception Logging
SLF4J interface and parameterized logging goes one step further to also support Exceptions
. If you pass
an Exception
object as the last parameter in the list — it is treated special and will
not have its toString()
called with the rest of the parameters. Depending on the configuration
in place, the stack trace for the Exception
is logged instead. The following snippet shows
an example of an Exception
being thrown, caught, and then logged.
public void run(String... args) throws Exception {
try {
log.info("calling iThrowException");
iThrowException();
} catch (Exception ex) {
log.warn("caught exception", ex); (1)
}
}
private void iThrowException() throws Exception {
throw new Exception("example exception");
}
1 | Exception passed to logger with message |
7.1. Exception Example Output
When we run the example, note that the message is printed in its normal location and a stack trace is
added for the supplied Exception
parameter.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=exceptions
13:41:17.119 INFO info.ejava.examples.app.config.logging.exceptions.ExceptionExample - calling iThrowException
13:41:17.121 WARN info.ejava.examples.app.config.logging.exceptions.ExceptionExample - caught exception (1)
java.lang.Exception: example exception (2)
at info.ejava.examples.app.config.logging.exceptions.ExceptionExample.iThrowException(ExceptionExample.java:23)
at info.ejava.examples.app.config.logging.exceptions.ExceptionExample.run(ExceptionExample.java:15)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:784)
...
at org.springframework.boot.loader.Launcher.launch(Launcher.java:51)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:52)
1 | normal message logged |
2 | stack trace for last Exception parameter logged |
7.2. Exception Logging and Formatting
Note that you can continue to use parameterized logging with Exceptions. The message passed in
above was actually a format with no parameters. The snippet below shows a format with two parameters
and an Exception
.
log.warn("caught exception {} {}", "p1","p2", ex);
The first two parameters are used in the formatting of the core message. The last Exception parameters is printed as a regular exception.
13:41:17.119 INFO info.ejava.examples.app.config.logging.exceptions.ExceptionExample - calling iThrowException
13:41:17.122 WARN info.ejava.examples.app.config.logging.exceptions.ExceptionExample - caught exception p1 p2 (1)
java.lang.Exception: example exception (2)
at info.ejava.examples.app.config.logging.exceptions.ExceptionExample.iThrowException(ExceptionExample.java:23)
at info.ejava.examples.app.config.logging.exceptions.ExceptionExample.run(ExceptionExample.java:15)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:784)
...
at org.springframework.boot.loader.Launcher.launch(Launcher.java:51)
at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:52)
1 | two early parameters ("p1" and "p2") where used to complete the message template |
2 | Exception passed as the last parameter had its stack trace logged |
8. Logging Pattern
Each of the previous examples showed logging output using a particular pattern. The pattern
was expressed using a logging.pattern.console
property. The
Logback Conversion Documentation
provides details about how the logging pattern is defined.
logging.pattern.console=%date{HH:mm:ss.SSS} %-5level %logger - %msg%n
The pattern consisted of:
-
%date (or %d)- time of day down to millisecs
-
%level (or %p, %le)- severity level left justified and padded to 5 characters
-
%logger (or %c, %lo)- full name of logger
-
%msg (or %m, %message) - full logged message
-
%n - operating system-specific new line
If you remember, that produced the following output.
java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=levels
06:00:38.891 INFO info.ejava.examples.app.config.logging.levels.LoggerLevels - info message
06:00:38.892 WARN info.ejava.examples.app.config.logging.levels.LoggerLevels - warn message
06:00:38.892 ERROR info.ejava.examples.app.config.logging.levels.LoggerLevels - error message
8.1. Default Console Pattern
Spring Boot comes out of the box with a slightly more verbose default pattern expressed with the CONSOLE_LOG_PATTERN property. The following snippet depicts the information found within the Logback property definition — with some new lines added in to help read it.
CONSOLE_LOG_PATTERN
from GitHub%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint}
%clr(${LOG_LEVEL_PATTERN:-%5p})
%clr(${PID:- }){magenta}
%clr(---){faint}
%clr([%15.15t]){faint}
%clr(%-40.40logger{39}){cyan}
%clr(:){faint}
%m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}
You should see some familiar conversion words from my earlier pattern example. However, there are some additional conversion words used as well. Again, keep the Logback Conversion Documentation close by to lookup any additional details.
-
%d - timestamp defaulting to full format
-
%p - severity level right justified and padded to 5 characters
-
$PID - system property containing the process ID
-
%t (or %thread) - thread name right justified and padded to 15 characters and chopped at 15 characters
-
%logger - logger name optimized to fit within 39 characters , left justified and padded to 40 characters, chopped at 40 characters
-
%m - fully logged message
-
%n - operating system-specific new line
8.2. Default Console Pattern Output
We will take a look at conditional variable substitution in a moment. This next example reverts to the
default CONSOLE_LOG_PATTERN
.
java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--logging.level.root=OFF \
--logging.level.info.ejava.examples.app.config.logging.levels.LoggerLevels=TRACE
2020-03-27 06:31:21.475 TRACE 31203 --- [ main] i.e.e.a.c.logging.levels.LoggerLevels : trace message
2020-03-27 06:31:21.477 DEBUG 31203 --- [ main] i.e.e.a.c.logging.levels.LoggerLevels : debug message
2020-03-27 06:31:21.477 INFO 31203 --- [ main] i.e.e.a.c.logging.levels.LoggerLevels : info message
2020-03-27 06:31:21.477 WARN 31203 --- [ main] i.e.e.a.c.logging.levels.LoggerLevels : warn message
2020-03-27 06:31:21.477 ERROR 31203 --- [ main] i.e.e.a.c.logging.levels.LoggerLevels : error message
Spring Boot defines color coding for the console that is not visible in the text of this document.
The color for severity level is triggered by the level — red for ERROR
, yellow for WARN
, and
green for the other three levels.
8.3. Variable Substitution
Logging configurations within Spring Boot make use of variable substitution. The value of LOG_DATEFORMAT_PATTERN
will be applied wherever the expression ${LOG_DATEFORMAT_PATTERN}
appears. The "${}"
characters are part
of the variable expression and will not be part of the result.
8.4. Conditional Variable Substitution
Variables can be defined with default values in the event they are not defined. In the following
expression ${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}
:
-
the value of LOG_DATEFORMAT_PATTERN will be used if defined
-
the value of "yyyy-MM-dd HH:mm:ss.SSS" will be used if not defined
The "${}" and embedded ":-" characters following the variable name are part of the expression
when appearing within an XML configuration file and will not be part of the result. The dash (- )
character should be removed if using within a property definition.
|
8.5. Date Format Pattern
As we saw from a peek at the Spring Boot CONSOLE_LOG_PATTERN
default definition, we can
change the format of the timestamp using the LOG_DATEFORMAT_PATTERN
system property.
That system property can flexibly be set using the logging.pattern.dateformat
property. See the
Spring Boot Documentation for information on this and other properties.
The following example shows setting that property using a command line argument.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--logging.level.root=OFF \
--logging.level.info.ejava.examples.app.config.logging.levels.LoggerLevels=INFO \
--logging.pattern.dateformat="HH:mm:ss.SSS" (1)
08:20:42.939 INFO 39013 --- [ main] i.e.e.a.c.logging.levels.LoggerLevels : info message
08:20:42.942 WARN 39013 --- [ main] i.e.e.a.c.logging.levels.LoggerLevels : warn message
08:20:42.942 ERROR 39013 --- [ main] i.e.e.a.c.logging.levels.LoggerLevels : error message
1 | setting LOG_DATEFORMAT_PATTERN using logging.pattern.dateformat property |
8.6. Log Level Pattern
We also saw from the default definition of CONSOLE_LOG_PATTERN
that the severity level
of the output can be changed using the LOG_LEVEL_PATTERN
system property. That system
property can be flexibly set with the logging.pattern.level
property. The following
example shows setting the format to a single character, left justified. Therefore, we can map
INFO
⇒ I
, WARN
⇒ W
, and ERROR
⇒ E
.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--logging.level.root=OFF \
--logging.level.info.ejava.examples.app.config.logging.levels.LoggerLevels=INFO \
--logging.pattern.dateformat="HH:mm:ss.SSS" \
--logging.pattern.level="%.-1p" (1)
(2)
08:59:17.376 I 44756 --- [ main] i.e.e.a.c.logging.levels.LoggerLevels : info message
08:59:17.379 W 44756 --- [ main] i.e.e.a.c.logging.levels.LoggerLevels : warn message
08:59:17.379 E 44756 --- [ main] i.e.e.a.c.logging.levels.LoggerLevels : error message
1 | logging.level.pattern expressed to be 1 character, left justified |
2 | single character produced in console log output |
8.7. Conversion Pattern Specifiers
Spring Boot Features Web Page documents some formatting rules. However, more details on the parts within the conversion pattern are located on the Logback Pattern Layout Web Page. The overall end-to-end pattern definition I have shown you is called a "Conversion Pattern". Conversion Patterns are made up of:
-
Literal Text (e.g.,
---
, whitespace,:
) — hard-coded strings providing decoration and spacing for conversion specifiers -
Conversion Specifiers - (e.g.,
%-40.40logger{39}
) — an expression that will contribute a formatted property of the current logging context-
starts with
%
-
followed by format modifiers — (e.g.,
-40.40
) — addresses min/max spacing and right/left justification-
optionally provide minimum number of spaces
-
use a negative number (
-#
) to make it left justified and a positive number (#
) to make it right justified
-
-
optionally provide maximum number of spaces using a decimal place and number (
.#
). Extra characters will be cut off-
use a negative number (
.-#
) to start from the left and positive number (.#
) to start from the right
-
-
-
followed by a conversion word (e.g.,
logger
,msg
) — keyword name for the property -
optional parameters (e.g.,
{39}
) — see individual conversion words for details on each
-
8.8. Format Modifier Impact Example
The following example demonstrates how the different format modifier expressions can impact the level
property.
logging.pattern.loglevel | output | comment |
---|---|---|
[%level] |
[INFO] [WARN] [ERROR] |
value takes whatever space necessary |
[%6level] |
[ INFO] [ WARN] [ ERROR] |
value takes at least 6 characters, right justified |
[%-6level] |
[INFO ] [WARN ] [ERROR ] |
value takes at least 6 characters, left justified |
[%.-2level] |
[IN] [WA] [ER] |
value takes no more than 2 characters, starting from the left |
[%.2level] |
[FO] [RN] [OR] |
value takes no more than 2 characters, starting from the right |
8.10. Expensive Conversion Words
I added two new helpful properties that could be considered controversial because they require extra overhead to obtain that information from the JVM. The technique has commonly involved throwing and catching an exception internally to determine the calling location from the self-generated stack trace:
-
%method (or %M) - name of method calling logger
-
%line (or %L) - line number of the file where logger call was made
The additional "expensive" fields are being used for console output for demonstrations using a demonstration profile. Consider your log information needs on a case-by-case basis and learn from this lesson what and how you can modify the logs for your specific needs. For example — to debug an error, you can switch to a more detailed and verbose profile without changing code. |
8.11. Example Override Output
We can activate the profile and demonstrate the modified format using the following command.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=layout
14:25:58.428 INFO - logging.levels.LoggerLevels#run:14 info message
14:25:58.430 WARN - logging.levels.LoggerLevels#run:15 warn message
14:25:58.430 ERROR - logging.levels.LoggerLevels#run:16 error message
The coloring does not show up above so the image below provides a perspective of what that looks like.
8.12. Layout Fields
Please see the Logback Layouts Documentation for a detailed list of conversion words and how to optionally format them.
9. Loggers
We have demonstrated a fair amount capability thus far without having to know much about the internals of the logger framework. However, we need to take a small dive into the logging framework in order to explain some further concepts.
-
Logger Ancestry
-
Logger Inheritance
-
Appenders
-
Logger Additivity
9.1. Logger Tree
Loggers are organized in a hierarchy starting with a root logger called "root". As you would expect, higher in the tree are considered ancestors and lower in the tree are called descendants.
Except for root, the ancestor/descendant structure of loggers depends on the hierarchical name of each logger. Based on the loggers in the diagram
-
X, Y.3, and security are descendants and direct children of root
-
Y.3 is example of logger lacking an explicitly defined parent in hierarchy before reaching root. We can skip many levels between child and root and still retain same hierarchical name
-
X.1, X.2, and X.3 are descendants of X and root and direct children of X
-
Y.3.p is descendant of Y.3 and root and direct child of Y.3
9.2. Logger Inheritance
Each logger has a set of allowed properties. Each logger may define its own value for those properties, inherit the value of its parent, or be assigned a default (as in the case for root).
9.3. Logger Threshold Level Inheritance
The first inheritance property we will look at is a familiar one to you — severity threshold level. As the diagram shows
-
root, loggerX, security, loggerY.3, loggerX.1 and loggerX.3 set an explicit value for their threshold.
-
loggerX.2 and loggerY.3.p inherit the threshold from their parent
9.4. Logger Effective Threshold Level Inheritance
The following table shows the specified and effective values applied to each logger for their threshold.
logger name | specified threshold | effective threshold |
---|---|---|
root |
OFF |
OFF |
X |
INFO |
INFO |
X.1 |
ERROR |
ERROR |
X.2 |
INFO |
|
X.3 |
OFF |
OFF |
Y.3 |
WARN |
WARN |
Y.3.p |
WARN |
|
security |
TRACE |
TRACE |
9.5. Example Logger Threshold Level Properties
These thresholds can be expressed in a property file.
logging.level.X=info
logging.level.X.1=error
logging.level.X.3=OFF
logging.level.security=trace
logging.level.Y.3=warn
logging.level.root=OFF
9.6. Example Logger Threshold Level Output
The output below demonstrates the impact of logging level inheritance from ancestors to descendants.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=tree
CONSOLE 05:58:14.956 INFO - X#run:25 X info
CONSOLE 05:58:14.959 WARN - X#run:26 X warn
CONSOLE 05:58:14.959 ERROR - X#run:27 X error
CONSOLE 05:58:14.960 ERROR - X.1#run:27 X.1 error (2)
CONSOLE 05:58:14.960 INFO - X.2#run:25 X.2 info (1)
CONSOLE 05:58:14.960 WARN - X.2#run:26 X.2 warn
CONSOLE 05:58:14.960 ERROR - X.2#run:27 X.2 error
CONSOLE 05:58:14.960 WARN - Y.3#run:26 Y.3 warn
CONSOLE 05:58:14.960 ERROR - Y.3#run:27 Y.3 error
CONSOLE 05:58:14.960 WARN - Y.3.p#run:26 Y.3.p warn (1)
CONSOLE 05:58:14.961 ERROR - Y.3.p#run:27 Y.3.p error
CONSOLE 05:58:14.961 TRACE - security#run:23 security trace (3)
CONSOLE 05:58:14.961 DEBUG - security#run:24 security debug
CONSOLE 05:58:14.962 INFO - security#run:25 security info
CONSOLE 05:58:14.962 WARN - security#run:26 security warn
CONSOLE 05:58:14.962 ERROR - security#run:27 security error
1 | X.2 and Y.3.p exhibit the same threshold level as their parents X (INFO ) and Y.3 (WARN ) |
2 | X.1 (ERROR ) and X.3 (OFF ) override their parent threshold levels |
3 | security is writing all levels >= TRACE |
10. Appenders
Loggers generate LoggerEvents
but do not directly log anything.
Appenders are responsible for taking a LoggerEvent
and producing a message to a log.
There are many types of appenders. We have been working exclusively with
a ConsoleAppender
thus far but will work with some others before we are done.
At this point — just know that a ConsoleLogger
uses:
-
an encoder to determine when to write messages to the log
-
a layout to determine how to transform an individual
LoggerEvent
to a String -
a pattern when using a
PatternLayout
to define the transformation
10.1. Logger has N Appenders
Each of the loggers in our tree has the chance to have 0..N appenders.
10.2. Logger Configuration Files
To date we have been able to work mostly with Spring Boot properties when using loggers. However, we will need to know a few things about the Logger Configuration File in order to define an appender and assign it to logger(s). We will start with how the logger configuration is found.
Logback and Log4J2 both use XML as their primary definition language. Spring Boot will automatically locate a well-known named configuration file in the root of the classpath:
-
logback.xml
orlogback-spring.xml
for Logback -
log4j2.xml
orlog4j2-spring.xml
for Log4J2
Spring Boot documentation
recommends using the -spring.xml
suffixed files over the provider default
named files in order for Spring Boot to assure that all documented features can be enabled.
Alternately, we can explicitly specify the location using the logging.config
property to
reference anywhere in the classpath or file system.
...
logging.config=classpath:/logging-configs/tree/logback-spring.xml (1)
...
1 | an explicit property reference to the logging configuration file to use |
10.3. Logback Root Configuration Element
The XML file has a root configuration
element which contains details of the appender(s) and logger(s).
See the
Spring Boot Configuration Documentation and the
Logback Configuration Documentation for details on how to configure.
<configuration debug="false">
...
</configuration>
10.4. Retain Spring Boot Defaults
We will lose most/all of the Spring Boot customizations for logging when we
define our own custom logging configuration file. We can restore them
by adding an
include. This is that same file that we looked at earlier for the definition of
CONSOLE_LOG_PATTERN
.
<configuration debug="false">
<!-- bring in Spring Boot defaults for Logback -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
...
</configuration>
10.5. Appender Configuration
Our example tree has three (3) appenders total. Each adds a literal string prefix so we know which appender is being called.
<!-- leverages what Spring Boot would have given us for console -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> (1)
<pattern>CONSOLE ${CONSOLE_LOG_PATTERN}</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<appender name="X-appender" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>X ${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="security-appender" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>SECURITY ${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
1 | PatternLayoutEncoder is the default encoder |
This example forms the basis for demonstrating logger/appender assignment
and appender additivity. ConsoleAppender is used in each case for ease of
demonstration and not meant to depict a realistic configuration.
|
10.6. Appenders Attached to Loggers
The appenders are each attached to a single logger using the appender-ref
element.
-
console is attached to the root logger
-
X-appender is attached to loggerX logger
-
security-appender is attached to security logger
I am latching the two child appender assignments within an appenders
profile to:
-
keep them separate from the earlier log level demo
-
demonstrate how to leverage Spring Boot extensions to build profile-based conditional logging configurations.
<springProfile name="appenders"> (1)
<logger name="X">
<appender-ref ref="X-appender"/> (2)
</logger>
<!-- this logger starts a new tree of appenders, nothing gets written to root logger -->
<logger name="security" additivity="false">
<appender-ref ref="security-appender"/>
</logger>
</springProfile>
<root>
<appender-ref ref="console"/>
</root>
1 | using Spring Boot Logback extension to only enable appenders when profile active |
2 | appenders associated with logger using appender-ref |
10.7. Appender Tree Inheritance
These appenders, in addition to level, are inherited from ancestor to descendant
unless there is an override defined by the property additivity=false
.
10.8. Appender Additivity Result
logger name | assigned threshold | assigned appender | effective threshold | effective appender |
---|---|---|---|---|
root |
OFF |
console |
OFF |
console |
X |
INFO |
X-appender |
INFO |
console, X-appender |
X.1 |
ERROR |
ERROR |
console, X-appender |
|
X.2 |
INFO |
console, X-appender |
||
X.3 |
OFF |
OFF |
console, X-appender |
|
Y.3 |
WARN |
WARN |
console |
|
Y.3.p |
WARN |
console |
||
security *additivity=false |
TRACE |
security-appender |
TRACE |
security-appender |
10.9. Logger Inheritance Tree Output
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=tree,appenders
X 19:12:07.220 INFO - X#run:25 X info (1)
CONSOLE 19:12:07.220 INFO - X#run:25 X info (1)
X 19:12:07.224 WARN - X#run:26 X warn
CONSOLE 19:12:07.224 WARN - X#run:26 X warn
X 19:12:07.225 ERROR - X#run:27 X error
CONSOLE 19:12:07.225 ERROR - X#run:27 X error
X 19:12:07.225 ERROR - X.1#run:27 X.1 error
CONSOLE 19:12:07.225 ERROR - X.1#run:27 X.1 error
X 19:12:07.225 INFO - X.2#run:25 X.2 info
CONSOLE 19:12:07.225 INFO - X.2#run:25 X.2 info
X 19:12:07.225 WARN - X.2#run:26 X.2 warn
CONSOLE 19:12:07.225 WARN - X.2#run:26 X.2 warn
X 19:12:07.226 ERROR - X.2#run:27 X.2 error
CONSOLE 19:12:07.226 ERROR - X.2#run:27 X.2 error
CONSOLE 19:12:07.226 WARN - Y.3#run:26 Y.3 warn (2)
CONSOLE 19:12:07.227 ERROR - Y.3#run:27 Y.3 error (2)
CONSOLE 19:12:07.227 WARN - Y.3.p#run:26 Y.3.p warn
CONSOLE 19:12:07.227 ERROR - Y.3.p#run:27 Y.3.p error
SECURITY 19:12:07.227 TRACE - security#run:23 security trace (3)
SECURITY 19:12:07.227 DEBUG - security#run:24 security debug (3)
SECURITY 19:12:07.227 INFO - security#run:25 security info (3)
SECURITY 19:12:07.228 WARN - security#run:26 security warn (3)
SECURITY 19:12:07.228 ERROR - security#run:27 security error (3)
1 | log messages written to logger X and descendants are written to console and X-appender appenders |
2 | log messages written to logger Y.3 and descendants are written only to console appender |
3 | log messages written to security logger are written only to security appender because of additivity=false |
11. Mapped Diagnostic Context
Thus far, we have been focusing on calls made within the code without much concern about the overall context in which they were made. In a multi-threaded, multi-user environment there is additional context information related to the code making the calls that we may want to keep track of — like userId and transactionId.
SLF4J and the logging implementations support the need for call context information
through the use of
Mapped Diagnostic Context (MDC).
The
MDC class is a essentially a
ThreadLocal
map of strings that are assigned for the
current thread. The values of the MDC are commonly set and cleared in container filters
that fire before and after client calls are executed.
11.1. MDC Example
The following is an example where the run()
method is playing the role of the container filter — setting and clearing the MDC. For this MDC map — I am setting a "user" and "requestId" key
with the current user identity and a value that represents the request. The doWork()
method
is oblivious of the MDC and simply logs the start and end of work.
import org.slf4j.MDC;
...
public class MDCLogger implements CommandLineRunner {
private static final String[] USERS = new String[]{"jim", "joe", "mary"};
private static final SecureRandom r = new SecureRandom();
@Override
public void run(String... args) throws Exception {
for (int i=0; i<5; i++) {
String user = USERS[r.nextInt(USERS.length)];
MDC.put("user", user); (1)
MDC.put("requestId", Integer.toString(r.nextInt(99999)));
doWork();
MDC.clear(); (2)
doWork();
}
}
public void doWork() {
log.info("starting work");
log.info("finished work");
}
}
1 | run() method simulates container filter setting context properties before call executed |
2 | context properties removed after all calls for the context complete |
11.2. MDC Example Pattern
To make use of the new "user" and "requestId" properties of the thread,
we can add the %mdc
(or %X) conversion word to the appender pattern as follows.
#application-mdc.properties
logging.pattern.console=%date{HH:mm:ss.SSS} %-5level [%-9mdc{user:-anonymous}][%5mdc{requestId}] %logger{0} - %msg%n
-
%mdc{user:-anonymous} - the identity of the user making the call or "anonymous" if not supplied
-
%mdc{requestId} - the specific request made or blank if not supplied
11.3. MDC Example Output
The following is an example of running the MDC example. Users are randomly selected and work is performed for both identified and anonymous users. This allows us to track who made the work request and sort out the results of each work request.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar --spring.profiles.active=mdc
17:11:59.100 INFO [jim ][61165] MDCLogger - starting work
17:11:59.101 INFO [jim ][61165] MDCLogger - finished work
17:11:59.101 INFO [anonymous][ ] MDCLogger - starting work
17:11:59.101 INFO [anonymous][ ] MDCLogger - finished work
17:11:59.101 INFO [mary ][ 8802] MDCLogger - starting work
17:11:59.101 INFO [mary ][ 8802] MDCLogger - finished work
17:11:59.101 INFO [anonymous][ ] MDCLogger - starting work
17:11:59.101 INFO [anonymous][ ] MDCLogger - finished work
17:11:59.101 INFO [mary ][86993] MDCLogger - starting work
17:11:59.101 INFO [mary ][86993] MDCLogger - finished work
17:11:59.101 INFO [anonymous][ ] MDCLogger - starting work
17:11:59.101 INFO [anonymous][ ] MDCLogger - finished work
17:11:59.102 INFO [mary ][67677] MDCLogger - starting work
17:11:59.102 INFO [mary ][67677] MDCLogger - finished work
17:11:59.102 INFO [anonymous][ ] MDCLogger - starting work
17:11:59.102 INFO [anonymous][ ] MDCLogger - finished work
17:11:59.102 INFO [jim ][25693] MDCLogger - starting work
17:11:59.102 INFO [jim ][25693] MDCLogger - finished work
17:11:59.102 INFO [anonymous][ ] MDCLogger - starting work
17:11:59.102 INFO [anonymous][ ] MDCLogger - finished work
Like standard ThreadLocal variables,
child threads do not inherit values of parent thread.
|
12. Markers
SLF4J and the logging implementations support markers. Unlike MDC data — which quietly sit in the background — markers are optionally supplied on a per-call basis. Markers have two primary uses
-
trigger reporting events to appenders — e.g., flush log, send the e-mail
-
implement additional severity levels — e.g.,
log.warn(FLESH_WOUND,"come back here!")
versuslog.warn(FATAL,"ouch!!!")
[1]
The additional functionality commonly is implemented through the use of
filters assigned to appenders looking for these Markers .
|
To me having triggers initiated by the logging statements does not sound appropriate (but still could be useful). However, when the thought of filtering comes up — I think of cases where we may want to better classify the subject(s) of the statement so that we have more to filter on when configuring appenders. More than once I have been in a situation where adjusting the verbosity of a single logger was not granular enough to provide an ideal result. |
12.1. Marker Class
Markers
have a single property called name and an optional collection of child Markers
.
The name and collection properties allow the parent marker to represent one or more values.
Appender filters test Markers
using the contains()
method to determine if the parent or any
children are the targeted value.
Markers
are obtained through the MarkerFactory
— which caches the Markers
it creates
unless requested to make them detached so they can be uniquely added to separate parents.
12.2. Marker Example
The following simple example issues two log events. The first is without a Marker
and the second with a Marker
that represents the value ALARM
.
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
...
public class MarkerLogger implements CommandLineRunner {
private static final Marker ALARM = MarkerFactory.getMarker("ALARM"); (1)
@Override
public void run(String... args) throws Exception {
log.warn("non-alarming warning statement"); (2)
log.warn(ALARM,"alarming statement"); (3)
}
}
1 | created single managed marker |
2 | no marker added to logging call |
3 | marker added to logging call to trigger something special about this call |
12.3. Marker Appender Filter Example
The Logback configuration has two appenders. The first appender — alarms
— is meant to
log only log events with an ALARM marker. I have applied the Logback-supplied
EvaluatorFilter
and OnMarkerEvaluator
to eliminate any log events that do not meet
that criteria.
<appender name="alarms" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator name="ALARM" class="ch.qos.logback.classic.boolex.OnMarkerEvaluator">
<marker>ALARM</marker>
</evaluator>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>%red(ALARM>>> ${CONSOLE_LOG_PATTERN})</pattern>
</encoder>
</appender>
The second appender — console — accepts all log events.
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
Both appenders are attached to the same root logger — which means that anything logged to the alarm appender will also be logged to the console appender.
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
...
<root>
<appender-ref ref="console"/>
<appender-ref ref="alarms"/>
</root>
</configuration>
12.4. Marker Example Result
The following shows the results of running the marker example — where both events
are written to the console appender and only the log event with the ALARM
Marker
is written to the alarm appender.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=markers
18:06:52.135 WARN [] MarkerLogger - non-alarming warning statement (1)
18:06:52.136 WARN [ALARM] MarkerLogger - alarming statement (1)
ALARM>>> 18:06:52.136 WARN [ALARM] MarkerLogger - alarming statement (2)
1 | non-ALARM and ALARM events are written to the console appender |
2 | ALARM event is also written to alarm appender |
13. File Logging
Each topic and example so far has been demonstrated using the console because it is simple to demonstrate and to try out for yourself. However, once we get into more significant use of our application we are going to need to write this information somewhere to analyze later when necessary.
For that purpose, Spring Boot has a built-in appender ready to go for file logging. It is not active by default but all we have to do is specify the file name or path to trigger its activation.
java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=levels \
--logging.file.name="mylog.log" (1) (2)
1 | adding this property adds file logging to default configuration |
2 | this expressed logfile will be written to mylog.log in current directory |
13.1. root Logger Appenders
As we saw earlier with appender additivity, multiple appenders can be associated with the same logger (root logger in this case). With the trigger property supplied, a file-based appender is added to the root logger to produce a log file in addition to our console output.
13.2. FILE Appender Output
Under these simple conditions, a file is produced in the current directory with the specified
mylog.log
filename and the following contents.
$ cat mylog.log (1)
2020-03-29 07:14:33.533 INFO 90958 --- [main] i.e.e.a.c.logging.levels.LoggerLevels : info message
2020-03-29 07:14:33.542 WARN 90958 --- [main] i.e.e.a.c.logging.levels.LoggerLevels : warn message
2020-03-29 07:14:33.542 ERROR 90958 --- [main] i.e.e.a.c.logging.levels.LoggerLevels : error message
1 | written to file specified by logging.file property |
The file and parent directories will be created if they do not exist. The default definition of the appender will append to an existing file if it already exists. Therefore — if we run the example a second time we get a second set of messages in the file.
$ cat mylog.log
2020-03-29 07:14:33.533 INFO 90958 --- [main] i.e.e.a.c.logging.levels.LoggerLevels : info message
2020-03-29 07:14:33.542 WARN 90958 --- [main] i.e.e.a.c.logging.levels.LoggerLevels : warn message
2020-03-29 07:14:33.542 ERROR 90958 --- [main] i.e.e.a.c.logging.levels.LoggerLevels : error message
2020-03-29 07:15:00.338 INFO 91090 --- [main] i.e.e.a.c.logging.levels.LoggerLevels : info message (1)
2020-03-29 07:15:00.342 WARN 91090 --- [main] i.e.e.a.c.logging.levels.LoggerLevels : warn message
2020-03-29 07:15:00.342 ERROR 91090 --- [main] i.e.e.a.c.logging.levels.LoggerLevels : error message
1 | messages from second execution appended to same log |
13.3. Spring Boot FILE Appender Definition
If we take a look at the definition for
Spring Boot’s Logback FILE Appender, we can see that it is a
Logback RollingFileAppender
with a
Logback SizeAndTimeBasedRollingPolicy
.
<appender name="FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">(1)
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> (2)
<cleanHistoryOnStart>${LOG_FILE_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
<fileNamePattern>${ROLLING_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
<maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize>
<maxHistory>${LOG_FILE_MAX_HISTORY:-7}</maxHistory>
<totalSizeCap>${LOG_FILE_TOTAL_SIZE_CAP:-0}</totalSizeCap>
</rollingPolicy>
</appender>
1 | performs file rollover functionality based on configured policy |
2 | specifies policy and policy configuration to use |
13.4. RollingFileAppender
The Logback RollingFileAppender will:
-
write log messages to a specified file — and at some point, switch to writing to a different file
-
use a triggering policy to determine the point in which to switch files (i.e., "when it will occur")
-
use a rolling policy to determine how the file switchover will occur (i.e., "what will occur")
-
use a single policy for both if the rolling policy implements both policy interfaces
-
use file append mode by default
13.5. SizeAndTimeBasedRollingPolicy
The Logback SizeAndTimeBasedRollingPolicy will:
-
trigger a file switch when the current file reaches a maximum size
-
trigger a file switch when the granularity of the primary date (%d) pattern in the file path/name would rollover to a new value
-
supply a name for the old/historical file using a mandatory date (%d) pattern and index (%i)
-
define a maximum number of historical files to retain
-
define a total size to allocate to current and historical files
-
define an option to process quotas at startup in addition to file changeover for short running applications
13.7. logging.file.path
If we specify only the logging.file.path
, the filename will default to spring.log
and will be written to the directory path we supply.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--logging.file.path=target/logs (1)
...
$ ls target/logs (2)
spring.log
1 | specifying logging.file.path as target/logs |
2 | produces a spring.log in that directory |
13.8. logging.file.name
If we specify only the logging.file.name
, the file will be written to the filename
and directory we explicitly supply.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--logging.file.name=target/logs/mylog.log (1)
...
$ ls target/logs (2)
mylog.log
1 | specifying a logging.file.name |
2 | produces a logfile with that path and name |
13.9. logging.file.max-size Trigger
One trigger for changing over to the next file is logging.file.max-size
. The condition
is satisfied when the current logfile reaches this value. The default is 10MB.
The following example changes that to 500 Bytes. Once each instance of logging.file.name
reached the logging.file.max-size
, it is compressed and moved to a filename with the
pattern from logging.pattern.rolling-file-name
.
java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=rollover \
--logging.file.name=target/logs/mylog.log \
--logging.file.max-size=500B (1)
...
$ ls -ltr target/logs
total 40
-rw-r--r-- 1 jim staff 154 Mar 29 16:00 mylog.log.2020-03-29.0.gz
-rw-r--r-- 1 jim staff 153 Mar 29 16:00 mylog.log.2020-03-29.1.gz
-rw-r--r-- 1 jim staff 156 Mar 29 16:00 mylog.log.2020-03-29.2.gz
-rw-r--r-- 1 jim staff 156 Mar 29 16:00 mylog.log.2020-03-29.3.gz (2)
-rw-r--r-- 1 jim staff 240 Mar 29 16:00 mylog.log (1)
1 | logging.file.max-size limits the size of the current logfile |
2 | historical logfiles renamed according to logging.pattern.rolling-file-name pattern |
13.10. logging.pattern.rolling-file-name
There are several aspects of logging.pattern.rolling-file-name
to be aware of
-
%d
timestamp pattern and%i
index are required and the FILE appender will be disabled if not specified -
the timestamp pattern directly impacts when the file changeover will occur when we are still below the
logging.file.max-size
. In that case — the changeover occurs when there is a value change in the result of applying the timestamp pattern. Many of my examples here use a pattern that includesHH:mm:ss
just for demonstration purposes. A more common pattern would be by date only. -
the index is used when the
logging.file.max-size
triggers the changeover and we already have a historical name with the same timestamp. -
the number of historical files is throttled using
logging.file.max-history
only when index is used and not when file changeover is due tologging.file.max-size
-
the historical file will be compressed if
gz
is specified as the suffix
13.11. Timestamp Rollover Example
The following example shows the file changeover occurring because the evaluation of
the %d
template expression within logging.pattern.rolling-file-name
changing.
The historical file is left uncompressed because the
logging.pattern.rolling-file-name
does not end in gz
.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=rollover \
--logging.file.name=target/logs/mylog.log \
--logging.file.max-size=500 \
--logging.pattern.rolling-file-name='${logging.file.name}.%d{yyyy-MM-dd-HH:mm:ss}.%i'.log (1)
...
$ ls -ltr target/logs
total 64
-rw-r--r-- 1 jim staff 79 Mar 29 17:50 mylog.log.2020-03-29-17:50:22.0.log (1)
-rw-r--r-- 1 jim staff 79 Mar 29 17:50 mylog.log.2020-03-29-17:50:23.0.log
-rw-r--r-- 1 jim staff 79 Mar 29 17:50 mylog.log.2020-03-29-17:50:24.0.log
-rw-r--r-- 1 jim staff 79 Mar 29 17:50 mylog.log.2020-03-29-17:50:25.0.log
-rw-r--r-- 1 jim staff 79 Mar 29 17:50 mylog.log.2020-03-29-17:50:26.0.log
-rw-r--r-- 1 jim staff 80 Mar 29 17:50 mylog.log.2020-03-29-17:50:27.0.log
-rw-r--r-- 1 jim staff 80 Mar 29 17:50 mylog.log.2020-03-29-17:50:28.0.log
-rw-r--r-- 1 jim staff 80 Mar 29 17:50 mylog.log
$ file target/logs/mylog.log.2020-03-29-17\:50\:28.0.log (2)
target/logs/mylog.log.2020-03-29-17:50:28.0.log: ASCII text
1 | logging.pattern.rolling-file-name pattern triggers changeover at the seconds boundary |
2 | historical logfiles are left uncompressed because of name suffix specified |
Using a date pattern to include minutes and seconds is just for demonstration and learning purposes. Most patterns would be daily. |
13.12. History Compression Example
The following example is similar to the previous one with the exception that the
logging.pattern.rolling-file-name
ends in gz
— triggering the historical file
to be compressed.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=rollover \
--logging.file.name=target/logs/mylog.log \
--logging.pattern.rolling-file-name='${logging.file.name}.%d{yyyy-MM-dd-HH:mm:ss}.%i'.gz (1)
...
$ ls -ltr target/logs
total 64
-rw-r--r-- 1 jim staff 97 Mar 29 16:26 mylog.log.2020-03-29-16:26:11.0.gz (1)
-rw-r--r-- 1 jim staff 97 Mar 29 16:26 mylog.log.2020-03-29-16:26:12.0.gz
-rw-r--r-- 1 jim staff 97 Mar 29 16:26 mylog.log.2020-03-29-16:26:13.0.gz
-rw-r--r-- 1 jim staff 97 Mar 29 16:26 mylog.log.2020-03-29-16:26:14.0.gz
-rw-r--r-- 1 jim staff 97 Mar 29 16:26 mylog.log.2020-03-29-16:26:15.0.gz
-rw-r--r-- 1 jim staff 97 Mar 29 16:26 mylog.log.2020-03-29-16:26:16.0.gz
-rw-r--r-- 1 jim staff 79 Mar 29 16:26 mylog.log
-rw-r--r-- 1 jim staff 97 Mar 29 16:26 mylog.log.2020-03-29-16:26:17.0.gz
$ file target/logs/mylog.log.2020-03-29-16\:26\:16.0.gz
target/logs/mylog.log.2020-03-29-16:26:16.0.gz: \
gzip compressed data, from FAT filesystem (MS-DOS, OS/2, NT), original size 79
1 | historical logfiles are compressed when pattern uses a .gz suffix |
13.13. logging.file.max-history Example
logging.file.max-history
will constrain the number of files created for
independent timestamps. In the example below, I constrained the limit to 2.
Note that the logging.file.max-history
property does not seem to apply to
files terminated because of size. For that, we can use
logging.file.total-size-cap
.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=rollover \
--logging.file.name=target/logs/mylog.log \
--logging.file.max-size=250 \
--logging.pattern.rolling-file-name='${logging.file.name}.%d{yyyy-MM-dd-HH:mm:ss}.%i'.log \
--logging.file.max-history=2 (1)
...
$ ls -ltr target/logs
total 24
-rw-r--r-- 1 jim staff 80 Mar 29 17:52 mylog.log.2020-03-29-17:52:58.0.log (1)
-rw-r--r-- 1 jim staff 80 Mar 29 17:52 mylog.log.2020-03-29-17:52:59.0.log (1)
-rw-r--r-- 1 jim staff 80 Mar 29 17:53 mylog.log
1 | specifying logging.file.max-history limited number of historical logfiles. Oldest
files exceeding the criteria are deleted. |
13.14. logging.file.total-size-cap Index Example
The following example triggers file changeover every 1000 Bytes and makes use of the index because we encounter multiple changes per timestamp pattern.
The files are aged-off at the point where total size for all logs reaches logging.file.total-size-cap
.
Thus historical files with indexes 1 and 2 have been deleted at this point in time in order to stay below the file size limit.
java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=rollover \
--logging.file.name=target/logs/mylog.log \
--logging.file.max-size=1000 \
--logging.pattern.rolling-file-name='${logging.file.name}.%d{yyyy-MM-dd}.%i'.log \
--logging.file.max-history=20 \
--logging.file.total-size-cap=3500 (1)
...
$ ls -ltr target/logs
total 32 (2)
-rw-r--r-- 1 jim staff 1040 Mar 29 18:09 mylog.log.2020-03-29.2.log (1)
-rw-r--r-- 1 jim staff 1040 Mar 29 18:10 mylog.log.2020-03-29.3.log (1)
-rw-r--r-- 1 jim staff 1040 Mar 29 18:10 mylog.log.2020-03-29.4.log (1)
-rw-r--r-- 1 jim staff 160 Mar 29 18:10 mylog.log (1)
1 | logging.file.total-size-cap constrains current plus historical files retained |
2 | historical files with indexes 1 and 2 were deleted to stay below file size limit |
13.15. logging.file.total-size-cap no Index Example
The following example triggers file changeover every second and makes no use of the index because the timestamp pattern is so granular that max-size
is not reached before the timestamp changes the base.
As with the previous example, the files are also aged-off when the total byte count reaches logging.file.total-size-cap
.
$ java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--spring.profiles.active=rollover \
--logging.file.name=target/logs/mylog.log \
--logging.file.max-size=100 \
--logging.pattern.rolling-file-name='${logging.file.name}.%d{yyyy-MM-dd-HH:mm:ss}.%i'.log \
--logging.file.max-history=200 \
--logging.file.total-size-cap=500 (1)
...
$ ls -ltr target/logs Jamess-MacBook-Pro.local: Sun Mar 29 18:33:41 2020
total 56
-rw-r--r-- 1 jim staff 79 Mar 29 18:33 mylog.log.2020-03-29-18:33:32.0.log (1)
-rw-r--r-- 1 jim staff 79 Mar 29 18:33 mylog.log.2020-03-29-18:33:33.0.log (1)
-rw-r--r-- 1 jim staff 79 Mar 29 18:33 mylog.log.2020-03-29-18:33:34.0.log (1)
-rw-r--r-- 1 jim staff 79 Mar 29 18:33 mylog.log.2020-03-29-18:33:35.0.log (1)
-rw-r--r-- 1 jim staff 80 Mar 29 18:33 mylog.log.2020-03-29-18:33:36.0.log (1)
-rw-r--r-- 1 jim staff 80 Mar 29 18:33 mylog.log.2020-03-29-18:33:37.0.log (1)
-rw-r--r-- 1 jim staff 80 Mar 29 18:33 mylog.log (1)
1 | logging.file.total-size-cap constrains current plus historical files retained |
The logging.file.total-size-cap value — if specified — must be larger than
the logging.file.max-size constraint. Otherwise the file appender will not be
activated.
|
14. Custom Configurations
At this point, you should have a good foundation in logging and how to get started with a decent logging capability and understand how the default configuration can be modified for your immediate and profile-based circumstances. For cases when this is not enough, know that:
-
detailed XML Logback and Log4J2 configurations can be specified — which allows the definition of loggers, appenders, filters, etc. of nearly unlimited power
-
Spring Boot provides include files that can be used as a starting point for defining the custom configurations without giving up most of what Spring Boot defines for the default configuration
14.2. Provided Logback Includes
-
defaults.xml - defines the logging configuration defaults we have been working with
-
base.xml - defines root logger with CONSOLE and FILE appenders we have discussed
-
puts you at the point of the out-of-the-box configuration
-
-
console-appender.xml - defines the
CONSOLE
appender we have been working with-
uses the
CONSOLE_LOG_PATTERN
-
-
file-appender.xml - defines the
FILE
appender we have been working with-
uses the
RollingFileAppender
withFILE_LOG_PATTERN
andSizeAndTimeBasedRollingPolicy
-
These files provide an XML representation of what Spring Boot configures with straight Java code. There are minor differences (e.g., enable/disable FILE Appender) between using the supplied XML files and using the out-of-the-box defaults. |
14.3. Customization Example: Turn off Console Logging
The following is an example custom configuration where we wish to turn off console logging and only rely on the logfiles. This result is essentially a copy/edit of the supplied base.xml.
<!-- logging-configs/no-console/logback-spring.xml (1)
Example Logback configuration file to turn off CONSOLE Appender and retain all other
FILE Appender default behavior.
-->
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/> (2)
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/> (3)
<include resource="org/springframework/boot/logging/logback/file-appender.xml"/> (4)
<root>
<appender-ref ref="FILE"/> (5)
</root>
</configuration>
1 | a logback-spring.xml file has been created to host the custom configuration |
2 | the standard Spring Boot defaults are included |
3 | LOG_FILE defined using the original expression from Spring Boot base.xml |
4 | the standard Spring Boot FILE appender is included |
5 | only the FILE appender is assigned to our logger(s) |
14.4. LOG_FILE Property Definition
The only complicated part is what I copy/pasted from
base.xml to express the LOG_FILE
property used by the included FILE appender:
<property name="LOG_FILE"
value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
-
use the value of
${LOG_FILE}
if that is defined -
otherwise use the filename
spring.log
and for the path-
use
${LOG_PATH}
if that is defined -
otherwise use
${LOG_TEMP}
if that is defined -
otherwise use
${java.io.tmpdir}
if that is defined -
otherwise use
/tmp
-
14.5. Customization Example: Leverage Restored Defaults
Our first execution uses all defaults and is written to ${java.io.tmpdir}/spring.log
java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--logging.config=src/main/resources/logging-configs/no-console/logback-spring.xml
(no console output)
$ ls -ltr $TMPDIR/spring.log (1)
-rw-r--r-- 1 jim staff 67238 Apr 2 06:42 /var/folders/zm/cskr47zn0yjd0zwkn870y5sc0000gn/T//spring.log
1 | logfile written to restored default ${java.io.tmpdir}/spring.log |
14.6. Customization Example: Provide Override
Our second execution specified an override for the logfile to use. This is expressed exactly as we did earlier with the default configuration.
java -jar target/appconfig-logging-example-*-SNAPSHOT-bootexec.jar \
--logging.config=src/main/resources/logging-configs/no-console/logback-spring.xml \
--logging.file.name="target/logs/mylog.log" (2)
(no console output)
$ ls -ltr target/logs (1)
total 136
-rw-r--r-- 1 jim staff 67236 Apr 2 06:46 mylog.log (1)
1 | logfile written to target/logs/mylog.log |
2 | defined using logging.file.name |
15. Spring Profiles
Spring Boot extends the logback.xml capabilities to allow us to easily
take advantage of profiles. Any of the elements within the configuration file
can be wrapped in a springProfile
element to make their activation depend
on the profile value.
<springProfile name="appenders"> (1)
<logger name="X">
<appender-ref ref="X-appender"/>
</logger>
<!-- this logger starts a new tree of appenders, nothing gets written to root logger -->
<logger name="security" additivity="false">
<appender-ref ref="security-appender"/>
</logger>
</springProfile>
1 | elements are activated when appenders profile is activated |
See Profile-Specific Configuration for more examples involving multiple profile names and boolean operations.
16. Summary
In this module we:
-
made a case for the value of logging
-
demonstrated how logging frameworks are much better than
System.out
logging techniques -
discussed the different interface, adapter, and implementation libraries involved with Spring Boot logging
-
learned how the interface of the logging framework is separate from the implementation
-
learned to log information at different severity levels using loggers
-
learned how to write logging statements that can be efficiently executed when disabled
-
learned how to establish a hierarchy of loggers
-
learned how to configure appenders and associate with loggers
-
learned how to configure pattern layouts
-
learned how to configure the FILE Appender
-
looked at additional topics like Mapped Data Context (MDC) and Markers that can augment standard logging events
We covered the basics in great detail so that you understood the logging framework, what kinds of things are available to you, how it was doing its job, and how it could be configured. However, we still did not cover everything. For example, we left topics like accessing and viewing logs within a distributed environment, structured appender formatters (e.g., JSON), etc.. It is important for you to know that this lesson placed you at a point where those logging extensions can be implemented by you in a straight forward manner.