Home
Logging from Clojure with log4j2
I’ve been experimenting with Clojure recently and I’m a huge fan so far, it reminds me of days long since passed learning Scheme. There’s a floppy disk somewhere in VA containing my Scheme elevator controller GUI written awhile back using a random IDE called Dr. Scheme, which is singlehandedly the most unassuming yet epic IDE to ever exist. Most everyone using Clojure appears to be using Leiningen to manage project builds/dependencies/etc., which initially I had my doubts about because it seemed like a needless abstraction. I changed my mind about that when I realized all the work that goes on behind the scenes in order to run Clojure projects on the JVM.
After I got a basic Hello World working, I struggled too long to find a way to use log4j2 from my Clojure files. There’s alot of info out there about how to use lo4j1.x, but I could find no one using log4j2 with Clojure. I’m slowly beginning to migrate parts of the OpenPplTools codebase from Java to Clojure, and I want to keep log4j2 around, mainly because my config file won’t work with 1.x.
The steps I took to use log4j2 are below. This is basic Clojure/lein 101, nothing crazy, I’m just putting this out there since there seems to be nothing (as of this writing) talking about log4j2 and Clojure.
Add log4j2 Dependencies to Your project.clj
There seem to be several logging abstractions available for Clojure projects, but I could get none of them to work with log4j2. I tried adding the “clojure/tools.logging” dependency to my project, but it would only work with log4j1.x. At that point, I gave up on finding a convenient abstraction/macro dependency and settled for simply being able to call log4j2 from .clj files in my project.
First, add the log4j-api
and log4j-core
artifacts as dependencies to your project.clj
file; my complete :dependencies entry looks like the following:
Excerpt of project.clj
:dependencies [[org.clojure/clojure "1.5.1"]
[org.apache.logging.log4j/log4j-api "2.0-beta9"]
[org.apache.logging.log4j/log4j-core "2.0-beta9"]]
As of this writing, the latest release of log4j2 is 2.0-beta9. If it’s been awhile since this was written, search for “log4j” at the Maven Central Repository and use the latest GroupId/ArtifactId and Latest Version values (lein uses Maven to manage dependencies).
You should be able to run lein deps
and see lein pull in the appropriate JARs, although I believe you can skip this step, as lein will pull them in the next time you call lein run
.
Calling log4j2 from Clojure
I wrote the following basic program to call log4j2:
src/clj/main.clj
(ns main
(import org.apache.logging.log4j.Logger)
(import org.apache.logging.log4j.LogManager))
(defn -main [& args]
(def log (. LogManager getLogger "main"))
(. log info "START In main...")
(. log warn "END In main...")
)
I’ll likely end up writing a wrapper for this since Java interop within Clojure leads to code that’s not really all that idiomatic/appealing. For now it works. After making the following modifications to my project.clj
file:
Excerpt of project.clj
:source-paths ["src/clj"]
:java-source-paths ["src/java"] ;; only necessary if you need Java interop
:main main ;; matches my use of "ns main" in main.clj above
I could run lein run
and successfully view log4j2 output for the INFO and WARN statements emitted in the code.
Configuring Log4j2
One other thing that took me too long to get was that my log4j2.xml configuration file needed to be on the classpath defined by/in lein; you can view that classpath using lein classpath
. The resources/
directory within my project directory is in that classpath, so after placing log4j2.xml in resources/
(note: it must be named log4j2.xml for it to be picked up by log4j2 at runtime, otherwise you’ll have to pass an argument to the JVM), I created a custom Appender for use from my Clojure files that excluded the %t
conversion specifier; this specifier outputs the name of the thread that generates each logging event present in the output, which will vary depending upon whether log4j2 is being called from within the context of the REPL (lein repl
) or lein run
, so I just took it out to clean things up:
Excerpt of resources/log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
<Appenders>
...excluding other appenders...
<Console name="Console-Clojure" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level ns:%logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="all">
<AppenderRef ref="Console-Less"/>
</Root>
<!-- CLOJURE LOGGERS -->
<Logger name="main" level="INFO" additivity="false">
<AppenderRef ref="Console-Clojure"/>
</Logger>
<!-- JAVA LOGGERS -->
...excluding filters for Java-based classes...
</Loggers>
</Configuration>
Calling lein run
again resulted in use of the new format, and at this point everything was good to go for me:
STDOUT from lein run
:
mquinn@evm:~/ops/src/clj$ lein run
00:59:39.449 INFO ns:main - START In main...
00:59:39.450 WARN ns:main - END In main...
mquinn@evm:~/ops/src/clj$
Note Regarding Logger Names
Typically, when using log4j2 within Java classes, you’ll give each class requiring logging facilities a static Logger object that’s named with the (package-prefixed) name of the class containing it:
package com.example.pkg;
...
// this Logger will be named "com.example.pkg.Main"
private static Logger log = LogManager.getLogger(Main.class.getName());
There’s not really an equivalent to this practice in Clojure, at least that I’ve found, given the fact that classes (as they exist in Java) do not exist in Closure. Right now I’ve settled for hard-coding the name of the namespace as a string and passing that to getLogger(<logger-name>)
, although in all likelihood there’s probably a better way to do this.
02-27-2014 Edit
There is indeed a better way to do that. Hard-coding the namespace as a string is not necessary; passing it to the str
form and using that as the Logger name is much cleaner. This can be wrapped up in a separate logging namespace, i.e.:
(ns runtime.log
(import org.apache.logging.log4j.Logger)
(import org.apache.logging.log4j.LogManager)
(:gen-class))
(defn get-log
[ns]
(. LogManager getLogger (str *ns*))
)
(defn INFO
([log msg] (INFO log msg (into-array [])))
([log msg placeholder-array]
(. log info msg placeholder-array))
)
(defn DEBUG
([log msg] (DEBUG log msg (into-array [])))
([log msg placeholder-array]
(. log debug msg placeholder-array))
)
and then called via other namespaces like so:
(ns main
(:use [runtime.log :only [get-log INFO DEBUG]])
(:gen-class))
(def ^{:private true} log (get-log *ns*))
(defn -main [& args]
(INFO log "main starting...")
)
11-08-2014 Edit
Sebastian Hennebrueder emailed me to tell me that you can use tools logging without any changes via slf4j to log4j2 by adding the following dependencies to your project.clj
file:
[org.clojure/tools.logging "0.3.1"]
[org.apache.logging.log4j/log4j-slf4j-impl "2.0.2"]
[org.apache.logging.log4j/log4j-core "2.0.2"]