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 "1.0.3"}
This version of the library depends on log4j2 2.25.0, which is the latest stable release as of June 2025.
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
accepts 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 provide a (fn [] ...)
which
will be used as a MessageSupplier
object:
(logger/info #(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.
Log4j2 is very flexible and can be confgured via XML, JSON, YAML, or properties files. See the log4j2 configuration documentation.
The configuration file can either be on the classpath (e.g., in your resources
folder), or you can specify the name/location using the log4j2.configurationFile
JVM property or the LOG4J_CONFIGURATION_FILE
environment variable.
I prefer configuration via properties files, so I usually create typically have
something like this in resources/log4j2.properties
:
rootLogger.level = info
rootLogger.appenderRef.stdout.ref = STDOUT
rootLogger.appenderRef.file.ref = logfile
appender.console.type = Console
appender.console.name = STDOUT
appender.console.filter.threshold.type = ThresholdFilter
appender.console.filter.threshold.level = info
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%c] %x %X %highlight{%m}%n
For development, I usually have a second properties file that is in a dev-only
directory (added to the classpath via an alias), called log4j2-test.properties
:
# so that log4j2 checks every 30 seconds for changes in configuration:
monitorInterval = 30
rootLogger.level = debug
rootLogger.appenderRef.stdout.ref = STDOUT
rootLogger.appenderRef.file.ref = logfile
appender.console.type = Console
appender.console.name = STDOUT
appender.console.filter.threshold.type = ThresholdFilter
appender.console.filter.threshold.level = debug
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%c] %x %X %highlight{%m}%n
You may also want to configure the log level threshold higher for some libraries
that are very chatty at info
or debug
:
# Chatty libraries we need to suppress:
logger.sshj.name = net.schmizz.sshj
logger.sshj.level = warn
logger.hikari.name = com.zaxxer.hikari
logger.hikari.level = warn
logger.authnet.name = net.authorize
logger.authnet.level = warn
logger.eclipse1.name = com.eclipse
logger.eclipse1.level = warn
logger.eclipse2.name = org.eclipse
logger.eclipse2.level = warn
java.util.logging
BridgeIn order to correctly bridge java.util.logging
(JUL) to log4j2, you need to
provide some sort of configuration. Several options are described in the
JUL-to-Log4j bridge docs,
which also explains the limitations imposed by JUL that make this necessary.
The simplest approach is to set the java.util.logging.manager
JVM property.
If you can guarantee that no logging is done before your application gets
control (in your -main
function), then you can set this property in code:
(System/setProperty "java.util.logging.manager"
"org.apache.logging.log4j.jul.LogManager")
You can put this as a top-level form in your main namespace, e.g., immediately
after the ns
form.
If your code is running in a context that may initialize JUL before your application gets control (e.g., a servlet container), then you need to set this property as a JVM property when starting the JVM, e.g.,
java -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-jar your.jar
If you are using the Clojure CLI, you can either set it directly or via an
alias. Setting it directly using the CLI's -J
option:
clojure -J-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-M -m your.namespace
Setting it via an alias in deps.edn
:
{:aliases
{:jul
{:jvm-opts ["-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager"]}}}
Then you can run your code with the alias:
clojure -M:jul -m your.namespace
If you configure logging levels for log4j2 and they do not seem to work as
expected, it's possible that you may be running into this
log4j-jul issue,
where some code is setting the JUL logging level programmatically and
overriding your configuration. With log4j2 2.24.0 and later, this should
not be a problem, but if you want that programmatic setting to be effective,
i.e., to override your configuration, you can set the log4j2.julLoggerAdapter
JVM property to org.apache.logging.log4j.jul.CoreLoggerAdapter
(which
was the default behavior in log4j2 2.23.0).
As above, you can set this via the CLI's -J
option or via an alias in
deps.edn
, or directly for the java
command:
java -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Dlog4j2.julLoggerAdapter=org.apache.logging.log4j.jul.CoreLoggerAdapter \
-jar your.jar
tools.build
If you are using tools.build
(build.clj
) to build uberjars,
and you are using either this library or using log4j2 directly,
you probably need to look at this
log4j2 conflict handler for tools.build
.
If your project has dependencies that provide log4j2 plugins
(such as templates), using the conflict handler will ensure
that those plugins have their configuration merged correctly.
clojure.tools.logging
Migration/FAQc.t.l has logf
but this library does not. For most uses (with %s
), you
can use a ParameterizedMessage
-- replace %s
with {}
and you
should get the same behavior. For more complex cases, you can use
clojure.core/format
with the arguments to build a string for logging.
You will no longer need -Dclojure.tools.logging.factory=clojure.tools.logging.impl/log4j2-factory
as this library uses log4j2 directly (c.t.l defaults to slf4j
).
If you want to get a Logger
object directly, you can use
org.apache.logging.log4j.LogManager/getLogger
, passing a namespace
name as a string. c.t.l provided a generic get-logger
in its impl
namespace.
Copyright © 2024-2025 Sean Corfield.
Distributed under the Eclipse Public License version 1.0.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close