Configures the logger system for Metabase. Sets up an in-memory logger in a ring buffer for showing in the UI. Other logging options are set in [[metabase.core.bootstrap]]: the context locator for log4j2 and ensuring log4j2 is the logger that clojure.tools.logging uses. | (ns metabase.logger
(:require
[amalloy.ring-buffer :refer [ring-buffer]]
[clj-time.coerce :as time.coerce]
[clj-time.format :as time.format]
^{:clj-kondo/ignore [:discouraged-namespace]}
[clojure.tools.logging :as log]
[clojure.tools.logging.impl :as log.impl]
[metabase.config :as config]
[metabase.plugins.classloader :as classloader])
(:import
(java.lang AutoCloseable)
(org.apache.commons.lang3.exception ExceptionUtils)
(org.apache.logging.log4j LogManager Level)
(org.apache.logging.log4j.core Appender LogEvent Logger LoggerContext)
(org.apache.logging.log4j.core.appender AbstractAppender FileAppender OutputStreamAppender)
(org.apache.logging.log4j.core.config AbstractConfiguration Configuration LoggerConfig))) |
(set! *warn-on-reflection* true) | |
(def ^:private ^:const max-log-entries 250) | |
(defonce ^:private messages* (atom (ring-buffer max-log-entries))) | |
Get the list of currently buffered log entries, from most-recent to oldest. | (defn messages [] (reverse (seq @messages*))) |
Elides the string to the specified length, adding '...' if it exceeds that length. | (defn- elide-string
[s max-length]
(if (> (count s) max-length)
(str (subs s 0 (- max-length 3)) "...")
s)) |
(defn- event->log-data [^LogEvent event]
{:timestamp (time.format/unparse (time.format/formatter :date-time)
(time.coerce/from-long (.getTimeMillis event)))
:level (.getLevel event)
:fqns (.getLoggerName event)
:msg (elide-string (str (.getMessage event)) 4000)
:exception (when-let [throwable (.getThrown event)]
(take 20 (map #(elide-string (str %) 500) (seq (ExceptionUtils/getStackFrames throwable)))))
:process_uuid config/local-process-uuid}) | |
(defn- metabase-appender ^Appender []
(let [^org.apache.logging.log4j.core.Filter filter nil
^org.apache.logging.log4j.core.Layout layout nil
^"[Lorg.apache.logging.log4j.core.config.Property;" properties nil]
(proxy [org.apache.logging.log4j.core.appender.AbstractAppender]
["metabase-appender" filter layout false properties]
(append [event]
(swap! messages* conj (event->log-data event))
nil)))) | |
(defonce ^:private has-added-appender? (atom false)) | |
Get global logging context. | (defn context ^LoggerContext [] (LogManager/getContext (classloader/the-classloader) false)) |
Get global logging configuration | (defn configuration ^Configuration [] (.getConfiguration (context))) |
(when-not *compile-files*
(when-not @has-added-appender?
(reset! has-added-appender? true)
(let [appender (metabase-appender)
config (configuration)]
(.start appender)
(.addAppender config appender)
(doseq [[_ ^LoggerConfig logger-config] (.getLoggers config)]
(.addAppender logger-config appender (.getLevel logger-config) (.getFilter logger-config))
(.updateLoggers (context)))))) | |
Custom loggers | |
Get string name from symbol or ns | (defn logger-name
^String [a-namespace]
(if (instance? clojure.lang.Namespace a-namespace)
(name (ns-name a-namespace))
(name a-namespace))) |
Is logging at | (defn level-enabled?
(^Boolean [level]
(level-enabled? *ns* level))
(^Boolean [a-namespace level]
(let [^Logger logger (log.impl/get-logger log/*logger-factory* a-namespace)]
(.isEnabled logger level)))) |
Get the logger that will be used for the namespace named by | (defn effective-ns-logger
^LoggerConfig [a-namespace]
(let [^Logger logger (log.impl/get-logger log/*logger-factory* a-namespace)]
(.get logger))) |
Find any logger with a specified layout. | (defn- find-logger-layout
[^LoggerConfig logger]
(when logger
(or (first (keep #(.getLayout ^AbstractAppender (val %)) (.getAppenders logger)))
(recur (.getParent logger))))) |
(defprotocol MakeAppender (make-appender ^AbstractAppender [out layout])) | |
(extend-protocol MakeAppender
java.io.File
(make-appender [^java.io.File out layout]
(.build
(doto (FileAppender/newBuilder)
(.setName "shared-appender-file")
(.setLayout layout)
(.withFileName (.getPath out)))))
java.io.OutputStream
(make-appender [^java.io.OutputStream out layout]
(.build
(doto (OutputStreamAppender/newBuilder)
(.setName "shared-appender-os")
(.setLayout layout)
(.setTarget out))))) | |
Add a logger for a given namespace to the configuration. | (defn add-ns-logger
[ns appender level additive]
(let [logger-name (str ns)
ns-logger (LoggerConfig. logger-name level additive)]
(.addAppender ns-logger appender level nil)
(.addLogger (configuration) logger-name ns-logger)
ns-logger)) |
Create separate logger for a given namespace(s) to fork out some logs. | (defn for-ns
^AutoCloseable [out nses & [{:keys [additive level]
:or {additive true
level Level/INFO}}]]
(let [nses (if (vector? nses) nses [nses])
config (configuration)
parents (mapv effective-ns-logger nses)
appender (make-appender out (find-logger-layout (first parents)))
loggers (vec (for [ns nses]
(add-ns-logger ns appender level additive)))]
(.start appender)
(.addAppender config appender)
(.updateLoggers (context))
(reify AutoCloseable
(close [_]
(let [^AbstractConfiguration config (configuration)]
(doseq [logger loggers]
(.removeLogger config (.getName ^LoggerConfig logger)))
(.stop appender)
;; this method is only present in AbstractConfiguration
(.removeAppender config (.getName appender))
(.updateLoggers (context))))))) |