Converting Clojure Logging to Timbre
I have been looking into the timbre logging system for Clojure as it's written by one of the very best Clojure devs I have known, and it has a lot going for it - async logging, loads of appenders, including ones that are log aggregators, which is really convenient, but I've been having some issues the last few days with configuration and errors.
Specifically, the configuration is not in a file - like log4j and slf4j, it's in the Clojure code, and that was a bit of a wrinkle. But once I figured that out, and started to dig into the code for the details I needed, it got a lot easier.
So let's go through my conversion to timbre from log4j and slf4j, and see what it took.
Dependencies
Since I an using jetty as the backbone of the web server, I needed to give slf4j a way to send logging to timbre, and that meant just a few dependencies:
:dependencies[... [com.taoensso/timbre "6.6.1"] [org.slf4j/slf4j-api "2.0.17"] [com.taoensso/timbre-slf4j "6.6.1"] ...]
where the last two provide that conduit free of charge by their simple inclusion. That's one less headache right off the bat. 🙂
Calling Changes
Here, the changes are pretty simple... where I would have had:
(ns my-app (:require [clojure.tools.logging :refer [error info infof]] ...))
I simply change the namespace I'm referring in the functions from to be:
(ns my-app (:require [taoensso.timbre :refer [error info infof]] ...))
and everything stays the same. Pretty nice.
Configuration
Here is where it can be done in a lot of ways, but I chose to have a single function to set up the logging based on the configuration of the instance - and have it all in one place. In the :main namespace of the app, I added:
(ns my-app (:require [taoensso.timbre :refer [merge-config! error info infof]] [taoensso.timbre.appenders.community.rotor :refer [rotor-appender]] ...)) (defn init-logs! "Function to initialize the Timbre logging system, which can be based on the config of this running instance. It will basically disable the default things we do not want, and add those things that we do, and will be called when we start a REPL, or the app itself. This will modify the Timbre *config*, and so we have the bang at the end of the name." [] (merge-config! {:min-level :info :appenders {:println {:enabled? false} :rotor (merge (rotor-appender {:path "log/my-app.log"}) {:async? true})}}))
This does a few things for me:
- Turn off the console appender - we don't need the :println appender, so merge in the "off" for that guy.
- Add rotate file appender - have this do asynchronous calls as well, and we shouldn't have to worry about the shutdown for now.
- Point to the file(s) location - we really need to tell it where to dump the log files.
At this point, we need to add this to the start of the app, and for me that's in the handle-args function of the same namespace:
(defn handle-args "Function to parse the arguments to the main entry point of this project and do what it's asking. By the time we return, it's all done and over." [args app] (init-logs!) ;; initialize the logging from the config (let [[params [action]] (cli args ["-p" "--port" "Listen on this port" :default 8080 :parse-fn #(Integer. %)] ["-v" "--verbose" :flag true]) quiet? (:quiet params) ignore? (:ignore params) reports? (:reports params)] ...))
And in order to fire init-logs! off when a REPL is started, we just need to update our project.clj file, as we're using Leiningen:
:repl-options {:init (init-logs!)}
and Leiningen will know to look in the :main namespace for that function. But it could have been anywhere in the codebase, if properly called.
Possible Issues
In doing the conversion, I had one problem with the log function from clojure.tools.logging. The original code was:
(let [logf (fn [s & args] (set-mdc!) (log ns level nil (apply format s args)))] ...)
and the log function in timbre doesn't have a matching one with the same arity, so I had to collapse it back to:
(let [logf (fn [s & args] (set-mdc!) (log level (apply format s args)))] ...)
and the timbre function worked just fine.
All tests worked just fine, the logging is solid and stable, and I'm sure I'll enjoy the flexibility that I can get with the additional appenders that I can add in init-logs! for testing and production when we get to that point. Very nice update! 🙂