This article will help you build a Spring Boot Java application to log messages with Log4j and use the MDC of this library (Mapping Diagnostic Context) to add contextualization data in addition to messages, especially for asynchronous tasks.
Let’s start by generating a classic Spring Boot application with the built-in Log4j library. This library allows us to use a logger that generates log messages of different types (info, error, warning, …)
pom.xml
...
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
...
src/main/resources/log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="FATAL">
<Appenders>
<Console name="Console" target="SYSTEM_OUT" ignoreExceptions="true">
<PatternLayout pattern="%clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint} %clr{%5p} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{%X}{faint} %clr{:}{faint} %m%n%xwEx"> </PatternLayout>
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
In the current state, if we launch the application (via an IDE for example or using Maven), no log message appears. We will create a component that uses the Log4j library to log an information message.
Create a src/main/java/com/sipios/example/mdc/Execute.java file with the code below. The package name is here com.sipios.example.mdc, of course, it should be replaced by yours :)
src/main/java/com/sipios/example/mdc/Execute.java
package com.sipios.example.mdc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class Execute implements CommandLineRunner {
private final Logger log = LoggerFactory.getLogger(Execute.class);
@Override
public void run(String... args) {
log.info("Test Log4J");
}
}
If you run the application, a first log message respecting the defined format is now displayed. It is also possible to use the error
and warning
methods. Log messages corresponding to these types will be displayed.
-05-23 09:12:49.115 INFO 15365 --- [ main] c.s.e.m.Execute {} : Test Log4j
Now that we know how to use the Log4j library, we will be able to use the Mapping Diagnostic Context (MDC) which allows us to associate a data Map with our message. Some examples of data we recommend you put in the MDC:
This Map is displayed in the logs if the %X
mask is used in the Log4j message format definition. This is the case here in our src/main/resources/log4j2.xml file. In the previous execution, we see {}
, which indicates an empty Map.
Using MDC is very simple and is used like a classic Map via put
, get
, remove
, clear
methods... Let's add two entries to the MDC and execute the application.
src/main/java/com/sipios/example/mdc/Execute.java
package com.sipios.example.mdc;
...
import org.slf4j.MDC;
@Component
public class Execute implements CommandLineRunner {
...
public void run(String... args) {
MDC.put("request_id", "128");
MDC.put("application", "runner");
log.info("Test Log4j with MDC");
}
}
-05-23 09:14:18.584 INFO 15376 --- [ main] c.s.e.m.Execute {application=runner, request_id=128} : Test Log4j
2018-05-23 09:14:18.586 INFO 15376 --- [ main] c.s.e.m.Application {application=runner, request_id=128} : Started Application in 1.061 seconds (JVM running for 2.156)
MDC is global and it’s preserved as long as it is not modified. If you want to empty it by leaving a component for example, just use the clear
method. This would then give the following result.
-05-23 09:14:18.584 INFO 15376 --- [ main] c.s.e.m.Execute {application=runner, request_id=128} : Test Log4j
2018-05-23 09:14:18.586 INFO 15376 --- [ main] c.s.e.m.Application {} : Started Application in 1.061 seconds (JVM running for 2.156)
Let’s try MDC with asynchronous components (a component executed on a parallel thread of the main thread)! First of all, we have to configure our application to execute such beans. We create a service with two methods, one is synchronous and the other asynchronous.
src/main/java/com/sipios/example/mdc/Application.java
package com.sipios.example.mdc;
...
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class Application
{
...
}
src/main/java/com/sipios/example/mdc/service/Example.java
package com.sipios.example.mdc.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class Example {
private static final Logger log = LoggerFactory.getLogger(Example.class);
public void logNormalThread() {
log.info("normal thread");
}
@Async
public void logAsyncThread() {
log.info("async thread");
}
}
src/main/java/com/sipios/example/mdc/Execute.java
package com.sipios.example.mdc;
...
import com.sipios.example.mdc.service.Example;
@Component
public class Execute implements CommandLineRunner {
private Example example;
public Execute(Example example) {
this.example = example;
}
@Override
public void run(String... args) {
MDC.put("request_id", "128");
MDC.put("application", "runner");
example.logNormalThread();
example.logAsyncThread();
}
}
-05-23 09:28:39.218 INFO 15403 --- [ main] c.s.e.m.s.Example {application=runner, request_id=128} : normal thread
2018-05-23 09:28:39.221 INFO 15403 --- [ main] .s.a.AnnotationAsyncExecutionInterceptor {application=runner, request_id=128} : No TaskExecutor bean found for async processing
2018-05-23 09:28:39.225 INFO 15403 --- [cTaskExecutor-1] c.s.e.m.s.Example {} : async thread
Add taskExecutor in Application class
src/main/java/com/sipios/example/mdc/Application.java
public class Application
{
...
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.initialize();
return executor;
}
}
Re-execute :
-05-23 09:30:41.852 INFO 15406 --- [ main] c.s.e.m.s.Example {application=runner, request_id=128} : normal thread
2018-05-23 09:30:41.855 INFO 15406 --- [asyncExecutor-1] c.s.e.m.s.Example {} : async thread
As we can see, MDC in async thread is empty. Indeed each asynchronous task is started in a new thread. Each thread is linked to a Map MDC initiated by the task executor. It is possible to play on this executor to obtain the same MDC as on the main thread. Let’s add a decorator in asyncExecutor in order to duplicate MDC!
src/main/java/com/sipios/example/mdc/ExampleTaskDecorator.java
package com.sipios.example.mdc;
import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;
import java.util.Map;
public class ExampleTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<string, string=""> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
MDC.setContextMap(contextMap);
runnable.run();
} finally {
MDC.clear();
}
};
}
}
Set this decorator in async config
src/main/java/com/sipios/example/mdc/Application.java
public class Application
{
...
public Executor asyncExecutor() {
...
executor.setTaskDecorator(new ExampleTaskDecorator());
return executor;
}
}
Relaunch application
-05-23 09:47:37.414 INFO 15447 --- [ main] c.s.e.m.s.Example {application=runner, request_id=128} : normal thread
2018-05-23 09:47:37.419 INFO 15447 --- [asyncExecutor-1] c.s.e.m.s.Example {application=runner, request_id=128} : async thread
There you go! We can convey in the logs all the context that we want in synchronous or asynchronous tasks.
Thanks to this, debugging an application is simplified and more understandable. As part of our project this has saved us a lot of time in the exchanges with our contributors. It’s up to you now :)