Structured logging and telemetry for Clojure/Script with tracing and performance monitoring
Adds structured logging, tracing, and performance monitoring for Clojure/Script. Use when you need to log events, trace function execution, or monitor performance by calling `t/log!`, `t/trace!`, or `t/spy!`.
/plugin marketplace add hugoduncan/library-skills/plugin install clojure-libraries@library-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
examples.cljmetadata.ednStructured logging and telemetry library for Clojure and ClojureScript. Next-generation successor to Timbre with unified API for logging, tracing, and performance monitoring.
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:
Artifact: com.taoensso/telemere
Latest Version: 1.1.0
License: EPL-1.0
Repository: https://github.com/taoensso/telemere
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]))
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.
Out-of-the-box settings:
:info*out* or browser consoleTwo-stage filtering:
Effective filtering reduces noise and improves performance.
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 resultID 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.
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.
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 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 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.
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.
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.
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 (*).
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.
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.
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 process signals and route them to destinations (console, files, databases, analytics).
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 functionsRemove handler by ID.
(t/remove-handler! :my-console)
(t/remove-handler! :custom)
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)))})
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"))
Check if level passes minimum threshold.
(t/check-min-level :info) ;;=> true/false
(t/check-min-level 'my.ns :debug) ;;=> true/false
Check if namespace passes filter.
(t/check-ns-filter 'my.app.core) ;;=> true/false
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.
Documentation on filtering.
t/help:filters
Documentation on handlers.
t/help:handlers
(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))))
;; 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}})))
(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)
(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)))
(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)))
;; 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
;; 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
;; 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}})
(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))))
;; 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))
;; 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))
;; 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))
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))
Benchmark results (2020 Macbook Pro M1):
Capacity: ~4.2 million filtered signals/sec
;; 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}})
Telemere fully supports Babashka. All core features work identically.
#!/usr/bin/env bb
(require '[taoensso.telemere :as t])
(t/log! :info "Running in Babashka")
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))))
Automatically captures SLF4J logging:
(t/check-interop)
;;=> {:slf4j {:present? true :sending->telemere? true}}
Automatically captures tools.logging:
(require '[clojure.tools.logging :as log])
;; These route through Telemere
(log/info "Message")
(log/error ex "Error occurred")
Integration requires additional handler setup (see documentation).
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:
Standard logging for web apps, services, and batch jobs.
Track request flow through microservices with nested traces.
Identify bottlenecks with automatic execution timing.
Centralized error collection with structured context.
Track user actions and system changes with event logging.
Rich contextual debugging with trace and spy.
Real-time monitoring with filtered, sampled telemetry.
Copyright © 2023-2025 Peter Taoussanis Distributed under the EPL-1.0 (same as Clojure)
Master authentication and authorization patterns including JWT, OAuth2, session management, and RBAC to build secure, scalable access control systems. Use when implementing auth systems, securing APIs, or debugging security issues.