Effective Way of Using Conditional Expressions in Logback

Today I’m going to show how to effectively use conditional expressions in logback.

This pattern has many use cases, for example:

  • Use different log output on server vs when application is running locally (JSON on server, regular logs on localhost)
  • Send ERRORs to your monitoring service - but only when application is deployed to production
  • Use different prefixes depending when application is running

Setup

In order to use conditional expressions in logback you need 2 things. Logback itself and Janino library.

Because I’m running on Java 11, I’m using following versions of the libraries:

  • logback-classic version 1.3.0-alpha4
  • janino version 3.0.12

This is the build.sbt snippet:

libraryDependencies ++ = Seq(
    "ch.qos.logback"             % "logback-classic"          % logbackVersion,
    "org.codehaus.janino"        % "janino"                   % janinoVersion
)

I recommand to adding a comment to explain the purpose of janino library because if you remove it (for example by mistake), your project will still compile but logging will be broken.

Example logback.xml

My comments are below

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.out</target>
        <if condition='isDefined("KUBERNETES_POD_NAME")'>
            <then>
                <encoder class="net.logstash.logback.encoder.LogstashEncoder">
                    <timeZone>UTC</timeZone>
                </encoder>
            </then>
            <else>
                <encoder>
                    <pattern>%d{"yyyy-MM-dd'T'HH:mm:ss.SSS"} [%thread] %-5level %logger{36} - %msg%n</pattern>
                </encoder>
            </else>
        </if>
    </appender>

    <logger name="com.orbitz.consul" level="INFO"/>

    <if condition='isDefined("ERROR_COLLECTION_SERVER")'>
        <then>
            <appender name="ERROR_COLLECTION" class="com.errors.logback.ErrorAppender">
                <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                    <level>ERROR</level>
                </filter>
                <endpoint>http://$ERROR_COLLECTION_SERVER/error</endpoint>
            </appender>
        </then>
    </if>

    <root level="DEBUG">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

In the example above I’ll be using 2 environment variables:

  • KUBERNETES_POD_NAME - is a made up name that will be set by Kubernetes when my application is running on the k8s cluster
  • ERROR_COLLECTION_SERVER- points to the server that collects errors from applications running on production

When an application is running on production k8s cluster, k8s will set values for those 2 environment variables. I use them to detect this scenario and configure my logging accordingly.

When KUBERNETES_POD_NAME is set, logs will be generated as JSON using LogstashEncoder, otherwise logs will be generated as regular text. This allows for easy readability of logs when testing locally, but on production logs can follow agreed on schema.

When ERROR_COLLECTION_SERVER is set, a ERROR level filter is applied and all errors are sent to apropirate server for analysis or monitoring. When running locally, this variable isn’t set, effectively disabling this feature.

Summary

The pattern described above is a simple and straightforward way to configure logging in your JVM application automatically based on different conditions.

I find it very easy and try to follow where possible to achieve best flexibility when running different applications in multiple environments like Kubernetes, Nomad, localhost, etc.

I didn’t find any drawbacks of this method, the only thing to keep in mind is the janino dependency that has to be added to your build setup.