From e2d33df4840203873c4d607ccdca5159c49815b7 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sat, 13 Jun 2026 14:58:47 -0400 Subject: [PATCH] feat: clojure.tools.logging shim for migratus (jolt-nzg) Faithful shim of the real clojure/tools.logging public API rather than a bespoke logger. clojure.tools.logging.impl provides the Logger/LoggerFactory protocols (matching upstream signatures) plus a jolt stderr-backed factory; find-factory returns it instead of probing slf4j/log4j/jul. The macro surface (logp/logf, level macros + f-variants, log, enabled?, spy) dispatches through those protocols. Departures forced by no-JVM: log* writes directly (no agent/ LockingTransaction), and the throwable-first-arg branch is dropped since (instance? Throwable x) is always false for jolt exception values. Workarounds for two jolt gaps found en route (filed as jolt-9av, jolt-6ym): macro bodies fully-qualify impl refs because syntax-quote does not resolve ns aliases, and def docstrings are kept in comments because (def name doc value) is mishandled. stderr via __eprint. trace/debug suppressed by default (impl/*level* :info). Conformance 335/335 x3 modes. --- src/jolt/clojure/tools/logging.clj | 73 +++++++++++++++++++++++++ src/jolt/clojure/tools/logging/impl.clj | 50 +++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 src/jolt/clojure/tools/logging.clj create mode 100644 src/jolt/clojure/tools/logging/impl.clj diff --git a/src/jolt/clojure/tools/logging.clj b/src/jolt/clojure/tools/logging.clj new file mode 100644 index 0000000..9709e93 --- /dev/null +++ b/src/jolt/clojure/tools/logging.clj @@ -0,0 +1,73 @@ +;; clojure.tools.logging — jolt shim of the public API. +;; +;; Faithful to the upstream macro surface (logp/logf, the level macros and their +;; f-variants, log, enabled?, spy), dispatching through the Logger/LoggerFactory +;; protocols in clojure.tools.logging.impl. Two deliberate departures from +;; upstream, both forced by jolt not being a JVM: +;; +;; - log* writes directly. The real log* may hand off to an agent when inside a +;; running clojure.lang.LockingTransaction; jolt has neither agents nor STM. +;; - The throwable-first-argument branch is dropped. Upstream splits args on +;; (instance? Throwable x); on jolt that is always false for exception values, +;; so the would-be throwable simply becomes part of the printed message. This +;; also avoids the double-evaluation the branch would otherwise cause here. +(ns clojure.tools.logging + (:require [clojure.tools.logging.impl :as impl])) + +;; The LoggerFactory used to obtain loggers. Defaults to the jolt stderr +;; factory; rebind to plug in a different backend. +(def ^:dynamic *logger-factory* (impl/find-factory)) + +(defn log* + "Write a log entry through the logger. Direct, unlike upstream's agent path." + [logger level throwable message] + (impl/write! logger level throwable message) + nil) + +(defmacro logp + "Log a message built by print-str of args, at the given level. Args are + evaluated only when the level is enabled." + [level & args] + `(let [logger# (clojure.tools.logging.impl/get-logger *logger-factory* ~(str *ns*))] + (when (clojure.tools.logging.impl/enabled? logger# ~level) + (log* logger# ~level nil (print-str ~@args))))) + +(defmacro logf + "Log a clojure.core/format message at the given level. Args are evaluated + only when the level is enabled." + [level & args] + `(let [logger# (clojure.tools.logging.impl/get-logger *logger-factory* ~(str *ns*))] + (when (clojure.tools.logging.impl/enabled? logger# ~level) + (log* logger# ~level nil (format ~@args))))) + +(defmacro log + ([level message] `(logp ~level ~message)) + ([level throwable message] `(logp ~level ~throwable ~message))) + +(defmacro trace [& args] `(logp :trace ~@args)) +(defmacro debug [& args] `(logp :debug ~@args)) +(defmacro info [& args] `(logp :info ~@args)) +(defmacro warn [& args] `(logp :warn ~@args)) +(defmacro error [& args] `(logp :error ~@args)) +(defmacro fatal [& args] `(logp :fatal ~@args)) + +(defmacro tracef [& args] `(logf :trace ~@args)) +(defmacro debugf [& args] `(logf :debug ~@args)) +(defmacro infof [& args] `(logf :info ~@args)) +(defmacro warnf [& args] `(logf :warn ~@args)) +(defmacro errorf [& args] `(logf :error ~@args)) +(defmacro fatalf [& args] `(logf :fatal ~@args)) + +(defmacro enabled? + "True if the given level is enabled for the (optionally given) logger ns." + ([level] `(clojure.tools.logging.impl/enabled? (clojure.tools.logging.impl/get-logger *logger-factory* ~(str *ns*)) ~level)) + ([level log-ns] `(clojure.tools.logging.impl/enabled? (clojure.tools.logging.impl/get-logger *logger-factory* ~log-ns) ~level))) + +(defmacro spy + "Evaluate expr, log it at the given level (default :debug) as expr => value, + and return the value." + ([expr] `(spy :debug ~expr)) + ([level expr] + `(let [a# ~expr] + (logp ~level (print-str '~expr "=>" (pr-str a#))) + a#))) diff --git a/src/jolt/clojure/tools/logging/impl.clj b/src/jolt/clojure/tools/logging/impl.clj new file mode 100644 index 0000000..41efb8a --- /dev/null +++ b/src/jolt/clojure/tools/logging/impl.clj @@ -0,0 +1,50 @@ +;; clojure.tools.logging.impl — jolt shim. +;; +;; The real impl probes slf4j / log4j / log4j2 / commons-logging / java.util.logging +;; via Class/forName and reflection. jolt has no JVM, so find-factory returns a +;; native stderr-backed factory instead. The Logger and LoggerFactory protocols +;; match the real signatures, so consumers (and any custom factory bound through +;; clojure.tools.logging/*logger-factory*) work unchanged. +(ns clojure.tools.logging.impl) + +(defprotocol Logger + (enabled? [logger level]) + (write! [logger level throwable message])) + +(defprotocol LoggerFactory + (name [factory]) + (get-logger [factory logger-ns])) + +;; Minimum level the default jolt stderr logger emits. trace and debug are +;; suppressed by default; bind or alter-var-root this to change the threshold. +(def ^:dynamic *level* :info) + +(def ^:private level-order + {:trace 0 :debug 1 :info 2 :warn 3 :error 4 :fatal 5}) + +;; The protocol method `name` shadows clojure.core/name in this namespace, so a +;; keyword->label uses an explicit table rather than (name level). +(defn- level-str [level] + (case level + :trace "TRACE" :debug "DEBUG" :info "INFO" + :warn "WARN" :error "ERROR" :fatal "FATAL" + (str level))) + +(defn- stderr-logger [logger-ns] + (reify Logger + (enabled? [_ level] + (>= (get level-order level 2) (get level-order *level* 2))) + (write! [_ level throwable message] + (__eprint + (str (level-str level) " " logger-ns " - " message + (when throwable (str " " throwable))))))) + +(def ^:private jolt-stderr-factory + (reify LoggerFactory + (name [_] "jolt/stderr") + (get-logger [_ logger-ns] (stderr-logger (str logger-ns))))) + +(defn find-factory + "jolt has no JVM logging backends; return the native stderr factory." + [] + jolt-stderr-factory)