Only used in tests and dev runs. Basic idea is we have a dynamic variable called [[capture-logs-fn]] with the signature (f namespace-str level-int) and if the logs should be captured at that level, it returns a function with the signature (f e message) that you should call with the logged exception (if any) and logged message to capture the message. Then the actual implementation can basically compose [[capture-logs-fn]] so you can have multiple functions capturing logs. The impl can store the logs in an atom or whatever that you can get later. | (ns metabase.util.log.capture
(:require
#?@(:cljs
[[goog.string :as gstring]])
[clojure.set :as set]
[clojure.spec.alpha :as s]
[clojure.string :as str])) |
Function with the signature that given a namespace string and log level (as an int), returns a function that should be used to capture a log message, if messages at that level should be captured. Its signature is (f namespace-str level-int) => (f e message) | (def ^:dynamic ^{:arglists '([namespace-str level-int])} *capture-logs-fn*
(constantly nil)) |
Code below converts log levels to integers right away, because a simple int comparison like | |
(def ^:private level->int
{:explode 0
:fatal 1
:error 2
:warn 3
:info 4
:debug 5
:trace 6
:whisper 7}) | |
(def ^:private int->level (set/map-invert level->int)) | |
(defn- capture-logs-fn [logs captured-namespace captured-level-int]
;; the prefix stuff is calculated outside of the function so we can be SUPER OPTIMIZED and not do anything
;; allocation in cases where we're not going to capture anything.
(let [captured-namespace-prefix (str captured-namespace \.)]
(fn [message-namespace message-level-int]
;; similarly, the level comparison is done first, because it's faster and will hopefully filter out a lot of
;; things we weren't going to capture anyway before we do the more expensive namespace check
(when (and (<= message-level-int captured-level-int)
(or (= message-namespace captured-namespace)
(str/starts-with? message-namespace captured-namespace-prefix)))
;; VERY IMPORTANT! Only return the capturing function if we actually want to capture a log message.
(fn capture-fn [e message]
(swap! logs conj {:namespace (symbol message-namespace)
:level (int->level message-level-int)
:e e
:message message})))))) | |
Impl for [[with-log-messages-for-level]]. | (defn do-with-log-messages-for-level
[captured-namespace captured-level-int f]
(let [logs (atom [])
old-capture-fn *capture-logs-fn*
capture-fn (capture-logs-fn logs captured-namespace captured-level-int)]
(binding [*capture-logs-fn* (fn composed-fn [a-message message-level-int]
(let [f1 (old-capture-fn a-message message-level-int)
f2 (capture-fn a-message message-level-int)]
(cond
(and f1 f2) (fn [e message]
(f1 e message)
(f2 e message))
f1 f1
f2 f2)))]
(f (fn [] @logs))))) |
(s/def ::namespace (some-fn symbol? string?)) | |
(s/def ::level
#{:explode :fatal :error :warn :info :debug :trace :whisper}) | |
(s/def ::with-log-messages-for-level-args
(s/cat :bindings (s/spec (s/+ (s/cat :messages-fn-binding symbol?
:ns-level (s/or :ns-level (s/spec (s/cat :ns-str ::namespace
:level ::level))
:ns ::namespace
:level ::level))))
:body (s/+ any?))) | |
Capture log messages at a given level in a given namespace and all 'child' namespaces inside Captured logs can be accessed by invoking
A code example is worth a thousand-word docstring, so here is an example (log.capture/with-log-messages-for-level [messages [metabase.util.log.capture-test :trace]] (is (= [] (messages))) (log/trace "message") (is (= [{:namespace 'metabase.util.log.capture-test :level :trace :e nil :message "message"}] (messages)))) See [[metabase.util.log.capture-test]] for more example usages. You can pass multiple bindings to this macro without them affecting one another, regardless of whether the things they capture overlap or not. See [[metabase.util.log.capture-test/multiple-captures-test]] for an example. | (defmacro with-log-messages-for-level
{:arglists '([[messages-fn-binding ns-level & more-bindings] & body])}
[& args]
(let [{:keys [bindings body]} (s/conform ::with-log-messages-for-level-args args)]
(reduce
(fn [form bindings]
(let [{:keys [messages-fn-binding ns-level]} bindings
[ns-level-type ns-level] ns-level
{:keys [ns-str level]} (case ns-level-type
:ns-level ns-level
:ns {:ns-str ns-level, :level :trace}
:level {:ns-str "metabase", :level ns-level})]
`(do-with-log-messages-for-level ~(str ns-str) ~(level->int level) (fn [~messages-fn-binding] ~form))))
`(do ~@body)
bindings))) |
(s/fdef with-log-messages-for-level :args ::with-log-messages-for-level-args :ret any?) | |
The macroexpansion of something like (log/trace "a picture") becomes (do (metabase.util.log.capture/capture-logp "metabase.util.log.capture-test" :trace "a picture") ;; clojure.tools.logging stuff ...) becomes (do (when-let [f# (metabase.util.log.capture/capture-logs-fn "metabase.util.log.capture-test" 5)] (metabase.util.log.capture/capture-logp! f# "a picture")) ...) VERY VERY IMPORTANT! [[capture-logs-fn]] only returns a function if logs for that namespace AND level are to be
captured! Only then is [[capture-logp!]] or [[capture-logf!]], which ultimately call the returned function | |
(defn- parse-args [[x & more]]
(if (instance? #?(:clj Throwable :cljs js/Error) x)
{:e x, :args more}
{:args (cons x more)})) | |
Impl for log message capturing for [[metabase.util.log/logp]]. | (defn capture-logp!
[f & args]
(let [{:keys [e args]} (parse-args args)]
(f e (str/join \space (map print-str args))))) |
Impl for log message capturing for [[metabase.util.log/logf]]. | (defn capture-logf!
[f & args]
(let [{e :e, [format-string & args] :args} (parse-args args)]
#_{:clj-kondo/ignore [:unresolved-namespace]}
(f e (apply #?(:clj format
:cljs gstring/format)
format-string args)))) |
Impl for log message capturing for [[metabase.util.log/logp]]. | (defmacro capture-logp
[namespace-str level & args]
`(when-let [f# (*capture-logs-fn* ~namespace-str ~(level->int level))]
(capture-logp! f# ~@args))) |
Impl for log message capturing for [[metabase.util.log/logf]]. | (defmacro capture-logf
[namespace-str level & args]
`(when-let [f# (*capture-logs-fn* ~namespace-str ~(level->int level))]
(capture-logf! f# ~@args))) |