[new] OpenTelemetry handler: add experimental trace output

This commit is contained in:
Peter Taoussanis 2024-08-07 17:52:50 +02:00
parent 599236f451
commit 67cb4941bf
7 changed files with 608 additions and 270 deletions

View file

@ -179,17 +179,17 @@ Detailed help is available without leaving your IDE:
See linked docstrings below for features and usage:
| Name | Platform | Output target | Output format |
| :------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Clj | `*out*` or `*err*` | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Cljs | Browser console | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:console-raw`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | Cljs | Browser console | Raw signals for [cljs-devtools](https://github.com/binaryage/cljs-devtools), etc. |
| [`handler:file`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | Clj | File/s on disk | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`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)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`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)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:tcp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | Clj | TCP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:udp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | Clj | UDP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| Name | Platform | Output target | Output format |
| :------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :---------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Clj | `*out*` or `*err*` | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Cljs | Browser console | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:console-raw`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | Cljs | Browser console | Raw signals for [cljs-devtools](https://github.com/binaryage/cljs-devtools), etc. |
| [`handler:file`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | Clj | File/s on disk | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`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](https://github.com/open-telemetry/opentelemetry-java) exporters | [LogRecord](https://opentelemetry.io/docs/specs/otel/logs/data-model/) and tracing data |
| [`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)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`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)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:tcp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | Clj | TCP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:udp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | Clj | UDP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
See [here](../../wiki/4-Handlers) for more/upcoming handlers, community handlers, info on **writing your own handlers**, etc.

View file

@ -53,6 +53,7 @@
[io.opentelemetry/opentelemetry-api "1.41.0"]
#_[io.opentelemetry/opentelemetry-sdk-extension-autoconfigure "1.41.0"]
#_[io.opentelemetry/opentelemetry-exporter-otlp "1.41.0"]
#_[io.opentelemetry/opentelemetry-exporters-jaeger "0.9.1"]
[metosin/jsonista "0.3.10"]
[com.draines/postal "2.0.5"]
[org.julienxx/clj-slack "0.8.3"]]

View file

@ -33,7 +33,6 @@
(enc/assert-min-encore-version [3 115 0])
;;;; TODO
;; - Native OpenTelemetry traces and spans
;; - Solution and docs for lib authors
;; - Add handlers: Logstash, Carmine, Datadog, Kafka
;; - Update Tufte (signal API, config API, signal keys, etc.)

View file

@ -1,27 +1,324 @@
(ns taoensso.telemere.open-telemetry
"OpenTelemetry handler using `opentelemetry-java`,
Ref. <https://github.com/open-telemetry/opentelemetry-java>."
Ref. <https://github.com/open-telemetry/opentelemetry-java>,
<https://javadoc.io/doc/io.opentelemetry/opentelemetry-api/latest/index.html>"
(:require
[clojure.string :as str]
[clojure.set :as set]
[taoensso.encore :as enc :refer [have have?]]
[taoensso.telemere.utils :as utils]
[taoensso.telemere.impl :as impl]
[taoensso.telemere :as tel])
(:import
[io.opentelemetry.api.logs LoggerProvider Severity]
[io.opentelemetry.api.common Attributes AttributesBuilder]
[io.opentelemetry.api GlobalOpenTelemetry]))
[io.opentelemetry.api.common AttributesBuilder Attributes]
[io.opentelemetry.api.logs LoggerProvider Severity]
[io.opentelemetry.api.trace TracerProvider Tracer Span]))
(comment
(remove-ns 'taoensso.telemere.open-telemetry)
(:api (enc/interns-overview)))
;;;; Implementation
;;;; TODO
;; - API for `remote-span-context`, trace state, span links?
;; - Ability to actually set (compatible) traceId, spanId?
;;;; Providers
(defn get-default-providers
"Experimental, subject to change. Feedback welcome!
Returns map with keys:
:logger-provider - default `io.opentelemetry.api.logs.LoggerProvider`
:tracer-provider - default `io.opentelemetry.api.trace.TracerProvider`
:via - #{:sdk-extension-autoconfigure :global}
Uses `AutoConfiguredOpenTelemetrySdk` when possible, or
`GlobalOpenTelemetry` otherwise.
See the relevant `opentelemetry-java` docs for details."
[]
(or
;; Via SDK autoconfiguration extension (when available)
(enc/compile-when
io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk
(enc/catching :common
(let [builder (io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk/builder)
sdk (.getOpenTelemetrySdk (.build builder))]
{:logger-provider (.getLogsBridge sdk)
:tracer-provider (.getTracerProvider sdk)
:via :sdk-extension-autoconfigure})))
;; Via Global (generally not recommended)
(let [g (io.opentelemetry.api.GlobalOpenTelemetry/get)]
{:logger-provider (.getLogsBridge g)
:tracer-provider (.getTracerProvider g)
:via :global})))
(def ^:no-doc default-providers_
(delay (get-default-providers)))
(comment
(get-default-providers)
(let [{:keys [logger-provider tracer-provider]} (get-default-providers)]
(def ^LoggerProvider my-lp logger-provider)
(def ^Tracer my-tr (.get tracer-provider "Telemere")))
;; Confirm that we have a real (not noop) SpanBuilder
(.spanBuilder my-tr "my-span"))
;;;; Attributes
(def ^:private ^String attr-name
"Returns cached OpenTelemetry-style name: `:a.b/c-d` -> \"a.b.c_d\", etc.
Ref. <https://opentelemetry.io/docs/specs/semconv/general/attribute-naming/>."
(enc/fmemoize
(fn self
([prefix x] (str (self prefix) "." (self x)))
([ x]
(if-not (enc/named? x)
(str/replace (str/lower-case (str x)) #"[-\s]" "_")
(if-let [ns (namespace x)]
(str/replace (str/lower-case (str ns "." (name x))) "-" "_")
(str/replace (str/lower-case (name x)) "-" "_")))))))
(comment (enc/qb 1e6 (attr-name :a.b/c-d) (attr-name :x.y/z :a.b/c-d))) ; [44.13 63.19]
;; AttributeTypes: String, Long, Double, Boolean, and arrays
(defprotocol ^:private IAttributesBuilder (^:private -put-attr! ^AttributesBuilder [attr-val attr-name attr-builder]))
(extend-protocol IAttributesBuilder
;; nil (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k "nil")) ; As pr-edn*
nil (-put-attr! [v ^String k ^AttributesBuilder ab] ab ) ; Noop
Boolean (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
String (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
java.util.UUID (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (str v))) ; "d4fc65a0..."
clojure.lang.Named (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (str v))) ; ":foo/bar"
Long (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
Integer (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (long v)))
Short (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (long v)))
Byte (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (long v)))
Double (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k v))
Float (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (double v)))
Number (-put-attr! [v ^String k ^AttributesBuilder ab] (.put ab k (double v)))
clojure.lang.IPersistentCollection
(-put-attr! [v ^String k ^AttributesBuilder ab]
(when-some [v1 (if (indexed? v) (nth v 0 nil) (first v))]
(or
(cond
(string? v1) (enc/catching :common (.put ab k ^"[Ljava.lang.String;" (into-array String v)))
(int? v1) (enc/catching :common (.put ab k (long-array v)))
(float? v1) (enc/catching :common (.put ab k (double-array v)))
(boolean? v1) (enc/catching :common (.put ab k (boolean-array v))))
(when-let [^String s (enc/catching :common (enc/pr-edn* v))]
(.put ab k s))))
ab)
Object
(-put-attr! [v ^String k ^AttributesBuilder ab]
(when-let [^String s (enc/catching :common (enc/pr-edn* v))]
(.put ab k s))))
(defmacro ^:private put-attr! [attr-builder attr-name attr-val]
`(-put-attr! ~attr-val ~attr-name ~attr-builder)) ; Fix arg order
(defn- merge-attrs!
"If given a map, merges prefixed key/values (~like `into`).
Otherwise just puts single named value."
[attr-builder name-or-prefix x]
(if (map? x)
(enc/run-kv! (fn [k v] (put-attr! attr-builder (attr-name name-or-prefix k) v)) x)
(do (put-attr! attr-builder name-or-prefix x))))
;;;; Spans
(defn- remote-span-context
"Returns new remote `io.opentelemetry.api.trace.SpanContext`
for use as `start-span` parent."
^io.opentelemetry.api.trace.SpanContext
[^String trace-id ^String span-id sampled? ?trace-state]
(io.opentelemetry.api.trace.SpanContext/createFromRemoteParent
trace-id span-id
(if sampled?
(io.opentelemetry.api.trace.TraceFlags/getSampled)
(io.opentelemetry.api.trace.TraceFlags/getDefault))
(enc/if-not [trace-state ?trace-state]
(io.opentelemetry.api.trace.TraceState/getDefault)
(cond
(map? trace-state)
(let [tsb (io.opentelemetry.api.trace.TraceState/builder)]
(enc/run-kv! (fn [k v] (.put tsb k v)) trace-state) ; NB only `a-zA-Z.-_` chars allowed
(.build tsb))
(instance? io.opentelemetry.api.trace.TraceState trace-state) trace-state
:else
(enc/unexpected-arg! trace-state
:context `remote-span-context
:param 'trace-state
:expected '#{nil {string string} io.opentelemetry.api.trace.TraceState})))))
(comment (enc/qb 1e6 (remote-span-context "c5b856d919f65e39a202bfb3034d65d8" "9740419096347616" false {"a" "A"}))) ; 111.13
(def ^:private ^String span-name
(enc/fmemoize
(fn [id]
#_(if id (str id) ":telemere/nil-id")
(if id (enc/as-qname id) "telemere/nil-id"))))
(comment (enc/qb 1e6 (span-name :foo/bar))) ; 46.09
(let [span-name span-name]
(defn- start-span
"Returns new `io.opentelemetry.api.trace.Span` with random `traceId` and `spanId`."
^Span [^Tracer tracer ?id ?uid ^java.time.Instant inst ?parent]
(let [sb (.spanBuilder tracer (span-name ?id))]
(when-let [parent ?parent]
(cond
;; Local parent span, etc.
(instance? Span parent)
(.setParent sb (.with (io.opentelemetry.context.Context/root) ^Span parent))
;; Remote parent context, etc.
(instance? io.opentelemetry.api.trace.SpanContext parent)
(.setParent sb
(.with
(io.opentelemetry.context.Context/root)
(Span/wrap ^io.opentelemetry.api.trace.SpanContext parent)))
:else
(enc/unexpected-arg! parent
{:context `start-span
:expected
#{io.opentelemetry.api.trace.Span
io.opentelemetry.api.trace.SpanContext}})))
(when-let [uid ?uid]
(.setAttribute sb "uid" (str uid)))
(.setStartTimestamp sb inst)
(.startSpan sb))))
(comment
(let [inst (enc/now-inst)] (enc/qb 1e6 (start-span my-tr :id1 :uid1 inst nil))) ; 217.47
(start-span my-tr :id1 :uid1 (enc/now-inst) (start-span my-tr :id2 :uid2 (enc/now-inst) nil))
(start-span my-tr :id1 :uid1 (enc/now-inst)
(remote-span-context "c5b856d919f65e39a202bfb3034d65d8" "1111111111111111" false nil)))
(enc/def* ^:private
^io.opentelemetry.api.common.AttributeKey uid-attr-key
(io.opentelemetry.api.common.AttributeKey/stringKey "uid"))
(defn- handle-tracing!
"Experimental! Takes care of relevant signal `Span` management.
Returns nil or `io.opentelemetry.api.trace.Span` for possible use as
`io.opentelemetry.api.logs.LogRecordBuilder` context.
Expect:
- `spans_` - latom: {<uid> <Span_>}
- `end-buffer_` - latom: #{[<uid> <end-inst>]}
- `gc-buffer_` - latom: #{<uid>}"
[tracer spans_ end-buffer_ gc-buffer_ signal]
;; Notes:
;; - Spans go to `SpanExporter` after `.end` call, ~random order okay
;; - Span data: t1 of self, and name + id + t0 of #{self parent trace}
;; - No API to directly create spans with needed data, so we ~simulate
;; typical usage
(enc/when-let
[root (get signal :root) ; Tracing iff root
root-uid (get root :uid)
:let [curr-spans (spans_)]
root-span
(force
(or ; Fetch/ensure Span for root
(get curr-spans root-uid)
(when-let [root-inst (get root :inst)]
(let [root-id (get root :id)]
(spans_ root-uid
(fn [old]
(or old
(delay
;; TODO Support remote-span-context parent and/or span links?
(start-span tracer root-id root-uid root-inst nil)))))))))]
(let [?parent-span ; May be identical to root-span
(when-let [parent (get signal :parent)]
(when-let [parent-uid (get parent :uid)]
(if (= parent-uid root-uid)
root-span
(force
(or ; Fetch/ensure Span for parent
(get curr-spans parent-uid)
(let [{parent-id :id, parent-inst :inst} parent]
(spans_ parent-uid
(fn [old]
(or old
(delay (start-span tracer parent-id parent-uid parent-inst root-span)))))))))))
{this-uid :uid, this-end-inst :end-inst} signal]
(enc/cond
;; No end-inst => no run-form =>
;; add `Event` (rather than child `Span`) to parent
:if-let [this-is-event? (not this-end-inst)]
(when-let [^Span parent-span ?parent-span]
(let [{this-id :id, this-inst :inst} signal
attrs (Attributes/of uid-attr-key (str this-uid))]
(.addEvent parent-span (span-name this-id) attrs ^java.time.Instant this-inst))
(do parent-span))
:if-let
[^Span this-span
(if (= this-uid root-uid)
root-span
(force
(or ; Fetch/ensure Span for this (child)
(get curr-spans this-uid)
(let [{this-id :id, this-inst :inst} signal]
(spans_ this-uid
(fn [old]
(or old
(delay
(start-span tracer this-id this-uid this-inst
(or ?parent-span root-span))))))))))]
(do
(if (utils/error-signal? signal)
(.setStatus this-span io.opentelemetry.api.trace.StatusCode/ERROR)
(.setStatus this-span io.opentelemetry.api.trace.StatusCode/OK))
(when-let [error (get signal :error)]
(when (instance? Throwable error)
(.recordException this-span error)))
;; (.end this-span this-end-inst) ; Ready for `SpanExporter`
(end-buffer_ (fn [old] (conj old [this-uid this-end-inst])))
(gc-buffer_ (fn [old] (conj old this-uid)))
this-span)))))
(comment
(do
(require '[taoensso.telemere :as t])
(def spans_ "{<uid> <Span_>}" (enc/latom {}))
(def end-buffer_ "#{[<uid> <end-inst>]}" (enc/latom #{}))
(def gc-buffer_ "#{<uid>}" (enc/latom #{}))
(let [[_ [s1 s2]] (t/with-signals (t/trace! ::id1 (t/trace! ::id2 "form2")))]
(def s1 s1)
(def s2 s2)))
[@gc-buffer_ @end-buffer_ @spans_]
(handle-tracing! my-tr spans_ end-buffer_ gc-buffer_ s1))
;;;; Logging
(defn- level->severity
^Severity [level]
(case level
(case level
:trace Severity/TRACE
:debug Severity/DEBUG
:info Severity/INFO
@ -33,220 +330,258 @@
(defn- level->string
^String [level]
(case level
:trace "TRACE"
:debug "DEBUG"
:info "INFO"
:warn "WARN"
:error "ERROR"
:fatal "FATAL"
:report "INFO4"
(str level)))
(when level
(case level
:trace "TRACE"
:debug "DEBUG"
:info "INFO"
:warn "WARN"
:error "ERROR"
:fatal "FATAL"
:report "INFO4"
(str level))))
(def ^:private ^String attr-name
"Returns cached OpenTelemetry-style name: `:foo/bar-baz` -> \"foo_bar_baz\", etc.
Ref. <https://opentelemetry.io/docs/specs/semconv/general/attribute-naming/>."
(enc/fmemoize
(fn
([prefix x] (str (attr-name prefix) "." (attr-name x))) ; For `merge-prefix-map`, etc.
([ x]
(if-not (enc/named? x)
(str/replace (str/lower-case (str x)) #"[-\s]" "_")
(if-let [ns (namespace x)]
(str/replace (str/lower-case (str ns "." (name x))) "-" "_")
(str/replace (str/lower-case (name x)) "-" "_")))))))
(comment (enc/qb 1e6 (attr-name :x1.x2/x3-x4 :Foo/Bar-BAZ))) ; 63.6
;; AttributeTypes: String, Long, Double, Boolean, and arrays
(defprotocol IAttr+ (^:private attr+ [_aval akey builder]))
(extend-protocol IAttr+
nil (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) "nil")) ; As pr-edn*
Boolean (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
String (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
java.util.UUID (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (str v))) ; "d4fc65a0..."
clojure.lang.Named (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (str v))) ; ":foo/bar"
Long (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
Integer (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (long v)))
Short (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (long v)))
Byte (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (long v)))
Double (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) v))
Float (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (double v)))
Number (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (double v)))
clojure.lang.IPersistentCollection
(attr+ [v k ^AttributesBuilder b]
(let [v1 (first v)]
(or
(cond
(boolean? v1) (enc/catching :common (.put b (attr-name k) (boolean-array (mapv boolean v))))
(int? v1) (enc/catching :common (.put b (attr-name k) (long-array (mapv long v))))
(float? v1) (enc/catching :common (.put b (attr-name k) (double-array (mapv double v)))))
(do (.put b (attr-name k) ^"[Ljava.lang.String;" (into-array String (mapv enc/pr-edn* v)))))))
Object (attr+ [v k ^AttributesBuilder b] (.put b (attr-name k) (enc/pr-edn* v))))
(defn- as-attrs
"Returns `io.opentelemetry.api.common.Attributes` for given map."
^Attributes [m]
(if (empty? m)
(Attributes/empty)
(let [builder (Attributes/builder)]
(enc/run-kv! (fn [k v] (attr+ v k builder)) m)
(.build builder))))
(comment (str (as-attrs {:s "s", :kw :foo/bar, :long 5, :double 5.0, :longs [5 5 5] :nil nil})))
(defn- merge-prefix-map
"Merges prefixed `from` into `to`."
[to prefix from]
(enc/cond
(map? from)
(reduce-kv
(fn [acc k v] (assoc acc (attr-name prefix k) v))
to from)
from (assoc to prefix from)
:else to))
(comment (merge-prefix-map {} "data" {:a/b1 "v1" :a/b2 "v2" :nil nil}))
(defn- signal->attrs-map
"Returns attributes map for given signal,
(defn- signal->attrs
"Returns `io.opentelemetry.api.common.Attributes` for given signal.
Ref. <https://opentelemetry.io/docs/specs/otel/logs/data-model/>."
[attrs-key signal]
(let [attrs-map
(let [{:keys [ns line file, kind level id uid parent,
run-form run-val run-nsecs, sample-rate]}
signal]
^Attributes [signal]
(let [ab (Attributes/builder)]
(put-attr! ab "error" (utils/error-signal? signal)) ; Standard
;; (put-attr! ab "host.name" (utils/hostname)) ; Standard
(enc/assoc-some nil
{"ns" ns
"line" line
"file" file
(when-let [{:keys [name ip]} (get signal :host)]
(put-attr! ab "host.name" name) ; Standard
(put-attr! ab "host.ip" ip))
"error" (utils/error-signal? signal) ; Standard key
"kind" kind
"level" (when level (level->string level))
"id" id
"uid" uid
(when-let [level (get signal :level)]
(put-attr! ab "level" ; Standard
(level->string level)))
"run.form" run-form
"run.val_type" (enc/class-sym run-val)
"run.val" run-val
"run.nsecs" run-nsecs
"sample" sample-rate
(when-let [{:keys [type msg trace data]} (enc/ex-map (get signal :error))]
(put-attr! ab "exception.type" type) ; Standard
(put-attr! ab "exception.message" msg) ; Standard
(when trace
(put-attr! ab "exception.stacktrace" ; Standard
(#'utils/format-clj-stacktrace trace)))
"parent.id" (get parent :id)
"parent.uid" (get parent :uid)}))
(when data (merge-attrs! ab "exception.data" data)))
attrs-map
(enc/if-not [{:keys [type msg data trace]} (enc/ex-map (get signal :error))]
attrs-map
(merge-prefix-map
(enc/assoc-some attrs-map
;; 3x standard keys
"exception.type" type
"exception.message" msg
"exception.stacktrace" (when trace (#'utils/format-clj-stacktrace trace)))
"exception.data" data))
(let [{:keys [ns line file, kind id uid]} signal]
(put-attr! ab "ns" ns)
(put-attr! ab "line" line)
(put-attr! ab "file" file)
kvs (get signal :kvs)
attr-kvs
(when attrs-key
(when-let [kvs (get signal attrs-key)]
(not-empty kvs)))
(put-attr! ab "kind" kind)
(put-attr! ab "id" id)
(put-attr! ab "uid" uid))
kvs
(if attr-kvs
(dissoc kvs attrs-key)
(do kvs))
(when-let [run-form (get signal :run-form)]
(let [{:keys [run-val run-nsecs]} signal]
(put-attr! ab "run.form" (if (nil? run-form) "nil" (str run-form)))
(put-attr! ab "run.val_type" (if (nil? run-val) "nil" (.getName (class run-val))))
(put-attr! ab "run.val" run-val)
(put-attr! ab "run.nsecs" run-nsecs)))
attrs-map
(-> attrs-map
(merge-prefix-map "ctx" (get signal :ctx))
(merge-prefix-map "data" (get signal :data))
(merge-prefix-map "kvs" (get signal :kvs))
(enc/merge attr-kvs) ; Unprefixed, undocumented
)]
(put-attr! ab "sample" (get signal :sample-rate))
attrs-map))
(when-let [{:keys [id uid]} (get signal :parent)]
(put-attr! ab "parent.id" id)
(put-attr! ab "parent.uid" uid))
(defn default-logger-provider
"Experimental, subject to change. Feedback welcome!
(when-let [{:keys [id uid]} (get signal :root)]
(put-attr! ab "root.id" id)
(put-attr! ab "root.uid" uid))
Returns `io.opentelemetry.api.logs.LoggerProvider` via:
`AutoConfiguredOpenTelemetrySdk` when possible, or
`GlobalOpenTelemetry` otherwise.
(when-let [ctx (get signal :ctx)] (merge-attrs! ab "ctx" ctx))
(when-let [data (get signal :data)] (merge-attrs! ab "data" data))
(when-let [attrs (get signal :otel/attrs)] ; Undocumented
(cond
(map? attrs) (enc/run-kv! (fn [k v] (put-attr! ab (attr-name k) v)) attrs) ; Unprefixed
(instance? Attributes attrs) (.putAll ab ^Attributes attrs) ; Unprefixed
:else
(enc/unexpected-arg! attrs
{:context `signal->attrs!
:expected #{nil map io.opentelemetry.api.common.Attributes}})))
See the relevant `opentelemetry-java` docs for details."
^LoggerProvider []
(or
;; Without Java agent
(enc/compile-when
io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk
(enc/catching :common
(let [builder (io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk/builder)]
(.getSdkLoggerProvider (.getOpenTelemetrySdk (.build builder))))))
(.build ab)))
;; With Java agent
(.getLogsBridge (GlobalOpenTelemetry/get))))
;;;; Handler
(comment
(enc/qb 1e6 ; 850.93
(signal->attrs
{:level :info :data {:ns/kw1 :v1 :ns/kw2 :v2}
:otel/attrs {:longs [1 1 2 3] :strs ["a" "b" "c"]}})))
(defn handler:open-telemetry-logger
"Experimental, subject to change. Feedback welcome!
"Highly experimental, possibly buggy, and subject to change!!
Feedback and bug reports very welcome! Please ping me (Peter) at:
<https://www.taoensso.com/telemere> or
<https://www.taoensso.com/telemere/slack>
Needs `opentelemetry-java`,
Ref. <https://github.com/open-telemetry/opentelemetry-java>.
Returns a signal handler that:
- Takes a Telemere signal (map).
- Emits the signal to `io.opentelemetry.api.logs.Logger` returned
by given `io.opentelemetry.api.logs.LoggerProvider`.
- Emits signal data to configured `io.opentelemetry.api.logs.Logger`
- Emits tracing data to configured `io.opentelemetry.api.logs.Tracer`
Options:
`:logger-provider` - `io.opentelemetry.api.logs.LoggerProvider`
Defaults to the LoggerProvider returned by (default-logger-provider),
see that docstring for details."
`:logger-provider` - #{nil :default <io.opentelemetry.api.logs.LoggerProvider>} [1]
`:tracer-provider` - #{nil :default <io.opentelemetry.api.trace.TracerProvider>} [1]
`:max-span-msecs` - (Advanced) Longest tracing span to support in milliseconds
(default 120 mins). If recorded spans exceed this max, emitted
data will be inaccurate. Larger values use more memory.
[1] See `get-default-providers` for more info"
;; Notes:
;; - Multi-threaded handlers may see signals ~out of order
;; - Sampling means that root/parent/child signals may never be handled
;; - `:otel/attrs` currently undocumented
([] (handler:open-telemetry-logger nil))
([{:keys
[^LoggerProvider logger-provider
attrs-signal-key ; Advanced, undocumented
]
([{:keys [logger-provider tracer-provider max-span-msecs]
:or
{logger-provider (default-logger-provider)
attrs-signal-key :open-telemetry/attrs}}]
{logger-provider :default
tracer-provider :default
max-span-msecs (enc/msecs :mins 120)}}]
(let [min-max-span-msecs (enc/msecs :mins 15)]
(when (< (long max-span-msecs) min-max-span-msecs)
(throw
(ex-info "`max-span-msecs` too small"
{:given max-span-msecs, :min min-max-span-msecs}))))
(let [?logger-provider (if (= logger-provider :default) (:logger-provider (force default-providers_)) logger-provider)
?tracer-provider (if (= tracer-provider :default) (:tracer-provider (force default-providers_)) tracer-provider)
?tracer
(when-let [^io.opentelemetry.api.trace.TracerProvider p ?tracer-provider]
(.get p "Telemere"))
;;; Tracing state
root-context (when ?tracer (io.opentelemetry.context.Context/root))
spans_ (when ?tracer (enc/latom {})) ; {<uid> <Span_>}
end-buffer1_ (when ?tracer (enc/latom #{})) ; #{[<uid> <end-inst>]}
sgc-buffer1_ (when ?tracer (enc/latom #{})) ; #{<uid>} ; Slow GC
stop-tracing!
(if-not ?tracer
(fn stop-tracing! []) ; Noop
(let [end-buffer2_ (enc/latom #{})
sgc-buffer2_ (enc/latom #{})
fgc-buffer1_ (enc/latom #{})
fgc-buffer2_ (enc/latom #{})
tmax (java.util.Timer. "autoTelemereOpenTelemetryHandlerTimerMax" (boolean :daemon))
t2m (java.util.Timer. "autoTelemereOpenTelemetryHandlerTimer2m" (boolean :daemon))
t3s (java.util.Timer. "autoTelemereOpenTelemetryHandlerTimer3s" (boolean :daemon))
schedule!
(fn [^java.util.Timer timer ^long interval-msecs f]
(.schedule timer (proxy [java.util.TimerTask] [] (run [] (f)))
interval-msecs interval-msecs))
gc-spans!
(fn [uids-to-gc]
(when-not (empty? uids-to-gc)
(let [uids-to-gc (set/intersection uids-to-gc (set (keys (spans_))))]
(when-not (empty? uids-to-gc)
;; Update in small batches to minimize spans_ contention
(doseq [batch (partition-all 10 uids-to-gc)]
(spans_ (fn [old] (reduce dissoc old batch))))))))
move-uids!
(fn [src_ dst_]
(let [drained (enc/reset-in! src_ #{})]
(when-not (empty? drained)
(dst_ (fn [old] (set/union old drained))))))]
;; Notes:
;; - Maintain local {<uid> <Span_>} state, creating spans as needed
;; - A timer+buffer system is used to delay calling `.end` on
;; spans, allowing parents to linger in case they're handled
;; before children.
;;
;; Internal buffer flow:
;; 1. handler->end1->end2->(end!)->fgc1->fgc2->(gc!) ; Fast GC path (span ended)
;; 2. handler ->sgc1->sgc2->(gc!) ; Slow GC path (span not ended)
;;
;; Properties:
;; - End spans 3-6 secs after trace handler ; Linger for possible out-of-order children
;; - GC spans 2-4 mins after ending ; '', children will noop
;; - GC spans 90-92 mins after span first created
;; Final catch-all for spans that may have been created but
;; never ended (e.g. due to sampling or filtering).
;; => Max span runtime!
(schedule! tmax max-span-msecs ; sgc2->(gc!)
(fn [] (gc-spans! (enc/reset-in! sgc-buffer2_ #{}))))
(schedule! t2m (enc/msecs :mins 2)
(fn []
(gc-spans! (enc/reset-in! fgc-buffer2_ #{})) ; fgc2->(gc!)
(move-uids! fgc-buffer1_ fgc-buffer2_) ; fgc1->fgc2
(move-uids! sgc-buffer1_ sgc-buffer2_) ; sgc1->sgc2
))
(schedule! t3s (enc/msecs :secs 3)
(fn []
(let [drained (enc/reset-in! end-buffer2_ #{})]
(when-not (empty? drained)
;; end2->(end!)
(let [spans (spans_)]
(doseq [[uid end-inst] drained]
(when-let [span_ (get spans uid)]
(.end ^Span (force span_) ^java.time.Instant end-inst))))
;; (end!)->fgc1
(let [uids (into #{} (map (fn [[uid _]] uid)) drained)]
(fgc-buffer1_ (fn [old] (set/union old uids))))))
;; end1->end2
(move-uids! end-buffer1_ end-buffer2_)))
(fn stop-tracing! []
(loop [] (when-not (empty? (end-buffer1_)) (recur))) ; Block to drain `end1`
(loop [] (when-not (empty? (end-buffer2_)) (recur))) ; Block to drain `end2`
(.cancel t3s) (.cancel t2m) (.cancel tmax))))]
(let []
(fn a-handler:open-telemetry-logger
([ ]) ; Stop => noop
([ ] (stop-tracing!))
([signal]
(let [{:keys [ns inst level msg_]} signal
logger (.get logger-provider (or ns "default"))
severity (level->severity level)
msg (force msg_)
attrs-map (signal->attrs-map attrs-signal-key signal)
attrs (as-attrs attrs-map)
(let [?span
(when-let [^io.opentelemetry.api.trace.Tracer tracer ?tracer]
(handle-tracing! tracer spans_ end-buffer1_ sgc-buffer1_ signal))]
b (.logRecordBuilder logger)]
(when-let [^io.opentelemetry.api.logs.LoggerProvider logger-provider ?logger-provider]
(let [{:keys [ns inst level msg_]} signal
logger (.get logger-provider (or ns "default"))
lrb (.logRecordBuilder logger)]
(.setTimestamp b inst)
(.setSeverity b severity)
(.setAllAttributes b attrs)
(when-let [body
(or msg
(when-let [error (get signal :error)]
(str (enc/ex-type error) ": " (enc/ex-message error))))]
(.setBody b body))
(.setTimestamp lrb inst)
(.setSeverity lrb (level->severity level))
(.setAllAttributes lrb (signal->attrs signal))
(.emit b)))))))
(when-let [^Span span ?span] ; Incl. traceId, SpanId, etc.
(let [span-in-context (.storeInContext span root-context)]
(.setContext lrb span-in-context)))
(when-let [body
(or
(force msg_)
(when-let [error (get signal :error)]
(when (instance? Throwable error)
(str (enc/ex-type error) ": " (enc/ex-message error)))))]
(.setBody lrb body))
;; Ready for `LogRecordExporter`
(.emit lrb)))))))))
(comment
(as-attrs
(signal->attrs-map :my-attrs
{:level :info :data {:ns/kw1 :v1 :ns/kw2 :v2}
:my-attrs {:longs [1 1 2 3] :strs ["a" "b" "c"]}})))
(do
(require '[taoensso.telemere :as t])
(def h1 (handler:open-telemetry-logger))
(let [[_ [s1 s2]] (t/with-signals (t/trace! ::id1 (t/trace! ::id2 "form2")))]
(def s1 s1)
(def s2 s2)))
(h1 s1))

View file

@ -938,6 +938,15 @@
(comment (def attrs-map otel/signal->attrs-map))
#?(:clj
(defn signal->attrs* [signal]
(enc/map-keys str
(into {}
(.asMap ^io.opentelemetry.api.common.Attributes
(#'otel/signal->attrs signal))))))
(comment (signal->attrs* {:level :info}))
#?(:clj
(deftest _open-telemetry
[(testing "attr-name"
@ -949,80 +958,67 @@
(is (= (#'otel/attr-name :x1.x2/x3-x4 :foo/bar-baz)
"x1.x2.x3_x4.foo.bar_baz"))])
(testing "merge-prefix-map"
[(is (= (#'otel/merge-prefix-map nil "pf" nil) nil))
(is (= (#'otel/merge-prefix-map nil "pf" {}) nil))
(is (= (#'otel/merge-prefix-map {"a" "A"} "pf" {:a :A}) {"a" "A", "pf.a" :A}))
(is (= (#'otel/merge-prefix-map {} "pf"
{:a/b1 "v1" :a/b2 "v2" :nil nil, :map {:k1 "v1"}})
{"pf.a.b1" "v1", "pf.a.b2" "v2", "pf.nil" nil, "pf.map" {:k1 "v1"}}))])
(testing "as-attrs"
(is (= (str
(#'otel/as-attrs
{:string "s", :keyword :foo/bar, :long 5, :double 5.0, :nil nil,
:longs [5 5.0 5.0],
:doubles [5.0 5 5],
:bools [true false nil],
:mixed [5 "5" nil],
:strings ["a" "b" "c"],
:map {:k1 "v1"}}))
"{bools=[true, false, false], double=5.0, doubles=[5.0, 5.0, 5.0], keyword=\":foo/bar\", long=5, longs=[5, 5, 5], map=[[:k1 \"v1\"]], mixed=[5, \"5\", nil], nil=\"nil\", string=\"s\", strings=[\"a\", \"b\", \"c\"]}")))
(testing "signal->attrs-map"
(let [attrs-map #'otel/signal->attrs-map]
[(is (= (attrs-map nil { }) {"error" false}))
(is (= (attrs-map :attrs {:attrs {:a1 :A1}}) {"error" false, :a1 :A1}))
(is
(sm?
(attrs-map :attrs
{:ns "ns"
:line 100
:file "file"
:error ex2
:kind :event
(testing "signal->attrs"
[(is (= (signal->attrs* {:level :info}) {"error" false, "level" "INFO"}))
(is (= (signal->attrs* {:level :info, :k1 :v1}) {"error" false, "level" "INFO"}) "app-level kvs excluded")
(is (sm?
(signal->attrs*
{:kind :event
:level :info
:id ::id1
:uid #uuid "7e9c1df6-78e4-40ac-8c5c-e2353df9ab82"
:ns "ns"
:file "file"
:line 100
:id ::id1
:uid #uuid "7e9c1df6-78e4-40ac-8c5c-e2353df9ab82"
:parent {:id ::parent-id1 :uid #uuid "443154cf-b6cf-47bf-b86a-8b185afee256"}
:root {:id ::root-id1 :uid #uuid "82a53b6a-b28a-4102-8025-9e735dee103c"}
:run-form '(+ 3 2)
:run-val 5
:run-nsecs 100
:sample-rate 0.5
:parent
{:id ::parent-id1
:uid #uuid "443154cf-b6cf-47bf-b86a-8b185afee256"}
:data
{:key-kw :val-kw
:num-set #{1 2 3 4 5}
:mix-set #{1 2 3 4 5 "foo"}}
:attrs {:a1 :A1}})
:error ex2
:otel/attrs {:a1 :A1}})
{"ns" "ns"
"line" 100
{"kind" ":event"
"level" "INFO"
"ns" "ns"
"file" "file"
"line" 100
"error" true
"exception.type" 'clojure.lang.ExceptionInfo
"exception.message" "Ex1"
"exception.stacktrace" (enc/pred string?)
"exception.data.k1" "v1"
"id" ":taoensso.telemere-tests/id1",
"uid" "7e9c1df6-78e4-40ac-8c5c-e2353df9ab82",
"parent.id" ":taoensso.telemere-tests/parent-id1",
"parent.uid" "443154cf-b6cf-47bf-b86a-8b185afee256",
"root.id" ":taoensso.telemere-tests/root-id1",
"root.uid" "82a53b6a-b28a-4102-8025-9e735dee103c",
"kind" :event
"level" "INFO"
"id" :taoensso.telemere-tests/id1
"parent.id" :taoensso.telemere-tests/parent-id1
"uid" #uuid "7e9c1df6-78e4-40ac-8c5c-e2353df9ab82"
"parent.uid" #uuid "443154cf-b6cf-47bf-b86a-8b185afee256"
"run.form" '(+ 3 2)
"run.form" "(+ 3 2)"
"run.val" 5
"run.val_type" 'java.lang.Long
"run.val_type" "java.lang.Long"
"run.nsecs" 100
"sample" 0.5
:a1 :A1}))]))]))
"data.key_kw" ":val-kw",
"data.num_set" [1 4 3 2 5],
"data.mix_set" "#{\"foo\" 1 4 3 2 5}",
"error" true
"exception.type" "clojure.lang.ExceptionInfo"
"exception.message" "Ex1"
"exception.data.k1" "v1"
"exception.stacktrace" (enc/pred string?)
"a1" ":A1"}))])]))
;;;;

View file

@ -86,12 +86,19 @@ Verify successful intake with [`check-intakes`](https://cljdoc.org/d/com.taoenss
## OpenTelemetry
Telemere can send signals as [`LogRecords`](https://opentelemetry.io/docs/specs/otel/logs/data-model/) to [OpenTelemetry](https://opentelemetry.io/).
> **OpenTelemetry interop is experimental** - feedback very welcome!
Telemere can send signals as correlated [`LogRecords`](https://opentelemetry.io/docs/specs/otel/logs/data-model/) and tracing data to configured JVM [OpenTelemetry](https://opentelemetry.io/) exporters.
To do this:
1. Ensure that you have the [OpenTelemetry Java](https://github.com/open-telemetry/opentelemetry-java) dependency.
2. Use [`handler:open-telemetry-logger`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.open-telemetry#handler:open-telemetry-logger) to create an appropriately configured handler, and register it with [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!).
2. Ensure that OpenTelemetry Java and relevant exporters are [appropriately configured](https://opentelemetry.io/docs/languages/java/configuration/).
3. Use [`handler:open-telemetry-logger`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.open-telemetry#handler:open-telemetry-logger) to create an appropriately configured handler, and register it with [`add-handler!`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#add-handler!).
Once registered, this handler will automatically process relevant Telemere signals to emit detailed log and trace data to your OpenTelemetry exporters.
Note that aside from configuring the exporters, Telemere's OpenTelemetry interop **does not require** any use of or familiarity with the OpenTelemetry Java API or concepts. Just use Telemere as you normally would.
## Tufte

View file

@ -8,17 +8,17 @@ You can also easily [write your own handlers](#writing-handlers) for any output
Alphabetically (see linked docstrings below for features and usage):
| Name | Platform | Output target | Output format |
| :------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Clj | `*out*` or `*err*` | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Cljs | Browser console | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:console-raw`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | Cljs | Browser console | Raw signals for [cljs-devtools](https://github.com/binaryage/cljs-devtools), etc. |
| [`handler:file`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | Clj | File/s on disk | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`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)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`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)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:tcp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | Clj | TCP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:udp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | Clj | UDP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| Name | Platform | Output target | Output format |
| :------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :---------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Clj | `*out*` or `*err*` | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:console`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console) | Cljs | Browser console | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:console-raw`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:console-raw) | Cljs | Browser console | Raw signals for [cljs-devtools](https://github.com/binaryage/cljs-devtools), etc. |
| [`handler:file`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#handler:file) | Clj | File/s on disk | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`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](https://github.com/open-telemetry/opentelemetry-java) exporters | [LogRecord](https://opentelemetry.io/docs/specs/otel/logs/data-model/) and tracing data |
| [`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)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`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)) | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:tcp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:tcp-socket) | Clj | TCP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
| [`handler:udp-socket`](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere.sockets#handler:udp-socket) | Clj | UDP socket | [edn/JSON](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#pr-signal-fn) or [human-readable](https://cljdoc.org/d/com.taoensso/telemere/CURRENT/api/taoensso.telemere#format-signal-fn) |
Planned (upcoming) handlers: