Claude Code Plugins

Community-maintained marketplace

Feedback

Pure Clojure/Script logging library with flexible configuration and powerful features

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name timbre
description Pure Clojure/Script logging library with flexible configuration and powerful features

Timbre

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:

  • :trace - Detailed diagnostic information
  • :debug - Debugging information
  • :info - Informational messages
  • :warn - Warning messages
  • :error - Error messages
  • :fatal - Critical failures
  • :report - Special reporting level

Appenders

Functions that handle log output: (fn [data]) -> ?effects

Each appender receives a data map containing:

  • :level - Log level keyword
  • :?msg - Log message
  • :timestamp - When log occurred
  • :hostname - System hostname
  • :?ns-str - Namespace string
  • :?file, :?line - Source location
  • :?err - Exception (if present)
  • 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:

  • :min-level - Minimum log level
  • :ns-filter - Namespace filtering
  • :middleware - Middleware functions
  • :timestamp-opts - Timestamp formatting
  • :output-fn - Output formatter
  • :appenders - Appender map

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

References