Add middleware support, refactor appender decoration

* Appenders are now decorated via 2 separate mechanisms: a per-appender
  decorator (as before), and a new per-juxt (i.e. combined) decorator.
  The per-juxt wrapping provides a performance-conscious hook for
  higher-level facilities like the new middleware feature.

* Moved compile-time config wrapping from per-appender to per-juxt,
  improving performance.

* Fixed appender wrapper ordering.
This commit is contained in:
Peter Taoussanis 2013-02-04 12:32:32 +07:00
parent e18f3a4c41
commit 4184491719
2 changed files with 95 additions and 63 deletions

View File

@ -14,7 +14,7 @@ Timbre is an attempt to make **simple logging simple** and more **complex loggin
* Small, uncomplicated **all-Clojure** library.
* **Super-simple map-based config**: no arcane XML or properties files!
* **Decent performance** (low overhead).
* Flexible **fn-centric appender model**.
* Flexible **fn-centric appender model** with **middleware**.
* Sensible built-in appenders including simple **email appender**.
* Tunable **flood control** and **asynchronous** logging support.
* Robust **namespace filtering**.
@ -86,6 +86,8 @@ Configuring Timbre couldn't be simpler. Let's check out (some of) the defaults:
:ns-whitelist []
:ns-blacklist []
:middleware [] ; As of 1.4.0, see source code
:timestamp-pattern "yyyy-MMM-dd HH:mm:ss ZZ"
:timestamp-locale nil

View File

