Skip to content

4 Handlers

Peter Taoussanis edited this page Sep 30, 2024 · 15 revisions

Telemere's signal handlers are just plain functions that take a signal (map) to do something with them (analyse them, write them to console/file/queue/db/etc.).

Here's a simple handler: (fn [signal] (println signal)).

A second 0-arg arity will be called when stopping the handler. This is handy for stateful handlers or handlers that need to release resources, e.g.:

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

Telemere includes a number of signal handlers out-the-box, and more may be available via the community.

You can also easily write your own handlers for any output or integration you need.

Included handlers

See ✅ links below for features and usage,
See ❤️ links below to vote on future handlers:

Target (↓) Clj Cljs
Apache Kafka ❤️ -
AWS Kinesis ❤️ -
Console
Console (raw) -
Datadog ❤️ ❤️
Email -
Graylog ❤️ -
Jaeger ❤️ -
Logstash ❤️ -
OpenTelemetry ❤️
Redis ❤️ -
SQL ❤️ -
Slack -
TCP socket -
UDP socket -
Zipkin ❤️ -

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

Handler dispatch opts includes dispatch priority (determines order in which handlers are called), handler filtering, handler middleware, a/sync queue semantics, back-pressure opts, etc.

See help:handler-dispatch-options for full info, and default-handler-dispatch-opts for defaults.

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 each 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.

Console handler

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 MyHost examples(56,1) ::my-id - My message
;;     data: {:x1 :x2}

edn output

To instead writes signals as edn:

;; Create console handler which writes signals as edn
(def my-handler
  (t/handler:console
    {:output-fn (t/pr-signal-fn {:pr-fn :edn})}))

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

JSON output

To instead writes signals as JSON:

;; Ref.  <https://github.com/metosin/jsonista> (or any alt JSON lib)
#?(:clj (require '[jsonista.core :as jsonista]))
(def my-handler
  (t/handler:console
    {:output-fn
     (t/pr-signal-fn
       {:pr-fn
        #?(:cljs :json ; Use js/JSON.stringify
           :clj  jsonista/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 provide an appropriate pr-fn. This lets you plug in the JSON serializer of your choice (jsonista is my default recommendation).

Handler-specific per-signal kvs

Telemere includes a handy mechanism for including arbitrary app-level data/opts in individual signals for use by custom middleware and/or handlers.

Any non-standard (app-level) keys you include in your signal constructor opts will automatically be included in created signals, e.g.:

(t/with-signal
  (t/event! ::my-id
    {:my-middleware-data "foo"
     :my-handler-data    "bar"}))

;; %>
;; {;; App-level kvs included inline (assoc'd to signal root)
;;  :my-middleware-data "foo"
;;  :my-handler-data    "bar"
;;  :kvs ; And also collected together under ":kvs" key
;;  {:my-middleware-data "foo"
;;   :my-handler-data    "bar"}
;;  ... }

These app-level data/opts are typically NOT included by default in handler output, making them a great way to convey data/opts to custom middleware/handlers.

Managing handlers

See help:handlers for info on signal 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.

Stopping handlers

Telemere supports complex handlers that may use internal state, buffers, etc.

For this reason, you should always manually call stop-handlers! somewhere appropriate to give registered handlers the opportunity to flush buffers, close files, etc.

The best place to do this is usually near the end of your application's -main or shutdown procedure, AFTER all other code has completed that could create signals.

You can use call-on-shutdown! to create a JVM shutdown hook.

Note that stop-handlers! will conveniently block to finish async handling of any pending signals. The max blocking time can be configured per-handler via the :drain-msecs handler dispatch option and defaults to 6 seconds.

Handler stats

By default, Telemere handlers maintain comprehensive internal stats including handling times and outcome counters.

This can be really useful for debugging handlers, and understanding handler performance and back-pressure behaviour in practice.

See get-handlers-stats for an output example, etc.

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.

  • Signals are just plain Clojure/Script maps.
  • Handlers just plain Clojure/Script fns of 2 arities:
(defn my-basic-handler
  ([])                        ; Arity-0 called when stopping the handler
  ([signal] (println signal)) ; Arity-1 called when handling a signal
  )

If you're making a customizable handler for use by others, it's often handy to define a handler constructor:

(defn handler:my-fancy-handler ; Note constructor naming convention
  "Needs `some-lib`, Ref. <https://github.com/example/some-lib>.

  Returns a signal handler that:
    - Takes a Telemere signal (map).
    - Does something useful with the signal!

  Options:
    `:option1` - Option description
    `:option2` - Option description

  Tips:
    - Tip 1
    - Tip 2"

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

   ;; Do option validation and other prep here, i.e. try to keep
   ;; expensive work outside handler function when possible!

   (let [handler-fn ; Fn of exactly 2 arities
         (fn a-handler:my-fancy-handler ; Note fn naming convention

           ([] ; Arity-0 called when stopping the handler
            ;; Flush buffers, close files, etc. May just noop.
            ;; Return value is ignored.
            )

           ([signal] ; Arity-1 called when handling a signal
            ;; Do something useful with the given signal (write to
            ;; console/file/queue/db, etc.). Return value is ignored.
            ))]

     ;; (Advanced, optional) You can use metadata to provide default
     ;; handler dispatch options (see `help:handler-dispatch-options`)

     (with-meta handler-fn
       {:dispatch-opts
        {:min-level  :info
         :rate-limit
         [[1   1000] ; Max 1  signal  per second
          [10 60000] ; Max 10 signals per minute
          ]}}))))

Example output

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

Clj console handler

API | string output:

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

Cljs console handler

API | Chrome console:

Default ClojureScript console handler output

Cljs raw console handler

API | Chrome console, with cljs-devtools:

Raw ClojureScript console handler output

Clj file handler

API | MacOS terminal:

Default Clojure file handler output