From ee855dbbc2f164f763c48bc60314b06fcab4c81f Mon Sep 17 00:00:00 2001 From: Alexander Taggart Date: Thu, 6 Feb 2020 09:26:15 -0400 Subject: [PATCH] Add support for logging structured data --- CHANGELOG.md | 3 + src/main/clojure/clojure/tools/logging.clj | 62 +++++++++++++++++++ .../clojure/clojure/tools/logging/impl.clj | 52 +++++++++------- 3 files changed, 96 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88cbda4..621732d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ([despite its flaws](https://www.youtube.com/watch?v=oyLBGkS5ICk)). ## [Unreleased] +### Added +- Add support for structured logging via `logd` and level-specific convenience + macros, e.g., `debugd`. ## [0.5.0] - 2019-07-22 ### Added diff --git a/src/main/clojure/clojure/tools/logging.clj b/src/main/clojure/clojure/tools/logging.clj index 93ad9f6..482ea1f 100644 --- a/src/main/clojure/clojure/tools/logging.clj +++ b/src/main/clojure/clojure/tools/logging.clj @@ -108,6 +108,32 @@ (log* logger# ~level x# (format ~@more)) (log* logger# ~level nil (format x# ~@more)))))))) +(defmacro logd + "Logs a message composed of a text description and a data map to support + structured logging. The text description must be a literal string, with all + dynamic information placed in the data map. Can optionally take a throwable as + its second arg. See level-specific macros, e.g., debugd. + + Implementation note: The text and data parts are used to create an instance of + StructuredMessage, which is then passed as the message arg to Logger/write!. + By default, all provided Logger implementations in clojure.tools.logging.impl + will assoc the text to the data at the ::description key, then pass that map + to the underlying logging implementation. Use custom Loggers to do more + interesting things with the StructuredMessage." + {:arglists '([level description data] [level throwable fmt & fmt-args])} + ([level description data] + `(logd level nil description data)) + ([level throwable description data] + (when-not (string? description) + (throw (IllegalArgumentException. "The description must be a literal string."))) + `(let [logger# (impl/get-logger *logger-factory* ~*ns*)] + (if (impl/enabled? logger# ~level) + (let [description# ~description + data# ~data + throwable# ~throwable + msg# (impl/->StructuredMessage description# data#)] + (log* logger# ~level throwable# msg#)))))) + (defmacro enabled? "Returns true if the specific logging level is enabled. Use of this macro should only be necessary if one needs to execute alternate code paths beyond @@ -247,6 +273,42 @@ [& args] `(logp :fatal ~@args)) +(defmacro traced + "Trace level logging using structured data." + {:arglists '([description data] [throwable description data])} + [& args] + `(logd :trace ~@args)) + +(defmacro debugd + "Debug level logging using structured data." + {:arglists '([description data] [throwable description data])} + [& args] + `(logd :debug ~@args)) + +(defmacro infod + "Info level logging using structured data." + {:arglists '([description data] [throwable description data])} + [& args] + `(logd :info ~@args)) + +(defmacro warnd + "Warn level logging using structured data." + {:arglists '([description data] [throwable description data])} + [& args] + `(logd :warn ~@args)) + +(defmacro errord + "Error level logging using structured data." + {:arglists '([description data] [throwable description data])} + [& args] + `(logd :error ~@args)) + +(defmacro fatald + "Fatal level logging using structured data." + {:arglists '([description data] [throwable description data])} + [& args] + `(logd :fatal ~@args)) + (defmacro tracef "Trace level logging using format." {:arglists '([fmt & fmt-args] [throwable fmt & fmt-args])} diff --git a/src/main/clojure/clojure/tools/logging/impl.clj b/src/main/clojure/clojure/tools/logging/impl.clj index 087b190..567d5a6 100644 --- a/src/main/clojure/clojure/tools/logging/impl.clj +++ b/src/main/clojure/clojure/tools/logging/impl.clj @@ -44,6 +44,13 @@ (name [_] "disabled") (get-logger [_ _] disabled-logger))) +(defrecord StructuredMessage [description data]) + +(defn extract-message [msg] + (if (instance? StructuredMessage msg) + (-> msg :data (assoc :clojure.tools.logging/description (:description msg))) + msg)) + (defn class-found? "Returns true if the Class associated with the given classname can be found using the context ClassLoader for the current thread." @@ -76,7 +83,7 @@ (throw (IllegalArgumentException. (str level#))))) :write! (fn [^org.slf4j.Logger logger# level# ^Throwable e# msg#] - (let [^String msg# (str msg#)] + (let [^String msg# (str (extract-message msg#))] (if e# (condp = level# :trace (.trace logger# msg# e#) @@ -121,23 +128,24 @@ (throw (IllegalArgumentException. (str level#))))) :write! (fn [^org.apache.commons.logging.Log logger# level# e# msg#] - (if e# - (condp = level# - :trace (.trace logger# msg# e#) - :debug (.debug logger# msg# e#) - :info (.info logger# msg# e#) - :warn (.warn logger# msg# e#) - :error (.error logger# msg# e#) - :fatal (.fatal logger# msg# e#) - (throw (IllegalArgumentException. (str level#)))) - (condp = level# - :trace (.trace logger# msg#) - :debug (.debug logger# msg#) - :info (.info logger# msg#) - :warn (.warn logger# msg#) - :error (.error logger# msg#) - :fatal (.fatal logger# msg#) - (throw (IllegalArgumentException. (str level#))))))}) + (let [msg# (extract-message msg#)] + (if e# + (condp = level# + :trace (.trace logger# msg# e#) + :debug (.debug logger# msg# e#) + :info (.info logger# msg# e#) + :warn (.warn logger# msg# e#) + :error (.error logger# msg# e#) + :fatal (.fatal logger# msg# e#) + (throw (IllegalArgumentException. (str level#)))) + (condp = level# + :trace (.trace logger# msg#) + :debug (.debug logger# msg#) + :info (.info logger# msg#) + :warn (.warn logger# msg#) + :error (.error logger# msg#) + :fatal (.fatal logger# msg#) + (throw (IllegalArgumentException. (str level#)))))))}) (reify LoggerFactory (name [_#] "org.apache.commons.logging") @@ -163,7 +171,8 @@ (.isEnabledFor logger# (get levels# level# level#))) :write! (fn [^org.apache.log4j.Logger logger# level# e# msg#] - (let [level# (get levels# level# level#)] + (let [level# (get levels# level# level#) + msg# (extract-message msg#)] (if e# (.log logger# level# msg# e#) (.log logger# level# msg#))))}) @@ -193,7 +202,8 @@ ^org.apache.logging.log4j.Level (get levels# level# level#))) :write! (fn [^org.apache.logging.log4j.Logger logger# level# e# msg#] - (let [level# (get levels# level# level#)] + (let [level# (get levels# level# level#) + msg# (extract-message msg#)] (if e# (.log ^org.apache.logging.log4j.Logger logger# ^org.apache.logging.log4j.Level level# @@ -228,7 +238,7 @@ :write! (fn [^java.util.logging.Logger logger# level# ^Throwable e# msg#] (let [^java.util.logging.Level level# (get levels# level# level#) - ^String msg# (str msg#)] + ^String msg# (str (extract-message msg#))] (if e# (.log logger# level# msg# e#) (.log logger# level# msg#))))})