Skip to content

Instantly share code, notes, and snippets.

@opqdonut
Created June 1, 2022 11:31
Show Gist options
  • Save opqdonut/442a52464e76f376e2002567f0dbed29 to your computer and use it in GitHub Desktop.
Save opqdonut/442a52464e76f376e2002567f0dbed29 to your computer and use it in GitHub Desktop.
Clojure structured logging via Log4J2
{:deps {org.apache.logging.log4j/log4j-api {:mvn/version "2.17.2"}
org.apache.logging.log4j/log4j-core {:mvn/version "2.17.2"}
org.apache.logging.log4j/log4j-layout-template-json {:mvn/version "2.17.2"}
org.apache.logging.log4j/log4j-slf4j18-impl {:mvn/version "2.17.2"}}}
(ns myproj.log
"Structured JSON logging via Log4J2. See resources/log4j2.xml for config."
(:import [org.apache.logging.log4j LogManager Logger Level]
[org.apache.logging.log4j.message MapMessage]))
(defn get-level [kw]
(case kw
:info Level/INFO
:warn Level/WARN
:error Level/ERROR))
;; We need careful type hinting here to avoid reflection for the .log
;; call. Otherwise the source location of the log line is
;; clojure.lang.Reflector.invokeMatchingMethod instead of the actual
;; calling function. Likewise, we need the info/warn/error defs to be
;; macros that expand to actual calls to .log. If we wrap .log in a
;; helper function, that function will show up as the source of the
;; log line.
(defn- log-impl [level message payload throwable]
(if throwable
`(.log ^Logger (LogManager/getLogger ~(str *ns*))
^Level (get-level ~level)
(MapMessage. (assoc ~payload "message" ~message))
^Throwable ~throwable)
`(.log ^Logger (LogManager/getLogger ~(str *ns*))
^Level (get-level ~level)
(MapMessage. (assoc ~payload "message" ~message)))))
(defmacro info [message payload & [throwable]]
(log-impl :info message payload throwable))
(defmacro warn [message payload & [throwable]]
(log-impl :warn message payload throwable))
(defmacro error [message payload & [throwable]]
(log-impl :error message payload throwable))
(defn- logging-demo []
(info "hello!" {:a 1 :foo "boing"})
(info "new customer" {:address {:number 1 :street "Mannerheimintie"} :name "Pekka Pekkarinen"})
(warn "unserializable" {:a (Object.)})
(error "an exception!" {:meta "data"} (Error. "BOOM")))
(comment
(logging-demo))
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="1">
<Appenders>
<!-- Separate appenders for other Java logs and our logs.
Will allow fine-tuning the format of application logs in the future. -->
<Console name="gcp_default" target="SYSTEM_OUT" follow="true">
<JsonTemplateLayout eventTemplateUri="classpath:GcpLayout.json" locationInfoEnabled="true" />
</Console>
<Console name="gcp_custom" target="SYSTEM_OUT" follow="true">
<JsonTemplateLayout eventTemplateUri="classpath:GcpLayout.json" locationInfoEnabled="true" >
<!-- Log structured MapMessages as JSON directly under the "message" key
instead of rendering them as key=value strings -->
<EventTemplateAdditionalField key="message" format="JSON" value='{"$resolver":"message"}'/>
</JsonTemplateLayout>
</Console>
</Appenders>
<Loggers>
<!-- Logging from Java libraries, either directly or via SLF4J -->
<Root level="info" includeLocation="true">
<AppenderRef ref="gcp_default"/>
</Root>
<!-- Grab all log messages from an.* namespaces -->
<Logger name="myproj" level="info" additivity="false">
<AppenderRef ref="gcp_custom"/>
</Logger>
<!-- REPL logging might originate from the user namespace -->
<Logger name="user" level="info" additivity="false">
<AppenderRef ref="gcp_custom"/>
</Logger>
</Loggers>
</Configuration>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment