Search code examples
jsonspringlogginglogbackslf4j

SLF4J Logging Exception as JSON or Single-Line String


I need to be able to log an Exception as a single record in my logs, which will make it much easier to investigate issues in Kibana / Elasticsearch. From what I can tell from the documentation for slf4j, the Logger interface requires messages to be Strings. Is my only option to remove newline characters from the Exception message before passing it to the Logger?

For context, I am using the following:

  • .m2/repository/org/slf4j/slf4j-api/1.7.28/slf4j-api-1.7.28.jar
  • Java 11
  • Sprint Boot version 2.1.8.RELEASE

This is a trimmed down version of my custom exception handler :

import my.error.Error; // custom Error class
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {

  private final Logger logger = LoggerFactory.getLogger(this.getClass());
  private void logError(Error error, Exception ex){
    logger.error(String.format("id: %s, message: %s", error.getId(), ex.getMessage()), ex);
  }

}

Initially I had attempted to alter the logging behavior with changes to my logback.xml file, within src/main/java/resources. Unfortunately, this appears to do nothing, so my assumption now is that the CustomExceptionHandler that I am creating is overruling the specification set in the logback.xml file. Specifically, the <pattern> of the <encoder> has been changed based on other research. It's attempting to replace all newline characters.

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" debug="true">
    <include resource="org/springframework/boot/logging/logback/base.xml" />

    <appender name="FILE-ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/gateway.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/archived/gateway/gateway.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <!-- each archived file, size max 5MB -->
            <maxFileSize>5MB</maxFileSize>
            <!-- total size of all archive files, if total size > 10GB, it will delete old archived file -->
            <totalSizeCap>10GB</totalSizeCap>
            <!-- 30 days to keep -->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d %p %c{1.} [%t] %m MULTIEXCEPTION %replace(%xException){'\n','\u2028'}%nopex%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="FILE-ROLLING" level="DEBUG" additivity="false"/>
    </root>

    <springProfile name="local">
        <logger name="my.gateway" level="TRACE" additivity="false">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="FILE-ROLLING" />
        </logger>
        <logger name="com.netflix" level="DEBUG" additivity="false">
            <appender-ref ref="CONSOLE" />
        </logger>
        <logger name="org.springframework" level="DEBUG" additivity="false">
            <appender-ref ref="CONSOLE" />
        </logger>
        <logger name="com" level="INFO" additivity="false">
            <appender-ref ref="CONSOLE" />
        </logger>
        <logger name="gov" level="INFO" additivity="false">
            <appender-ref ref="CONSOLE" />
        </logger>
        <logger name="org" level="INFO" additivity="false">
            <appender-ref ref="CONSOLE" />
        </logger>
    </springProfile>
</configuration>

Links


Solution

  • I changed my logError method to this:

    import org.slf4j.MDC; // new import to add property
    // ... other imports from before
    
      private void logError(Error error, Exception exception){
        MDC.put("error id", error.getId().toString()); // add a new property to the thread context
        LOGGER.error(exception.getMessage(), exception);
        MDC.clear(); // remove all new properties after logging the error
      }
    

    My new pom.xml File Dependencies:

      <dependencies>
        <!-- logging -->
        <dependency>
          <groupId>net.logstash.logback</groupId>
          <artifactId>logstash-logback-encoder</artifactId>
          <version>5.2</version>
        </dependency>
    
        <dependency>
          <groupId>ch.qos.logback.contrib</groupId>
          <artifactId>logback-jackson</artifactId>
          <version>0.1.5</version>
        </dependency>
        <!-- ch.qos.logback.contrib.json.classic.JsonLayout -->
        <dependency>
          <groupId>ch.qos.logback.contrib</groupId>
          <artifactId>logback-json-classic</artifactId>
          <version>0.1.5</version>
        </dependency>
        <!-- Other dependencies -->
        <!-- ... -->
        <!-- ... -->
      </dependencies>
    

    New logback.xml file:

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
      <appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="FILE-ROLLING">
        <!-- Old encoder is removed: 
        <encoder>
          <pattern>%d %p %c{1.} [%t] %m%n</pattern>
        </encoder>
        -->
        <file>${LOG_PATH}/data.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
          <fileNamePattern>${LOG_PATH}/archived/data/data.%d{yyyy-MM-dd}.%i.log.gz
          </fileNamePattern>
          <!-- each archived file, size max 10MB -->
          <maxFileSize>10MB</maxFileSize>
          <!-- total size of all archive files, if total size > 20GB, it will delete old archived file -->
          <maxHistory>60</maxHistory>
          <!-- 60 days to keep -->
          <totalSizeCap>20GB</totalSizeCap>
        </rollingPolicy>
        <!-- TODO : configure a pretty printer -->
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
          <provider class="net.logstash.logback.composite.loggingevent.ArgumentsJsonProvider"/>
        </encoder>
      </appender>
    
      <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!-- TODO : configure a pretty printer -->
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
          <provider class="net.logstash.logback.composite.loggingevent.ArgumentsJsonProvider"/>
        </encoder>
      </appender>
      <!-- The base logback configuration is removed, but note that this will also remove the default Spring start-up messages, so those need to be added back into the <root> 
      <include resource="org/springframework/boot/logging/logback/base.xml"/>
      -->
      <root level="INFO">
        <appender-ref ref="FILE-ROLLING"/>
        <appender-ref ref="CONSOLE"/> <!-- This adds Spring start-up messages back into the logs -->
      </root>
      <springProfile name="local">
        <logger additivity="false" level="TRACE" name="org.springframework.web">
          <appender-ref ref="CONSOLE"/>
        </logger>
      </springProfile>
      <springProfile name="dev">
        <logger additivity="false" level="DEBUG" name="org.springframework">
          <appender-ref ref="FILE-ROLLING"/>
        </logger>
      </springProfile>
      <springProfile name="prod">
        <logger additivity="false" level="DEBUG" name="my.app.path">
          <appender-ref ref="FILE-ROLLING"/>
        </logger>
        <logger additivity="false" level="DEBUG" name="org.jooq">
          <appender-ref ref="FILE-ROLLING"/>
        </logger>
        <logger additivity="false" level="DEBUG" name="org.springframework">
          <appender-ref ref="FILE-ROLLING"/>
        </logger>
      </springProfile>
    </configuration>
    

    SLF4J Docs - Mapped Diagnostic Context (MDC)
    Baeldung Example - MDC + Log4J2