telemere/wiki/4-Handlers.md
2024-04-29 12:15:19 +02:00

11 KiB

Signal handlers process created signals to do something with them (analyse them, write them to console/file/queue/db, etc.).

Included handlers

The following signal handlers are currently included out-the-box:

Name Platform Output target Output format
handler:console Clj *out* or *err* Formatted string [1]
handler:console Cljs Browser console Formatted string [1]
handler:console-raw Cljs Browser console Raw signal data [2]
handler:file Clj File/s on disk Formatted string [1]
handler:postal Clj Email (via postal) Formatted string [1]
handler:open-telemetry-logger Clj OpenTelemetry Java client LogRecord
  • [1] Configurable: human-readable (default), edn, JSON, etc.
  • [2] For use with browser formatting tools like cljs-devtools.
  • See relevant docstrings (links above) for features, usage, etc.
  • See section 8-Community for more (community-supported) handlers.
  • If there's other handlers you'd like to see, feel free to ping me, or ask on the #telemere Slack channel. It helps to know what people most need!

Configuring handlers

There's two kinds of config relevant to all signal handlers:

  1. Dispatch opts (common to all handlers), and
  2. Handler-specific opts

Dispatch opts

Dispatch opts includes dispatch priority, handler filtering, handler middleware, queue semantics, back-pressure opts, etc.

This is all specified when calling add-handler! - and documented there.

Note that handler middleware in particular is an often overlooked but powerful feature, allowing you to arbitrarily transform and/or filter every signal map before it is given to the handler.

Handler-specific opts

Handler-specific opts are specified when calling a particular handler constructor (like handler:console) - and documented by the constructor.

Note that it's common for Telemere handlers to be customized by providing Clojure/Script functions to the relevant handler constructor call.

See the utils namespace for tools useful for customizing and writing signal handlers.

Example

The standard Clj/s console handler (handler:console) writes signals as strings to *out*/*err or browser console.

By default it writes formatted strings intended for human consumption:

;; Create a test signal
(def my-signal
  (t/with-signal
    (t/log! {:id ::my-id, :data {:x1 :x2}} "My message")))

;; Create console handler with default opts (writes formatted string)
(def my-handler (t/handler:console))

;; Test handler, remember it's just a (fn [signal])
(my-handler my-signal) ; =>
;; 2024-04-11T10:54:57.202869Z INFO LOG Schrebermann.local examples(56,1) ::my-id - My message
;;     data: {:x1 :x2}

To instead writes signals as edn:

;; Create console which writes edn
(def my-handler
  (t/handler:console
    {:format-signal-fn (taoensso.telemere.utils/format-signal->edn-fn)}))

(my-handler my-signal) ; =>
;; {:inst #inst "2024-04-11T10:54:57.202869Z", :msg_ "My message", :ns "examples", ...}

To instead writes signals as JSON:

;; Create console which writes JSON
(def my-handler
  (t/handler:console
    {:format-signal-fn
     (taoensso.telemere.utils/format-signal->json-fn
       {:pr-json-fn jsonista.core/write-value-as-string})}))

(my-handler my-signal) ; =>
;; {"inst":"2024-04-11T10:54:57.202869Z","msg_":"My message","ns":"examples", ...}

Note that when writing JSON with Clojure, you must specify a pr-json-fn. This lets you plug in the JSON serializer of your choice (jsonista is my default recommendation).

Managing handlers

See help:signal-handlers for info on handler management.

Managing handlers on startup

Want to add or remove a particular handler when your application starts?

Just make an appropriate call to add-handler! or remove-handler!.

Environmental config

If you want to manage handlers conditionally based on environmental config (JVM properties, environment variables, or classpath resources) - Telemere provides the highly flexible get-env util.

Use this to easily define your own arbitrary cross-platform config, and make whatever conditional handler management decisions you'd like.

Writing handlers

Writing your own signal handlers for Telemere is straightforward, and a reasonable choice if you prefer customizing behaviour that way, or want to write signals to a DB/format/service for which a ready-made handler isn't available.

Remember that signals are just plain Clojure/Script maps, and handlers just plain Clojure/Script functions that do something with those maps.

Here's a simple Telemere handler:

(fn my-handler [signal] (println signal))

For more complex cases, or for handlers that you want to make available for use by other folks, here's the general template that Telemere uses for all its included handlers:

(defn handler:my-handler ; Note naming convention
  "Returns a (fn handler [signal] that:
    - Takes a Telemere signal.
    - Does something with it.

  Options:
    `:option1` - Description
    `:option2` - Description"

  ([] (handler:my-handler nil)) ; Use default opts
  ([{:as constructor-opts}]

   ;; Do option validation and expensive prep *outside* returned handler
   ;; fn whenever possible - i.e. at (one-off) construction time rather than
   ;; at every handler call.
   (let []

     (fn a-handler:my-handler ; Note naming convention

       ;; Shutdown arity - called by Telemere exactly once when the handler is
       ;; to be shut down. This is your opportunity to finalize/free resources, etc.
       ([])

       ;; Main arity - called by Telemere whenever the handler should handle the
       ;; given signal. Never called after shutdown.
       ([signal]
        ;; TODO Do something with given signal
        )))))

Example output

(t/log! {:id ::my-id, :data {:x1 :x2}} "My message") =>

Clj console handler

String output:

2024-04-11T10:54:57.202869Z INFO LOG Schrebermann.local examples(56,1) ::my-id - My message
    data: {:x1 :x2}

Cljs console handler

Chrome console:

Default ClojureScript console handler output

Cljs raw console handler

Chrome console, with cljs-devtools:

Raw ClojureScript console handler output

Clj file handler

MacOS terminal:

Default Clojure file handler output