A Clojure wrapper for log4j2, intended as a partial replacement for
clojure.tools.logging
, that
supports MDC (mapped diagnostic context), NDC (nested diagnostic context)
and markers directly, in a Clojure-friendly manner.
This library explicitly depends on log4j2 and all of the bridge libraries that route other logging frameworks to log4j2 (jcl, jul, log4j 1.x, slf4j 1.x and 2.x).
Note: requires Clojure 1.11 or later.
Add the following dependency to your deps.edn
file:
com.github.seancorfield/logging4j2 {:mvn/version "0.1.0-SNAPSHOT"}
Note: this library is a work in progress -- feedback is appreciated!
Require the main org.corfield.logging4j2
namespace (:as
whatever alias
you prefer -- the examples below use logger
):
The library provides the following macros that you can use to log information:
trace
, debug
, info
, warn
, error
, and fatal
. There is also a
generic log
macro that accepts a level keyword as its first argument.
(logger/info "Hello, world!")
(logger/log :info "Hello, world!") ; equivalent to the above
Support for MDC and NDC is provided via the with-log-context
, with-log-tag
,
and with-log-uuid
macros (see MDC and NDC below for more detail):
with-log-context
accepts a hash map as its first argument, followed by a
body of code to execute. The keys and values in the hash map are added to the
context as strings, for the execution of the body.with-log-tag
accepts a keyword or string as its first argument, followed
by a body of code to execute. The tag is pushed onto the context, for
the execution of the body.with-log-uuid
a body of code to execute. A unique tag is pushed onto the
context, for the execution of the body.(logger/with-log-context {:uid (:id user)}
(logger/info "Hello, world!")) ; INFO {uid=1234} Hello, world!
(logger/with-log-tag (str "user_" (:id user))
(logger/info "Hello, world!")) ; INFO [user_1234] Hello, world!
(logger/with-log-uuid
(logger/info "Hello, world!")) ; is equivalent to
(logger/with-log-tag (str (random-uuid))
(logger/info "Hello, world!")) ; INFO [8b21769c-33c5-42cb-b6c4-146ce8bb875f] Hello, world!
Support for markers is provided by the as-marker
function, which accepts
one or more keywords or strings and returns a marker object that can be passed
as the first argument to any of the logging macros (or the second argument to
the generic log
macro).
(as-marker :sql-update :sql)
returns a marker object that represents the
string "SQL_UPDATE"
with the marker "SQL"
as its parent.
Most logging will look like this:
(logger/info "some message")
;; or
(logger/info my-exception "some message")
;; or
(logger/info my-marker "some message")
;; or
(logger/info my-marker my-exception "some message")
If multiple message arguments are provided, they are generally converted to strings and concatenated with spaces between them, except as follows:
{}
placeholders, then the remaining arguments are treated as values for
those placeholders and a
ParameterizedMessage
is constructed.MapMessage
is constructed, with the keys of the Clojure hash map converted to strings and
the values left as-is.You can build a Message
directly with logger/as-message
:
(logger/as-message "Hello, {}!" "Parameter")
;; => ParameterizedMessage
(logger/as-message {:hello "world"})
;; => MapMessage
(logger/as-message "Hello," "World!")
;; => SimpleMessage
If you are using Clojure 1.12 (or later), you can prove a (fn [] ...)
which
will be used as a MessageSupplier
object:
(logger/info (fn [] (logger/as-message "Hello, Supplier!")))
On Clojure 1.11, you have to construct the MessageSupplier
object directly,
for example by using reify
to implement the MessageSupplier
interface
and provide the get
method:
(logger/info (reify org.apache.logging.log4j.message.MessageSupplier
(get [_] (logger/as-message "Hello, Supplier!"))))
Mapped Diagnostic Context (MDC) and Nested Diagnostic Context (NDC) are supported
(as noted above) by the three with-log-*
macros. Nested calls to these macros
will accumulate the map context and the stack of tags automatically, within
the current thread.
The underlying context for log4j2 is thread-local by default: each thread
starts out with an empty context. This library also tracks MDC and NDC using
a dynamic var in Clojure, so you can use with-log-inherited
inside a spawned
thread to inherit the MDC and NDC from the parent thread.
(logger/with-log-context {:uid (:id user)}
(future
(logger/with-log-inherited
(logger/info "Hello, world!")))) ; INFO {uid=1234} Hello, world!
If you're passing functions between threads, you may need to use bound-fn
or bound-fn*
in order to convey the dynamic context into the new thread.
Note:
with-log-inherited
will inherit the entire MDC/NDC from the dynamic parent context, even if there are intervening calls towith-log-context
,with-log-tag
, orwith-log-uuid
in the child thread, that did not inherit the context.
Copyright © 2024 Sean Corfield.
Distributed under the Eclipse Public License version 1.0.