Spring Boot Logging using Log4J2
- Digital Engineering
Spring Boot Logging using Log4J2
What is Logging?
Logging is defined as storing some key information about a process or application in a file or database. Like date and time, filename, some messages etc.
Why do we need to log in to our application:
Logging is essential in every robust application. If there will be any issue or you want to extract some old information then logging will help you definitely. You can store some custom messages for particular actions, it will help you to identify bugs in your application.
There are many approaches to store your logs like log4j, SLF4J, Log4j2. But I will prefer you to Log4j2 and now in this article, we will learn how to apply log4j2 in our spring-boot application and what are the key points to be taken care of while logging. In this article, we will also learn how to save your logs in a NoSql database like MongoDB.
Why should we use Log4j2?
- Log4j2 is an advanced version of the very popular library log4j.
- It contains all features of log4j and added some more features.
- It handles both synchronous and asynchronous logging efficiently.
Configuring Log4j2:
Till now we see that log4j2 is a good logging framework but the most crucial thing is how we will implement it in our application. Click here to go to Log4j2 official documentation.
What if we are already using log4j then how would we upgrade to log4j2?
We will cover every point in this article. So, let’s start the fun.
Basic Log4j2 Configuration:
We need to add the following dependencies to our pom.xml file to start with Log4j2:
1 2 3 4 5 6 7 8 9 10 |
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.14.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.14.1</version> </dependency> |
Note: if we are already using log4j then we can replace 1st dependency with the following dependency so you will need not update your logger declaration everywhere in your application:
1 2 3 4 5 |
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-1.2-api</artifactId> <version>2.3</version> </dependency> |
Now to use logs in your file you have to define an object of Log4j2 in your file.
Create an instance of Logger using LogManager class. Import Logger from org.apache.logging.log4j package.
1 |
private static Logger logger = LogManager.getLogger(MyService.class); |
Now you are ready to use basic logs in your application:
1 |
logger.info(“This is an info message from log4j2”); |
Log4j2 custom configuration:
In this section, we will learn how to print logs to the console, save them in a file, and NoSQL database.
First of all, we need to create a log4j2.xml file in our classpath. Our log4j2 is configured using this file only.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<?xml version="1.0" encoding="UTF-8"?> <Configuration packages="com.test.log4j" status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c:%L - %m%n"/> </Console> <File name="File" fileName="/tmp/TestLogs.log" append="true"> <PatternLayout> <Pattern>%d{yyyy-MM-dd HH:mm:ss} %-5p %c:%L - %m%n</Pattern> </PatternLayout> </File> <NoSql name="Mongo4"> <MongoDb4 capped="true" collectionSize="104857600" connection="mongodb://localhost:27017/Logs.log4j2" /> </NoSql> </Appenders> <Loggers> <Logger name="com.test.log4j2" level="info" additivity="true"> <AppenderRef ref="Console"/> </Logger> <Root level="info"> <AppenderRef ref="File"/> </Root> <Logger name="com.test.log4j2" level="info"> <AppenderRef ref="Mongo4"/> </Logger> </Loggers> </Configuration> |
Now let’s understand the tags that we used in our configuration file:
- Configuration:
- It is the root element of our configuration file. We write all configurations under this tag.
- Appenders:
- This tag contains all the appenders like ConsoleAppender, FileAppender, JDBCAppender, NoSqlAppender etc.
- Loggers:
- This tag consists of all logger instances and root is a standard logger which outputs all messages whether it is system_out or manually logged messages.
In the next section, we will learn how to configure appenders for effective logging.
Configure Appenders:
In log4j2, Appenders are those who deliver log events to their destination.
There are some predefined appenders:
-
- ConsoleAppender: Write logs to the system console.
- RollingFileAppender: Write logs to a defined file and rolls data according to a defined policy.
- AsyncAppender: It encapsulates other appenders and writes data in a separate thread.
- JDBCAppender: It writes logs to a relational database.
Log4j2 Layouts:
Layouts are used to define a particular pattern to write logs on their destination. Or we can say layouts are used to define how logs will be formatted.
Mostly used log4j2 layouts are :
- PatternLayout: It configures logs in a string pattern.
- JsonLayout: It is defined for writing logs in JSON format.
- CsvLayout: It is defined for writing logs in CSV format.
- HTMLLayout: It is used to write data in HTML format.
- XMLLayout: It is used to write data in XML format.
PatternLayout:
It is a flexible and simple way to define a layout for logs. We only have to write the following line in our appender.
1 |
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c:%L - %m%n"/> |
Components of PatternLayouts are :
- %d{yyyy-MM-dd HH:mm:ss}: It will print the date and time of the log event.
- %5-p: It means the priority of the logging event should be left-justified to a width of five characters.
- %c: It defines the class name of the log event.
- %L: It defines line numbers.
- %m: It writes the log message.
JSONLayout:
Logging data in JSON format has many advantages like it is easier to analyze JSON data in logging tools.
To configure our log4j2 to output Logs in JSON format we will have to add the following dependency to our pom.xml format.
1 2 3 4 5 |
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.8.8.1</version> </dependency> |
To configure the appender to JSON layout we can simply define the below tag:
1 |
<JSONLayout complete="true" compact="false"/> |
It will produce a well-formed JSON format of log events.
Configure Loggers:
As we all know ROOT is a standard logger in log4j2. In log4j2 we can also define additional Logger tag with several log levels, appenders or filters.
Syntax to define a Logger is as follows:
1 2 3 4 5 6 7 8 |
<Loggers> <Logger name="com.test.log4j2" level="info" additivity="true"> <AppenderRef ref="Console"/> </Logger> <Root level="info"> <AppenderRef ref="File"/> </Root> </Loggers> |
Here in the Loggers element, we can define our root logger and other loggers with level and appenders as well.
Configure Log4j2 for MongoDB:
Here we are on our most important topics. I was working on a project and in my project, I had to log all events in MongoDB, and then I made my mind to write a blog on it so others can get help.
To achieve logs in mongo first we have to add the following dependencies in our pom.xml file:
1 2 3 4 5 6 7 8 9 10 |
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-mongodb4</artifactId> <version>2.14.0</version> </dependency> <dependency> <groupId>org.mongodb</groupId> <artifactId>mongodb-driver-sync</artifactId> <version>4.1.1</version> </dependency> |
After we configure our pom.xml it’s time to configure our log4j2.xml file. In this, we have to define an appender for NoSql where we will set up our MongoDb credentials.
You can check out the official documentation of Log4j2 NoSql here.
1 2 3 4 5 |
<NoSql name="Mongo4"> <MongoDb4 capped="true" collectionSize="104857600” connection="mongodb:/username:password@hostname:27017/Dbname.CollectionName" /> |
Note: If your mongodb password contains characters like !$@ etc. then you have to escape these characters by url encoding your password.
Now you can log your messages to MongoDB. It works but now the problem starts when you try to write your exception in MongoDB.
1 2 3 4 5 |
try { throw new RuntimeException("Test exception"); } catch (Exception e) { logger.error("Logging the exception", e); } |
Whenever you try to log an exception you will get the following error.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Log4j2-TF-3-AsyncLoggerConfig-3 ERROR An exception occurred processing Appender Mongo4 org.apache.logging.log4j.core.appender.AppenderLoggingException: Unable to write to database in appender: Can't find a codec for class org.apache.logging.log4j.mongodb4.MongoDb4DocumentObject. at org.apache.logging.log4j.core.appender.db.AbstractDatabaseAppender.append(AbstractDatabaseAppender.java:118) at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156) … 13 more Caused by: org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for class org.apache.logging.log4j.mongodb4.MongoDb4DocumentObject. at org.bson.internal.CodecCache.lambda$getOrThrow$1(CodecCache.java:52) at java.util.Optional.orElseThrow(Optional.java:290) at org.bson.internal.CodecCache.getOrThrow(CodecCache.java:51) at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:64) at org.bson.internal.ChildCodecRegistry.get(ChildCodecRegistry.java:52) |
Problem:
Generally, the MongoDB driver allows different types of data to be written to the database. For non-standard structures, however, we need to register a codec class that will allow conversion of the log objects to the document format, which is the basic structure of the DB. An example of such a codec is the org.apache.logging.log4j.mongodb4.MongoDb4LevelCodec provided by log4j.
The logger is based on the MongoDb4DocumentObject class that implements the org.apache.logging.log4j.core.appender.nosql.NoSqlObject.NoSqlObject <org.bson.Document> interface. This allows you to create a native document that will be saved in the database. In case of errors logging, MongoDb4DocumentObject objects are added as stacktrace, which are not converted into a document during the saving process.
Solution:
To overcome this problem We have to register a custom codec class. Now we have to copy the implementation provided by log4j and replace it with our custom class.
We have to replace the codec list in org.apache.logging.log4j.mongodb4.MongoDb4Provider class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
private static final CodecRegistry CODEC_REGISTRIES = CodecRegistries.fromRegistries( // NEW: CodecRegistries.fromCodecs(new Codec<MongoDbDocumentObject>() { private Codec<Document> documentCodec = new DocumentCodec(); @Override public void encode(BsonWriter writer, MongoDbDocumentObject value, EncoderContext encoderContext) { documentCodec.encode(writer, value.unwrap(), encoderContext); } @Override public Class<MongoDbDocumentObject> getEncoderClass() { return MongoDbDocumentObject.class; } @Override public MongoDbDocumentObject decode(BsonReader reader, DecoderContext decoderContext) { MongoDbDocumentObject object = new MongoDbDocumentObject(); Document document = documentCodec.decode(reader, decoderContext); for (var entry : document.entrySet()) { object.set(entry.getKey(), entry.getValue()); } return object; } }), // OLD: CodecRegistries.fromCodecs(LevelCodec.INSTANCE), MongoClient.getDefaultCodecRegistry()); |
We have to create a class in our project and paste the content of org.apache.logging.log4j.mongodb4.MongoDb4Provider class and replace the codec list with the above. And we have to configure the package of the custom class in the log4j configuration file.
Log4j MDC :
The Map Diagnostic Context (MDC) is a map that is used to store the context data of a particular thread. We can store any important information which could be used to identify a particular application. Like user data or steps of the algorithm etc.
We have to simply put the key-value pair in MDC.
Syntax:
1 |
MDC.put(“key”, “value”); |
To extract info from MDC we use %X in our log4j2.xml file.
1 |
%X{key}-%d{yyyy-MM-dd HH:mm:ss} %-5p %c:%L - %m%n |
Asynchronous Logging:
Now when we are all set to write logs in the console, file, and even database but if we have high logging overhead then all we need to do is save logs using a separate thread in asynchronous mode. If we have a high CPU count on our server, asynchronous logging is best to reduce logging overhead on our system.
Now to achieve asynchronous logging we need to add the following dependency in our pom.xml file:
1 2 3 4 5 |
<dependency> <groupId>com.lmax</groupId> <artifactId>disruptor</artifactId> <version>3.4.2</version> </dependency> |
We can achieve async logging in many ways like by making complete logging asynchronous or we can use hybrid logging(sync+async).
To achieve complete asynchronous logging we will have to set log4j2.contextSelector system property to org.apache.logging.log4j.core.async.AsyncLoggerContextSelector.
We can do this by setting VM arguments in the runtime configuration of our application.
To achieve hybrid logging we can use asyncLogger in our log4j2.xml file. You can check the official document of Log4j AsyncLogger here.
1 2 3 4 5 |
<Loggers> <asyncLogger name="com.test.log4j2" level="info" includeLocation="true"> <AppenderRef ref="Mongo4"/> </asyncLogger> </Loggers> |
Note: In asyncLogger, you should always set property includeLocation=”true”, else if you are logging into a file it won’t get line number.
That’s it, now you are all set to log your messages.
Related content
Auriga: Leveling Up for Enterprise Growth!
Auriga’s journey began in 2010 crafting products for India’s