Production-ready Clojure Logging

Logging in java is a mess. There’s a fascinating history to it all, but practically we need to just get going. I couldn’t find an article on setting up Clojure logging for production, so I figured I’d make a short article to document an approach.

Too long; didn’t read
{:deps
 {org.clojure/tools.logging {:mvn/version "1.1.0"}
  org.slf4j/jcl-over-slf4j {:mvn/version "1.7.25"}
  org.slf4j/jul-to-slf4j {:mvn/version "1.7.25"}
  org.slf4j/log4j-over-slf4j {:mvn/version "1.7.25"}
  ch.qos.logback/logback-classic {:mvn/version "1.2.3"}}
 :aliases
 {:dev
  {:extra-paths ["dev"]
   :extra-deps
   {com.stuartsierra/log.dev {:mvn/version "0.2.0"}}}
  :release {:extra-paths ["release"]}}}
In user.clj and main.clj:
(org.slf4j.bridge.SLF4JBridgeHandler/removeHandlersForRootLogger)
(org.slf4j.bridge.SLF4JBridgeHandler/install)
dev/log_dev_app.properties
app_root_logger=your.ns.prefix

Finally, create a release/logback.xml. Use tools.logging to make logs.

Due to the aforementioned history of logging on the JVM, there’s plenty of different ways logging happens. This is particularly pertinent when using Java libraries, as they’ll have made their own individual choices. The trick is to route them all into a common logging façade, and from there use a single output. You can think of the façade as a "logging protocol" which there are multiple implementations of.

slf4j has bridges for all the major logging frameworks in use. You can add implementations for almost all of them like so:

deps.edn
{:deps
 {org.slf4j/jcl-over-slf4j {:mvn/version "1.7.25"}
  org.slf4j/jul-to-slf4j {:mvn/version "1.7.25"}
  org.slf4j/log4j-over-slf4j {:mvn/version "1.7.25"}}

I say almost, because it is not sufficient to only add a dependency on "org.slf4j/jul-to-slf4j". "jul" is short for java.util.Logging, which cannot be replaced as it’s built into the JVM[1]. There is a performance impact to enabling "jul-to-slf4j" too, but I’d default to on (more logs is a safer default!) and you can always disable it later if you have problems (I’m yet to have a problem where logs are the bottleneck!).

In order to activate the jul bridge, we must execute code at runtime. I suggest placing this in your dev/user.clj so it is activated automatically during development, and in your main.clj (the entrypoint you use in production to start your project).

(org.slf4j.bridge.SLF4JBridgeHandler/removeHandlersForRootLogger)
(org.slf4j.bridge.SLF4JBridgeHandler/install)

For the output, you should use logback. Sure, there’s alternatives, and yes, it uses XML. But logback is solid, fast, has lots and lots of appenders (places to output logs) as well as sophisticated rotation configuration.

deps.edn
{:deps
 {ch.qos.logback/logback-classic {:mvn/version "1.2.3"}}}

You’ll also need a logback.xml.

For development I suggest log.dev (or just the logback.xml). It’ll perform some rotation for the logs so they don’t build up forever, and filters your application logs for you.

Add a dependency on it to a :dev alias, or put the logback.xml in dev/logback.xml.

deps.edn
{:aliases
 {:dev
  {:extra-paths ["dev"]
   :extra-deps
   {com.stuartsierra/log.dev {:mvn/version "0.2.0"}}}}}

Then you need to tell it which prefix of namespaces belong to your application. These will be filtered so that log/app.log contains only logs from your application.

dev/log_dev_app.properties
app_root_logger=your.ns.prefix

For a production, staging etc. logback.xml, it will be highly dependent on where you’re deployed. Elastic Beanstalk favours STDOUT and STDERR for logging. If you’re using Rollbar or Sentry, they have their own respective logback appenders which you should look into. Some deployment environments may give you a specific file to write to. If you’re on EC2, writing to CloudWatch is a good idea as it centralizes your logs. Unfortunately, this is one place where I don’t have a go-to file, it varies based on too many other choices.

Either way, place the logback.xml in a folder named release, which contains files you only use in production, staging etc. Final file will be release/logback.xml.

deps.edn
{:aliases
 {:release
  {:extra-paths ["release"]}}}

Finally, to make your application logs use tools.logging. It’s got a very simple API, and will default to logging to slf4j (which is the façade we chose earlier).

(ns my.ns
  (:require
    [clojure.tools.logging :as log]))

(defn do-thing
  []
  (log/info "Doing a thing"))

A tip I recently learned is the ".readable" namespace for logging, which logs with pr instead of print:

(ns my.ns
  (:require
    [clojure.tools.logging.readable :as logr]))

(defn do-thing
  [m]
  (log/info "Normal: %s" m)
  (logr/infof "Readable: %s" m))

;; (do-thing {:a "foo"})
;; Log will be:
;; Normal: {:a foo}
;; Readable: {:a "foo"}

There, you have it, a fully functional production-ready (ok, short of a logback.xml) setup for logging in Clojure.


1. Yes, there’s a built in logging system but it’s rarely used