telemere/main/src/taoensso/telemere/impl.cljc
Peter Taoussanis 8a3ae14f45 [fix] Correctly handle nil :run opt
Before commit: {:run nil} didn't    register  as a tracing signal
After  commit: {:run nil} correctly registers as a tracing signal with `nil` form

nil forms aren't typically useful or used, but can come up by accident
so it's important to handle these correctly.

The `trace!` docstring also promises that the return value will always be
equal to the given input. This didn't hold before.
2025-12-04 11:22:52 +01:00

801 lines
34 KiB
Clojure

(ns ^:no-doc taoensso.telemere.impl
"Private ns, implementation detail.
Signal design shared by: Telemere, Tufte, Timbre."
(:require
[clojure.set :as set]
[taoensso.truss :as truss]
[taoensso.encore :as enc]
[taoensso.encore.signals :as sigs])
#?(:cljs
(:require-macros
[taoensso.telemere.impl :refer [with-signal]])))
(comment
(remove-ns (symbol (str *ns*)))
(:api (enc/interns-overview)))
#?(:clj
(enc/declare-remote
^:dynamic taoensso.telemere/*ctx*
^:dynamic taoensso.telemere/*xfn*
^:dynamic taoensso.telemere/*uid-fn*
^:dynamic taoensso.telemere/*otel-tracer*))
;;;; Config
#?(:clj
(do
(def present:tools-logging? (enc/have-resource? "clojure/tools/logging.clj"))
(def present:slf4j? (enc/compile-if org.slf4j.Logger true false))
(def present:telemere-slf4j? (enc/compile-if com.taoensso.telemere.slf4j.TelemereLogger true false))
(def present:otel? (enc/compile-if io.opentelemetry.context.Context true false))
(def enabled:tools-logging?
"Documented at `taoensso.telemere.tools-logging/tools-logging->telemere!`."
(enc/get-env {:as :bool, :default false} :clojure.tools.logging/to-telemere))
(def enabled:otel-tracing?
"Documented at `taoensso.telemere/otel-tracing?`."
(enc/get-env {:as :bool, :default present:otel?}
:taoensso.telemere/otel-tracing<.platform>))))
(def uid-kind
"Documented at `taoensso.telemere/*uid-fn*`."
(enc/get-env {:as :edn, :default :default}
:taoensso.telemere/uid-kind<.platform><.edn>))
#?(:clj
(let [base (enc/get-env {:as :edn} :taoensso.telemere/ct-filters<.platform><.edn>)
kind-filter (enc/get-env {:as :edn} :taoensso.telemere/ct-kind-filter<.platform><.edn>)
ns-filter (enc/get-env {:as :edn} :taoensso.telemere/ct-ns-filter<.platform><.edn>)
id-filter (enc/get-env {:as :edn} :taoensso.telemere/ct-id-filter<.platform><.edn>)
min-level (enc/get-env {:as :edn} :taoensso.telemere/ct-min-level<.platform><.edn>)]
(enc/defonce ct-call-filter
"`SpecFilter` used for compile-time elision, or nil."
(sigs/spec-filter
{:kind-filter (or kind-filter (get base :kind-filter))
:ns-filter (or ns-filter (get base :ns-filter))
:id-filter (or id-filter (get base :id-filter))
:min-level (or min-level (get base :min-level))}))))
(let [base (enc/get-env {:as :edn} :taoensso.telemere/rt-filters<.platform><.edn>)
kind-filter (enc/get-env {:as :edn} :taoensso.telemere/rt-kind-filter<.platform><.edn>)
ns-filter (enc/get-env {:as :edn} :taoensso.telemere/rt-ns-filter<.platform><.edn>)
id-filter (enc/get-env {:as :edn} :taoensso.telemere/rt-id-filter<.platform><.edn>)
min-level (enc/get-env {:as :edn, :default :info} :taoensso.telemere/rt-min-level<.platform><.edn>)]
(enc/defonce ^:dynamic *rt-call-filter*
"`SpecFilter` used for runtime filtering, or nil."
(sigs/spec-filter
{:kind-filter (or kind-filter (get base :kind-filter))
:ns-filter (or ns-filter (get base :ns-filter))
:id-filter (or id-filter (get base :id-filter))
:min-level (or min-level (get base :min-level))})))
(comment (enc/get-env {:as :edn, :return :explain} :taoensso.telemere/rt-filters<.platform><.edn>))
;;;; Utils
#?(:clj
(defmacro on-init [& body]
(let [sym (with-meta '__on-init {:private true})
compiling? (if (:ns &env) false `*compile-files*)]
`(defonce ~sym (when-not ~compiling? ~@body nil)))))
(comment (macroexpand-1 '(on-init (println "foo"))))
;;;; Messages
(deftype MsgSkip [])
(deftype MsgSplice [args])
(def ^:public msg-skip
"For use within signal message vectors.
Special value that will be ignored (noop) when creating message.
Useful for conditionally skipping parts of message content, etc.:
(signal! {:msg [\"Hello\" (if <cond> <then> msg-skip) \"world\"] <...>}) or
(log! [\"Hello\" (if <cond> <then> msg-skip) \"world\"]), etc.
%> {:msg_ \"Hello world\" <...>}"
(MsgSkip.))
(defn ^:public msg-splice
"For use within signal message vectors.
Wraps given arguments so that they're spliced when creating message.
Useful for conditionally splicing in extra message content, etc.:
(signal! {:msg [(when <cond> (msg-splice [\"Username:\" \"Steve\"])) <...>]}) or
(log! [(when <cond> (msg-splice [\"Username:\" \"Steve\"]))])
%> {:msg_ \"Username: Steve\"}"
[args] (MsgSplice. args))
(let [;; xform (map #(if (nil? %) "nil" %))
xform
(fn [rf]
(let [;; Protocol-based impln (extensible but ~20% slower)
;; rf* (fn rf* [acc in] (reduce-msg-arg in acc rf))
rf*
(fn rf* [acc in]
(enc/cond
(instance? MsgSplice in) (reduce rf* acc (.-args ^MsgSplice in))
(instance? MsgSkip in) acc
(nil? in) (rf acc "nil")
:else (rf acc in)))]
(fn
([ ] (rf))
([acc ] (rf acc))
([acc in] (rf* acc in)))))]
(defn signal-msg
"Returns string formed by joining all args with \" \" separator,
rendering nils as \"nil\". Supports `msg-skip`, `msg-splice`.
API intended to be usefully different to `str`:
- `str`: no spacers, skip nils, no splicing
- `signal-msg`: auto spacers, show nils, opt-in splicing"
{:tag #?(:clj 'String :cljs 'string)}
[args] (enc/str-join " " xform args)))
(comment
(enc/qb 2e6 ; [305.61 625.35]
(str "a" "b" "c" nil :kw) ; "abc:kw"
(signal-msg ["a" "b" "c" nil :kw (msg-splice ["d" "e"])]) ; "a b c nil :kw d e"
))
#?(:clj
(defn- parse-msg-form [msg-form]
(when msg-form
(enc/cond
(string? msg-form) msg-form
(vector? msg-form)
(enc/cond
(empty? msg-form) nil
:let [[m1 & more] msg-form]
(and (string? m1) (nil? more)) m1
:else `(delay (signal-msg ~msg-form)))
;; Auto delay-wrap (user should never delay-wrap!)
;; :else `(delay ~msg-form)
;; Leave user to delay-wrap when appropriate (document)
:else msg-form))))
(defn default-trace-msg
[form value error nsecs]
(if error
(str (if (nil? form) "nil" form) " !> " (truss/ex-type error))
(str (if (nil? form) "nil" form) " => " (if (nil? value) "nil" value))))
(comment
(default-trace-msg "(+ 1 2)" 3 nil 12345)
(default-trace-msg "(+ 1 2)" nil (Exception. "Ex") 12345))
;;;; Tracing
(enc/def* ^:dynamic *trace-root* "?{:keys [id uid]}" nil) ; Fixed once bound
(enc/def* ^:dynamic *trace-parent* "?{:keys [id uid]}" nil) ; Changes each nesting level
;; Root Telemere ids: {:parent nil, :id id1, :uid uid1 :root {:id id1, :uid uid1}}
;; Root OTel ids: {:parent nil, :id id1, :uid span1,:root {:id id1, :uid trace1}}
;;;; OpenTelemetry
#?(:clj
(enc/compile-when present:otel?
(do
(enc/def* ^:dynamic *otel-context* "`?Context`" nil)
(defmacro otel-context [] `(or *otel-context* (io.opentelemetry.context.Context/current)))
(defn otel-trace-id
"Returns valid `traceId` or nil."
[^io.opentelemetry.context.Context context]
(let [sc (.getSpanContext (io.opentelemetry.api.trace.Span/fromContext context))]
(when (.isValid sc) (.getTraceId sc))))
(defn otel-span-id
"Returns valid `spanId` or nil."
[^io.opentelemetry.context.Context context]
(let [sc (.getSpanContext (io.opentelemetry.api.trace.Span/fromContext context))]
(when (.isValid sc) (.getSpanId sc))))
(defn viable-tracer
"Returns viable `Tracer`, or nil."
[tracer]
(when-let [tracer ^io.opentelemetry.api.trace.Tracer tracer]
(let [sb (.spanBuilder tracer "test-span")
span (.startSpan sb)]
(when (.isValid (.getSpanContext span))
tracer))))
(def ^String otel-name (enc/fmemoize (fn [id] (if id (enc/as-qname id) "telemere/no-id"))))
(defn otel-context+span
"Returns new `Context` that includes minimal `Span` in given parent `Context`.
We leave the (expensive) population of attributes, etc. for signal handler.
Interop needs only the basics (t0, traceId, spanId, spanName) right away."
^io.opentelemetry.context.Context
[id inst ?parent-context ?span-kind]
(let [parent-context (or ?parent-context (otel-context))]
(enc/if-not [tracer (force taoensso.telemere/*otel-tracer*)]
parent-context ; Can't add Span without Tracer
(let [sb (.spanBuilder ^io.opentelemetry.api.trace.Tracer tracer (otel-name id))]
(.setStartTimestamp sb ^java.time.Instant inst)
(.setSpanKind sb
(case ?span-kind
(nil :internal) io.opentelemetry.api.trace.SpanKind/INTERNAL
:client io.opentelemetry.api.trace.SpanKind/CLIENT
:server io.opentelemetry.api.trace.SpanKind/SERVER
:consumer io.opentelemetry.api.trace.SpanKind/CONSUMER
:producer io.opentelemetry.api.trace.SpanKind/PRODUCER
(truss/unexpected-arg! ?span-kind
{:expected #{nil :internal :client :server :consumer :producer}})))
(.with ^io.opentelemetry.context.Context parent-context
(.startSpan sb)))))))))
(comment
(enc/qb 1e6 (otel-context) (otel-context+span ::id1 (enc/now-inst) nil nil)) ; [46.42 186.89]
(viable-tracer (force taoensso.telemere/*otel-tracer*))
(otel-trace-id (otel-context)))
;;;; Main types
(defrecord Signal
;; Telemere's main public data type, we avoid nesting and duplication
[schema inst uid, ns coords,
#?@(:clj [host thread _otel-context]),
sample, kind id level, ctx parent root, data kvs msg_,
error run-form run-val end-inst run-nsecs]
Object (toString [sig] (str "taoensso.telemere.Signal" (enc/pr-edn* (into {} sig)))))
;; Verbose constructors for readability + to support extra keys
(do (enc/def-print-impl [sig Signal] (str "#taoensso.telemere.Signal" (enc/pr-edn* (into {} sig)))))
#?(:clj (enc/def-print-dup [sig Signal] (str "#taoensso.telemere.impl.Signal" (enc/pr-edn* (into {} sig)))))
(defn signal? #?(:cljs {:tag 'boolean}) [x] (instance? Signal x))
(def impl-signal-keys #{:_otel-context})
(def standard-signal-keys
(set/difference (set (keys (map->Signal {:schema 0})))
impl-signal-keys))
(deftype #_defrecord WrappedSignal
[kind ns id level signal-value_]
sigs/ISignalHandling
(allow-signal? [_ spec-filter] (spec-filter kind ns id level))
(signal-debug [_] {:kind kind, :ns ns, :id id, :level level})
(signal-value [_ handler-sample-rate]
(sigs/signal-with-combined-sample-rate handler-sample-rate
(force signal-value_))))
(defn wrap-signal
"Used by `taoensso.telemere/dispatch-signal!`."
[signal]
(when (map? signal)
(let [{:keys [kind ns id level]} signal]
(WrappedSignal. kind ns id level signal))))
;;;; Handlers
(enc/defonce ^:dynamic *sig-handlers* "?[<wrapped-handler-fn>]" nil)
(defrecord SpyOpts [vol_ last-only? trap?])
(def ^:dynamic *sig-spy* "?SpyOpts" nil)
(defn force-msg-in-sig [sig]
(if-not (map? sig)
sig
(if-let [e (find sig :msg_)]
(assoc sig :msg_ (force (val e)))
(do sig))))
#?(:clj
(defmacro ^:public with-signal
"Executes given form, trapping errors. Returns the LAST signal created by form.
Useful for tests/debugging.
Options:
`trap-signals?` (default false)
Should ALL signals created by form be trapped to prevent normal dispatch
to registered handlers?
`raw-msg?` (default false)
Should delayed `:msg_` in returned signal be retained as-is?
Delay is otherwise replaced by realized string.
See also `with-signals` for more advanced options."
([ form] `(with-signal false false ~form))
([ trap-signals? form] `(with-signal false ~trap-signals? ~form))
([raw-msg? trap-signals? form]
`(let [sig_# (volatile! nil)]
(binding [*sig-spy* (SpyOpts. sig_# true ~trap-signals?)]
(truss/try* ~form (catch :all _#)))
(if ~raw-msg?
(do @sig_#)
(force-msg-in-sig @sig_#))))))
#?(:clj
(defmacro ^:public with-signals
"Like `with-signal` but returns {:keys [value error signals]}.
Useful for more advanced tests/debugging.
Destructuring example:
(let [{:keys [value error] [sig1 sig2] :signals} (with-signals ...)]
...)"
([ form] `(with-signals false false ~form))
([ trap-signals? form] `(with-signals false ~trap-signals? ~form))
([raw-msgs? trap-signals? form]
`(let [sigs_# (volatile! nil)
base-map#
(binding [*sig-spy* (SpyOpts. sigs_# false ~trap-signals?)]
(truss/try*
(do {:value ~form})
(catch :all t# {:error t#})))
sigs#
(not-empty
(if ~raw-msgs?
(do @sigs_#)
(mapv force-msg-in-sig @sigs_#)))]
(if sigs#
(assoc base-map# :signals sigs#)
(do base-map#))))))
#?(:clj (def ^:dynamic *sig-spy-off-thread?* false))
(defn dispatch-signal!
"Dispatches given signal to registered handlers, supports `with-signal/s`."
[signal]
(or
(when-let [{:keys [vol_ last-only? trap?]} *sig-spy*]
(let [sv
#?(:cljs (sigs/signal-value signal nil)
:clj
(if *sig-spy-off-thread?* ; Simulate async handler
(deref (enc/promised :user (sigs/signal-value signal nil)))
(do (sigs/signal-value signal nil))))]
(if last-only?
(vreset! vol_ sv)
(vswap! vol_ #(conj (or % []) sv))))
(when trap? :trapped))
(sigs/call-handlers! *sig-handlers* signal)
:dispatched))
;;;; API helpers
#?(:clj (defmacro docstring [ rname] (enc/slurp-resource (str "docs/" (name rname) ".txt"))))
#?(:clj (defmacro defhelp [sym rname] `(enc/def* ~sym {:doc ~(eval `(docstring ~rname))} "See docstring")))
#?(:clj
(defn arglists [macro-id]
;; + Undocumented [elide? allow? callsite-id host thread otel/context]
(case macro-id
:signal-allowed? ; opts => allowed?
'( [& opts-kvs]
[{:as opts-map :keys
[elidable? coords #_inst #_uid #_xfn #_xfn+,
sample kind ns id level when limit limit-by,
#_ctx #_ctx+ #_parent #_root #_trace?, #_do #_let #_data #_msg #_error #_run #_& #_kvs]}])
:signal! ; opts => allowed? / run result (value or throw)
'( [& opts-kvs]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error run & kvs]}])
:log! ; ?level + msg => nil / allowed?
'([opts-or-msg]
[level msg]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error #_run & kvs]}
msg])
:event! ; id + ?level => nil / allowed?
'([opts-or-id]
[id level]
[id
{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error #_run & kvs]}])
:trace! ; ?id + run => run result (value or throw)
'([opts-or-run]
[id run]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error run & kvs]}
run])
:spy! ; ?level + run => run result (value or throw)
'([opts-or-run]
[level run]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error run & kvs]}
run])
:error! ; ?id + error => given error
'([opts-or-error]
[id error]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error #_run & kvs]}
error])
:catch->error! ; ?id + run => run value or ?catch-val
'([opts-or-run]
[id run]
[{:as opts-map :keys
[catch-val,
elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error #_run & kvs]}
run])
:uncaught->error! ; ?id => nil
'([]
[opts-or-id]
[{:as opts-map :keys
[elidable? coords inst uid xfn xfn+ #_kvs+,
sample kind ns id level when limit limit-by,
ctx ctx+ parent root trace?, do let data msg error #_run & kvs]}])
(truss/unexpected-arg! macro-id))))
;;;; Signal macro
(deftype RunResult [value error ^long run-nsecs]
#?(:clj clojure.lang.IFn :cljs IFn)
(#?(:clj invoke :cljs -invoke) [_] (if error (throw error) value))
(#?(:clj invoke :cljs -invoke) [_ signal_]
(if error
(truss/ex-info! "Signal `:run` form error"
(truss/try*
(do {:taoensso.telemere/signal (force signal_)})
(catch :all t {:taoensso.telemere/signal-error t}))
error)
value)))
(defn inst+nsecs
"Returns given platform instant plus given number of nanosecs."
[inst run-nsecs]
#?(:clj (.plusNanos ^java.time.Instant inst run-nsecs)
:cljs (js/Date. (+ (.getTime inst) (/ run-nsecs 1e6)))))
(comment (enc/qb 1e6 (inst+nsecs (enc/now-inst) 1e9)))
#?(:clj
(defn- valid-opts! [macro-form macro-env caller opts]
(if (map? opts)
(do opts)
(truss/ex-info!
(str "`" caller "` needs compile-time map opts at "
(sigs/format-callsite (enc/get-source macro-form macro-env)))))))
#?(:clj (defn- auto-> [form auto-form] (if (= form :auto) auto-form form)))
#?(:clj
(defmacro signal-allowed?
"Returns true iff signal with given opts would meet filtering conditions.
Wrapped for public API."
([ opts] (truss/keep-callsite `(signal-allowed? nil ~opts)))
([base-opts opts]
(valid-opts! &form &env 'telemere/signal-allowed? (or base-opts {}))
(valid-opts! &form &env 'telemere/signal-allowed? (or opts {}))
(let [opts (merge {:kind :generic, :level :info} base-opts opts)
{:keys [#_callsite-id elide? allow?]}
(sigs/filter-call
{:cljs? (boolean (:ns &env))
:sf-arity 4
:ct-call-filter ct-call-filter
:*rt-call-filter* `*rt-call-filter*}
(assoc opts
:ns (auto-> (get opts :ns :auto) (str *ns*))))]
(if elide? false `(if ~allow? true false))))))
(comment (macroexpand '(signal-allowed? {:level :info})))
#?(:clj
(defmacro signal!
"Generic low-level signal creator. Wrapped for public API."
([ opts] (truss/keep-callsite `(signal! nil ~opts)))
([base-opts opts]
(valid-opts! &form &env 'telemere/signal! (or base-opts {}))
(valid-opts! &form &env 'telemere/signal! (or opts {}))
(let [cljs? (boolean (:ns &env))
clj? (not cljs?)
opts (merge {:kind :generic, :level :info} base-opts opts)
run-form? (contains? opts :run)
run-form (get opts :run)
ns-form* (get opts :ns :auto)
ns-form (auto-> ns-form* (str *ns*))
show-run-val (get opts :run-val '_run-val)
show-run-form
(when run-form?
(get opts :run-form
(if (and
(enc/list-form? run-form)
(> (count run-form) 1)
(> (count (str run-form)) 32))
(list (first run-form) '...)
(do run-form))))
{:keys [#_callsite-id elide? allow?]}
(sigs/filter-call
{:cljs? cljs?
:sf-arity 4
:ct-call-filter ct-call-filter
:*rt-call-filter* `*rt-call-filter*}
(assoc opts
:ns ns-form
:local-forms
{:kind '__kind
:ns '__ns
:id '__id
:level '__level}))]
(if elide?
run-form
(let [coords (get opts :coords (when (= ns-form* :auto) (truss/callsite-coords &form)))
{inst-form :inst
kind-form :kind
id-form :id
level-form :level} opts
trace? (get opts :trace? run-form?)
_
(when-not (contains? #{true false nil} trace?)
(truss/ex-info!
(str "Signal needs compile-time `:trace?` value at "
(sigs/format-callsite ns-form coords))))
host-form (auto-> (get opts :host :auto) (when clj? `(enc/host-info)))
thread-form (auto-> (get opts :thread :auto) (when clj? `(enc/thread-info)))
inst-form (auto-> (get opts :inst :auto) `(enc/now-inst*))
parent-form (get opts :parent `*trace-parent*)
root-form0 (get opts :root `*trace-root*)
uid-form (get opts :uid (when trace? :auto))
signal-delay-form
(let [{do-form :do
let-form :let
msg-form :msg
data-form :data
error-form :error
sample-form :sample} opts
let-form (or let-form '[])
msg-form (parse-msg-form msg-form)
ctx-form
(if-let [ctx+ (get opts :ctx+)]
`(taoensso.encore.signals/update-ctx taoensso.telemere/*ctx* ~ctx+)
(get opts :ctx `taoensso.telemere/*ctx*))
xfn-form
(if-let [xfn+ (get opts :xfn+)]
`(taoensso.encore.signals/comp-xfn taoensso.telemere/*xfn* ~xfn+)
(get opts :xfn `taoensso.telemere/*xfn*))
kvs-form
(let [base
(not-empty
(dissoc opts
:elidable? :coords :inst :uid :xfn :xfn+ :kvs+,
:sample :ns :kind :id :level :filter :when #_:limit #_:limit-by,
:ctx :ctx+ :parent #_:trace?, :do :let :data :msg :error,
:run :run-form :run-val, :elide? :allow? #_:callsite-id,
:host :thread :otel/context))]
(if-let [kvs+ (get opts :kvs+)] ; Undocumented
(if base
`(not-empty (conj ~base ~kvs+))
`(not-empty ~kvs+))
base))
_ ; Compile-time validation
(do
(when (and run-form? error-form) ; Ambiguous source of error
(truss/ex-info!
(str "Signal cannot have both `:run` and `:error` opts at "
(sigs/format-callsite ns-form coords))))
(when-let [e (find opts :msg_)] ; Common typo/confusion
(truss/ex-info!
(str "Signal cannot have `:msg_` opt (did you mean `:msg`?) at "
(sigs/format-callsite ns-form coords)))))
signal-form
(let [record-form
(let [clause [(if run-form? :run :no-run) (if clj? :clj :cljs)]]
(case clause
[:run :clj ] `(Signal. 1 ~'__inst ~'__uid, ~'__ns ~coords ~host-form ~'__thread ~'__otel-context1, ~sample-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root1, ~data-form ~kvs-form ~'_msg_, ~'_run-err '~show-run-form ~show-run-val ~'_end-inst ~'_run-nsecs)
[:run :cljs] `(Signal. 1 ~'__inst ~'__uid, ~'__ns ~coords ~sample-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root1, ~data-form ~kvs-form ~'_msg_, ~'_run-err '~show-run-form ~show-run-val ~'_end-inst ~'_run-nsecs)
[:no-run :clj ] `(Signal. 1 ~'__inst ~'__uid, ~'__ns ~coords ~host-form ~'__thread ~'__otel-context1, ~sample-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root1, ~data-form ~kvs-form ~msg-form, ~error-form nil nil nil nil)
[:no-run :cljs] `(Signal. 1 ~'__inst ~'__uid, ~'__ns ~coords ~sample-form, ~'__kind ~'__id ~'__level, ~ctx-form ~parent-form ~'__root1, ~data-form ~kvs-form ~msg-form, ~error-form nil nil nil nil)
(truss/ex-info!
(str "Unexpected signal constructor args at "
(sigs/format-callsite ns-form coords)))))
record-form
(if-not run-form?
record-form
`(let [~(with-meta '_run-result {:tag `RunResult}) ~'__run-result
~'_run-nsecs (.-run-nsecs ~'_run-result)
~'_run-val (.-value ~'_run-result)
~'_run-err (.-error ~'_run-result)
~'_end-inst (inst+nsecs ~'__inst ~'_run-nsecs)
~'_msg_
(let [mf# ~msg-form]
(if (fn? mf#) ; Undocumented, handy for `trace!`/`spy!`, etc.
(delay (mf# '~show-run-form ~show-run-val ~'_run-err ~'_run-nsecs))
mf#))]
~record-form))]
(if-not kvs-form
record-form
`(let [signal# ~record-form]
(reduce-kv assoc signal# (.-kvs signal#)))))]
`(enc/bound-delay
;; Delay (cache) shared by all handlers, incl. `:let` eval,
;; signal construction, transform (xfn), etc. Throws caught by handler.
~do-form
(let [~@let-form ; Allow to throw, eval BEFORE data, msg, etc.
signal# ~signal-form]
;; Final unwrapped signal value visible to users/handler-fns, allow to throw
(if-let [xfn# ~xfn-form]
(xfn# signal#)
(do signal#)))))
;; Trade-off: avoid double `run-form` expansion
run-fn-form (when run-form? `(fn [] ~run-form))
run-form* (when run-form? `(~'__run-fn-form))
into-let-form
(enc/cond!
(not trace?) ; Don't trace
`[~'__otel-context1 nil
~'__uid ~(auto-> uid-form `(taoensso.telemere/*uid-fn* (if ~'__root0 false true)))
~'__root1 ~'__root0 ; Retain, but don't establish
~'__run-result
~(when run-form?
`(let [t0# (enc/now-nano*)]
(truss/try*
(do (RunResult. ~run-form* nil (- (enc/now-nano*) t0#)))
(catch :all t# (RunResult. nil t# (- (enc/now-nano*) t0#))))))]
;; Trace without OpenTelemetry
(or cljs? (not enabled:otel-tracing?))
`[~'__otel-context1 nil
~'__uid ~(auto-> uid-form `(taoensso.telemere/*uid-fn* (if ~'__root0 false true)))
~'__root1 (or ~'__root0 ~(when trace? `{:id ~'__id, :uid ~'__uid}))
~'__run-result
~(when run-form?
`(binding [*trace-root* ~'__root1
*trace-parent* {:id ~'__id, :uid ~'__uid}]
(let [t0# (enc/now-nano*)]
(truss/try*
(do (RunResult. ~run-form* nil (- (enc/now-nano*) t0#)))
(catch :all t# (RunResult. nil t# (- (enc/now-nano*) t0#)))))))]
;; Trace with OpenTelemetry
(and clj? enabled:otel-tracing?)
`[~'__otel-context0 ~(get opts :otel/context `(otel-context)) ; Context
~'__otel-context1 ~(if run-form? `(otel-context+span ~'__id ~'__inst ~'__otel-context0 ~(get opts :otel/span-kind)) ~'__otel-context0)
~'__uid ~(auto-> uid-form `(or (otel-span-id ~'__otel-context1) (com.taoensso.encore.Ids/genHexId16)))
~'__root1
(or ~'__root0
~(when trace?
`{:id ~'__id, :uid (or (otel-trace-id ~'__otel-context1) (com.taoensso.encore.Ids/genHexId32))}))
~'__run-result
~(when run-form?
`(binding [*otel-context* ~'__otel-context1
*trace-root* ~'__root1
*trace-parent* {:id ~'__id, :uid ~'__uid}]
(let [otel-scope# (.makeCurrent ~'__otel-context1)
t0# (enc/now-nano*)]
(truss/try*
(do (RunResult. ~run-form* nil (- (enc/now-nano*) t0#)))
(catch :all t# (RunResult. nil t# (- (enc/now-nano*) t0#)))
(finally (.close otel-scope#))))))])]
`((fn [] ; iife for better IoC compatibility
;; Unless otherwise specified, allow errors to throw on call
(let [~'__run-fn-form ~run-fn-form
~'__kind ~kind-form
~'__ns ~ns-form
~'__id ~id-form
~'__level ~level-form]
(enc/if-not ~allow?
~run-form*
(let [~'__inst ~inst-form
~'__thread ~thread-form
~'__root0 ~root-form0 ; ?{:keys [id uid]}
~@into-let-form ; Inject conditional bindings
signal# ~signal-delay-form]
(dispatch-signal!
;; Unconditionally send same wrapped signal to all handlers.
;; Each handler will use wrapper for handler filtering,
;; unwrapping (realizing) only allowed signals.
(WrappedSignal. ~'__kind ~'__ns ~'__id ~'__level signal#))
(if ~'__run-result
( ~'__run-result signal#)
true))))))))))))
(comment
(with-signal (signal! {:level :warn :let [x :x] :msg ["Test" "message" x] :data {:a :A :x x} :run (+ 1 2)}))
(macroexpand '(signal! {:level :warn :let [x :x] :msg ["Test" "message" x] :data {:a :A :x x} :run (+ 1 2)}))
(macroexpand '(signal! {:level :info}))
(do
(println "---")
(sigs/with-handler *sig-handlers* "hf1" (fn hf1 [x] (println x)) {}
(signal! {:level :info, :run "run"}))))
;;;; Interop
#?(:clj
(do
(enc/defonce ^:private interop-checks_
"{<source-id> (fn check [])}"
(atom
{:tools-logging (fn [] {:present? present:tools-logging?, :enabled-by-env? enabled:tools-logging?})
:slf4j (fn [] {:present? present:slf4j?, :telemere-provider-present? present:telemere-slf4j?})
:open-telemetry (fn [] {:present? present:otel?, :use-tracer? enabled:otel-tracing?})}))
(defn add-interop-check! [source-id check-fn] (swap! interop-checks_ assoc source-id check-fn))
(defn ^:public check-interop
"Runs Telemere's registered interop checks and returns info useful
for tests/debugging, e.g.:
{:open-telemetry {:present? false}
:tools-logging {:present? false}
:slf4j {:present? true
:sending->telemere? true
:telemere-receiving? true}
...}"
[]
(enc/map-vals (fn [check-fn] (check-fn))
@interop-checks_))
(defn test-interop! [msg test-fn]
(let [msg (str "Interop test: " msg " (" (enc/uuid-str) ")")
signal
(binding [*rt-call-filter* nil] ; Without runtime filters
(with-signal :raw :trap (test-fn msg)))]
(= (force (get signal :msg_)) msg)))))