mirror of
https://github.com/taoensso/telemere.git
synced 2025-12-16 17:41:12 +00:00
[mod] Rename, refactor signal formatting utils
- Simplified some util name (only relevant to folks customizing handler behaviour) - Merged `format-signal->edn-fn`, `format-signal->json-fn` to single `pr-signal-fn`
This commit is contained in:
parent
f3659146bf
commit
21cb44e709
14 changed files with 245 additions and 249 deletions
17
README.md
17
README.md
|
|
@ -133,15 +133,14 @@ See relevant docstrings (links below) for usage info-
|
|||
|
||||
### Internal help
|
||||
|
||||
| Var | Help with |
|
||||
| :-------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
|
||||
| [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) | List of signal creators |
|
||||
| [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) | Options for signal creators |
|
||||
| [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) | Signal map content |
|
||||
| [`help:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) | Ordered flow from signal creation to handling |
|
||||
| [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) | API for configuring signal filters |
|
||||
| [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) | API for configuring signal handlers |
|
||||
| [`help:signal-formatters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-formatters) | Signal formatters for use by handlers |
|
||||
| Var | Help with |
|
||||
| :---------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- |
|
||||
| [`help:signal-creators`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-creators) | List of signal creators |
|
||||
| [`help:signal-options`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-options) | Options for signal creators |
|
||||
| [`help:signal-content`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-content) | Signal map content |
|
||||
| [`help:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) | Ordered flow from signal creation to handling |
|
||||
| [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) | API for configuring signal filters |
|
||||
| [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) | API for configuring signal handlers |
|
||||
|
||||
### Example handler output
|
||||
|
||||
|
|
|
|||
|
|
@ -141,17 +141,19 @@
|
|||
;; Create console which writes signals as edn
|
||||
(def my-handler
|
||||
(t/handler:console
|
||||
{:format-signal-fn (taoensso.telemere.utils/format-signal->edn-fn)}))
|
||||
{:output-fn (t/pr-signal-fn :edn)}))
|
||||
|
||||
(my-handler my-signal) ; =>
|
||||
;; {:inst #inst "2024-04-11T10:54:57.202869Z", :msg_ "My message", :ns "examples", ...}
|
||||
|
||||
;; Create console which writes signals as JSON
|
||||
#?(:clj (require '[jsonista.core :as jsonista]))
|
||||
(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})}))
|
||||
{:output-fn
|
||||
(t/pr-signal-fn
|
||||
#?(:cljs :json
|
||||
:clj jsonista.core/write-value-as-string))}))
|
||||
|
||||
(my-handler my-signal) ; =>
|
||||
;; {"inst":"2024-04-11T10:54:57.202869Z","msg_":"My message","ns":"examples", ...}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@
|
|||
[io.opentelemetry/opentelemetry-api "1.37.0"]
|
||||
#_[io.opentelemetry/opentelemetry-sdk-extension-autoconfigure "1.37.0"]
|
||||
#_[io.opentelemetry/opentelemetry-exporter-otlp "1.37.0"]
|
||||
[metosin/jsonista "0.3.8"]
|
||||
[com.draines/postal "2.0.5"]]
|
||||
|
||||
:plugins
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
Common signal formatters include:
|
||||
(utils/format-signal->str-fn) {<opts>}) ; For human-readable string output (default)
|
||||
(utils/format-signal->edn-fn) {<opts>}) ; For edn output
|
||||
(utils/format-signal->json-fn {<opts>}) ; For JSON output
|
||||
|
||||
See relevant docstrings for details.
|
||||
|
|
@ -79,7 +79,9 @@
|
|||
#?(:clj impl/with-signal)
|
||||
#?(:clj impl/with-signals)
|
||||
#?(:clj impl/signal!)
|
||||
utils/error-signal?)
|
||||
utils/error-signal?
|
||||
utils/pr-signal-fn
|
||||
utils/format-signal-fn)
|
||||
|
||||
;;;; Help
|
||||
|
||||
|
|
@ -89,7 +91,6 @@
|
|||
(impl/defhelp help:signal-content :signal-content)
|
||||
(enc/defalias help:signal-filters help:filters) ; Via Encore
|
||||
(enc/defalias help:signal-handlers help:handlers) ; Via Encore
|
||||
(impl/defhelp help:signal-formatters :signal-formatters)
|
||||
|
||||
;;;; Context
|
||||
|
||||
|
|
|
|||
|
|
@ -24,14 +24,13 @@
|
|||
signals formatted as edn, JSON, or human-readable strings.
|
||||
|
||||
Options:
|
||||
`:format-signal-fn` - (fn [signal]) => output, see `help:signal-formatters`
|
||||
|
||||
`:stream` - `java.io.writer`
|
||||
`:output-fn` - (fn [signal]) => output string, see `format-signal-fn` or `pr-signal-fn`
|
||||
`:stream` - `java.io.writer`
|
||||
Defaults to `*err*` if `utils/error-signal?` is true, and `*out*` otherwise."
|
||||
|
||||
([] (handler:console nil))
|
||||
([{:keys [format-signal-fn stream]
|
||||
:or {format-signal-fn (utils/format-signal->str-fn)}}]
|
||||
([{:keys [output-fn stream]
|
||||
:or {output-fn (utils/format-signal-fn)}}]
|
||||
|
||||
(let [stream (case stream :*out* *out*, :*err* *err* stream)
|
||||
error-signal? utils/error-signal?]
|
||||
|
|
@ -41,7 +40,7 @@
|
|||
([signal]
|
||||
(let [^java.io.Writer stream
|
||||
(or stream (if (error-signal? signal) *err* *out*))]
|
||||
(when-let [output (format-signal-fn signal)]
|
||||
(when-let [output (output-fn signal)]
|
||||
(.write stream (str output))
|
||||
(.flush stream))))))))
|
||||
|
||||
|
|
@ -57,11 +56,11 @@
|
|||
signals formatted as edn, JSON, or human-readable strings.
|
||||
|
||||
Options:
|
||||
`:format-signal-fn` - (fn [signal]) => output, see `help:signal-formatters`"
|
||||
`:output-fn` - (fn [signal]) => output string, see `format-signal-fn` or `pr-signal-fn`"
|
||||
|
||||
([] (handler:console nil))
|
||||
([{:keys [format-signal-fn]
|
||||
:or {format-signal-fn (utils/format-signal->str-fn)}}]
|
||||
([{:keys [output-fn]
|
||||
:or {output-fn (utils/format-signal-fn)}}]
|
||||
|
||||
(when (exists? js/console)
|
||||
(let [js-console-logger utils/js-console-logger]
|
||||
|
|
@ -69,7 +68,7 @@
|
|||
(fn a-handler:console
|
||||
([]) ; Shut down (no-op)
|
||||
([signal]
|
||||
(when-let [output (format-signal-fn signal)]
|
||||
(when-let [output (output-fn signal)]
|
||||
(let [logger (js-console-logger (get signal :level))]
|
||||
(.call logger logger (str output)))))))))))
|
||||
|
||||
|
|
@ -94,16 +93,16 @@
|
|||
Ref. <https://github.com/binaryage/cljs-devtools>."
|
||||
|
||||
([] (handler:console-raw nil))
|
||||
([{:keys [format-signal->prelude-fn format-nsecs-fn] :as opts
|
||||
([{:keys [preamble-fn format-nsecs-fn] :as opts
|
||||
:or
|
||||
{format-signal->prelude-fn (utils/format-signal->prelude-fn) ; (fn [signal])
|
||||
format-nsecs-fn (utils/format-nsecs-fn) ; (fn [nanosecs])
|
||||
{preamble-fn (utils/signal-preamble-fn)
|
||||
format-nsecs-fn (utils/format-nsecs-fn) ; (fn [nanosecs])
|
||||
}}]
|
||||
|
||||
(when (and (exists? js/console) (exists? js/console.group))
|
||||
(let [js-console-logger utils/js-console-logger
|
||||
signal-content-handler ; (fn [signal hf vf]
|
||||
(utils/signal-content-handler
|
||||
content-fn ; (fn [signal append-fn val-fn])
|
||||
(utils/signal-content-fn
|
||||
{:format-nsecs-fn format-nsecs-fn
|
||||
:format-error-fn nil
|
||||
:raw-error? true})]
|
||||
|
|
@ -115,8 +114,8 @@
|
|||
logger (js-console-logger level)]
|
||||
|
||||
;; Unfortunately groups have no level
|
||||
(.group js/console (format-signal->prelude-fn signal))
|
||||
(signal-content-handler signal (logger-fn logger) identity)
|
||||
(.group js/console (preamble-fn signal))
|
||||
(content-fn signal (logger-fn logger) identity)
|
||||
|
||||
(when-let [stack (and error (.-stack (enc/ex-root error)))]
|
||||
(.call logger logger stack))
|
||||
|
|
|
|||
|
|
@ -284,9 +284,10 @@
|
|||
`/logs/telemere.log-2020-01-01m.8.gz` ; Archive for Jan 2020, part 8 (oldest entries)
|
||||
|
||||
Options:
|
||||
`:format-signal-fn`- (fn [signal]) => output, see `help:signal-formatters`.
|
||||
`:path` - Path string of the target output file (default `logs/telemere.log`).
|
||||
`:interval` - ∈ #{nil :daily :weekly :monthly} (default `:monthly`).
|
||||
`:output-fn`- (fn [signal]) => output string, see `format-signal-fn` or `pr-signal-fn`
|
||||
`:path` - Path string of the target output file (default `logs/telemere.log`)
|
||||
|
||||
`:interval` - ∈ #{nil :daily :weekly :monthly} (default `:monthly`)
|
||||
When non-nil, causes interval-based archives to be maintained.
|
||||
|
||||
`:max-file-size` ∈ #{nil <pos-int>} (default 4MB)
|
||||
|
|
@ -300,7 +301,7 @@
|
|||
|
||||
([] (handler:file nil))
|
||||
([{:keys
|
||||
[format-signal-fn
|
||||
[output-fn
|
||||
path interval
|
||||
max-file-size
|
||||
max-num-parts
|
||||
|
|
@ -308,7 +309,7 @@
|
|||
gzip-archives?]
|
||||
|
||||
:or
|
||||
{format-signal-fn (utils/format-signal->str-fn)
|
||||
{output-fn (utils/format-signal-fn)
|
||||
path "logs/telemere.log" ; Main path, we'll ALWAYS write to this exact file
|
||||
interval :monthly
|
||||
max-file-size (* 1024 1024 4) ; 4MB
|
||||
|
|
@ -362,7 +363,7 @@
|
|||
(fn a-handler:file
|
||||
([] (locking lock (fw))) ; Close writer
|
||||
([signal]
|
||||
(when-let [output (format-signal-fn signal)]
|
||||
(when-let [output (output-fn signal)]
|
||||
(let [new-interval? (when interval (new-interval!?))
|
||||
>max-file-size? (when max-file-size (>max-file-size?))
|
||||
reset-stream? (or new-interval? >max-file-size?)]
|
||||
|
|
|
|||
|
|
@ -13,23 +13,23 @@
|
|||
|
||||
;;;; Implementation
|
||||
|
||||
(defn format-signal->subject-fn
|
||||
(defn signal-subject-fn
|
||||
"Experimental, subject to change.
|
||||
Returns a (fn format [signal]) that:
|
||||
- Takes a Telemere signal.
|
||||
- Returns a formatted email subject like:
|
||||
\"INFO EVENT :taoensso.telemere.postal/ev-id1 - msg\""
|
||||
([] (format-signal->subject-fn nil))
|
||||
([] (signal-subject-fn nil))
|
||||
([{:keys [max-len subject-signal-key]
|
||||
:or
|
||||
{max-len 128
|
||||
subject-signal-key :postal/subject}}]
|
||||
|
||||
(fn format-signal->subject [signal]
|
||||
(fn signal-subject [signal]
|
||||
(or
|
||||
(get signal subject-signal-key) ; Custom subject
|
||||
|
||||
;; Simplified `format-signal->prelude-fn`
|
||||
;; Simplified `utils/signal-preamble-fn`
|
||||
(let [{:keys [level kind #_ns id msg_]} signal
|
||||
sb (enc/str-builder)
|
||||
s+spc (enc/sb-appender sb " ")]
|
||||
|
|
@ -41,9 +41,7 @@
|
|||
|
||||
(enc/get-substr-by-len (str sb) 0 max-len))))))
|
||||
|
||||
(comment
|
||||
((format-signal->subject-fn)
|
||||
(tel/with-signal (tel/event! ::ev-id1 #_{:postal/subject "My subject"}))))
|
||||
(comment ((signal-subject-fn) (tel/with-signal (tel/event! ::ev-id1 #_{:postal/subject "My subject"}))))
|
||||
|
||||
;;;; Handler
|
||||
|
||||
|
|
@ -80,8 +78,8 @@
|
|||
:cc \"engineering@example.com\"
|
||||
:X-MyHeader \"A custom header\"}
|
||||
|
||||
`:format-signal-fn` - (fn [signal]) => output, see `help:signal-formatters`
|
||||
`:format-signal->subject-fn` - (fn [signal]) => email subject string
|
||||
`:subject-fn` - (fn [signal]) => email subject string
|
||||
`:body-fn` - (fn [signal]) => email body content string, see `format-signal-fn` or `pr-signal-fn`
|
||||
|
||||
Tips:
|
||||
|
||||
|
|
@ -107,12 +105,12 @@
|
|||
([{:keys
|
||||
[postal/conn-opts
|
||||
postal/msg-opts
|
||||
format-signal-fn
|
||||
format-signal->subject-fn]
|
||||
subject-fn
|
||||
body-fn]
|
||||
|
||||
:or
|
||||
{format-signal-fn (utils/format-signal->str-fn)
|
||||
format-signal->subject-fn (format-signal->subject-fn)}}]
|
||||
{subject-fn (signal-subject-fn)
|
||||
body-fn (utils/format-signal-fn)}}]
|
||||
|
||||
(when-not conn-opts (throw (ex-info "No `:postal/conn-opts` was provided" {})))
|
||||
(when-not msg-opts (throw (ex-info "No `:postal/msg-opts` was provided" {})))
|
||||
|
|
@ -121,14 +119,16 @@
|
|||
(defn a-handler:postal
|
||||
([]) ; Shut down (no-op)
|
||||
([signal]
|
||||
(enc/when-let [content (format-signal-fn signal)
|
||||
subject (format-signal->subject-fn signal)]
|
||||
(enc/when-let [subject (subject-fn signal)
|
||||
body (body-fn signal)]
|
||||
(let [msg
|
||||
(assoc msg-opts
|
||||
:subject (str subject)
|
||||
:body
|
||||
[{:type "text/plain; charset=utf-8"
|
||||
:content (str content)}])
|
||||
(if (string? body)
|
||||
[{:type "text/plain; charset=utf-8"
|
||||
:content (str body)}]
|
||||
body))
|
||||
|
||||
[result ex]
|
||||
(try
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@
|
|||
`host` - Destination TCP socket hostname string
|
||||
`port` - Destination TCP socket port int
|
||||
|
||||
`:socket-opts` - {:keys [ssl? connect-timeout-msecs]}
|
||||
`:format-signal-fn`- (fn [signal]) => output, see `help:signal-formatters`.
|
||||
`:socket-opts` - {:keys [ssl? connect-timeout-msecs]}
|
||||
`:output-fn` - (fn [signal]) => output string, see `format-signal-fn` or `pr-signal-fn`
|
||||
|
||||
Limitations:
|
||||
- Failed writes will be retried only once.
|
||||
|
|
@ -39,14 +39,14 @@
|
|||
|
||||
([host port] (handler:tcp-socket host port nil))
|
||||
([host port
|
||||
{:keys [socket-opts format-signal-fn]
|
||||
:or {format-signal-fn (utils/format-signal->str-fn)}}]
|
||||
{:keys [socket-opts output-fn]
|
||||
:or {output-fn (utils/format-signal-fn)}}]
|
||||
|
||||
(let [sw (utils/tcp-socket-writer host port socket-opts)]
|
||||
(defn a-handler:tcp-socket
|
||||
([] (sw)) ; Shut down
|
||||
([signal]
|
||||
(when-let [output (format-signal-fn signal)]
|
||||
(when-let [output (output-fn signal)]
|
||||
(sw output)))))))
|
||||
|
||||
(defn handler:udp-socket
|
||||
|
|
@ -60,7 +60,7 @@
|
|||
`host` - Destination UDP socket hostname string
|
||||
`port` - Destination UDP socket port int
|
||||
|
||||
`:format-signal-fn` - (fn [signal]) => output, see `help:signal-formatters`.
|
||||
`:output-fn` - (fn [signal]) => output string, see `format-signal-fn` or `pr-signal-fn`
|
||||
`:max-packet-bytes` - Max packet size (in bytes) before truncating output (default 512)
|
||||
`:truncation-warning-fn` - Optional (fn [{:keys [max actual signal]}]) to call whenever
|
||||
output is truncated. Should be appropriately rate-limited!
|
||||
|
|
@ -75,10 +75,10 @@
|
|||
|
||||
([host port] (handler:udp-socket host port nil))
|
||||
([host port
|
||||
{:keys [max-packet-bytes truncation-warning-fn format-signal-fn]
|
||||
{:keys [output-fn max-packet-bytes truncation-warning-fn]
|
||||
:or
|
||||
{max-packet-bytes 512
|
||||
format-signal-fn (utils/format-signal->str-fn)}}]
|
||||
{output-fn (utils/format-signal-fn)
|
||||
max-packet-bytes 512}}]
|
||||
|
||||
(let [max-packet-bytes (int max-packet-bytes)
|
||||
socket (DatagramSocket.) ; No need to change socket once created
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
(defn a-handler:udp-socket
|
||||
([] (.close socket)) ; Shut down
|
||||
([signal]
|
||||
(when-let [output (format-signal-fn signal)]
|
||||
(when-let [output (output-fn signal)]
|
||||
(let [ba (enc/str->utf8-ba (str output))
|
||||
ba-len (alength ba)
|
||||
packet (DatagramPacket. ba (min ba-len max-packet-bytes))]
|
||||
|
|
|
|||
|
|
@ -452,17 +452,21 @@
|
|||
(do (enc/ex-map (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"}))))
|
||||
(println (str "--\n" ((format-error-fn) (ex-info "Ex2" {:k2 "v2"} (ex-info "Ex1" {:k1 "v1"}))))))
|
||||
|
||||
(defn format-signal->prelude-fn
|
||||
;;;;
|
||||
|
||||
(defn signal-preamble-fn
|
||||
"Experimental, subject to change.
|
||||
Returns a (fn format [signal]) that:
|
||||
Returns a (fn preamble [signal]) that:
|
||||
- Takes a Telemere signal.
|
||||
- Returns a formatted prelude string like:
|
||||
\"2024-03-26T11:14:51.806Z INFO EVENT Hostname taoensso.telemere(2,21) ::ev-id - msg\""
|
||||
([] (format-signal->prelude-fn nil))
|
||||
- Returns a signal preamble ?string like:
|
||||
\"2024-03-26T11:14:51.806Z INFO EVENT Hostname taoensso.telemere(2,21) ::ev-id - msg\"
|
||||
|
||||
See arglists for options."
|
||||
([] (signal-preamble-fn nil))
|
||||
([{:keys [format-inst-fn]
|
||||
:or {format-inst-fn (format-inst-fn)}}]
|
||||
|
||||
(fn format-signal->prelude [signal]
|
||||
(fn signal-preamble [signal]
|
||||
(let [{:keys [inst level kind ns id msg_]} signal
|
||||
sb (enc/str-builder)
|
||||
s+spc (enc/sb-appender sb " ")]
|
||||
|
|
@ -484,173 +488,169 @@
|
|||
|
||||
(when id (s+spc (format-id ns id)))
|
||||
(when-let [msg (force msg_)] (s+spc "- " msg))
|
||||
(str sb)))))
|
||||
|
||||
(comment ((format-signal->prelude-fn) (tel/with-signal (tel/event! ::ev-id))))
|
||||
(when-not (zero? (enc/sb-length sb))
|
||||
(str sb))))))
|
||||
|
||||
(defn ^:no-doc signal-content-handler
|
||||
"Private, don't use.
|
||||
Returns a (fn handle [signal handle-fn value-fn]) for internal use.
|
||||
Content equivalent to `format-signal->prelude-fn`."
|
||||
([] (signal-content-handler nil))
|
||||
(comment ((signal-preamble-fn) (tel/with-signal (tel/event! ::ev-id))))
|
||||
|
||||
(defn signal-content-fn
|
||||
"Experimental, subject to change.
|
||||
Returns a (fn content [signal]) that:
|
||||
- Takes a Telemere signal.
|
||||
- Returns a signal content ?string (incl. data, ctx, etc.)
|
||||
|
||||
See arglists for options."
|
||||
([] (signal-content-fn nil))
|
||||
([{:keys
|
||||
[format-nsecs-fn
|
||||
format-error-fn
|
||||
raw-error?
|
||||
incl-thread?
|
||||
incl-kvs?]
|
||||
[incl-thread? incl-kvs? raw-error?,
|
||||
format-nsecs-fn format-error-fn]
|
||||
|
||||
:or
|
||||
{format-nsecs-fn (format-nsecs-fn) ; (fn [nanosecs])
|
||||
format-error-fn (format-error-fn) ; (fn [error])
|
||||
}}]
|
||||
|
||||
(let [err-start (str newline "<<< error <<<" newline)
|
||||
err-stop (str newline ">>> error >>>")]
|
||||
(let [nl newline
|
||||
err-start (str nl "<<< error <<<" nl)
|
||||
err-stop (str nl ">>> error >>>")]
|
||||
|
||||
(fn a-signal-content-handler [signal hf vf]
|
||||
(let [{:keys [uid parent data kvs ctx #?(:clj thread) sample-rate]} signal]
|
||||
(when sample-rate (hf "sample: " (vf sample-rate)))
|
||||
(when uid (hf " uid: " (vf uid)))
|
||||
(when parent (hf "parent: " (vf parent)))
|
||||
#?(:clj (when (and thread incl-thread?) (hf "thread: " (vf thread))))
|
||||
(when data (hf " data: " (vf data)))
|
||||
(when (and kvs incl-kvs?) (hf " kvs: " (vf kvs)))
|
||||
(when ctx (hf " ctx: " (vf ctx))))
|
||||
(fn signal-content
|
||||
([signal]
|
||||
(let [sb (enc/str-builder)
|
||||
s++ (enc/sb-appender sb nl)]
|
||||
(signal-content signal s++ enc/pr-edn*)
|
||||
(when-not (zero? (enc/sb-length sb))
|
||||
(str sb))))
|
||||
|
||||
(let [{:keys [run-form error]} signal]
|
||||
(when run-form
|
||||
(let [{:keys [run-val run-nsecs]} signal
|
||||
run-time (when run-nsecs (when-let [ff format-nsecs-fn] (ff run-nsecs)))
|
||||
run-info
|
||||
(if error
|
||||
{:form run-form
|
||||
:time run-time
|
||||
:nsecs run-nsecs}
|
||||
;; Undocumented, advanced arity
|
||||
([signal append-fn val-fn]
|
||||
(let [af append-fn
|
||||
vf val-fn]
|
||||
|
||||
{:form run-form
|
||||
:time run-time
|
||||
:nsecs run-nsecs
|
||||
:val run-val
|
||||
#?@(:clj [:val-type (enc/class-sym run-val)])})]
|
||||
(let [{:keys [uid parent data kvs ctx #?(:clj thread) sample-rate]} signal]
|
||||
(when sample-rate (af " sample: " (vf sample-rate)))
|
||||
(when uid (af " uid: " (vf uid)))
|
||||
(when parent (af " parent: " (vf parent)))
|
||||
#?(:clj (when (and thread incl-thread?) (af " thread: " (vf thread))))
|
||||
(when data (af " data: " (vf data)))
|
||||
(when (and kvs incl-kvs?) (af " kvs: " (vf kvs)))
|
||||
(when ctx (af " ctx: " (vf ctx))))
|
||||
|
||||
(hf " run: " (vf run-info))))
|
||||
(let [{:keys [run-form error]} signal]
|
||||
(when run-form
|
||||
(let [{:keys [run-val run-nsecs]} signal
|
||||
run-time (when run-nsecs (when-let [ff format-nsecs-fn] (ff run-nsecs)))
|
||||
run-info
|
||||
(if error
|
||||
{:form run-form
|
||||
:time run-time
|
||||
:nsecs run-nsecs}
|
||||
|
||||
(when error
|
||||
(if raw-error?
|
||||
(hf " error: " error)
|
||||
(when-let [ff format-error-fn]
|
||||
(hf err-start (ff error) err-stop)))))))))
|
||||
{:form run-form
|
||||
:time run-time
|
||||
:nsecs run-nsecs
|
||||
:val run-val
|
||||
#?@(:clj [:val-type (enc/class-sym run-val)])})]
|
||||
(af " run: " (vf run-info))))
|
||||
|
||||
;;;; Signal formatters
|
||||
(when error
|
||||
(if raw-error?
|
||||
(af " error: " error)
|
||||
(when-let [ff format-error-fn]
|
||||
(af err-start (ff error) err-stop)))))))))))
|
||||
|
||||
(defn format-signal->edn-fn
|
||||
(comment
|
||||
((signal-content-fn) (tel/with-signal (tel/event! ::ev-id)))
|
||||
((signal-content-fn) (tel/with-signal (tel/event! ::ev-id {:data {:k1 "v1"}}))))
|
||||
|
||||
(defn pr-signal-fn
|
||||
"Experimental, subject to change.
|
||||
Returns a (fn format->edn [signal]) that:
|
||||
Returns a (fn pr-signal [signal]) that:
|
||||
- Takes a Telemere signal.
|
||||
- Returns edn string of the (minified) signal."
|
||||
([] (format-signal->edn-fn nil))
|
||||
([{:keys
|
||||
[incl-kvs? end-with-newline?,
|
||||
pr-edn-fn prep-fn]
|
||||
- Returns machine-readable serialized string of the (minified) signal.
|
||||
|
||||
Options include:
|
||||
`pr-fn` ∈ #{<unary-fn> :edn :json (Cljs only)}
|
||||
See arglists for more.
|
||||
|
||||
Examples:
|
||||
(pr-signal-fn :edn {<opts>})
|
||||
(pr-signal-fn :json {<opts>}) ; Cljs only
|
||||
|
||||
;; To output JSON for Clj, you must provide an appropriate `pr-fn`.
|
||||
;; `jsonista` is a good option, Ref. <https://github.com/metosin/jsonista>:
|
||||
(require '[jsonista.core :as jsonista])
|
||||
(pr-signal-fn jsonista/write-value-as-string {<opts>})
|
||||
|
||||
See also `format-signal-fn` for human-readable output."
|
||||
([pr-fn] (pr-signal-fn pr-fn nil))
|
||||
([pr-fn
|
||||
{:keys [incl-thread? incl-kvs? incl-newline?, prep-fn]
|
||||
:or
|
||||
{end-with-newline? true,
|
||||
pr-edn-fn pr-edn
|
||||
prep-fn (comp error-in-signal->maps minify-signal)}}]
|
||||
|
||||
(let [nl newline]
|
||||
(fn format-signal->edn [signal]
|
||||
(let [signal (if (or incl-kvs? (not (map? signal))) signal (dissoc signal :kvs))
|
||||
signal (if prep-fn (prep-fn signal) signal)
|
||||
output (pr-edn-fn signal)]
|
||||
|
||||
(if end-with-newline?
|
||||
(str output nl)
|
||||
(do output)))))))
|
||||
|
||||
(comment ((format-signal->edn-fn) {:level :info, :msg "msg", :kvs {:k1 :v1}}))
|
||||
|
||||
(defn format-signal->json-fn
|
||||
"Experimental, subject to change.
|
||||
Returns a (fn format->json [signal]) that:
|
||||
- Takes a Telemere signal.
|
||||
- Returns JSON string of the (minified) signal.
|
||||
|
||||
(Clj only): An appropriate `:pr-json-fn` MUST be provided.
|
||||
jsonista is one good option, Ref. <https://github.com/metosin/jsonista>:
|
||||
|
||||
(require '[jsonista.core :as jsonista])
|
||||
(format-signal->json-fn {:pr-json-fn jsonista/write-value-as-string ...})"
|
||||
|
||||
([] (format-signal->json-fn nil))
|
||||
([{:keys
|
||||
[incl-kvs? end-with-newline?,
|
||||
pr-json-fn prep-fn]
|
||||
|
||||
:or
|
||||
{end-with-newline? true,
|
||||
#?@(:cljs [pr-json-fn pr-json])
|
||||
prep-fn (comp error-in-signal->maps minify-signal)}}]
|
||||
|
||||
(when-not pr-json-fn
|
||||
(throw
|
||||
(ex-info (str "No `" `format-signal->json-fn "` `:pr-json-fn` was provided") {})))
|
||||
|
||||
(let [nl newline]
|
||||
(fn format-signal->json [signal]
|
||||
(let [signal (if (or incl-kvs? (not (map? signal))) signal (dissoc signal :kvs))
|
||||
signal (if prep-fn (prep-fn signal) signal)
|
||||
output (pr-json-fn signal)]
|
||||
|
||||
(if end-with-newline?
|
||||
(str output nl)
|
||||
(do output)))))))
|
||||
|
||||
(comment ((format-signal->json-fn) {:level :info, :msg "msg", :kvs {:k1 :v1}}))
|
||||
|
||||
(defn format-signal->str-fn
|
||||
"Experimental, subject to change.
|
||||
Returns a (fn format->str [signal]) that:
|
||||
- Takes a Telemere signal.
|
||||
- Returns a formatted string intended for text consoles, etc."
|
||||
([] (format-signal->str-fn nil))
|
||||
([{:keys
|
||||
[format-signal->prelude-fn
|
||||
format-nsecs-fn
|
||||
format-error-fn
|
||||
incl-thread?
|
||||
incl-kvs?
|
||||
end-with-newline?]
|
||||
|
||||
:or
|
||||
{format-signal->prelude-fn (format-signal->prelude-fn) ; (fn [signal])
|
||||
format-nsecs-fn (format-nsecs-fn) ; (fn [nanosecs])
|
||||
format-error-fn (format-error-fn) ; (fn [error])
|
||||
end-with-newline? true}}]
|
||||
{incl-newline? true
|
||||
prep-fn
|
||||
(comp error-in-signal->maps
|
||||
minify-signal)}}]
|
||||
|
||||
(let [nl newline
|
||||
signal-content-handler ; (fn [signal hf vf]
|
||||
(signal-content-handler
|
||||
{:format-nsecs-fn format-nsecs-fn
|
||||
:format-error-fn format-error-fn
|
||||
:incl-thread? incl-thread?
|
||||
:incl-kvs? incl-kvs?})]
|
||||
pr-fn
|
||||
(or
|
||||
(case pr-fn
|
||||
:edn pr-edn
|
||||
#?@(:cljs [:json pr-json])
|
||||
|
||||
(fn format-signal->str [signal]
|
||||
(let [sb (enc/str-builder)
|
||||
s+ (partial enc/sb-append sb)
|
||||
s++ (partial enc/sb-append sb (str newline " "))]
|
||||
(if (fn? pr-fn)
|
||||
(do pr-fn)
|
||||
(enc/unexpected-arg! pr-fn
|
||||
{:context `pr-signal-fn
|
||||
:param 'pr-fn
|
||||
:expected
|
||||
#?(:clj '#{:edn unary-fn}
|
||||
:cljs '#{:edn :json unary-fn})}))
|
||||
|
||||
(when-let [ff format-signal->prelude-fn] (s+ (ff signal))) ; Prelude
|
||||
(signal-content-handler signal s++ enc/pr-edn*) ; Content
|
||||
(when end-with-newline? (enc/sb-append sb nl))
|
||||
(str sb))))))
|
||||
(have fn? pr-fn)))]
|
||||
|
||||
(fn pr-signal [signal]
|
||||
(let [not-map? (not (map? signal))
|
||||
signal (if (or incl-kvs? not-map?) signal (dissoc signal :kvs))
|
||||
signal (if (or incl-thread? not-map?) signal (dissoc signal :thread))
|
||||
signal (if prep-fn (prep-fn signal) signal)
|
||||
output (pr-fn signal)]
|
||||
|
||||
(if incl-newline?
|
||||
(str output nl)
|
||||
(do output)))))))
|
||||
|
||||
(comment ((pr-signal-fn :edn) (tel/with-signal (tel/event! ::ev-id {:kvs {:k1 "v1"}}))))
|
||||
|
||||
(defn format-signal-fn
|
||||
"Experimental, subject to change.
|
||||
Returns a (fn format [signal]) that:
|
||||
- Takes a Telemere signal.
|
||||
- Returns human-readable formatted string.
|
||||
|
||||
See also `pr-signal-fn` for machine-readable output."
|
||||
([] (format-signal-fn nil))
|
||||
([{:keys [incl-newline? preamble-fn content-fn]
|
||||
:or
|
||||
{incl-newline? true
|
||||
preamble-fn (signal-preamble-fn)
|
||||
content-fn (signal-content-fn)}}]
|
||||
|
||||
(let [nl newline]
|
||||
(fn format-signal [signal]
|
||||
(let [preamble (when preamble-fn (preamble-fn signal))
|
||||
content (when content-fn (content-fn signal))]
|
||||
|
||||
(if (and preamble content)
|
||||
(str preamble nl content (when incl-newline? nl))
|
||||
(str preamble content (when incl-newline? nl))))))))
|
||||
|
||||
(comment
|
||||
(tel/with-ctx {:c :C}
|
||||
(println
|
||||
((format-signal->str-fn)
|
||||
((format-signal-fn)
|
||||
(tel/with-signal
|
||||
(tel/event! ::ev-id
|
||||
{:user-k1 #{:a :b :c}
|
||||
|
|
|
|||
|
|
@ -674,15 +674,16 @@
|
|||
#?(:clj " Root: clojure.lang.ExceptionInfo - Ex1\n data: {:k1 \"v1\"}\n\nCaused: clojure.lang.ExceptionInfo - Ex2\n data: {:k2 \"v2\"}\n\nRoot stack trace:\n"
|
||||
:cljs " Root: cljs.core/ExceptionInfo - Ex1\n data: {:k1 \"v1\"}\n\nCaused: cljs.core/ExceptionInfo - Ex2\n data: {:k2 \"v2\"}\n\nRoot stack trace:\n")))
|
||||
|
||||
(let [sig (with-sig (tel/event! ::ev-id {:inst t0}))
|
||||
prelude ((utils/format-signal->prelude-fn) sig)] ; "2024-06-09T21:15:20.170Z INFO EVENT taoensso.telemere-tests(592,35) ::ev-id"
|
||||
[(is (enc/str-starts-with? prelude "2024-06-09T21:15:20.170Z INFO EVENT"))
|
||||
(is (enc/str-ends-with? prelude "::ev-id"))
|
||||
(is (string? (re-find #"taoensso.telemere-tests\(\d+,\d+\)" prelude)))])
|
||||
(testing "signal-preamble-fn"
|
||||
(let [sig (with-sig (tel/event! ::ev-id {:inst t0}))
|
||||
preamble ((utils/signal-preamble-fn) sig)] ; "2024-06-09T21:15:20.170Z INFO EVENT taoensso.telemere-tests(592,35) ::ev-id"
|
||||
[(is (enc/str-starts-with? preamble "2024-06-09T21:15:20.170Z INFO EVENT"))
|
||||
(is (enc/str-ends-with? preamble "::ev-id"))
|
||||
(is (string? (re-find #"taoensso.telemere-tests\(\d+,\d+\)" preamble)))]))
|
||||
|
||||
(testing "format-signal->edn-fn"
|
||||
(testing "pr-signal-fn/edn"
|
||||
(let [sig (update (with-sig (tel/event! ::ev-id {:inst t0})) :inst enc/inst->udt)
|
||||
sig* (enc/read-edn ((utils/format-signal->edn-fn) sig))]
|
||||
sig* (enc/read-edn ((tel/pr-signal-fn :edn) sig))]
|
||||
(is
|
||||
(enc/submap? sig*
|
||||
{:schema 1, :kind :event, :id ::ev-id, :level :info,
|
||||
|
|
@ -692,20 +693,20 @@
|
|||
:column pnat-int?}))))
|
||||
|
||||
#?(:cljs
|
||||
(testing "format-signal->json-fn"
|
||||
(testing "pr-signal-fn/json"
|
||||
(let [sig (with-sig (tel/event! ::ev-id {:inst t0}))
|
||||
sig* (enc/read-json ((utils/format-signal->json-fn) sig))]
|
||||
sig* (enc/read-json ((tel/pr-signal-fn :json) sig))]
|
||||
(is
|
||||
(enc/submap? sig*
|
||||
{"schema" 1, "kind" "event", "id" "taoensso.telemere-tests/ev-id",
|
||||
"level" "info", "ns" "taoensso.telemere-tests",
|
||||
"level" "info", "ns" "taoensso.telemere-tests",
|
||||
"inst" t0s
|
||||
"line" pnat-int?
|
||||
"column" pnat-int?})))))
|
||||
|
||||
(testing "format-signal->str-fn"
|
||||
(testing "format-signal-fn"
|
||||
(let [sig (with-sig (tel/event! ::ev-id {:inst t0}))]
|
||||
(is (enc/str-starts-with? ((utils/format-signal->str-fn) sig)
|
||||
(is (enc/str-starts-with? ((tel/format-signal-fn) sig)
|
||||
"2024-06-09T21:15:20.170Z INFO EVENT"))))])])
|
||||
|
||||
;;;; File handler
|
||||
|
|
|
|||
|
|
@ -216,4 +216,3 @@ Telemere includes extensive internal help docstrings:
|
|||
| [`help:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) | Ordered flow from signal creation to handling |
|
||||
| [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) | API for configuring signal filters |
|
||||
| [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) | API for configuring signal handlers |
|
||||
| [`help:signal-formatters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-formatters) | Signal formatters for use by handlers |
|
||||
|
|
|
|||
|
|
@ -28,4 +28,3 @@ For more info see:
|
|||
| [`help:signal-flow`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-flow) | Ordered flow from signal creation to handling |
|
||||
| [`help:signal-filters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-filters) | API for configuring signal filters |
|
||||
| [`help:signal-handlers`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-handlers) | API for configuring signal handlers |
|
||||
| [`help:signal-formatters`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#help:signal-formatters) | Signal formatters for use by handlers |
|
||||
|
|
|
|||
|
|
@ -6,20 +6,21 @@ A number of signal handlers are included out-the box. Alphabetically:
|
|||
|
||||
| Name | Platform | Output target | Output format |
|
||||
| :------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------- |
|
||||
| [`handler:carmine`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.carmine#handler:carmine) | Clj | [Redis](https://redis.io/) (via [Carmine](https://www.taoensso.com/carmine)) | Serialized signals [1] |
|
||||
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Clj | `*out*` or `*err*` | Formatted string [2] |
|
||||
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Cljs | Browser console | Formatted string [2] |
|
||||
| [`handler:carmine`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.carmine#handler:carmine) [0] | Clj | [Redis](https://redis.io/) (via [Carmine](https://www.taoensso.com/carmine)) | Serialized signals [1] |
|
||||
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Clj | `*out*` or `*err*` | String [2] |
|
||||
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Cljs | Browser console | String [2] |
|
||||
| [`handler:console-raw`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | Cljs | Browser console | Raw signals [3] |
|
||||
| [`handler:file`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | Clj | File/s on disk | Formatted string [2] |
|
||||
| [`handler:logstash`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.logstash#handler:logstash) | Clj | [Logstash](https://www.elastic.co/logstash) | TODO |
|
||||
| [`handler:file`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | Clj | File/s on disk | String [2] |
|
||||
| [`handler:logstash`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.logstash#handler:logstash) [0] | Clj | [Logstash](https://www.elastic.co/logstash) | TODO |
|
||||
| [`handler:open-telemetry-logger`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.open-telemetry#handler:open-telemetry-logger) | Clj | [OpenTelemetry](https://opentelemetry.io/) [Java client](https://github.com/open-telemetry/opentelemetry-java) | [LogRecord](https://opentelemetry.io/docs/specs/otel/logs/data-model/) |
|
||||
| [`handler:postal`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.postal#handler:postal) | Clj | Email (via [postal](https://github.com/drewr/postal)) | Formatted string [2] |
|
||||
| [`handler:slack`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.slack#handler:slack) | Clj | [Slack](https://slack.com/) (via [clj-slack](https://github.com/julienXX/clj-slack)) | Formatted string [2] |
|
||||
| [`handler:tcp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | Clj | TCP socket | Formatted string [2] |
|
||||
| [`handler:udp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | Clj | UDP socket | Formatted string [2] |
|
||||
| [`handler:postal`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.postal#handler:postal) | Clj | Email (via [postal](https://github.com/drewr/postal)) | String [2] |
|
||||
| [`handler:slack`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.slack#handler:slack) [0] | Clj | [Slack](https://slack.com/) (via [clj-slack](https://github.com/julienXX/clj-slack)) | String [2] |
|
||||
| [`handler:tcp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | Clj | TCP socket | String [2] |
|
||||
| [`handler:udp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | Clj | UDP socket | String [2] |
|
||||
|
||||
- \[0] Coming soon
|
||||
- \[1] Uses [Nippy](https://taoensso.com/nippy) to support all Clojure's rich data types
|
||||
- \[2] [Configurable](https://cljdoc.org/d/com.taoensso/telemere/1.0.0-beta3/api/taoensso.telemere#help:signal-formatters): human-readable (default), [edn](https://github.com/edn-format/edn), [JSON](https://www.json.org/), etc.
|
||||
- \[2] [Human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) (default), or [machine-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) ([edn](https://github.com/edn-format/edn), [JSON](https://www.json.org/), etc.).
|
||||
- \[3] For use with browser formatting tools like [cljs-devtools](https://github.com/binaryage/cljs-devtools).
|
||||
- See relevant docstrings (links above) for features, usage, etc.
|
||||
- See section [8-Community](8-Community.md) for more (community-supported) handlers.
|
||||
|
|
@ -75,7 +76,7 @@ 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)}))
|
||||
{:output-fn (t/pr-signal-fn :edn)}))
|
||||
|
||||
(my-handler my-signal) ; =>
|
||||
;; {:inst #inst "2024-04-11T10:54:57.202869Z", :msg_ "My message", :ns "examples", ...}
|
||||
|
|
@ -84,18 +85,17 @@ To instead writes signals as edn:
|
|||
To instead writes signals as JSON:
|
||||
|
||||
```clojure
|
||||
;; Create console which writes JSON
|
||||
;; Create console which writes signals as JSON
|
||||
#?(:clj (require '[jsonista.core :as jsonista]))
|
||||
(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", ...}
|
||||
{:output-fn
|
||||
(t/pr-signal-fn
|
||||
#?(:cljs :json
|
||||
:clj jsonista.core/write-value-as-string))}))
|
||||
```
|
||||
|
||||
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](https://github.com/metosin/jsonista) is my default recommendation).
|
||||
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](https://github.com/metosin/jsonista) is my default recommendation).
|
||||
|
||||
### Handler-specific per-signal kvs
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue