[new] Add streams interop, handler binding conveyance, etc.

This commit is contained in:
Peter Taoussanis 2024-03-03 12:24:44 +01:00
parent b75b158f2d
commit 824a057a99
7 changed files with 247 additions and 67 deletions

View file

@ -8,7 +8,7 @@
:url "https://www.eclipse.org/legal/epl-v10.html"}
:dependencies
[[com.taoensso/encore "3.88.0"]
[[com.taoensso/encore "3.89.0"]
[org.clj-commons/pretty "2.2.1"]]
:test-paths ["test" #_"src"]

View file

@ -1,6 +1,6 @@
(ns ^:no-doc taoensso.telemere.slf4j
"Private ns, implementation detail.
Interop support: SLF4J API v2 -> Telemere.
Interop support: SLF4J -> Telemere.
To use Telemere as your SLF4J backend/provider, just include the
`com.taoensso/slf4j-telemere` dependency on your classpath.

View file

@ -10,6 +10,7 @@
[taoensso.encore :as enc :refer [have have?]]
[taoensso.encore.signals :as sigs]
[taoensso.telemere.impl :as impl]
#?(:clj [taoensso.telemere.streams :as streams])
#?(:clj [clj-commons.format.exceptions :as fmt-ex])
#?(:clj [clj-commons.ansi :as fmt-ansi])))
@ -18,7 +19,7 @@
(remove-ns 'taoensso.telemere)
(:api (enc/interns-overview)))
(enc/assert-min-encore-version [3 88 0])
(enc/assert-min-encore-version [3 89 0])
;;;; Roadmap
;; x Fundamentals
@ -32,13 +33,12 @@
;; - Update Timbre (signal API, config API, signal fields, backport improvements)
;;;; TODO
;; - `clojure.tools.logging/log-capture!`, `with-logs`, etc.
;; - Via Timbre: core handlers, any last utils?
;; - Cljs (.log js/console <js/Error>) better than string stacktrace (clickable, etc.)
;;
;; - Tests for utils (hostname, formatters, etc.)?
;; - Remaining docstrings and TODOs
;; - Document kinds: #{:log :spy :trace :event :error <user>}
;; - Document kinds: #{:log :spy :trace :event :error :system/out :system/err <user>}
;; - General polish
;;
;; - Reading plan
@ -330,82 +330,106 @@
(when-let [cause (ex-cause error)] (str nl nl "Caused by:" nl (format-error cause))))))
:clj
([{:keys [fonts sort]
;; TODO Review API, esp. re: *color-enabled*, etc.
([{:keys [use-fonts? sort-stacktrace-by fonts]
:or
{fonts clj-commons.format.exceptions/default-fonts
sort :chronological #_:depth-first}}
{use-fonts? :auto
sort-stacktrace-by :chronological #_:depth-first
fonts clj-commons.format.exceptions/default-fonts}}
error]
(binding [fmt-ansi/*color-enabled* (not (empty? fonts))
fmt-ex/*fonts* fonts
(binding [fmt-ansi/*color-enabled*
(if (enc/identical-kw? use-fonts? :auto)
nil ; => Guess based on environment
use-fonts?)
fmt-ex/*fonts* fonts
fmt-ex/*traditional*
(case sort
(case sort-stacktrace-by
:depth-first true ; Traditional
:chronological false ; Modern (default)
(enc/unexpected-arg! sort
(enc/unexpected-arg! sort-stacktrace-by
{:context `format-error
:param 'sort
:param 'sort-stacktrace-by
:expected #{:depth-first :chronological}}))]
(fmt-ex/format-exception error)))))
(comment (println (format-error (ex-info "Ex2" {:k2 :v2} (ex-info "Ex1" {:k1 :v1})))))
;;;; Interop (`clojure.tools.logging`, SLF4J)
;;;; Interop
#?(:clj
(def ^:private have-tools-logging?
(enc/compile-if
(do (require '[taoensso.telemere.tools-logging :as ttl]) true)
true false)))
(do
(def ^:private have-tools-logging?
(enc/compile-if
(do (require '[taoensso.telemere.tools-logging :as ttl]) true)
true false))
#?(:clj
(enc/compile-when have-tools-logging?
(enc/defalias ttl/tools-logging->telemere!)
(when (enc/get-env {:as :bool} :clojure.tools.logging->telemere?)
(ttl/tools-logging->telemere!))))
(enc/compile-when have-tools-logging?
(enc/defalias ttl/tools-logging->telemere!)
(when (enc/get-env {:as :bool} :clojure.tools.logging->telemere?)
(ttl/tools-logging->telemere!)))
#?(:clj
(defn- interop-test! [msg form-fn]
(let [msg (str msg " (" (enc/uuid-str) ")")
signal
(binding [impl/*rt-sig-filter* nil]
(impl/with-signal {:stop-propagation? true, :return :signal}
(form-fn msg)))]
(enc/defaliases
streams/with-out->telemere
streams/with-err->telemere
streams/with-streams->telemere
streams/streams->telemere!
streams/streams->reset!)
(= (force (get signal :msg_)) msg))))
(defn- interop-test! [msg form-fn]
(let [msg (str "Interop test: " msg " (" (enc/uuid-str) ")")
signal
(binding [impl/*rt-sig-filter* nil]
(impl/with-signal {:stop-propagation? true, :return :signal}
(form-fn msg)))]
#?(:clj
(defn interop-check
"Tests Telemere's interop with `clojure.tools.logging` and SLF4J.
Returns {:keys [tools-logging slf4j]} with sub-maps:
{:keys [present? set->telemere? receiving?]}."
[]
(let [base-present {:present? true, :send->telemere? false, :receiving? false}]
{:tools-logging
(if-not (enc/have-resource? "clojure/tools/logging.clj")
{:present? false}
(merge base-present
(enc/compile-when have-tools-logging?
{:send->telemere? (ttl/tools-logging->telemere?)
:receiving?
(interop-test!
"Interop test: `clojure.tools.logging`->Telemere"
(fn [msg] (clojure.tools.logging/info msg)))})))
(= (force (get signal :msg_)) msg)))
:slf4j
(if-not (enc/have-class? "org.slf4j.Logger")
{:present? false}
(merge base-present
(enc/compile-when
(and org.slf4j.Logger com.taoensso.telemere.slf4j.TelemereLogger)
(let [^org.slf4j.Logger sl
(org.slf4j.LoggerFactory/getLogger "InteropTestTelemereLogger")]
(defn interop-check
"Tests Telemere's interop with `clojure.tools.logging` and SLF4J, useful
for tests/debugging. Returns {:keys [tools-logging slf4j streams]} with
{:keys [send->telemere? receiving? ...]} sub-maps."
[]
(let [base-present {:present? true, :send->telemere? false, :receiving? false}]
{:tools-logging
(if-not (enc/have-resource? "clojure/tools/logging.clj")
{:present? false}
(merge base-present
(enc/compile-when have-tools-logging?
(let [sending? (ttl/tools-logging->telemere?)]
{:send->telemere? sending?
:receiving? (and sending?
(interop-test! "`clojure.tools.logging` -> Telemere"
#(clojure.tools.logging/info %)))}))))
{:send->telemere? (instance? com.taoensso.telemere.slf4j.TelemereLogger sl)
:receiving? (interop-test! "Interop test: SLF4J->Telemere" (fn [msg] (.info sl msg)))}))))})))
:slf4j
(if-not (enc/have-class? "org.slf4j.Logger")
{:present? false}
(merge base-present
(enc/compile-when
(and org.slf4j.Logger com.taoensso.telemere.slf4j.TelemereLogger)
(let [^org.slf4j.Logger sl
(org.slf4j.LoggerFactory/getLogger "InteropTestTelemereLogger")
(comment (check-interop))
sending? (instance? com.taoensso.telemere.slf4j.TelemereLogger sl)]
{:send->telemere? sending?
:receiving? (and sending? (interop-test! "SLF4J -> Telemere" #(.info sl %)))}))))
:streams
{:out
(let [sending? (boolean @streams/orig-out_)]
{:send->telemere? sending?
:receiving? (and sending? (interop-test! "`System/out` -> Telemere" #(.println System/out %)))})
:err
(let [sending? (boolean @streams/orig-err_)]
{:send->telemere? sending?
:receiving? (and sending? (interop-test! "`System/err` -> Telemere" #(.println System/err %)))})}}))))
(comment (interop-check))
;;;; Flow benchmarks

View file

@ -9,6 +9,11 @@
(remove-ns 'taoensso.telemere.impl)
(:api (enc/interns-overview)))
#?(:clj
(enc/declare-remote ; For macro expansions
^:dynamic taoensso.telemere/*ctx*
^:dynamic taoensso.telemere/*middleware*))
;;;; Utils
#?(:clj (defmacro threaded [& body] `(let [t# (Thread. (fn [] ~@body))] (.start t#) t#)))

View file

@ -0,0 +1,130 @@
(ns ^:no-doc taoensso.telemere.streams
"Private ns, implementation detail.
Interop support: standard out/err streams -> Telemere."
(:require
[taoensso.encore :as enc :refer [have have?]]
[taoensso.telemere.impl :as impl]))
(enc/defonce orig-*out* "Original `*out*` on ns load" *out*)
(enc/defonce orig-*err* "Original `*err*` on ns load" *err*)
(enc/defonce ^:dynamic prev-*out* "Previous `*out*` (prior to any Telemere binds)" nil)
(enc/defonce ^:dynamic prev-*err* "Previous `*err*` (prior to any Telemere binds)" nil)
(def ^:private ^:const default-out-opts {:kind :system/out, :level :info})
(def ^:private ^:const default-err-opts {:kind :system/err, :level :error})
(defn osw ^java.io.OutputStreamWriter [x] (java.io.OutputStreamWriter. x))
(defn telemere-print-stream
"Returns a `java.io.PrintStream` that will flush to Telemere signals with given opts."
^java.io.PrintStream [{:as sig-opts :keys [kind level id]}]
(let [baos
(proxy [java.io.ByteArrayOutputStream] []
(flush []
(let [^java.io.ByteArrayOutputStream this this]
(proxy-super flush)
(let [msg (.trim (.toString this))]
(proxy-super reset)
(when-not (.isEmpty msg)
(binding [*out* (or prev-*out* orig-*out*)
*err* (or prev-*err* orig-*err*)]
(impl/signal!
{:location nil
:ns nil
:kind kind
:level level
:id id
:msg msg})))))))]
(java.io.PrintStream. baos true ; Auto flush
java.nio.charset.StandardCharsets/UTF_8)))
(defmacro ^:public with-out->telemere
"Executes form with `*out*` bound to flush to Telemere signals with given opts."
([ form] `(with-out->telemere nil ~form))
([opts form]
`(binding [prev-*out* (or prev-*out* *out*)
*out* (osw (telemere-print-stream ~(conj default-out-opts opts)))]
~form)))
(defmacro ^:public with-err->telemere
"Executes form with `*err*` bound to flush to Telemere signals with given opts."
([ form] `(with-err->telemere nil ~form))
([opts form]
`(binding [prev-*err* (or prev-*err* *err*)
*err* (osw (telemere-print-stream ~(conj default-err-opts opts)))]
~form)))
(defmacro ^:public with-streams->telemere
"Executes form with `*out*` and/or `*err*` bound to flush to Telemere signals
with given opts."
([form] `(with-streams->telemere nil ~form))
([{:keys [out err]
:or {out default-out-opts
err default-err-opts}} form]
`(binding [prev-*out* (or prev-*out* *out*)
prev-*err* (or prev-*err* *err*)
*out* (if-let [out# ~out] (osw (telemere-print-stream out#)) *out*)
*err* (if-let [err# ~err] (osw (telemere-print-stream err#)) *err*)]
~form)))
(comment (impl/with-signal (with-out->telemere (println "hello"))))
(enc/defonce orig-out_ "Original `System/out`, or nil" (atom nil))
(enc/defonce orig-err_ "Original `System/err`, or nil" (atom nil))
(let [monitor (Object.)]
#_
(defn streams->telemere? []
;; Not easy to actually identify current `System/out`, `System/err` vals
(locking monitor
{:out (boolean @orig-out_)
:err (boolean @orig-err_)}))
(defn ^:public streams->reset!
"Experimental, subject to change without notice!
Resets `System/out` and `System/err` to their original value (prior to any
`streams->telemere!` call)."
[]
(locking monitor
(let [reset-out? (when-let [[out] (reset-vals! orig-out_ nil)] (System/setOut out) true)
reset-err? (when-let [[err] (reset-vals! orig-err_ nil)] (System/setErr err) true)]
(or reset-out? reset-err?))))
(defn ^:public streams->telemere!
"Experimental, subject to change without notice!
When given `out`, sets JVM's `System/out` to flush to Telemere signals with those opts.
When given `err`, sets JVM's `System/err` to flush to Telemere signals with those opts.
Note that setting `System/out` won't necessarily affect Clojure's `*out*`,
and setting `System/err` won't necessarily affect Clojure's `*err*`.
See also:
`with-out->telemere`,
`with-err->telemere`,
`with-streams->telemere`."
([] (streams->telemere! nil))
([{:keys [out err]
:or {out default-out-opts
err default-err-opts}}]
(when (or out err)
(let [out (when out (telemere-print-stream out))
err (when err (telemere-print-stream err))]
(locking monitor
(when out (compare-and-set! orig-out_ nil System/out) (System/setOut out))
(when err (compare-and-set! orig-err_ nil System/err) (System/setErr err)))
true)))))
(comment
(streams->telemere?)
(streams->telemere! {})
(streams->reset!))

View file

@ -2,9 +2,9 @@
"Private ns, implementation detail.
Interop support: `clojure.tools.logging` -> Telemere."
(:require
[clojure.tools.logging :as ctl]
[taoensso.encore :as enc :refer [have have?]]
[taoensso.telemere.impl :as impl]))
[taoensso.telemere.impl :as impl]
[clojure.tools.logging :as ctl]))
(defmacro ^:private when-debug [& body] (when #_true false `(do ~@body)))

View file

@ -27,10 +27,12 @@
#?(:clj (defmacro wsv [form] `(impl/-with-signal (fn [] ~form) {:force-msg? true, :return :signal})))
(do
(def ^:private ex1 (ex-info "TestEx" {}))
(def ^:private ex1? #(= % ex1))
(def ^:private ex1-rv? #(= (:error %) ex1))
(def ^:private ex1-pred (enc/pred ex1?)))
(def ex1 (ex-info "TestEx" {}))
(def ex1? #(= % ex1))
(def ex1-rv? #(= (:error %) ex1))
(def ex1-pred (enc/pred ex1?)))
(def ^:dynamic *dynamic-var* nil)
;;;;
@ -255,9 +257,18 @@
(is (= c1 7) "1x run + 4x handler middleware + 2x call middleware")
(is (= c2 8) "2x run + 4x handler middleware + 2x call middleware")
(is (= c3 15) "3x run + 8x handler middleware + 4x call middleware")
(is (= c4 19) "3x run + 12x handler middleware + 4x call middleware")]))))])
(is (= c4 19) "3x run + 12x handler middleware + 4x call middleware")]))))
(def ^:private ^:dynamic *throwing-handler-middleware?* false)
(testing "Handler binding conveyance"
(let [a (atom nil)
wh1
(sigs/wrap-handler :hid1 (fn [x] (reset! a *dynamic-var*))
nil #?(:clj {:async {:mode :dropping}} :cljs nil))]
(binding [*dynamic-var* "bound", impl/*sig-handlers* [wh1]] (sig! {:level :info}))
(is (= (do #?(:clj (Thread/sleep 500)) @a) "bound"))))])
(def ^:dynamic *throwing-handler-middleware?* false)
(deftest _throwing
(let [sv_ (atom :nx)
@ -465,6 +476,16 @@
(is (sm? (wsv (ctl/warn "Hello" "x" "y")) {:level :warn, :location nil, :ns nil, :kind :log, :id :taoensso.telemere/tools-logging, :msg_ "Hello x y"}))
(is (sm? (wsv (ctl/error ex1 "An error")) {:level :error, :error ex1}) "Errors")])
(testing "Standard out/err streams -> Telemere"
[(is (sm? (tel/interop-check) {:streams {:out {:send->telemere? false, :receiving? false}, :err {:send->telemere? false, :receiving? false}}}))
(is (true? (tel/streams->telemere!)))
(is (sm? (tel/interop-check) {:streams {:out {:send->telemere? true, :receiving? true}, :err {:send->telemere? true, :receiving? true}}}))
(is (true? (tel/streams->reset!)))
(is (sm? (tel/interop-check) {:streams {:out {:send->telemere? false, :receiving? false}, :err {:send->telemere? false, :receiving? false}}}))
(is
(sm? (wsv (tel/with-out->telemere (println "Hello" "x" "y")))
{:level :info, :location nil, :ns nil, :kind :system/out, :msg_ "Hello x y"}))])
(testing "SLF4J -> Telemere"
[(is (sm? (tel/interop-check) {:slf4j {:present? true, :send->telemere? true, :receiving? true}}))
(let [^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "MyTelemereSLF4JLogger")]