Log generation best practices, with a Spring Boot example

What a good log line contains, a working Spring Boot setup with request ids, and how to send the logs to Graylog.

Good log analysis starts before anything is collected. If an application writes logs that are missing ids, mixed in time zones or full of secrets, no tool can make them easy to read later. This guide covers what to log and how to log it, using a Java Spring Boot service as the example. The ideas apply to any language, and the last section covers sending the logs to Graylog.

What makes a log line useful

A good log line answers four questions: when it happened, how serious it was, which part of the system wrote it, and which request or order it belongs to. Message text should describe the event in a stable form, so the same event always looks the same. Put the variable parts, such as ids and amounts, in named fields rather than building them into the message, so that you can group and filter them later.

A Spring Boot example

Spring Boot uses Logback by default. The example below has three parts: a filter that gives each request an id, a Logback configuration that puts that id on every line, and a service that logs a payment step with its outcome.

1. Give every request an id

The filter reads an id from the X-Request-Id header if the caller sent one, or makes a new one. It stores the id in MDC, a per-thread map that Logback can print on every line, and it removes the id when the request ends so that it cannot leak into the next request on the same thread. It also returns the id in the response, so a client can quote it when reporting a problem.

package com.example.checkout;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
public class RequestIdFilter extends OncePerRequestFilter {

    static final String HEADER = "X-Request-Id";
    static final String MDC_KEY = "requestId";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String id = Optional.ofNullable(request.getHeader(HEADER))
                .filter(s -> !s.isBlank())
                .orElseGet(() -> UUID.randomUUID().toString());
        MDC.put(MDC_KEY, id);
        response.setHeader(HEADER, id);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.remove(MDC_KEY);
        }
    }
}

2. Put the id and the time on every line

This Logback configuration writes readable lines to the console. The pattern puts an ISO timestamp with its offset first, then the level, the thread, the service name, the request id (or none outside a request), the logger name and the message. Save it as src/main/resources/logback-spring.xml. Spring Boot reads this file automatically.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <property name="APP_NAME" value="checkout"/>

  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX} %-5level [%thread] service=${APP_NAME} requestId=%X{requestId:-none} %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="CONSOLE"/>
  </root>
  <logger name="com.example.checkout" level="DEBUG"/>
</configuration>

If your logs are read by a program, JSON is easier to parse than a text pattern. The logstash-logback-encoder library writes one JSON object per line, and it includes every MDC value as a field. To use it, add the library to your build and replace the encoder above with net.logstash.logback.encoder.LogstashEncoder. Check the library's README for the current version and its setup steps.

3. Log the events, not the noise

The service below logs when a payment starts and how it ends. It uses placeholders ({}) instead of string concatenation, so the message is only built when the level is enabled. It logs the exception as the last argument, so the stack trace is kept. It logs ids and amounts, but never card numbers or tokens.

package com.example.checkout;

import java.time.Duration;
import java.util.concurrent.TimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    private static final Logger log = LoggerFactory.getLogger(PaymentService.class);
    private static final Duration TIMEOUT = Duration.ofSeconds(2);

    public void charge(String orderId, long amountCents) {
        long started = System.nanoTime();
        log.info("payment started orderId={} amountCents={}", orderId, amountCents);
        try {
            callGateway(orderId, amountCents);
            log.info("payment succeeded orderId={} durationMs={}", orderId, millisSince(started));
        } catch (TimeoutException e) {
            log.warn("payment timed out orderId={} timeoutMs={} durationMs={}",
                    orderId, TIMEOUT.toMillis(), millisSince(started));
            throw new IllegalStateException("payment timed out", e);
        }
    }

    private void callGateway(String orderId, long amountCents) throws TimeoutException {
        // the real call to the payment provider goes here
    }

    private static long millisSince(long startNanos) {
        return (System.nanoTime() - startNanos) / 1_000_000;
    }
}

Notice the three log lines. Each one has the same field names, so they can be searched and grouped together. The timeout is logged at WARN with its ids and then rethrown. The service does not log the stack trace itself, so the handler higher up the call stack logs it once, rather than every layer logging the same error.

Best practices

Sending the logs to Graylog

Once the application writes good lines, the logs need to reach Graylog. There are three common routes:

  1. GELF over TCP or UDP. The application sends structured messages straight to Graylog. Set up a GELF input in Graylog first, then point an appender at it. The Graylog GELF input documentation explains the input options, including the protocol, port and TLS setting.
  2. A log shipper reading the files. Filebeat, Fluent Bit or Fluentd reads the log files and forwards them. This keeps the application unchanged, and the shipper can add host and pod names. Check the shipper's documentation for the Graylog output it supports.
  3. Syslog. For servers and older systems, a Syslog input in Graylog receives the lines directly. It is simple, but it carries fewer structured fields than GELF or JSON.

In all three cases, the Graylog inputs documentation describes how to create the input and check that messages arrive. After the logs arrive, the Graylog extractors guide shows how to pull fields such as requestId out of plain text lines when the application does not send them as structured fields.

Check your setup

Before you rely on the logs in an incident, run a test. Send a request with a known X-Request-Id, then search Graylog for that id. If the search returns the request's lines from every service it touched, the setup works. If it returns nothing, check the time zone, the field names and whether the input is running.

Next steps

Once the logs arrive, you can read them in order with the guides on reading production logs across several services and sorting a Graylog export. For a CSV file exported from Graylog, the Log Sorter can put the lines in time order and filter them. The Log Playground opens several log files side by side.

All log analysis tools · All guides

Questions

Do I need Spring Boot to use these practices?

No. The practices apply to any language. The Spring Boot example shows one common way to put them into code.

Which Graylog input should I use?

GELF is the richest option for application logs, because it sends structured fields. Filebeat or Fluent Bit suits applications that already write files. Syslog suits servers. The Graylog inputs documentation describes how to set each one up.