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.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
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"
manifest: skills/data/clojure-telemere/SKILL.md
source content

Telemere

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:

FunctionQuick ArgsReturnsUse Case
log!
[?level msg]
nilTraditional log messages
event!
[id ?level]
nilStructured events
trace!
[?id form]
form resultTrace execution + timing
spy!
[?level form]
form resultDebug form values
error!
[?id error]
errorLog errors
catch->error!
[?id form]
value or fallbackCatch & log errors
signal!
[opts]
dependsLow-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
    ,
    :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 created

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:

  • 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)

Interop

SLF4J (Java Logging)

  1. Add dependencies:

    • org.slf4j/slf4j-api
      (v2+)
    • com.taoensso/telemere-slf4j
  2. SLF4J calls automatically become Telemere signals

Verify:

(tel/check-interop)
=>
{:slf4j {:telemere-receiving? true}}

tools.logging

  1. Add
    org.clojure/tools.logging
    dependency
  2. Call
    (tel/tools-logging->telemere!)
    or set env config

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

  1. Always call

    stop-handlers!
    on shutdown to flush buffers and close resources. Use
    tel/call-on-shutdown!
    for JVM shutdown hooks.

  2. Signals are filtered before creation - data in

    :let
    ,
    :data
    ,
    :msg
    ,
    :do
    is only evaluated if the signal passes filters.

  3. Handler filters are additive - handlers can be MORE restrictive than call filters, not less.

  4. Messages are lazy - message building only happens if the signal is created and handled.

  5. :error
    value !=
    :error
    level
    - signals can have error values at any level, and vice versa.

  6. Cache validators - use

    tel/validator
    ,
    tel/decoder
    , etc. once, not per signal.

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

Internal Help

Telemere includes extensive docstrings accessible from your REPL:

  • tel/help:signal-creators
    - Creating signals
  • tel/help:signal-options
    - All signal options
  • tel/help:signal-content
    - Signal map content
  • tel/help:filters
    - Filtering and transformations
  • tel/help:handlers
    - Handler management
  • tel/help:handler-dispatch-options
    - Handler dispatch configuration
  • tel/help:environmental-config
    - JVM/env/classpath configuration