mirror of
https://github.com/taoensso/telemere.git
synced 2025-12-30 14:48:24 +00:00
[new] Add streams interop, handler binding conveyance, etc.
This commit is contained in:
parent
b75b158f2d
commit
824a057a99
7 changed files with 247 additions and 67 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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#)))
|
||||
|
|
|
|||
130
src/taoensso/telemere/streams.clj
Normal file
130
src/taoensso/telemere/streams.clj
Normal 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!))
|
||||
|
|
@ -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)))
|
||||
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
Loading…
Reference in a new issue