Claude-skill-registry clojure-telemere
Structured telemetry library for Clojure/Script. Use when working with logging, tracing, structured logging, events, signal handling, observability, or replacing Timbre/tools.logging.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/clojure-telemere" ~/.claude/skills/majiayu000-claude-skill-registry-clojure-telemere && rm -rf "$T"
skills/data/clojure-telemere/SKILL.mdTelemere
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.
Setup
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]))
Quick Start
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_ ...]}
Core Signal Creators
Telemere provides multiple signal creators optimized for different use cases. All accept an opts map:
| Function | Quick Args | Returns | Use Case |
|---|---|---|---|
| | nil | Traditional log messages |
| | nil | Structured events |
| | form result | Trace execution + timing |
| | form result | Debug form values |
| | error | Log errors |
| | value or fallback | Catch & log errors |
| | 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
Signal Options
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
(default),:info
,:warn
,:error
, or integer:fatal
- qualified keyword for identifying this signal type:id
- structured data map (preserved as data):data
- message string or vector to join:msg
- bindings available to:let
and:data:msg
- random sampling rate (0.0 to 1.0):sample
- conditional execution:when
- rate limiting map:limit
- side effects to execute when signal is created:do
Filtering
Filtering 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.
Signal Handlers
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!)
Included 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:
- Write to files (Clj only)handler:file
- Email via Postal (Clj only)handler:postal
- Slack notifications (Clj only)handler:slack
/handler:tcp-socket
- Network sockets (Clj only)handler:udp-socket
- OpenTelemetry integration (Clj only)handler:open-telemetry
Interop
SLF4J (Java Logging)
-
Add dependencies:
(v2+)org.slf4j/slf4j-apicom.taoensso/telemere-slf4j
-
SLF4J calls automatically become Telemere signals
Verify:
(tel/check-interop) => {:slf4j {:telemere-receiving? true}}
tools.logging
- Add
dependencyorg.clojure/tools.logging - Call
or set env config(tel/tools-logging->telemere!)
Verify:
(tel/check-interop) => {:tools-logging {:telemere-receiving? true}}
System Streams
Redirect
System/out and System/err to Telemere:
(tel/streams->telemere!)
OpenTelemetry
See references/config.md for OpenTelemetry integration.
Common Patterns
Message Building
;; 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)])
Tracing Nested Operations
(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))))
Dynamic Context
;; 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"))
Error Handling
;; 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))
Key Gotchas
-
Always call
on shutdown to flush buffers and close resources. Usestop-handlers!
for JVM shutdown hooks.tel/call-on-shutdown! -
Signals are filtered before creation - data in
,:let
,:data
,:msg
is only evaluated if the signal passes filters.:do -
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.
-
value !=:error
level - signals can have error values at any level, and vice versa.:error -
Cache validators - use
,tel/validator
, etc. once, not per signal.tel/decoder
Performance
Telemere is highly optimized:
- Filtered signals: ~350 nsecs/call
- Compile-time elision: 0 nsecs (completely removed)
- Handler dispatch is typically async with backpressure control
Tips for performance:
- Use compile-time filtering for hot paths
- Use sampling for high-volume signals
- Use rate limiting for expensive operations
- Cache validators/transformers outside signal calls
Detailed References
- Getting Started - Setup, usage, default config
- Architecture - How signal flow works
- Config - Filtering, handlers, interop configuration
- Handlers - Writing and configuring handlers
- FAQ - Common questions (vs Timbre, vs μ/log, etc.)
- Tips - Best practices for observable systems
External References
- GitHub: https://github.com/taoensso/telemere
- Wiki: https://github.com/taoensso/telemere/wiki
- API Docs: https://cljdoc.org/d/com.taoensso/telemere/
- Slack: https://www.taoensso.com/telemere/slack
Internal Help
Telemere includes extensive docstrings accessible from your REPL:
- Creating signalstel/help:signal-creators
- All signal optionstel/help:signal-options
- Signal map contenttel/help:signal-content
- Filtering and transformationstel/help:filters
- Handler managementtel/help:handlers
- Handler dispatch configurationtel/help:handler-dispatch-options
- JVM/env/classpath configurationtel/help:environmental-config