Clojure Ring Logging

A quick tutorial on how to set up request logging for a Clojure web app backend when using Ring and Jetty adapter.

First, create a new project with clj-new.

clojure -Tclj-new app :name acme/app

And then add the Ring dependencies to the deps.edn file.

<  :deps {org.clojure/clojure {:mvn/version "1.11.1"}}
---
>  :deps {org.clojure/clojure {:mvn/version "1.11.1"}
>         ring/ring-core {:mvn/version "1.6.3"}
>         ring/ring-jetty-adapter {:mvn/version "1.6.3"}
>         ring-logger/ring-logger {:mvn/version "1.1.1"}}

Next, set up the clojure tools logging with log4j2 logging backend.

Update the deps.edn file by adding clj-log4j2 dependency and set:jvm-opts

5c5,6
<         ring-logger/ring-logger {:mvn/version "1.1.1"}}
---
>         ring-logger/ring-logger {:mvn/version "1.1.1"}
>         clj-log4j2/clj-log4j2 {:mvn/version "0.4.0"}}
7c8,9
<  {:run-m {:main-opts ["-m" "acme.app"]}
---
>  {:run-m {:main-opts ["-m" "acme.app"]
>           :jvm-opts ["-Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory"]}

Create a file resources/log4j2.properties and save the file with the following content. Read more about the configuration options from the here.

status = warn
monitorInterval = 5

appender.console.type = Console
appender.console.name = STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %date %level:%logger: %message%n%throwable

rootLogger.level = debug
rootLogger.appenderRef.stdout.ref = STDOUT

Next wrap the ring handler with the wrap-with-logger middleware to log the requests. Configuration instructions can be found from the library github page.

(ns acme.app
  (:require [clojure.tools.logging :as logging]
            [ring.middleware.params]
            [ring.adapter.jetty :as jetty]
            [ring.logger :as logger])
  (:gen-class))

(def port 3000)

(defn ring-handler [_request]
  (logging/info "Info message")
  (logging/debug "Debug message")
  (logging/warn "Warn message")
  {:status 200
   :body "OK"})

(defonce server (atom nil))

(def app
  (-> #'ring-handler
      logger/wrap-with-logger))

(defn start! []
  (logging/info "Listening on port: " port)
  (reset! server
          (jetty/run-jetty #'app  {:port port})))

(defn -main []
  (start!))

Now you are ready to run the server with clj -M:run-m and you should see something like this in your console when the API is called with a POST request.

❯ clj -M:run-m
2023-08-12 14:23:50.358:INFO::main: Logging initialized @1171ms
2023-08-12 14:23:50,440 INFO:acme.app: Server starting with arguments: {}
2023-08-12 14:23:50.452:INFO:oejs.Server:main: jetty-9.2.21.v20170120
2023-08-12 14:23:50.472:INFO:oejs.ServerConnector:main: Started ServerConnector@4602f874{HTTP/1.1}{0.0.0.0:3000}
2023-08-12 14:23:50.472:INFO:oejs.Server:main: Started @1284ms
2023-08-12 14:23:51,716 INFO:ring.logger: {:request-method :post, :uri "/", :server-name "localhost", :ring.logger/type :starting}
2023-08-12 14:23:51,717 INFO:acme.app: Info message
2023-08-12 14:23:51,717 WARN:acme.app: Warn message
2023-08-12 14:23:51,717 INFO:ring.logger: {:request-method :post, :uri "/", :server-name "localhost", :ring.logger/type :finish, :status 200, :ring.logger/ms 1}

That's about it for the minimal setup. Read more about advanced configuration from the sources.

Thanks for reading, hope you found this helpful.