diff --git a/project.clj b/project.clj index e3c231b..14ad705 100644 --- a/project.clj +++ b/project.clj @@ -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"] diff --git a/slf4j/src/taoensso/telemere/slf4j.clj b/slf4j/src/taoensso/telemere/slf4j.clj index ddac9f9..9679935 100644 --- a/slf4j/src/taoensso/telemere/slf4j.clj +++ b/slf4j/src/taoensso/telemere/slf4j.clj @@ -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. diff --git a/src/taoensso/telemere.cljc b/src/taoensso/telemere.cljc index d8fe1de..54709e7 100644 --- a/src/taoensso/telemere.cljc +++ b/src/taoensso/telemere.cljc @@ -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 ) better than string stacktrace (clickable, etc.) ;; ;; - Tests for utils (hostname, formatters, etc.)? ;; - Remaining docstrings and TODOs -;; - Document kinds: #{:log :spy :trace :event :error } +;; - Document kinds: #{:log :spy :trace :event :error :system/out :system/err } ;; - 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 diff --git a/src/taoensso/telemere/impl.cljc b/src/taoensso/telemere/impl.cljc index 8e452d4..d61f690 100644 --- a/src/taoensso/telemere/impl.cljc +++ b/src/taoensso/telemere/impl.cljc @@ -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#))) diff --git a/src/taoensso/telemere/streams.clj b/src/taoensso/telemere/streams.clj new file mode 100644 index 0000000..d3e12fd --- /dev/null +++ b/src/taoensso/telemere/streams.clj @@ -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!)) diff --git a/src/taoensso/telemere/tools_logging.clj b/src/taoensso/telemere/tools_logging.clj index b0a2073..f049546 100644 --- a/src/taoensso/telemere/tools_logging.clj +++ b/src/taoensso/telemere/tools_logging.clj @@ -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))) diff --git a/test/taoensso/telemere_tests.cljc b/test/taoensso/telemere_tests.cljc index 3bc2a02..35b3bf0 100644 --- a/test/taoensso/telemere_tests.cljc +++ b/test/taoensso/telemere_tests.cljc @@ -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")]