Skip to content

Instantly share code, notes, and snippets.

@ptaoussanis
Last active February 27, 2026 22:44
Show Gist options
  • Select an option

  • Save ptaoussanis/f8a80f85d3e0f89b307a470ce6e044b5 to your computer and use it in GitHub Desktop.

Select an option

Save ptaoussanis/f8a80f85d3e0f89b307a470ce6e044b5 to your computer and use it in GitHub Desktop.
Example of using Telemere with Bling (rich text console printing lib for Clj/s+)
;; Example created: 2024-12-23
;; Last updated: 2025-03-11 by @paintparty
;; Dependencies:
;; Telemere - Ref. https://github.com/taoensso/telemere v1.0.0-RC5 (2025-03-10)
;; Bling - Ref. https://github.com/paintparty/bling v0.5.2 (2025-03-11)
;; Platform: Clj only for now (haven't tried yet with Cljs)
;; Improvements very welcome!
;; Original context: https://github.com/taoensso/telemere/issues/33
(require '[taoensso.telemere :as tel])
(require '[taoensso.telemere.utils :as tel-utils])
(require '[bling.core :as bling])
(defn- signal->callout-opts
"Returns {:keys [type label data?]} for use with `bling/callout`, etc."
[{:keys [level]}]
(let [label (tel-utils/format-level level)
type
(case level
(:report :info) :info
(:fatal :error) :error
(:warn) :warning
(do nil))
colorway
(case level
(:trace :debug) :subtle
(do :neutral))]
{:type type,
:colorway colorway
:label label,
:data? true,
:margin-top 0,
:margin-bottom 1}))
(tel/add-handler! :my-bling-handler
(let [;; Set your preferred formatting options below as usual:
format-signal-fn (tel/format-signal-fn {:incl-newline? false})
my-output-fn
(fn [signal]
(let [norm-output (format-signal-fn signal)] ; Normal Telemere output
(bling.core/callout ; Add Bling "callout" formatting
(signal->callout-opts signal)
norm-output)))]
(tel/handler:console {:output-fn my-output-fn})))
@ptaoussanis
Copy link
Author

Note also https://github.com/paintparty/fireworks from the same author as Bling πŸ‘

@paintparty
Copy link

paintparty commented Mar 10, 2025

Thanks for the fireworks shoutout!

I just released a new version of Bling with some minor breaking changes. I realized the interface to bling.core/callout around the :type option needed some decomplecting and there is now a distinct :colorway option that can be used if you want a colored callout that is not of type :error :warning or :info(which all add labels to the top of the callout by default).

I think the signal->callout-opts function in the above gist should be changed to:

(defn- signal->callout-opts
  "Returns {:keys [type label data?]} for use with `bling/callout`, etc."
  [{:keys [level]}]
  (let [label (tel-utils/format-level level)
        type
        (case level
          (:report :info) :info
          (:fatal :error) :error
          (:warn)         :warning
          (do             nil))
        colorway
        (case level
          (:trace :debug) :subtle
          (do             :neutral))]

    {:type type,
     :colorway colorway
     :label label,
     :data? true,
     :margin-top 0,
     :margin-bottom 1}))

I just tested this on my side with Telemere in a .clj file and it seemed to work. You might want to check it on your side as this test was my first time using Telemere, so I'm not super familiar with it yet.

I did notice that I got no printed ouput when I set the :level to :debug like so:
(tel/log! {:level :debug, :data {:foo :bar}} "Hello again!")

Also tried it in a repl, just telemere without the handler or anything:

foo.core=> (require '[taoensso.telemere :as tel])
nil
foo.core=> (tel/log! {:level :info, :data {:a :foo}} "Hello again!")
2025-03-10T22:35:52.337197Z INFO LOG mycomputer.local foo.core[1,1] Hello again!
   data: {:a :foo}
nil
foo.core=> (tel/log! {:level :debug, :data {:a :foo}} "Hello again!")
nil
foo.core=>

Is that the expected behavior?

Thank you!

@ptaoussanis
Copy link
Author

I just released a new version of Bling with some minor breaking changes. [...]

Sounds good πŸ‘

I think the signal->callout-opts function in the above gist should be changed to:

Updated, thanks!

I did notice that I got no printed ouput when I set the :level to :debug like so [...] Is that the expected behavior?

Yes, that's expected since the default minimum level is :info. One can decrease the minimum level with set-min-level!, etc.

Thanks for checking on this, and for the updated gist - cheers! :-)

@cormacc
Copy link

cormacc commented Feb 27, 2026

I put together a raw console handler using bling earlier, with a little help from Claude:

(ns stack.telemetry.handlers.bling
  (:require
   [clojure.string :as str]
   [taoensso.telemere.utils :as utils]
   [bling.core :as bling]
   [bling.browser :as bling-browser]))

(def telemere-format-preamble-fn (utils/signal-preamble-fn))

(defn- level->bling-color
  "Maps a telemere signal level to a bling color keyword."
  [level]
  (case level
    (:fatal :error) :red
    :warn           :orange
    (:report :info) :dark-blue
    (:debug :trace) :gray
    :blue))

(defn telemere-bling-preamble-fn
  "Returns the telemere preamble as a bling ANSI-tagged string with only the
  level token (e.g. INFO, WARN, ERROR) coloured according to severity."
  [signal]
  (let [preamble    (telemere-format-preamble-fn signal)
        level-token (utils/format-level (:level signal))
        color       (level->bling-color (:level signal))
        idx         (str/index-of preamble level-token)]
    (if idx
      (let [before (subs preamble 0 idx)
            after  (subs preamble (+ idx (count level-token)))]
        (bling/bling before [color level-token] after))
      ;; Fallback β€” shouldn't happen, but return plain preamble if level not found
      preamble)))

(defn telemere-bling-preamble-all-fn
  "Returns the telemere preamble as a bling ANSI-tagged string with the entire
  line coloured according to severity."
  [signal]
  (let [color (level->bling-color (:level signal))]
    (bling/bling [color (telemere-format-preamble-fn signal)])))

(defn handler:bling-raw
  "Custom console-raw handler that renders the preamble group label with bling
  colour formatting.  Telemere's built-in `handler:console-raw` passes a single
  string to `console.group`, which doesn't support `%c` CSS substitution.  This
  handler converts the bling ANSI-tagged preamble into a browser `%c` array and
  applies it to `console.group` so the colour renders in the browser console.

  Options:
    :colour-all - when true, colour the entire preamble line by level
                  (default: false β€” only the level token is coloured)"
  ([] (handler:bling-raw {}))
  ([{:keys [colour-all]}]
   (let [preamble-fn     (if colour-all
                           telemere-bling-preamble-all-fn
                           telemere-bling-preamble-fn)
         format-nsecs-fn (utils/format-nsecs-fn)
         content-fn      (utils/signal-content-fn {:format-nsecs-fn format-nsecs-fn
                                                     :format-error-fn nil
                                                     :raw-error?      true})
         logger-fn       (fn [logger]
                           (fn
                             ([x1]             (.call logger logger x1))
                             ([x1 x2]         (.call logger logger x1 x2))
                             ([x1 x2 x3]      (.call logger logger x1 x2 x3))
                             ([x1 x2 x3 & xs] (apply       logger x1 x2 x3 xs))))]
     (fn a-handler:bling-raw
       ([] ) ;; Stop => noop
       ([signal]
        (let [{:keys [level error]} signal
              logger (utils/js-console-logger level)
              ;; Convert bling ANSI string β†’ #js ["%ctext%c", "css", ""] for console
              group-args (-> (preamble-fn signal)
                             bling-browser/ansi-sgr-string->browser-dev-console-array)]
          (.apply (.-group js/console) js/console group-args)
          (content-fn signal (logger-fn logger) identity)
          (when-let [stack (and error (.-stack (ex-cause error)))]
            (.call logger logger stack))
          (.groupEnd js/console)))))))

@paintparty
Copy link

@cormacc very cool, thank you for sharing!

I would recommend this change, as Bling has built-in semantic aliases precisely for use cases like this:

(defn- level->bling-color
  "Maps a telemere signal level to a bling color keyword."
  [level]
  (case level
    (:fatal :error) :error
    :warn           :warning
    (:report :info) :info
    (:debug :trace) :subtle
    :neutral))  ;; <- changed from :blue to :neutral (maybe you want blue though)

I haven't tested the above on my end, but lmk if it doesn't do what you expect.

Also curious to know if claude made the color translation choices in the above case logic? Because if so, I should consider augmenting the docs to better steer LLMs. In particular, the choice of :dark-blue seems like a bit hallucinatory. I don't think any of the *-dark or *-light variants of the colors have been publicly documented yet. They were added recently in order for bling to use internally, to provide better contrast, but only if the user has a BLING_MOOD env var set. Otherwise, they shouldn't be used to color text as they could potentially render something with very poor contrast, which kind of goes against the whole design of Bling's color pallette approach. For example, :dark-blue would work fine if the user has a light-themed terminal, but very poorly if the user has a dark-themed terminal.

@cormacc
Copy link

cormacc commented Feb 27, 2026

Thanks yourself Jeremiah for a very useful (and fun) library

Re. colour translation choices --they're entirely my fault. Had spent some of the afternoon writing an indexed-db handler for telemere, which reminded me that I'd stolen this gist ages ago but not used it because it wasn't compatible with the cljs-devtools formatters. Did this quickly for fun before shuttting down -- first asked claude to format the log preamble using 'appropriate colors -- e.g. red for error, orange for warning'. Its initial choice for :info was green -- so then asked it to use blue instead, then to use a darker blue -- and here we are :) I'lll certainly use the semantic aliases you've provided going forward.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment