Library-skills timbre
Pure Clojure/Script logging library with flexible configuration and powerful features
git clone https://github.com/hugoduncan/library-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/hugoduncan/library-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/clojure-libraries/skills/timbre" ~/.claude/skills/hugoduncan-library-skills-timbre && rm -rf "$T"
plugins/clojure-libraries/skills/timbre/SKILL.mdTimbre
Pure Clojure/Script logging library with zero dependencies, simple configuration, and powerful features like async logging, rate limiting, and flexible appenders.
Overview
Timbre is a logging library designed for Clojure and ClojureScript applications. Unlike Java logging frameworks requiring XML or properties files, Timbre uses pure Clojure data structures for all configuration.
Key characteristics:
- Full Clojure and ClojureScript support
- Single config map - no XML/properties files
- Zero overhead compile-time level/namespace elision
- Built-in async logging and rate limiting
- Function-based appenders and middleware
- Optional tools.logging and SLF4J interop
Library:
com.taoensso/timbre
Latest Version: 6.8.0
License: EPL-1.0
Note: For new projects, consider Telemere - a modern rewrite of Timbre. Existing Timbre users have no pressure to migrate.
Installation
;; deps.edn {:deps {com.taoensso/timbre {:mvn/version "6.8.0"}}} ;; Leiningen [com.taoensso/timbre "6.8.0"]
Core Concepts
Log Levels
Seven standard levels in ascending severity:
- Detailed diagnostic information:trace
- Debugging information:debug
- Informational messages:info
- Warning messages:warn
- Error messages:error
- Critical failures:fatal
- Special reporting level:report
Appenders
Functions that handle log output:
(fn [data]) -> ?effects
Each appender receives a data map containing:
- Log level keyword:level
- Log message:?msg
- When log occurred:timestamp
- System hostname:hostname
- Namespace string:?ns-str
,:?file
- Source location:?line
- Exception (if present):?err- Additional context data
Middleware
Functions that transform log data:
(fn [data]) -> ?data
Applied before appenders receive data, enabling:
- Data enrichment
- Filtering
- Transformation
- Context injection
Configuration
Timbre uses a single atom
*config* containing:
- Minimum log level:min-level
- Namespace filtering:ns-filter
- Middleware functions:middleware
- Timestamp formatting:timestamp-opts
- Output formatter:output-fn
- Appender map:appenders
Basic Usage
Simple Logging
(require '[taoensso.timbre :as timbre]) ;; Basic logging at different levels (timbre/trace "Entering function") (timbre/debug "Variable value:" x) (timbre/info "Server started on port" port) (timbre/warn "Deprecated function used") (timbre/error "Failed to connect to database") (timbre/fatal "Critical system failure") (timbre/report "Daily metrics" {:users 1000 :requests 5000})
Formatted Logging
;; Printf-style formatting (timbre/infof "User %s logged in from %s" username ip) (timbre/warnf "Cache miss rate: %.2f%%" miss-rate) (timbre/errorf "Request failed: %d %s" status-code message)
Logging with Exceptions
(try (risky-operation) (catch Exception e (timbre/error e "Operation failed"))) ;; Multiple values (timbre/error e "Failed processing" {:user-id 123 :item-id 456})
Spy - Log and Return
;; Log value and return it (let [result (timbre/spy :info (expensive-calculation x y))] (process result)) ;; With custom message (timbre/spy :debug {:msg "Calculation result"} (expensive-calculation x y))
Configuration
Setting Minimum Level
;; Global minimum level (timbre/set-min-level! :info) ;; Per-namespace level (timbre/set-ns-min-level! {:deny #{"noisy.namespace.*"} :allow #{"important.namespace.*"}})
Modifying Config
;; Replace entire config (timbre/set-config! new-config) ;; Merge into existing config (timbre/merge-config! {:min-level :debug :appenders {:println (println-appender)}}) ;; Swap with function (timbre/swap-config! update :min-level (constantly :warn))
Scoped Configuration
;; Temporarily change config (timbre/with-config custom-config (timbre/info "Logged with custom config")) ;; Merge temporary changes (timbre/with-merged-config {:min-level :trace} (timbre/trace "Temporarily enabled trace logging")) ;; Temporary level change (timbre/with-min-level :debug (timbre/debug "Debug enabled for this scope"))
Appenders
Built-in Appenders
Console Output
(require '[taoensso.timbre.appenders.core :as appenders]) ;; println appender (default) {:appenders {:println (appenders/println-appender)}}
File Output
;; Simple file appender {:appenders {:spit (appenders/spit-appender {:fname "/var/log/app.log"})}}
Custom Appender
(defn my-appender "Appender that writes to a custom destination" [opts] {:enabled? true :async? false :min-level nil ; Inherit from config :rate-limit nil :output-fn :inherit :fn (fn [data] (let [{:keys [output-fn]} data formatted (output-fn data)] ;; Custom logic here (send-to-custom-system formatted)))}) ;; Use custom appender (timbre/merge-config! {:appenders {:custom (my-appender {})}})
Appender Configuration Options
{:enabled? true ; Enable/disable :async? false ; Async logging? :min-level :info ; Appender-specific min level :rate-limit [[5 1000]] ; Max 5 logs per 1000ms :output-fn :inherit ; Use config's output-fn :fn (fn [data] ...)} ; Handler function
Advanced Features
Context (MDC)
;; Set context for current thread (timbre/with-context {:user-id 123 :request-id "abc"} (timbre/info "Processing request") (do-work)) ;; Add to existing context (timbre/with-context+ {:session-id "xyz"} (timbre/info "Additional context"))
Middleware
;; Add hostname to all logs (defn add-hostname-middleware [data] (assoc data :hostname (get-hostname))) ;; Add timestamp middleware (defn add-custom-timestamp [data] (assoc data :custom-ts (System/currentTimeMillis))) ;; Apply middleware (timbre/merge-config! {:middleware [add-hostname-middleware add-custom-timestamp]})
Rate Limiting
;; Per-appender rate limit {:appenders {:println {:enabled? true :rate-limit [[10 1000] ; Max 10 per second [100 60000]] ; Max 100 per minute :fn (fn [data] (println (:output-fn data)))}}} ;; At log call site (timbre/log {:rate-limit [[1 5000]]} ; Max once per 5 seconds :info "Rate limited message")
Async Logging
;; Enable async for appender {:appenders {:async-file {:enabled? true :async? true ; Process logs asynchronously :fn (fn [data] (spit "/var/log/app.log" (str (:output-fn data) "\n") :append true))}}}
Conditional Logging
;; Log only sometimes (probabilistic) (timbre/sometimes 0.1 ; 10% probability (timbre/info "Sampled log message")) ;; Conditional error logging (timbre/log-errors (risky-operation)) ; Logs if exception thrown ;; Log and rethrow (timbre/log-and-rethrow-errors (risky-operation)) ; Logs then rethrows exception
Exception Handling
;; Capture uncaught JVM exceptions (Clojure only) (timbre/handle-uncaught-jvm-exceptions!) ;; Log errors in futures (timbre/logged-future (risky-async-operation))
Output Formatting
Custom Output Function
(defn my-output-fn [{:keys [level ?ns-str ?msg-fmt vargs timestamp_ ?err]}] (str (force timestamp_) " " (str/upper-case (name level)) " " "[" ?ns-str "] - " (apply format ?msg-fmt vargs) (when ?err (str "\n" (timbre/stacktrace ?err))))) ;; Use custom output (timbre/merge-config! {:output-fn my-output-fn})
Timestamp Configuration
{:timestamp-opts {:pattern "yyyy-MM-dd HH:mm:ss.SSS" :locale (java.util.Locale/getDefault) :timezone (java.util.TimeZone/getDefault)}}
Colors
(require '[taoensso.timbre :refer [color-str]]) ;; ANSI color output (defn colored-output-fn [{:keys [level] :as data}] (let [base-output (timbre/default-output-fn data)] (case level :error (color-str :red base-output) :warn (color-str :yellow base-output) :info (color-str :green base-output) base-output)))
Namespace Filtering
;; Whitelist/blacklist namespaces (timbre/merge-config! {:ns-filter {:deny #{"noisy.lib.*" "chatty.namespace"} :allow #{"important.module.*"}}}) ;; Per-namespace min levels (timbre/set-ns-min-level! '{my.app.core :trace my.app.utils :info ["com.external.*"] :warn})
Interoperability
tools.logging Integration
;; Route tools.logging to Timbre (require '[taoensso.timbre.tools.logging :as tools-logging]) (tools-logging/use-timbre) ;; Now tools.logging calls use Timbre (require '[clojure.tools.logging :as log]) (log/info "Routed through Timbre")
SLF4J Integration
Timbre can act as an SLF4J backend for Java logging. Requires additional setup with timbre-slf4j-appender or similar.
Common Patterns
Application Setup
(ns myapp.logging (:require [taoensso.timbre :as timbre] [taoensso.timbre.appenders.core :as appenders])) (defn init-logging! [] (timbre/merge-config! {:min-level :info :ns-filter {:deny #{"verbose.library.*"}} :middleware [] :timestamp-opts {:pattern "yyyy-MM-dd HH:mm:ss"} :appenders {:println (appenders/println-appender) :spit (appenders/spit-appender {:fname "/var/log/myapp.log"})}})) (init-logging!)
Structured Logging
;; Log with structured data (timbre/info "User action" {:action :login :user-id 123 :ip "192.168.1.1" :timestamp (System/currentTimeMillis)}) ;; Custom output for structured logs (defn json-output-fn [{:keys [level msg_ ?ns-str timestamp_ vargs]}] (json/write-str {:timestamp (force timestamp_) :level level :namespace ?ns-str :message (force msg_) :data (first vargs)}))
Environment-Specific Config
(defn config-for-env [env] (case env :dev {:min-level :trace :appenders {:println (appenders/println-appender)}} :staging {:min-level :debug :appenders {:spit (appenders/spit-appender {:fname "/var/log/staging.log"})}} :prod {:min-level :info :appenders {:spit (appenders/spit-appender {:fname "/var/log/production.log" :async? true})}})) (timbre/set-config! (config-for-env :prod))
Request Logging
(defn wrap-logging [handler] (fn [request] (let [start (System/currentTimeMillis) request-id (str (random-uuid))] (timbre/with-context {:request-id request-id} (timbre/info "Request started" {:method (:request-method request) :uri (:uri request)}) (let [response (handler request) duration (- (System/currentTimeMillis) start)] (timbre/info "Request completed" {:status (:status response) :duration-ms duration}) response)))))
Database Query Logging
(defn log-query [query params] (timbre/spy :debug {:msg (str "Executing query: " query)} (db/execute! query params))) ;; Or with timing (defn log-slow-queries [query params] (let [start (System/currentTimeMillis) result (db/execute! query params) duration (- (System/currentTimeMillis) start)] (when (> duration 1000) (timbre/warn "Slow query detected" {:query query :duration-ms duration})) result))
Error Handling
Exception Logging Patterns
;; Basic exception logging (try (risky-op) (catch Exception e (timbre/error e "Operation failed"))) ;; With context (try (process-item item) (catch Exception e (timbre/error e "Failed to process item" {:item-id (:id item) :item-type (:type item)}))) ;; Log and continue (defn safe-process [items] (doseq [item items] (timbre/log-errors (process-item item)))) ;; Log and rethrow (defn critical-operation [] (timbre/log-and-rethrow-errors (perform-critical-task)))
Performance Considerations
Compile-Time Elision
;; Set via JVM property or env var ;; Only :info and above will be compiled ;; :trace and :debug calls removed at compile time ;; -Dtimbre.min-level=:info ;; Verify elision (timbre/debug "This won't be in bytecode if min-level >= :info")
Async Appenders
;; Offload I/O to background thread {:appenders {:file {:async? true :fn (fn [data] ;; Expensive I/O operation (write-to-file data))}}}
Conditional Evaluation
;; Arguments evaluated only if level enabled (timbre/debug (expensive-debug-string)) ; Not called if debug disabled ;; For very expensive operations, use explicit check (when (timbre/log? :debug) (timbre/debug (very-expensive-operation)))
ClojureScript Usage
(ns myapp.core (:require [taoensso.timbre :as timbre])) ;; Same API as Clojure (timbre/info "Running in browser") ;; Configure for browser (timbre/set-config! {:level :debug :appenders {:console {:enabled? true :fn (fn [data] (let [{:keys [output-fn]} data] (.log js/console (output-fn data))))}}})
Testing
Test Configuration
(ns myapp.test (:require [clojure.test :refer :all] [taoensso.timbre :as timbre])) ;; Disable logging in tests (use-fixtures :once (fn [f] (timbre/with-merged-config {:min-level :fatal} (f)))) ;; Or capture logs for assertions (defn with-log-capture [f] (let [logs (atom [])] (timbre/with-merged-config {:appenders {:test {:enabled? true :fn (fn [data] (swap! logs conj data))}}} (f logs))))
Migration from tools.logging
;; Before (tools.logging) (require '[clojure.tools.logging :as log]) (log/info "Message") (log/error e "Error occurred") ;; After (Timbre) (require '[taoensso.timbre :as timbre]) (timbre/info "Message") (timbre/error e "Error occurred") ;; Or keep tools.logging imports and use bridge (require '[taoensso.timbre.tools.logging :as tools-logging]) (tools-logging/use-timbre) ;; Now existing tools.logging code routes to Timbre
Troubleshooting
No Output
Check configuration:
;; Verify config @timbre/*config* ;; Check min level (:min-level @timbre/*config*) ;; Verify appenders enabled (->> @timbre/*config* :appenders vals (filter :enabled?))
Missing Logs
;; Check namespace filters (timbre/may-log? :info) ; In current namespace (timbre/may-log? :info "some.namespace") ; Specific namespace
Performance Issues
;; Enable async for expensive appenders {:appenders {:file {:async? true ...}}} ;; Add rate limiting {:appenders {:email {:rate-limit [[1 60000]] ...}}} ;; Use compile-time elision in production ;; -Dtimbre.min-level=:info