| name | telemere |
| description | Structured logging and telemetry for Clojure/Script with tracing and performance monitoring |
Telemere
Structured logging and telemetry library for Clojure and ClojureScript. Next-generation successor to Timbre with unified API for logging, tracing, and performance monitoring.
Overview
Telemere provides a unified approach to application observability, handling traditional logging, structured telemetry, distributed tracing, and performance monitoring through a single consistent API.
Key Features:
- Structured data throughout pipeline (no string parsing)
- Compile-time signal elision (zero runtime cost for disabled signals)
- Runtime filtering (namespace, level, ID, rate limiting, sampling)
- Async and sync handler dispatch
- OpenTelemetry, SLF4J, and tools.logging interoperability
- Zero-configuration defaults
- ClojureScript support
Artifact: com.taoensso/telemere
Latest Version: 1.1.0
License: EPL-1.0
Repository: https://github.com/taoensso/telemere
Installation
Add to deps.edn:
{:deps {com.taoensso/telemere {:mvn/version "1.1.0"}}}
Or Leiningen project.clj:
[com.taoensso/telemere "1.1.0"]
Import in namespace:
(ns my-app
(:require [taoensso.telemere :as t]))
Core Concepts
Signals
Signals are structured telemetry events represented as Clojure maps with standardized attributes. They preserve data types throughout the logging pipeline rather than converting to strings.
Signal attributes include: namespace, level, ID, timestamp, thread info, line number, form data, return values, custom data maps.
Default Configuration
Out-of-the-box settings:
- Minimum level:
:info - Handler: Console output to
*out*or browser console - Automatic interop with SLF4J, tools.logging when present
Filtering Philosophy
Two-stage filtering:
- Call-time (compile + runtime): Determines if signal is created
- Handler-time (runtime): Determines which handlers process signal
Effective filtering reduces noise and improves performance.
API Reference
Signal Creation
log!
Traditional and structured logging.
;; Basic logging with level
(t/log! :info "Processing started")
(t/log! :warn "High memory usage")
(t/log! :error "Database connection failed")
;; With message arguments
(t/log! :info ["User logged in:" {:user-id 123}])
;; Structured data
(t/log! {:level :info
:data {:user-id 123 :action "login"}})
;; With ID for filtering
(t/log! {:id :user-action
:level :info
:data {:user-id 123}})
Levels (priority order):
:trace < :debug < :info < :warn < :error < :fatal < :report
Options:
:level- Signal level (keyword):id- Signal ID for filtering (keyword):data- Structured data map:msg- Message string or vector:error- Exception/error object:ctx- Context map:sample-rate- Signal sampling (0.0-1.0):rate-limit- Rate limiting spec:run- Form to evaluate and include result
event!
ID and level-based event logging.
;; Simple event
(t/event! :user-signup)
(t/event! :payment-processed)
;; With level
(t/event! :cache-miss :warn)
;; With data
(t/event! :user-signup
{:data {:user-id 123 :email "user@example.com"}})
;; With level and data
(t/event! :slow-query :warn
{:data {:duration-ms 1200 :query "SELECT ..."}})
Events are filtered by ID, making them ideal for metrics and tracking specific occurrences.
trace!
Tracks form execution with nested flow tracking.
;; Basic tracing
(t/trace! :fetch-user
(fetch-user-from-db user-id))
;; Returns form result while logging execution
(def user
(t/trace! :fetch-user
(fetch-user-from-db 123)))
;; With data
(t/trace! {:id :process-order
:data {:order-id 456}}
(process-order 456))
;; Nested tracing shows parent-child relationships
(t/trace! :outer
(do
(t/trace! :inner-1 (step-1))
(t/trace! :inner-2 (step-2))))
Trace signals include execution time and return value. Nested traces maintain parent-child relationships.
spy!
Execution tracing with return value capture.
;; Spy on expression
(t/spy! :debug
(+ 1 2 3))
;;=> 6 (also logs the expression and result)
;; Spy in pipeline
(->> data
(map inc)
(t/spy! :debug) ; See intermediate value
(filter even?))
;; With custom ID
(t/spy! {:id :computation :level :trace}
(* 42 (expensive-calc)))
Spy always returns the form result, making it useful in pipelines.
error!
Error logging with exception handling.
;; Log error
(t/error! (ex-info "Failed" {:reason :timeout}))
;; With ID
(t/error! :db-error
(ex-info "Connection lost" {:host "db.example.com"}))
;; With additional data
(t/error! {:id :api-error
:data {:endpoint "/users" :status 500}}
(ex-info "API failed" {}))
Returns the error object.
catch->error!
Catch and log exceptions.
;; Basic error catching
(t/catch->error!
(risky-operation))
;; With ID
(t/catch->error! :db-operation
(db-query))
;; With data
(t/catch->error! {:id :api-call
:data {:endpoint "/users"}}
(http-request "/users"))
;; Returns nil on error, result on success
(if-let [result (t/catch->error! (fetch-data))]
(process result)
(handle-error))
Catches exceptions, logs them, and returns nil. Returns form result if no exception.
signal!
Low-level signal creation with full control.
;; Full signal specification
(t/signal!
{:kind :log
:level :info
:id :custom-event
:ns (str *ns*)
:data {:key "value"}
:msg "Custom message"
:run (do-something)})
Most use cases are better served by higher-level functions.
Configuration
set-min-level!
Set global or namespace-specific minimum level.
;; Global minimum level
(t/set-min-level! :warn)
;; Namespace-specific
(t/set-min-level! 'my.app.core :debug)
(t/set-min-level! 'my.app.* :info)
;; Per-namespace map
(t/set-min-level!
[['my.app.* :info]
['my.app.db :debug]
['noisy.library.* :error]])
Signals below minimum level are filtered at call-time.
set-ns-filter!
Configure namespace filtering.
;; Allow only specific namespaces
(t/set-ns-filter! {:allow #{"my.app.*"}})
;; Disallow specific namespaces
(t/set-ns-filter! {:disallow #{"noisy.library.*"}})
;; Combined
(t/set-ns-filter!
{:allow #{"my.app.*"}
:disallow #{"my.app.test.*"}})
Namespace patterns support wildcards (*).
with-min-level
Temporarily override minimum level.
;; Enable debug logging for block
(t/with-min-level :debug
(t/log! :debug "Debug info") ; Logged
(process-data))
;; Nested overrides
(t/with-min-level :warn
(t/with-min-level :trace ; Inner level applies
(t/log! :trace "Trace info")))
Scope is thread-local and dynamic.
with-signal
Capture last signal for testing.
;; Capture signal map
(def sig
(t/with-signal
(t/log! {:level :info :data {:x 1}})))
(:level sig) ;;=> :info
(:data sig) ;;=> {:x 1}
;; Test signal creation
(let [sig (t/with-signal
(t/event! :test-event {:data {:y 2}}))]
(assert (= :test-event (:id sig)))
(assert (= {:y 2} (:data sig))))
Returns signal map instead of nil.
with-signals
Capture all signals from form.
;; Capture multiple signals
(def sigs
(t/with-signals
(t/log! :info "First")
(t/log! :warn "Second")
(t/event! :third)))
(count sigs) ;;=> 3
(map :level sigs) ;;=> (:info :warn :info)
Returns vector of signal maps.
Handlers
Handlers process signals and route them to destinations (console, files, databases, analytics).
add-handler!
Register signal handler.
;; Console handler (built-in)
(t/add-handler! :my-console
(t/handler:console))
;; Custom handler function
(t/add-handler! :custom
(fn [signal]
(println "Custom:" (:msg signal))))
;; With filtering
(t/add-handler! :error-only
(t/handler:console)
{:min-level :error})
;; With async dispatch
(t/add-handler! :async-log
(fn [signal] (log-to-db signal))
{:async {:buffer-size 1024
:n-threads 2}})
;; With sampling
(t/add-handler! :sampled
(t/handler:console)
{:sample-rate 0.1}) ; 10% of signals
Handler Options:
:min-level- Minimum signal level:ns-filter- Namespace filter:id-filter- ID filter:sample-rate- Sampling rate (0.0-1.0):rate-limit- Rate limiting spec:async- Async dispatch config:middleware- Transform functions
remove-handler!
Remove handler by ID.
(t/remove-handler! :my-console)
(t/remove-handler! :custom)
handler:console
Built-in console handler with formatting.
;; Default text format
(t/handler:console)
;; JSON format
(t/handler:console {:format :json})
;; EDN format
(t/handler:console {:format :edn})
;; Custom format function
(t/handler:console
{:format (fn [signal]
(pr-str (:data signal)))})
handler:stream
Output to Java OutputStream or Writer.
;; File output
(t/add-handler! :file
(t/handler:stream
(io/output-stream "app.log")
{:format :json}))
;; With rotation (requires additional setup)
(t/add-handler! :rotating-file
(rotating-file-handler "logs/app.log"))
Filtering Utilities
check-min-level
Check if level passes minimum threshold.
(t/check-min-level :info) ;;=> true/false
(t/check-min-level 'my.ns :debug) ;;=> true/false
check-ns-filter
Check if namespace passes filter.
(t/check-ns-filter 'my.app.core) ;;=> true/false
Utilities
check-interop
Verify interoperability status.
(t/check-interop)
;;=> {:slf4j {:present? true :sending->telemere? true}
;; :tools.logging {:present? true :sending->telemere? true}
;; :streams {:out :telemere :err :telemere}}
Shows which external logging systems are captured.
help:filters
Documentation on filtering.
t/help:filters
help:handlers
Documentation on handlers.
t/help:handlers
Common Patterns
Basic Application Logging
(ns my-app.core
(:require [taoensso.telemere :as t]))
;; Set minimum level for production
(t/set-min-level! :info)
;; Disable noisy libraries
(t/set-ns-filter! {:disallow #{"noisy.library.*"}})
(defn process-request [req]
(t/log! :info ["Processing request" {:path (:uri req)}])
(try
(let [result (handle-request req)]
(t/log! :debug {:data {:result result}})
result)
(catch Exception e
(t/error! :request-error e)
(throw e))))
Structured Event Tracking
;; Track user actions
(defn record-action [user-id action data]
(t/event! action
{:data (merge {:user-id user-id} data)}))
(record-action 123 :login {:method "oauth"})
(record-action 123 :purchase {:amount 99.99 :item "widget"})
;; Query-specific tracking
(defn track-slow-query [query duration-ms]
(when (> duration-ms 1000)
(t/event! :slow-query :warn
{:data {:query query :duration-ms duration-ms}})))
Distributed Tracing
(defn fetch-user-data [user-id]
(t/trace! :fetch-user-data
(let [user (t/trace! :db-query
(db/get-user user-id))
prefs (t/trace! :fetch-preferences
(api/get-preferences user-id))]
(merge user prefs))))
;; Traces show nested execution:
;; :fetch-user-data (parent)
;; :db-query (child)
;; :fetch-preferences (child)
Performance Monitoring
(defn monitored-operation [data]
(t/trace! {:id :operation
:data {:input-size (count data)}}
(let [result (expensive-processing data)]
;; Trace automatically captures execution time
result)))
;; Check performance
(t/spy! :debug
(reduce + (range 1000000)))
Error Handling
(defn safe-api-call [endpoint]
(t/catch->error! {:id :api-call
:data {:endpoint endpoint}}
(http/get endpoint)))
;; With fallback
(defn fetch-with-fallback [url]
(or (t/catch->error! :primary-fetch
(fetch-primary url))
(t/catch->error! :fallback-fetch
(fetch-fallback url))
(do
(t/log! :error "All fetch attempts failed")
nil)))
Rate Limiting
;; Limit signal rate
(t/log! {:level :info
:rate-limit {"my-limit" [10 1000]}} ; 10/sec
"High-frequency event")
;; Per-handler rate limiting
(t/add-handler! :limited
(t/handler:console)
{:rate-limit {"handler-limit" [100 60000]}}) ; 100/min
Sampling
;; Sample 10% of debug signals
(t/log! {:level :debug
:sample-rate 0.1}
"Debug info")
;; Sample at handler level
(t/add-handler! :sampled-analytics
(fn [sig] (send-to-analytics sig))
{:sample-rate 0.05}) ; 5% to analytics
Multi-Handler Setup
;; Console for development
(t/add-handler! :console
(t/handler:console)
{:min-level :debug})
;; File for all errors
(t/add-handler! :error-file
(t/handler:stream (io/output-stream "errors.log"))
{:min-level :error
:format :json})
;; Analytics for events
(t/add-handler! :analytics
(fn [sig]
(when (= :event (:kind sig))
(send-to-analytics sig)))
{:sample-rate 0.1})
;; OpenTelemetry for traces
(t/add-handler! :otel
(otel-handler)
{:kind-filter #{:trace}})
Testing with Signals
(require '[clojure.test :refer [deftest is]])
(deftest test-logging
(let [sig (t/with-signal
(my-function-that-logs))]
(is (= :info (:level sig)))
(is (= :expected-id (:id sig)))
(is (= expected-data (:data sig)))))
(deftest test-multiple-signals
(let [sigs (t/with-signals
(process-batch items))]
(is (= 5 (count sigs)))
(is (every? #(= :info (:level %)) sigs))))
Dynamic Configuration
;; Enable debug logging temporarily
(defn debug-user-request [user-id]
(t/with-min-level :trace
(t/set-ns-filter! {:allow #{"my.app.*"}})
(process-user user-id)))
;; Feature flag integration
(when (feature-enabled? :verbose-logging)
(t/set-min-level! 'my.app.* :debug))
Error Handling
Exception Logging
;; Automatic exception capture
(try
(risky-operation)
(catch Exception e
(t/error! e)))
;; With context
(try
(db-operation user-id)
(catch Exception e
(t/error! {:id :db-error
:data {:user-id user-id}}
e)))
;; Catch helper
(t/catch->error! :operation
(risky-operation))
Error Context
;; Include error in structured data
(t/log! {:level :error
:id :processing-failed
:data {:user-id user-id
:error (ex-message e)
:cause (ex-cause e)}})
;; Error with trace
(t/trace! {:id :failing-operation
:data {:input data}}
(operation-that-might-fail data))
Performance Considerations
Compile-Time Elision
Signals are compiled away when filtered by minimum level:
;; With min-level :info, this compiles to nil (zero cost)
(t/log! :trace "Expensive" (expensive-computation))
Runtime Performance
Benchmark results (2020 Macbook Pro M1):
- Compile-time filtered: 0 ns/call
- Runtime filtered: 350 ns/call
- Enabled with handler: 1000 ns/call
Capacity: ~4.2 million filtered signals/sec
Optimization Tips
;; Defer expensive computations
(t/log! {:level :debug
:run (expensive-data-builder)}) ; Only runs if logged
;; Use sampling for high-frequency signals
(t/log! {:level :debug
:sample-rate 0.01} ; 1%
"High-frequency event")
;; Async handlers for I/O
(t/add-handler! :db-log
(fn [sig] (write-to-db sig))
{:async {:buffer-size 10000
:n-threads 4}})
Platform-Specific Notes
Babashka
Telemere fully supports Babashka. All core features work identically.
#!/usr/bin/env bb
(require '[taoensso.telemere :as t])
(t/log! :info "Running in Babashka")
ClojureScript
Full ClojureScript support with browser console output.
(ns my-app.core
(:require [taoensso.telemere :as t]))
;; Outputs to browser console
(t/log! :info "ClojureScript logging")
;; Custom handlers for ClojureScript
(t/add-handler! :custom
(fn [sig]
(js/console.log "Custom:" (pr-str sig))))
Interoperability
SLF4J Integration
Automatically captures SLF4J logging:
(t/check-interop)
;;=> {:slf4j {:present? true :sending->telemere? true}}
tools.logging Integration
Automatically captures tools.logging:
(require '[clojure.tools.logging :as log])
;; These route through Telemere
(log/info "Message")
(log/error ex "Error occurred")
OpenTelemetry
Integration requires additional handler setup (see documentation).
Migration from Timbre
Telemere includes Timbre compatibility layer:
;; Use Timbre API
(require '[taoensso.timbre :as timbre])
;; Routes through Telemere
(timbre/info "Message")
(timbre/error ex "Error")
Key differences:
- Telemere emphasizes structured data over string messages
- Filtering is more powerful and flexible
- Tracing is first-class, not an add-on
- Handlers use different configuration format
Use Cases
Application Logging
Standard logging for web apps, services, and batch jobs.
Distributed Tracing
Track request flow through microservices with nested traces.
Performance Monitoring
Identify bottlenecks with automatic execution timing.
Error Tracking
Centralized error collection with structured context.
Audit Logging
Track user actions and system changes with event logging.
Debugging
Rich contextual debugging with trace and spy.
Production Observability
Real-time monitoring with filtered, sampled telemetry.
Resources
- GitHub: https://github.com/taoensso/telemere
- Wiki: https://github.com/taoensso/telemere/wiki
- API Docs: https://cljdoc.org/d/com.taoensso/telemere
- Videos:
- 7-min intro: https://www.youtube.com/watch?v=...
- 24-min REPL demo: https://www.youtube.com/watch?v=...
License
Copyright © 2023-2025 Peter Taoussanis Distributed under the EPL-1.0 (same as Clojure)