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.
This commit is contained in:
Yogthos 2026-06-13 14:58:47 -04:00
parent 6abcb11e92
commit e2d33df484
2 changed files with 123 additions and 0 deletions

View file

@ -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#)))

View file

@ -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)