diff --git a/project.clj b/project.clj index b525a2a..d1a3fdb 100644 --- a/project.clj +++ b/project.clj @@ -66,9 +66,10 @@ :dependencies [[org.clojure/test.check "1.1.1"] [org.clojure/tools.logging "1.3.0"] + [org.slf4j/slf4j-api "2.0.16"] + [com.taoensso/timbre-slf4j "6.6.0-SNAPSHOT"] [com.taoensso/nippy "3.4.2"] - [com.taoensso/carmine "3.4.1" - :exclusions [com.taoensso/timbre]] + [com.taoensso/carmine "3.4.1" :exclusions [com.taoensso/timbre]] [com.draines/postal "2.0.5"]] :plugins diff --git a/slf4j/.gitignore b/slf4j/.gitignore new file mode 100644 index 0000000..f9929ef --- /dev/null +++ b/slf4j/.gitignore @@ -0,0 +1,15 @@ +pom.xml* +.lein* +.nrepl-port +*.jar +*.class +.env +.DS_Store +/lib/ +/classes/ +/target/ +/checkouts/ +/logs/ +/.clj-kondo/.cache +.idea/ +*.iml diff --git a/slf4j/project.clj b/slf4j/project.clj new file mode 100644 index 0000000..a86f2b3 --- /dev/null +++ b/slf4j/project.clj @@ -0,0 +1,29 @@ +(defproject com.taoensso/timbre-slf4j "6.6.0-SNAPSHOT" + :author "Peter Taoussanis " + :description "Timbre backend/provider for SLF4J API v2" + :url "https://www.taoensso.com/timbre" + + :license + {:name "Eclipse Public License - v 1.0" + :url "https://www.eclipse.org/legal/epl-v10.html"} + + :scm {:name "git" :url "https://github.com/taoensso/timbre"} + + :java-source-paths ["src/java"] + :javac-options ["--release" "8" "-g"] ; Support Java >= v8 + :dependencies [] + + :profiles + {:provided + {:dependencies + [[org.clojure/clojure "1.11.4"] + [org.slf4j/slf4j-api "2.0.16"] + [com.taoensso/timbre "6.6.0-SNAPSHOT"]]} + + :dev + {:plugins + [[lein-pprint "1.3.2"] + [lein-ancient "0.7.0"]]}} + + :aliases + {"deploy-lib" ["do" #_["build-once"] ["deploy" "clojars"] ["install"]]}) diff --git a/slf4j/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider b/slf4j/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider new file mode 100755 index 0000000..0faf1a6 --- /dev/null +++ b/slf4j/resources/META-INF/services/org.slf4j.spi.SLF4JServiceProvider @@ -0,0 +1 @@ +com.taoensso.timbre.slf4j.TimbreServiceProvider diff --git a/slf4j/src/java/com/taoensso/timbre/slf4j/TimbreLogger.java b/slf4j/src/java/com/taoensso/timbre/slf4j/TimbreLogger.java new file mode 100644 index 0000000..c05bb05 --- /dev/null +++ b/slf4j/src/java/com/taoensso/timbre/slf4j/TimbreLogger.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.taoensso.timbre.slf4j; +// Based on `org.slf4j.simple.SimpleLogger` + +import java.io.Serializable; + +import org.slf4j.Logger; +import org.slf4j.Marker; +import org.slf4j.event.Level; +import org.slf4j.event.LoggingEvent; +import org.slf4j.helpers.LegacyAbstractLogger; +import org.slf4j.spi.LoggingEventAware; + +import clojure.java.api.Clojure; +import clojure.lang.IFn; + +public class TimbreLogger extends LegacyAbstractLogger implements LoggingEventAware, Serializable { + + private static final long serialVersionUID = -1999356203037132557L; + + private static boolean INITIALIZED = false; + static void lazyInit() { + if (INITIALIZED) { return; } + INITIALIZED = true; + init(); + } + + private static IFn logFn; + private static IFn isAllowedFn; + + static void init() { + IFn requireFn = Clojure.var("clojure.core", "require"); + requireFn.invoke(Clojure.read("taoensso.timbre.slf4j")); + isAllowedFn = Clojure.var("taoensso.timbre.slf4j", "allowed?"); + logFn = Clojure.var("taoensso.timbre.slf4j", "log!"); + } + + protected TimbreLogger(String name) { this.name = name; } + + protected boolean isLevelEnabled(Level level) { return (boolean) isAllowedFn.invoke(this.name, level); } + public boolean isTraceEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.TRACE); } + public boolean isDebugEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.DEBUG); } + public boolean isInfoEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.INFO); } + public boolean isWarnEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.WARN); } + public boolean isErrorEnabled() { return (boolean) isAllowedFn.invoke(this.name, Level.ERROR); } + + public void log(LoggingEvent event) { logFn.invoke(this.name, event); } // Fluent (modern) API, called after level check + + @Override protected String getFullyQualifiedCallerName() { return null; } + @Override + protected void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, Throwable throwable) { + logFn.invoke(this.name, level, throwable, messagePattern, arguments, marker); // Legacy API, called after level check + } + +} diff --git a/slf4j/src/java/com/taoensso/timbre/slf4j/TimbreLoggerFactory.java b/slf4j/src/java/com/taoensso/timbre/slf4j/TimbreLoggerFactory.java new file mode 100644 index 0000000..ab48b0a --- /dev/null +++ b/slf4j/src/java/com/taoensso/timbre/slf4j/TimbreLoggerFactory.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.taoensso.timbre.slf4j; +// Based on `org.slf4j.simple.SimpleLoggerFactory` + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.slf4j.Logger; +import org.slf4j.ILoggerFactory; + +public class TimbreLoggerFactory implements ILoggerFactory { + + ConcurrentMap loggerMap; + + public TimbreLoggerFactory() { + loggerMap = new ConcurrentHashMap<>(); + TimbreLogger.lazyInit(); + } + + public Logger getLogger(String name) { return loggerMap.computeIfAbsent(name, this::createLogger); } + protected Logger createLogger(String name) { return new TimbreLogger(name); } + protected void reset() { loggerMap.clear(); } +} diff --git a/slf4j/src/java/com/taoensso/timbre/slf4j/TimbreServiceProvider.java b/slf4j/src/java/com/taoensso/timbre/slf4j/TimbreServiceProvider.java new file mode 100755 index 0000000..6901ee1 --- /dev/null +++ b/slf4j/src/java/com/taoensso/timbre/slf4j/TimbreServiceProvider.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2004-2011 QOS.ch + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.taoensso.timbre.slf4j; +// Based on `org.slf4j.simple.SimpleServiceProvider` + +import org.slf4j.ILoggerFactory; +import org.slf4j.IMarkerFactory; +import org.slf4j.helpers.BasicMarkerFactory; +import org.slf4j.helpers.BasicMDCAdapter; +import org.slf4j.spi.MDCAdapter; +import org.slf4j.spi.SLF4JServiceProvider; + +public class TimbreServiceProvider implements SLF4JServiceProvider { + + public static String REQUESTED_API_VERSION = "2.0.99"; // Should not be final + + private ILoggerFactory loggerFactory; + private IMarkerFactory markerFactory; + private MDCAdapter mdcAdapter; + + public ILoggerFactory getLoggerFactory() { return loggerFactory; } + @Override public IMarkerFactory getMarkerFactory() { return markerFactory; } + @Override public MDCAdapter getMDCAdapter() { return mdcAdapter; } + @Override public String getRequestedApiVersion() { return REQUESTED_API_VERSION; } + @Override + public void initialize() { + loggerFactory = new TimbreLoggerFactory(); + markerFactory = new BasicMarkerFactory(); + mdcAdapter = new BasicMDCAdapter(); + } + +} diff --git a/slf4j/src/taoensso/timbre/slf4j.clj b/slf4j/src/taoensso/timbre/slf4j.clj new file mode 100644 index 0000000..38a50f5 --- /dev/null +++ b/slf4j/src/taoensso/timbre/slf4j.clj @@ -0,0 +1,143 @@ +(ns taoensso.timbre.slf4j + "Interop support for SLF4Jv2 -> Timbre. + Adapted from `taoensso.telemere.slf4j`." + {:author "Peter Taoussanis (@ptaoussanis)"} + (:require + [taoensso.encore :as enc :refer [have have?]] + [taoensso.timbre :as timbre]) + + (:import + [org.slf4j Logger] + [com.taoensso.timbre.slf4j TimbreLogger])) + +;;;; Utils + +(defmacro ^:private when-debug [& body] (when #_true false `(do ~@body))) + +(defn- timbre-level + "Returns Timbre level for given `org.slf4j.event.Level`." + [^org.slf4j.event.Level level] + (enc/case-eval (.toInt level) + org.slf4j.event.EventConstants/TRACE_INT :trace + org.slf4j.event.EventConstants/DEBUG_INT :debug + org.slf4j.event.EventConstants/INFO_INT :info + org.slf4j.event.EventConstants/WARN_INT :warn + org.slf4j.event.EventConstants/ERROR_INT :error + (throw + (ex-info "Unexpected `org.slf4j.event.Level`" + {:level (enc/typed-val level)})))) + +(comment (enc/qb 1e6 (timbre-level org.slf4j.event.Level/INFO))) ; 36.47 + +(defn- get-marker "Private util for tests, etc." + ^org.slf4j.Marker [n] (org.slf4j.MarkerFactory/getMarker n)) + +(defn- est-marker! + "Private util for tests, etc. + Globally establishes (compound) `org.slf4j.Marker` with name `n` and mutates it + (all occurences!) to have exactly the given references. Returns the (compound) marker." + ^org.slf4j.Marker [n & refs] + (let [m (get-marker n)] + (enc/reduce-iterator! (fn [_ in] (.remove m in)) nil (.iterator m)) + (doseq [n refs] (.add m (get-marker n))) + m)) + +(comment [(est-marker! "a1" "a2") (get-marker "a1") (= (get-marker "a1") (get-marker "a1"))]) + +(def ^:private marker-names + "Returns #{}. Cached => assumes markers NOT modified after creation." + ;; We use `BasicMarkerFactory` so: + ;; 1. Our markers are just labels (no other content besides their name). + ;; 2. Markers with the same name are identical (enabling caching). + (enc/fmemoize + (fn marker-names [marker-or-markers] + (if (instance? org.slf4j.Marker marker-or-markers) + + ;; Single marker + (let [^org.slf4j.Marker m marker-or-markers + acc #{(.getName m)}] + + (if-not (.hasReferences m) + acc + (enc/reduce-iterator! + (fn [acc ^org.slf4j.Marker in] + (if-not (.hasReferences in) + (conj acc (.getName in)) + (into acc (marker-names in)))) + acc (.iterator m)))) + + ;; Vector of markers + (reduce + (fn [acc in] (into acc (marker-names in))) + #{} (have vector? marker-or-markers)))))) + +(comment + (let [m1 (est-marker! "M1") + m2 (est-marker! "M1") + cm (est-marker! "Compound" "M1" "M2") + ms [m1 m2]] + + (enc/qb 1e6 ; [45.52 47.48 44.85] + (marker-names m1) + (marker-names cm) + (marker-names ms)))) + +;;;; Interop fns (called by `TimbreLogger`) + +(defn- allowed? + "Called by `com.taoensso.timbre.slf4j.TimbreLogger`." + [logger-name level] + (when-debug (println [:slf4j/allowed? (timbre-level level) logger-name])) + (timbre/may-log? (timbre-level level) logger-name)) + +(defn- normalized-log! + [logger-name level inst error msg-pattern args marker-names kvs] + (when-debug (println [:slf4j/normalized-log! (timbre-level level) logger-name])) + (timbre/log! + {:may-log? true ; Pre-filtered by `allowed?` call + :level (timbre-level level) + :loc {:ns logger-name} + :instant inst + :msg-type :p + :?err error + :vargs [(org.slf4j.helpers.MessageFormatter/basicArrayFormat + msg-pattern args)] + :?base-data + (enc/assoc-some nil + :slf4j/args (when args (vec args)) + :slf4j/marker-names marker-names + :slf4j/kvs kvs + :slf4j/context + (when-let [hmap (org.slf4j.MDC/getCopyOfContextMap)] + (clojure.lang.PersistentHashMap/create hmap)))}) + nil) + +(defn- log! + "Called by `com.taoensso.timbre.slf4j.TimbreLogger`." + + ;; Modern "fluent" API calls + ([logger-name ^org.slf4j.event.LoggingEvent event] + (let [inst (or (when-let [ts (.getTimeStamp event)] (java.util.Date. ts)) (enc/now-dt*)) + level (.getLevel event) + error (.getThrowable event) + msg-pattern (.getMessage event) + args (when-let [args (.getArgumentArray event)] args) + markers (when-let [markers (.getMarkers event)] (marker-names (vec markers))) + kvs (when-let [kvps (.getKeyValuePairs event)] + (reduce + (fn [acc ^org.slf4j.event.KeyValuePair kvp] + (assoc acc (.-key kvp) (.-value kvp))) + nil kvps))] + + (when-debug (println [:slf4j/fluent-log-call (timbre-level level) logger-name])) + (normalized-log! logger-name level inst error msg-pattern args markers kvs))) + + ;; Legacy API calls + ([logger-name ^org.slf4j.event.Level level error msg-pattern args marker] + (let [marker-names (when marker (marker-names marker))] + (when-debug (println [:slf4j/legacy-log-call (timbre-level level) logger-name])) + (normalized-log! logger-name level (enc/now-dt*) error msg-pattern args marker-names nil)))) + +(comment + (def ^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "my.class")) + (-> sl (.info "x={},y={}" "1" "2"))) diff --git a/src/taoensso/timbre.cljc b/src/taoensso/timbre.cljc index dbeb657..ed3d37e 100644 --- a/src/taoensso/timbre.cljc +++ b/src/taoensso/timbre.cljc @@ -447,15 +447,14 @@ (defn ^:no-doc -log! "Core low-level log fn. Private, don't use!" - ;; Back compatible arities for convenience of AOT tools, + ;; Back compatible arities for AOT tools, ;; Ref. - ([config level ?ns-str ?file ?line msg-type ?err vargs_ ?base-data ] (-log! config level ?ns-str ?file ?line nil msg-type ?err vargs_ ?base-data nil false nil)) - ([config level ?ns-str ?file ?line msg-type ?err vargs_ ?base-data callsite-id ] (-log! config level ?ns-str ?file ?line nil msg-type ?err vargs_ ?base-data callsite-id false nil)) - ([config level ?ns-str ?file ?line msg-type ?err vargs_ ?base-data callsite-id spying? ] (-log! config level ?ns-str ?file ?line nil msg-type ?err vargs_ ?base-data callsite-id spying? nil)) - ([config level ?ns-str ?file ?line msg-type ?err vargs_ ?base-data callsite-id spying? may-log] (-log! config level ?ns-str ?file ?line nil msg-type ?err vargs_ ?base-data callsite-id spying? may-log)) - ([config level ?ns-str ?file ?line ?column msg-type ?err vargs_ ?base-data callsite-id spying? may-log] + ([config level ?ns-str ?file ?line msg-type ?err vargs_ ?base-data ] (-log! config level ?ns-str ?file ?line nil msg-type ?err vargs_ ?base-data nil false nil nil)) + ([config level ?ns-str ?file ?line msg-type ?err vargs_ ?base-data callsite-id ] (-log! config level ?ns-str ?file ?line nil msg-type ?err vargs_ ?base-data callsite-id false nil nil)) + ([config level ?ns-str ?file ?line msg-type ?err vargs_ ?base-data callsite-id spying? ] (-log! config level ?ns-str ?file ?line nil msg-type ?err vargs_ ?base-data callsite-id spying? nil nil)) + ([config level ?ns-str ?file ?line ?column msg-type ?err vargs_ ?base-data callsite-id spying? inst may-log] (when (or may-log (may-log? :trace level ?ns-str config)) - (let [instant (enc/now-dt*) + (let [instant (or inst (enc/now-dt*)) context *context* vargs @vargs_ @@ -625,7 +624,8 @@ ([{:as opts :keys [loc level msg-type args vargs - config ?err ?base-data spying? #_may-log?] + config ?err ?base-data spying? + #_instant #_may-log?] :or {config `*config* ?err :auto}}] @@ -651,7 +651,9 @@ ;; Note pre-resolved expansion `(taoensso.timbre/-log! ~config ~level ~ns ~file ~line ~column ~msg-type ~?err - (delay ~vargs-form) ~?base-data ~callsite-id ~spying? ~(get opts :may-log?)))))) + (delay ~vargs-form) ~?base-data ~callsite-id ~spying? + ~(get opts :instant) + ~(get opts :may-log?)))))) ([level msg-type args & [opts]] (let [loc (enc/get-source &form &env) diff --git a/test/taoensso/timbre_tests.cljc b/test/taoensso/timbre_tests.cljc index 2754bde..7a538bc 100644 --- a/test/taoensso/timbre_tests.cljc +++ b/test/taoensso/timbre_tests.cljc @@ -1,10 +1,13 @@ (ns taoensso.timbre-tests (:require - [clojure.test :as test :refer [deftest testing is]] - #?(:clj [clojure.tools.logging :as ctl]) - [taoensso.encore :as enc :refer [throws? submap?] :rename {submap? sm?}] + [clojure.test :as test :refer [deftest testing is]] + [taoensso.encore :as enc :refer [throws? submap?] :rename {submap? sm?}] [taoensso.timbre :as timbre] - #?(:clj [taoensso.timbre.tools.logging :as ttl])) + + #?@(:clj + [[clojure.tools.logging :as ctl] + [taoensso.timbre.tools.logging :as ttl] + [taoensso.timbre.slf4j :as slf4j]])) #?(:cljs (:require-macros @@ -150,13 +153,50 @@ ;;;; Interop -#?(:clj (def dt-pred (enc/pred (fn [x] (instance? java.util.Date x))))) +(comment (def ^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "my.class"))) +#?(:clj (def dt-pred (enc/pred (fn [x] (instance? java.util.Date x))))) +(def ex1 (ex-info "Ex1" {})) +(def ex1-pred (enc/pred (fn [x] (= (enc/ex-root x) ex1)))) + #?(:clj (deftest _interop [(testing "tools.logging -> Timbre" (ttl/use-timbre) [ (is (sm? (log-data (ctl/info "a" "b" "c")) {:level :info, :?ns-str "taoensso.timbre-tests", :instant dt-pred, :msg_ "a b c"})) - (is (let [ex (ex-info "Ex" {})] (sm? (log-data (ctl/error ex "a" "b" "c")) {:level :error, :?ns-str "taoensso.timbre-tests", :instant dt-pred, :msg_ "a b c", :?err ex})))])])) + (is (let [ex (ex-info "Ex" {})] (sm? (log-data (ctl/error ex "a" "b" "c")) {:level :error, :?ns-str "taoensso.timbre-tests", :instant dt-pred, :msg_ "a b c", :?err ex})))]) + + (testing "SLF4J -> Timbre" + (let [^org.slf4j.Logger sl (org.slf4j.LoggerFactory/getLogger "my.class")] + [(testing "Basics" + [(is (sm? (log-data (.info sl "Hello")) {:level :info, :?ns-str "my.class", :msg_ "Hello", :instant dt-pred}) "Legacy API: info basics") + (is (sm? (log-data (.warn sl "Hello")) {:level :warn, :?ns-str "my.class", :msg_ "Hello", :instant dt-pred}) "Legacy API: warn basics") + (is (sm? (log-data (-> (.atInfo sl) (.log "Hello"))) {:level :info, :?ns-str "my.class", :msg_ "Hello", :instant dt-pred}) "Fluent API: info basics") + (is (sm? (log-data (-> (.atWarn sl) (.log "Hello"))) {:level :warn, :?ns-str "my.class", :msg_ "Hello", :instant dt-pred}) "Fluent API: warn basics")]) + + (testing "Message formatting" + (let [msgp "x={},y={}", expected {:msg_ "x=1,y=2", :slf4j/args ["1" "2"]}] + [(is (sm? (log-data (.info sl msgp "1" "2")) expected) "Legacy API: formatted message, raw args") + (is (sm? (log-data (-> (.atInfo sl) (.setMessage msgp) (.addArgument "1") (.addArgument "2") (.log))) expected) "Fluent API: formatted message, raw args")])) + + (is (sm? (log-data (-> (.atInfo sl) (.addKeyValue "k1" "v1") (.addKeyValue "k2" "v2") (.log))) {:slf4j/kvs {"k1" "v1", "k2" "v2"}}) "Fluent API: kvs") + + (testing "Markers" + (let [m1 (#'slf4j/est-marker! "M1") + m2 (#'slf4j/est-marker! "M2") + cm (#'slf4j/est-marker! "Compound" "M1" "M2")] + + [(is (sm? (log-data (.info sl cm "Hello")) {:slf4j/marker-names #{"Compound" "M1" "M2"}}) "Legacy API: markers") + (is (sm? (log-data (-> (.atInfo sl) (.addMarker m1) (.addMarker cm) (.log))) {:slf4j/marker-names #{"Compound" "M1" "M2"}}) "Fluent API: markers")])) + + (testing "Errors" + [(is (sm? (log-data (.warn sl "An error" ^Throwable ex1)) {:level :warn, :?err ex1-pred}) "Legacy API: errors") + (is (sm? (log-data (-> (.atWarn sl) (.setCause ex1) (.log))) {:level :warn, :?err ex1-pred}) "Fluent API: errors")]) + + (testing "MDC (Mapped Diagnostic Context)" + (with-open [_ (org.slf4j.MDC/putCloseable "k1" "v1")] + (with-open [_ (org.slf4j.MDC/putCloseable "k2" "v2")] + [(is (sm? (log-data (-> sl (.info "Hello"))) {:level :info, :slf4j/context {"k1" "v1", "k2" "v2"}}) "Legacy API: MDC") + (is (sm? (log-data (-> (.atInfo sl) (.log "Hello"))) {:level :info, :slf4j/context {"k1" "v1", "k2" "v2"}}) "Fluent API: MDC")])))]))])) ;;;;