Trove
A minimal, modern logging facade for Clojure/Script. It's a lightweight alternative to tools.logging, designed for library authors who want rich logging without forcing users to adopt a specific backend.
Trove supports both traditional string-based logging and structured data-oriented logging, with rich filtering capabilities (by namespace, id, level, data, etc.). It's tiny (1 macro, 0 deps, ~100 loc) and works with Clojure, ClojureScript, GraalVM, and Babashka.
Setup
deps.edn:
com.taoensso/trove {:mvn/version "1.1.0"}
Leiningen:
[com.taoensso/trove "1.1.0"]
See https://clojars.org/com.taoensso/trove for the latest version.
Quick Start
Library author usage (emitting logs):
(ns my-library.core
(:require [taoensso.trove :as trove]))
;; Traditional logging (string messages)
(trove/log! {:level :info, :msg "User logged in"})
(trove/log! {:level :warn, :msg "Connection retry attempt"})
;; Structured logging (data-oriented)
(trove/log! {:level :info
:id :auth/login
:data {:user-id 1234, :session-id "abc123"}
:msg "User authenticated"})
Application user setup (choosing backend):
(ns my-app.core (:require [taoensso.trove :as trove] [taoensso.trove.console] ; or .telemere, .timbre, .mulog, .tools-logging, .slf4j )) ;; Use default console backend (prints to *out* or js/console) ;; Default is already set, no action needed ;; Or switch to a different backend (trove/set-log-fn! (taoensso.trove.telemere/get-log-fn)) ;; Or disable all logging (trove/set-log-fn! nil)
Core Concepts
Trove is a facade - it provides a single logging API (trove/log!) that library authors use. Application users choose which backend handles those logs by setting trove/*log-fn*.
Key design features:
- •Map-based API (same as Telemere)
- •Automatic lazy evaluation of expensive data
- •Backend-agnostic filtering
- •Rich structured data support
- •Zero runtime dependencies
Logging API
Basic Usage
The log! macro accepts a map of options:
(trove/log!
{:level :info ; Required: :trace :debug :info :warn :error :fatal :report
:id :user/login ; Optional: keyword identifier for this event
:msg "User login" ; Optional: human-readable message
:data {:user-id 42} ; Optional: structured data map
:error ex}) ; Optional: exception/throwable
Log Levels
Standard levels from least to most severe:
- •
:trace- Very detailed diagnostic info - •
:debug- Debugging information - •
:info- Informational messages (default) - •
:warn- Warning messages - •
:error- Error conditions - •
:fatal- Critical failures - •
:report- Special high-priority reports
Traditional vs Structured Logging
Traditional (message-focused):
(trove/log! {:level :info, :msg "Processing order #1234"})
(trove/log! {:level :error, :msg "Database connection failed", :error ex})
Structured (data-focused):
(trove/log! {:level :info
:id :order/process
:data {:order-id 1234, :user-id 567, :total 99.99}})
(trove/log! {:level :error
:id :db/connection-failed
:data {:host "db.example.com", :port 5432}
:error ex})
Structured logging is preferred because:
- •Retains rich data types throughout pipeline
- •Easier filtering and analysis
- •Faster (avoid premature serialization)
- •Better suited for databases and analytics tools
Event IDs
Use keyword IDs to categorize events:
;; Namespace-qualified keywords recommended
(trove/log! {:id :auth/login, :data {...}})
(trove/log! {:id :payment/success, :data {...}})
(trove/log! {:id ::order-complete, :data {...}}) ; Auto-namespaced
;; IDs enable powerful filtering
;; - Filter by ID prefix: :auth/*
;; - Track specific events
;; - Build metrics and dashboards
Lazy Evaluation
Trove automatically delays expensive data, so backends can filter before paying computation costs:
;; This expensive call only runs if the log passes filtering
(trove/log! {:level :debug
:data (expensive-computation)})
;; Use :let for shared bindings across lazy args
(trove/log! {:level :info
:let [result (expensive-call)]
:msg (format-result result)
:data (transform-result result)})
The :let bindings are only evaluated if the log passes filtering.
Backend Configuration
Available Backends
Trove includes adapters for common backends:
;; Console (default) - prints to *out* or js/console (require '[taoensso.trove.console]) (trove/set-log-fn! (taoensso.trove.console/get-log-fn)) ;; Telemere - modern structured logging (require '[taoensso.trove.telemere]) (trove/set-log-fn! (taoensso.trove.telemere/get-log-fn)) ;; Timbre - popular Clojure logging (require '[taoensso.trove.timbre]) (trove/set-log-fn! (taoensso.trove.timbre/get-log-fn)) ;; μ/log - structured events (require '[taoensso.trove.mulog]) (trove/set-log-fn! (taoensso.trove.mulog/get-log-fn)) ;; tools.logging - Java interop (require '[taoensso.trove.tools-logging]) (trove/set-log-fn! (taoensso.trove.tools-logging/get-log-fn)) ;; SLF4J - Java interop (require '[taoensso.trove.slf4j]) (trove/set-log-fn! (taoensso.trove.slf4j/get-log-fn))
Console Backend Options
The default console backend supports filtering:
;; Only log :warn and above
(trove/set-log-fn!
(taoensso.trove.console/get-log-fn
{:min-level :warn}))
Dynamic Backend Switching
Use binding for temporary backend changes:
;; Disable logging in tests (binding [trove/*log-fn* nil] (run-tests)) ;; Use custom backend in specific context (binding [trove/*log-fn* my-custom-log-fn] (perform-operation))
Writing Custom Backends
Implement a function matching the *log-fn* signature:
(defn my-log-fn
[ns coords level id lazy_]
;; ns - String namespace, e.g. "my-app.utils"
;; coords - [line column] or nil
;; level - Keyword: :trace :debug :info :warn :error :fatal :report
;; id - Keyword or nil, e.g. :auth/login
;; lazy_ - Map or delayed map: {:keys [msg data error kvs]}
;; Force lazy_ to get the actual values
(let [{:keys [msg data error kvs]} (force lazy_)]
;; Implement filtering
(when (should-log? level id)
;; Perform logging side effects
(send-to-backend {:level level :id id :msg msg :data data}))))
;; Configure it
(trove/set-log-fn! my-log-fn)
Key implementation notes:
- •Force
lazy_to access:msg,:data,:error,:kvs - •Implement filtering before forcing to avoid expensive computation
- •The log-fn is called synchronously - use async/threading for expensive work
- •Return value is ignored
Advanced Options
Custom Namespace and Coordinates
Override the defaults:
(trove/log! {:level :info
:ns "custom.namespace"
:coords [100 50]
:msg "Override defaults"})
Custom Log Function Per Call
Use a different backend for specific logs:
(trove/log! {:level :info
:log-fn my-special-log-fn
:msg "Uses custom backend"})
Custom Key-Value Pairs
Pass additional data to your custom log-fn:
(trove/log! {:level :info
:msg "Custom event"
:my-custom-key "value"
:another-key 123})
;; Your log-fn receives these in the :kvs key
(defn my-log-fn [ns coords level id lazy_]
(let [{:keys [kvs]} (force lazy_)]
(println "Custom keys:" (:my-custom-key kvs))))
Common Patterns
Library Usage Pattern
As a library author, just use trove/log!:
(ns my-library.api
(:require [taoensso.trove :as trove]))
(defn process-data [data]
(trove/log! {:level :debug
:id ::process-start
:data {:count (count data)}})
(try
(let [result (do-processing data)]
(trove/log! {:level :info
:id ::process-complete
:data {:processed (count result)}})
result)
(catch Exception ex
(trove/log! {:level :error
:id ::process-failed
:error ex
:data {:count (count data)}})
(throw ex))))
Your users control the backend without changing your code.
Application Setup Pattern
In your application entry point:
(ns my-app.main (:require [taoensso.trove :as trove] [taoensso.trove.telemere] [my-library.api :as lib])) (defn -main [] ;; Configure logging backend once (trove/set-log-fn! (taoensso.trove.telemere/get-log-fn)) ;; All libraries using Trove now log to Telemere (lib/process-data [...]))
Conditional Logging
Use when expressions for conditional logic:
(when (dev-mode?)
(trove/log! {:level :debug
:data (expensive-debug-info)}))
;; Or use level filtering in the backend
(trove/set-log-fn!
(taoensso.trove.console/get-log-fn {:min-level :info}))
Key Gotchas
- •
Log-fn is synchronous: The
*log-fn*runs on the calling thread. Implement async/backpressure for expensive operations in your log-fn. - •
Lazy evaluation: Values like
:dataand:msgmay be wrapped indelay. Alwaysforcethelazy_argument in custom log-fns. - •
Backend setup timing: Set
*log-fn*before any logging occurs. Do this early in application startup. - •
Nil log-fn: When
*log-fn*isnil, all logging noops. This is intentional - useful for disabling logs. - •
Map required: The
log!macro requires a compile-time map. Variables won't work:clojure;; This works (trove/log! {:level :info, :msg "ok"}) ;; This fails (let [opts {:level :info}] (trove/log! opts)) ; Compile error! - •
ClojureScript console: In ClojureScript, the console backend checks for
js/consoleexistence before logging.
When to Use Trove
Use Trove when:
- •Writing libraries that need logging
- •You want structured logging without committing to a backend
- •You need rich filtering capabilities
- •You want ClojureScript compatibility
- •You prefer a data-oriented logging API
Don't use Trove when:
- •Writing an application (use Telemere, Timbre, etc. directly)
- •You need advanced features like log appenders, formatting, rotation (use a full backend)
- •You have no logging needs (obviously)
Trove is specifically designed for library authors. Application developers should typically use a full-featured backend directly.
References
- •GitHub: https://github.com/taoensso/trove
- •API Docs: https://cljdoc.org/d/com.taoensso/trove/
- •Clojars: https://clojars.org/com.taoensso/trove
- •Slack: #trove on Clojurians Slack
- •Related: Telemere (full-featured backend using same API)