Structured telemetry library for Clojure/Script. Use when working with logging, tracing, structured logging, events, signal handling, observability, or replacing Timbre/tools.logging.
Telemere is a next-generation structured telemetry library for Clojure/Script. It provides one unified API for traditional logging, structured logging, tracing, and basic performance monitoring.
deps.edn:
com.taoensso/telemere {:mvn/version "1.2.1"}
Leiningen:
[com.taoensso/telemere "1.2.1"]
See https://clojars.org/com.taoensso/telemere for the latest version.
Namespace setup:
(ns my-app
(:require [taoensso.telemere :as tel]))
Telemere works out-of-the-box with no config needed. Signals print to console by default:
(require '[taoensso.telemere :as tel])
;; Traditional logging (string messages)
(tel/log! {:level :info, :msg "User logged in!"})
;; Structured logging (explicit id and data)
(tel/log! {:level :info, :id :auth/login, :data {:user-id 1234}})
;; Mixed style (id, data, and message)
(tel/log! {:level :info
:id :auth/login
:data {:user-id 1234}
:msg "User logged in!"})
;; Trace execution with runtime tracking
(tel/trace! {:id ::my-id :data {:step "processing"}}
(do-some-work))
;; Check signal content for debugging
(tel/with-signal (tel/log! {...})) ; => {:keys [ns level id data msg_ ...]}
Telemere provides multiple signal creators optimized for different use cases. All accept an opts map:
| Function | Quick Args | Returns | Use Case |
|---|---|---|---|
log! | [?level msg] | nil | Traditional log messages |
event! | [id ?level] | nil | Structured events |
trace! | [?id form] | form result | Trace execution + timing |
spy! | [?level form] | form result | Debug form values |
error! | [?id error] | error | Log errors |
catch->error! | [?id form] | value or fallback | Catch & log errors |
signal! | [opts] | depends | Low-level, full control |
Examples:
;; log! - for messages
(tel/log! "Simple message")
(tel/log! :warn "Warning message")
(tel/log! {:level :info, :data {:x 1}} "Message with data")
;; event! - for structured events
(tel/event! ::user-login)
(tel/event! ::user-login :info)
(tel/event! ::user-login {:level :info, :data {:user-id 42}})
;; trace! - tracks runtime and return value
(tel/trace! (expensive-operation))
(tel/trace! ::complex-op (multi-step-process))
;; spy! - debug form values
(tel/spy! (+ 1 2)) ; => 3, logs the value
;; error! - log errors
(try
(risky-operation)
(catch Exception e
(tel/error! e)))
;; catch->error! - automatic error handling
(tel/catch->error! ::my-op
(risky-operation)) ; returns value or nil on error
All signal creators accept a map of options:
(tel/log!
{:level :debug
:id ::my-id
;; Filtering
:sample 0.75 ; 75% sampling (noop 25% of time)
:when (enabled?) ; conditional execution
:limit {"1/sec" [1 1000]
"5/min" [5 60000]}
;; Data and execution
:let [x (expensive-calc)] ; lazy bindings
:data {:result x} ; structured data
:do (inc-metric!) ; side effects
;; Context
:ctx {:user-id 123} ; arbitrary context
:parent trace-parent} ; tracing parent
"Message using bindings")
Key options:
:level - :trace, :debug, :info (default), :warn, :error, :fatal, or integer:id - qualified keyword for identifying this signal type:data - structured data map (preserved as data):msg - message string or vector to join:let - bindings available to :data and :msg:sample - random sampling rate (0.0 to 1.0):when - conditional execution:limit - rate limiting map:do - side effects to execute when signal is createdFiltering happens at multiple stages for efficiency:
;; Set minimum level globally
(tel/set-min-level! :warn) ; All signals
(tel/set-min-level! :log :debug) ; Just log! signals
;; Filter by namespace patterns
(tel/set-ns-filter! {:disallow "taoensso.*" :allow "taoensso.sente.*"})
;; Filter by ID patterns
(tel/set-id-filter! {:allow #{::my-id "my-app/*"}})
;; Set level per namespace pattern
(tel/set-min-level! :log "taoensso.sente.*" :warn)
;; Transform signals (can modify or filter)
(tel/set-xfn!
(fn [signal]
(if (-> signal :data :skip-me?)
nil ; Filter out
(assoc signal :enriched true))))
;; Dynamic context overrides
(tel/with-min-level :trace
(tel/log! {:level :debug} "This will log"))
Filtering is O(1) except for rate limits (O(n-windows)). Compile-time filtering can completely elide signal calls for zero overhead.
Handlers process created signals (write to console, file, DB, etc.):
;; Add custom handler
(tel/add-handler! :my-handler
(fn [signal] (println "Got signal:" (:id signal))))
;; Add handler with filtering and async dispatch
(tel/add-handler! :my-handler
(fn
([signal] (save-to-db signal))
([] (close-db-connection))) ; Called on shutdown
{:async {:mode :dropping
:buffer-size 1024
:n-threads 1}
:priority 100
:min-level :info
:sample 0.5
:ns-filter {:disallow "noisy.namespace.*"}
:limit {"1/sec" [1 1000]}})
;; View current handlers
(tel/get-handlers)
;; Handler statistics
(tel/get-handlers-stats)
;; Remove handler
(tel/remove-handler! :my-handler)
;; Stop all handlers (IMPORTANT: call on shutdown!)
(tel/stop-handlers!)
Console handlers (output as formatted text, edn, or JSON):
;; Human-readable text (default)
(tel/add-handler! :console
(tel/handler:console
{:output-fn (tel/format-signal-fn {})}))
;; EDN output
(tel/add-handler! :console-edn
(tel/handler:console
{:output-fn (tel/pr-signal-fn {:pr-fn :edn})}))
;; JSON output (Clj needs JSON library)
(require '[jsonista.core :as json])
(tel/add-handler! :console-json
(tel/handler:console
{:output-fn (tel/pr-signal-fn
{:pr-fn json/write-value-as-string})}))
Other included handlers:
handler:file - Write to files (Clj only)handler:postal - Email via Postal (Clj only)handler:slack - Slack notifications (Clj only)handler:tcp-socket / handler:udp-socket - Network sockets (Clj only)handler:open-telemetry - OpenTelemetry integration (Clj only)Add dependencies:
org.slf4j/slf4j-api (v2+)com.taoensso/telemere-slf4jSLF4J calls automatically become Telemere signals
Verify: (tel/check-interop) => {:slf4j {:telemere-receiving? true}}
org.clojure/tools.logging dependency(tel/tools-logging->telemere!) or set env configVerify: (tel/check-interop) => {:tools-logging {:telemere-receiving? true}}
Redirect System/out and System/err to Telemere:
(tel/streams->telemere!)
See references/config.md for OpenTelemetry integration.
;; Fixed message
(tel/log! "User logged in")
;; Joined message vector
(tel/log! ["User" user-id "logged in"])
;; With preprocessing
(tel/log!
{:let [username (str/upper-case raw-name)
balance (parse-double raw-balance)]
:data {:username username
:balance balance}}
["User" username "balance:" (format "$%.2f" balance)])
(defn process-order [order-id]
(tel/trace! {:id ::process-order :data {:order-id order-id}}
(let [order (fetch-order order-id)
_ (tel/trace! {:id ::validate-order}
(validate-order order))
_ (tel/trace! {:id ::charge-payment}
(charge-payment order))]
(ship-order order))))
;; Set context for all signals in scope
(tel/with-ctx {:request-id request-id
:user-id user-id}
(tel/log! {:id ::processing} "Started")
(process-request)
(tel/log! {:id ::complete} "Done"))
;; Simple error logging
(try
(risky-op)
(catch Exception e
(tel/error! ::operation-failed e)))
;; Automatic error catching with fallback
(tel/catch->error! ::fetch-user
{:catch-val {:id nil :name "Guest"}}
(fetch-user-from-db user-id))
Always call stop-handlers! on shutdown to flush buffers and close resources. Use tel/call-on-shutdown! for JVM shutdown hooks.
Signals are filtered before creation - data in :let, :data, :msg, :do is only evaluated if the signal passes filters.
Handler filters are additive - handlers can be MORE restrictive than call filters, not less.
Messages are lazy - message building only happens if the signal is created and handled.
:error value != :error level - signals can have error values at any level, and vice versa.
Cache validators - use tel/validator, tel/decoder, etc. once, not per signal.
Telemere is highly optimized:
Tips for performance:
Telemere includes extensive docstrings accessible from your REPL:
tel/help:signal-creators - Creating signalstel/help:signal-options - All signal optionstel/help:signal-content - Signal map contenttel/help:filters - Filtering and transformationstel/help:handlers - Handler managementtel/help:handler-dispatch-options - Handler dispatch configurationtel/help:environmental-config - JVM/env/classpath configuration