Skip to content

Commit

Permalink
[new] Add SLF4Jv2 backend/provider
Browse files Browse the repository at this point in the history
Adapted from Telemere
  • Loading branch information
ptaoussanis committed Aug 29, 2024
1 parent ae5d0d8 commit 6b60f38
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 17 deletions.
5 changes: 3 additions & 2 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions slf4j/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
pom.xml*
.lein*
.nrepl-port
*.jar
*.class
.env
.DS_Store
/lib/
/classes/
/target/
/checkouts/
/logs/
/.clj-kondo/.cache
.idea/
*.iml
29 changes: 29 additions & 0 deletions slf4j/project.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
(defproject com.taoensso/timbre-slf4j "6.6.0-SNAPSHOT"
:author "Peter Taoussanis <https://www.taoensso.com>"
: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"]]})
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.taoensso.timbre.slf4j.TimbreServiceProvider
78 changes: 78 additions & 0 deletions slf4j/src/java/com/taoensso/timbre/slf4j/TimbreLogger.java
Original file line number Diff line number Diff line change
@@ -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
}

}
54 changes: 54 additions & 0 deletions slf4j/src/java/com/taoensso/timbre/slf4j/TimbreLoggerFactory.java
Original file line number Diff line number Diff line change
@@ -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.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<String, Logger> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}

}
143 changes: 143 additions & 0 deletions slf4j/src/taoensso/timbre/slf4j.clj
Original file line number Diff line number Diff line change
@@ -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 #{<MarkerName>}. 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")))
Loading

0 comments on commit 6b60f38

Please sign in to comment.