I've just read https://juxt.pro/blog/posts/logging.html and I'm wondering whether people tend to pass logger instance as an explicit argument. I haven't seen that used much - it looks reasonable but I feel it unnecessarily bloats functions signatures.
I've seen it in some non-Clojure projects but I haven't yet seen any Clojure projects that pass a logger "object" via the argument chain (although perhaps there are Component-based projects where a logger subcomponent is part of the overall "system" being passed in?).
I like the clojure.tools.logging
approach: code that wants to log can require that just like any other namespace and then invoke the macros defined in it. The implementation used is controllable externally, via a JVM option, or you can accept the default based on what c.t.l finds on the classpath.
It's common in Java, I think in Clojure people tend to use macros that create a logger behind the scenes based on the namespace. That's also what pedestal.log and Glögi do, although they also allow specifying a specific logger. If you're using goog.log or one of the Java libs directly you'll have to create your own loggers, I've seen Figwheel do that.
Great, thanks for the info!
How do you deal with logging common "context" (like request-id)? With timbre, we use with-context
and wrap the handler invocation with it - something like this:
(log/with-context (add-user-data-to-context user-id {:request-id (generate-request-id)})
(handler request))
On the java level this is done through something called MDC (mapped diagnostic context). Pedestal-log has a with-context
macro as well, it uses this MDC under the hood. Not sure if Timbre does so as well.
not too familiar with the details of MDC, but it's basically a way of setting context like this on a per-thread basis which logging frameworks can then pick up
http://Aviso.io logging lib has a nice way of using the MDC with clojure.tools.logging: https://cljdoc.org/d/io.aviso/logging/0.3.2/api/io.aviso.logging.mdc
in general i would say you could do context like that with dynamic vars
that feels like what they are for
iff you need to roll something like that yourself
Interesting - I haven't used dynamic vars for anything beyond global config in 3rd party libs. Our request handlers/consumers always pass a context map so the MCD is set based on that
I remember reading about MDC when we first switched to log4j2 and thinking "Hmm, that might come in handy" but clearly I have since forgotten about it, so thanks for the reminder @jumar! The CloseableThreadContext
in log4j2 would work very nicely with Clojure's with-open
-- something like this:
(with-open [ctc (org.apache.logging.log4j.CloseableThreadContext/putAll {"map" "of", "clojure" "data"})]
(do-stuff-that-logs :things))
(as long as you have %X
in your log appender pattern)
Yup, MDC is just java working around the lack of binding
.
"Context" is also handy when emitting metrics.
It's mildly annoying that MDC is usually Map<String,String>
and not Map<String,Object>
since the latter would allow for more interesting options when emitting structured logs.
@conormcd Given that you'd have to stringize the keys in a Clojure map before calling .putAll()
, you might as well pr-str
(or just str
) the value as well...
Yeah, it's the decoding back to, say a number for JSON encoding for transport to a log collector that's irritating.
or use a json encoder for the data you'd put under the value
It's all fixable, just not nicely.
but that might not be as nice for your json logger API, right