In my previous post, I described how to setup logging with docker and the Elasticstack. In this article, I will refer to the Elastic Stack (Elasticsearch and Kibana), but other tools work similarly.
With Elasticsearch in place, let’s discuss how to make better log statements to benefit from the searchable log aggregator. The examples are based on a Spring Boot application using the default logging framework Logback with SLF4J.
First, we enable JSON logging in Spring Boot. Then, we discuss how to write log statements to benefit from the structured log format.
Enable JSON logging
When using log aggregation and log indexing tools such as Elastic Stack, Splunk, or Graylog, it is best practice to generate logs in a structured format. Today, that format is JSON.
So, instead of (or additionally to) logging as traditional console output
the log output is generated as a single line JSON:
For readability, all JSON examples will be rendered in a pretty-printed form in this article:
The structured key-value format enables log collectors (such as filebeat, fluentd) to efficiently ship the data to Elasticsearch. There is no need to parse and extract data with tools like Grok, which error-prone to extract the correkt information, especially for multiline entries.
In Spring Boot, the default log implementation is Logback with SLF4J. JSON output is enabled through the Logstash JSON Encoder, which needs to be added as an additional dependency:
And enabled with an entry in the logback.xml configuration:
Tip: For unit tests, it is helpful to stay with the more compact traditional console output. This can be configured through Spring Profiles:
Structured Log Statements
OK, since JSON output is active now, let’s discuss how to write better log entries.
A traditional log statement often looks like this:
which produces
One problem with this log statement is that you cannot search for all “Order saved” entries, as the orderId is part of the message in between.
So let’s optimize it to:
which produces
Now we can use Kibana to issue a search for all Order saved
events or for orderId=123
to get all log statements for this orderId. However, this requires a full text search on the message field. It would be nice to have the orderId indexed in Elasticsearch as a separate field.
MDC
The traditional way is to use the Mapped Diagnostic Context (MDC), provided by all major logging frameworks:
or using try-with-resources:
which both produce
The LogstashEncoder writes all MDC entries to a separate JSON field. To show MDC entries in the classic console output, the appender layout pattern must be extended.
The MDC uses a ThreadLocal, so it is important to clear it after exiting the thread. The MDC is handy, it is complicated for multi-threaded applications and it does not work with reactive implementations out of the box, such as Spring WebFlux and Netty as requests are processed by multiple threads. See a workaround.
Structured Arguments
The logstash-logback-encoder library has the concept of event specific custom fields and brings Markers to add additional fields to the generated JSON output and StructuredArguments use these markers to add data to the JSON output and to the actual log message.
As markers have a terrible API, I highly recommend to use StructuredArguments. The normal {}-placeholders can be used, just wrap the data in a StructuredArguments.value()
or StructuredArguments.keyValue()
factory method:
which produces
The orderId
is both added to the message (through the {}
-placeholder) and to the JSON output as a separate field. This can be searched and displayed in Kibana:
Also, multiple fields are supported, it is optional to use them in the message, and there is a shorter kv
alias to use:
which produces
The value()
or v()
method does not write the key name to the message, while the field entry stays the same.
which produces
And there is a nice feature to log complex objects:
which produces a JSON representation of the object:
Summary
Always use JSON log output for complex applications and collect them to a log aggregator such as Elasticsearch.
Structured Arguments makes it easy to add additional data to the index with Spring Boot’s default logging framework Logback.
The main downside of using Structured Arguments is the compile dependency to logstash-logback-encoder
(which is required for JSON output anyway as runtime dependency).
Structured log statements make it much easier to find related logs with analyzing tools, such as Kibana.
View code examples on Github.
Header Photo by Jan Antonin Kolar on Unsplash.