Claude Code Plugins

Community-maintained marketplace

Feedback

Structured logging and telemetry for Clojure/Script with tracing and performance monitoring

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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:

  1. Call-time (compile + runtime): Determines if signal is created
  2. 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

License

Copyright © 2023-2025 Peter Taoussanis Distributed under the EPL-1.0 (same as Clojure)