@ -41,23 +41,26 @@
;;;; Default configuration and appenders
(utils/defonce* config
"This map atom controls everything about the way Timbre operates. In
particular note the flexibility to add arbitrary appenders.
"This map atom controls everything about the way Timbre operates.
APPENDERS
An appender is a map with keys:
:doc, :min-level, :enabled?, :async?, :max-message-per-msecs, :fn?
:doc, :min-level, :enabled?, :async?, :max-message-per-msecs, :fn
An appender's fn takes a single map argument with keys:
:level, :message, :more ; From all logging macros (`info`, etc.)
:profiling-stats ; From `profile` macro
:ap-config ; `shared-appender-config`
:prefix ; Output of `prefix-fn`
And also: :instant, :timestamp, :hostname, :ns, :error?
Other keys include: :instant, :timestamp, :hostname, :ns, :error?
MIDDLEWARE
Middleware are fns (applied right-to-left) that transform the map argument
dispatched to appender fns. If any middleware returns nil, no dispatching
will occur (i.e. the event will be filtered).
See source code for examples.
See `set-config!`, `merge-config!`, `set-level!` for convenient config
editing."
See source code for examples. See `set-config!`, `merge-config!`, `set-level!`
for convenient config editing."
(atom {:current-level :debug
;;; Control log filtering by namespace patterns (e.g. ["my-app.*"]).
@ -65,16 +68,9 @@
:ns-whitelist []
:ns-blacklist []
;; TODO Generalized transformation/filtering unary fns to operate on
;; logging requests to either either filter or transform logging
;; messages (e.g. obscure security credentials).
;;
;; Could use a cacheable comp/juxt and include ns white/black list
;; functionality? Possibly even just prepend to the regular appender
;; juxt (assuming we keep ns filtering separate)? Note that this'd
;; also make any additional middlware cost async-able.
;;
;; :middleware []
;; Fns (applied right-to-left) to transform/filter appender fn args.
;; Useful for obfuscating credentials, pattern filtering, etc.
:middleware []
;;; Control :timestamp format
:timestamp-pattern "yyyy-MMM-dd HH:mm:ss ZZ" ; SimpleDateFormat pattern
@ -133,44 +129,12 @@
;;;; Appender-fn decoration
(defn- make-timestamp-fn
"Returns a unary fn that formats instants using given pattern string and an
optional Locale."
[^String pattern ^Locale locale]
(let [format (if locale
(SimpleDateFormat. pattern locale)
(SimpleDateFormat. pattern))]
(fn [^Date instant] (.format ^SimpleDateFormat format instant))))
(comment ((make-timestamp-fn "yyyy-MMM-dd" nil) (Date.)))
(def get-hostname
(utils/memoize-ttl
60000 (fn [] (.. java.net.InetAddress getLocalHost getHostName))))
(defn- wrap-appender-fn
"Wraps compile-time appender fn with additional runtime capabilities
controlled by compile-time config."
[{apfn :fn :keys [async? max-message-per-msecs] :as appender}]
(->
;; Wrap to add compile-time stuff to runtime appender arguments
(let [{ap-config :shared-appender-config
:keys [timestamp-pattern timestamp-locale prefix-fn]} @config
timestamp-fn (make-timestamp-fn timestamp-pattern timestamp-locale)]
(fn [{:keys [instant] :as apfn-args}]
(let [apfn-args (merge apfn-args {:ap-config ap-config
:timestamp (timestamp-fn instant)
:hostname (get-hostname)})]
(apfn (assoc apfn-args :prefix (prefix-fn apfn-args))))))
;; Wrap for asynchronicity support
((fn [apfn]
(if-not async?
(->> ; Wrapping applies capabilities bottom-to-top
apfn
(let [agent (agent nil :error-mode :continue)]
(fn [apfn-args] (send-off agent (fn [_] (apfn apfn-args))))))))
;; Wrap for runtime flood-safety support
((fn [apfn]
@ -199,7 +163,60 @@
(->> (keys timers-snapshot)
(filter #(allow? (timers-snapshot %))))]
(when (seq expired-timers)
(apply swap! flood-timers dissoc expired-timers))))))))))))
(apply swap! flood-timers dissoc expired-timers))))))))))
;; Wrap for async (agent) support
((fn [apfn]
(if-not async?
apfn
(let [agent (agent nil :error-mode :continue)]
(fn [apfn-args] (send-off agent (fn [_] (apfn apfn-args))))))))))
(defn- make-timestamp-fn
"Returns a unary fn that formats instants using given pattern string and an
optional Locale."
[^String pattern ^Locale locale]
(let [format (if locale
(SimpleDateFormat. pattern locale)
(SimpleDateFormat. pattern))]
(fn [^Date instant] (.format ^SimpleDateFormat format instant))))
(comment ((make-timestamp-fn "yyyy-MMM-dd" nil) (Date.)))
(def get-hostname
(utils/memoize-ttl
60000 (fn [] (.. java.net.InetAddress getLocalHost getHostName))))
(defn- wrap-appender-juxt
"Wraps compile-time appender juxt with additional runtime capabilities
(incl. middleware) controller by compile-time config. Like `wrap-appender-fn`
but operates on the entire juxt at once."
[juxtfn]
(->> ; Wrapping applies capabilities bottom-to-top
juxtfn
;; Wrap to add middleware transforms/filters
((fn [juxtfn]
(if-let [middleware (seq (:middleware @config))]
(let [composed-middleware
(apply comp (map (fn [mf] (fn [args] (when args (mf args))))
middleware))]
(fn [juxtfn-args]
(when-let [juxtfn-args (composed-middleware juxtfn-args)]
(juxtfn juxtfn-args))))
juxtfn)))
;; Wrap to add compile-time stuff to runtime appender arguments
((fn [juxtfn]
(let [{ap-config :shared-appender-config
:keys [timestamp-pattern timestamp-locale prefix-fn]} @config
timestamp-fn (make-timestamp-fn timestamp-pattern timestamp-locale)]
(fn [{:keys [instant] :as juxtfn-args}]
(let [juxtfn-args (merge juxtfn-args {:ap-config ap-config
:timestamp (timestamp-fn instant)
:hostname (get-hostname)})]
(juxtfn (assoc juxtfn-args :prefix (prefix-fn juxtfn-args))))))))))
;;;; Caching
@ -208,7 +225,7 @@
(def appenders-juxt-cache
"Per-level, combined relevant appender-fns to allow for fast runtime
appender-fn dispatch:
{:level (juxt wrapped-appender-fn wrapped-appender-fn ...) or nil
{:level (wrapped-juxt wrapped-appender-fn wrapped-appender-fn ...) or nil
...}"
(atom {}))
@ -233,10 +250,11 @@
(when-let [ap-ids (keys rel-aps)]
(->> ap-ids
(map #(wrap-appender-fn (rel-aps %)))
(apply juxt))))))))
(apply juxt)
(wrap-appender-juxt))))))))
(reset! appenders-juxt-cache)))
;;; Namespace filter ; TODO Generalize to arbitrary configurable middleware juxt?
;;; Namespace filter
(def ns-filter-cache "@ns-filter-cache => (fn relevant-ns? [ns] ...)"
(atom (constantly true)))
@ -372,4 +390,16 @@
(spy (* 6 5 4 3 2 1))
(spy :debug :factorial6 (* 6 5 4 3 2 1))
(info (Exception. "noes!") "bar")
(spy (/ 4 0)))
(spy (/ 4 0))
;; Middleware
(info {:name "Robert Paulson" :password "Super secret"})
(set-config!
[:middleware]
[(fn [{:keys [hostname message] :as args}]
(cond (= hostname "filtered-host") nil ; Filter
(map? message)
(if (contains? message :password)
(assoc args :message (assoc message :password "*****"))
args)
:else args))]))