timbre/src/taoensso/timbre.clj

515 lines
19 KiB
Clojure
Raw Normal View History

2013-11-29 13:53:38 +07:00
(ns taoensso.timbre "Simple, flexible, all-Clojure logging. No XML!"
{:author "Peter Taoussanis"}
(:require [clojure.string :as str]
[io.aviso.exception :as aviso-ex]
[taoensso.timbre.utils :as utils])
(:import [java.util Date Locale]
[java.text SimpleDateFormat]))
;;;; Public utils
(defn str-println
"Like `println` but prints all objects to output stream as a single
atomic string. This is faster and avoids interleaving race conditions."
2013-11-29 13:53:38 +07:00
[& xs] (print (str (str/join \space (filter identity xs)) \newline))
(flush))
2013-05-15 19:37:17 +07:00
(defn color-str [color & xs]
2013-11-29 13:53:38 +07:00
(let [ansi-color #(format "\u001b[%sm"
(case % :reset "0" :black "30" :red "31"
:green "32" :yellow "33" :blue "34"
:purple "35" :cyan "36" :white "37"
"0"))]
2013-05-15 19:37:17 +07:00
(str (ansi-color color) (apply str xs) (ansi-color :reset))))
2013-01-03 23:07:22 +07:00
(def default-out (java.io.OutputStreamWriter. System/out))
(def default-err (java.io.PrintWriter. System/err))
(defmacro with-default-outs
2013-01-29 16:57:28 +07:00
"Evaluates body with Clojure's default *out* and *err* bindings."
[& body] `(binding [*out* default-out *err* default-err] ~@body))
2013-11-29 13:53:38 +07:00
(defmacro with-err-as-out "Evaluates body with *err* bound to *out*."
[& body] `(binding [*err* *out*] ~@body))
(defn stacktrace "Default stacktrace formatter for use by appenders, etc."
[throwable & [separator]]
(when throwable
(str separator (aviso-ex/format-exception throwable))))
;;;; Logging levels
;; Level precendence: compile-time > dynamic > atom
2013-01-03 23:07:22 +07:00
(def level-compile-time
"Constant, compile-time logging level determined by the `TIMBRE_LOG_LEVEL`
environment variable. When set, overrules dynamically-configurable logging
level as a performance optimization (e.g. for use in performance sensitive
production environments)."
(keyword (System/getenv "TIMBRE_LOG_LEVEL")))
(def ^:dynamic *level-dynamic* nil)
(defmacro with-log-level
"Allows thread-local config logging level override. Useful for dev & testing."
[level & body] `(binding [*level-dynamic* ~level] ~@body))
(def level-atom (atom :debug))
(defn set-level! [level] (reset! level-atom level))
;;;
(def ^:private levels-ordered [:trace :debug :info :warn :error :fatal :report])
(def ^:private levels-scored (assoc (zipmap levels-ordered (next (range))) nil 0))
(defn error-level? [level] (boolean (#{:error :fatal} level))) ; For appenders, etc.
(defn- level-checked-score [level]
(or (levels-scored level)
(throw (Exception. (format "Invalid logging level: %s" level)))))
(def ^:private levels-compare (memoize (fn [x y] (- (level-checked-score x)
(level-checked-score y)))))
(declare config)
(defn- level-sufficient? [level]
(>= (levels-compare level
(or level-compile-time
*level-dynamic*
(:current-level @config) ; DEPRECATED, here for backwards comp
@level-atom)) 0))
;;;; Default configuration and appenders
(def example-config
"APPENDERS
An appender is a map with keys:
:doc, :min-level, :enabled?, :async?, :limit-per-msecs, :fn
An appender's fn takes a single map with keys:
:level, :throwable
:args, ; Raw logging macro args (as given to `info`, etc.).
:message, ; Stringified logging macro args, or nil.
:default-output ; Output of `fmt-output-fn`, used by built-in appenders.
:ap-config ; `shared-appender-config`.
:profile-stats ; From `profile` macro.
And also: :instant, :timestamp, :hostname, :ns, :error?
MIDDLEWARE
Middleware are fns (applied right-to-left) that transform the map
dispatched to appender fns. If any middleware returns nil, no dispatching
will occur (i.e. the event will be filtered).
The `example-config` code contains more details.
See also `set-config!`, `merge-config!`, `set-level!`."
{;;; Control log filtering by namespace patterns (e.g. ["my-app.*"]).
;;; Useful for turning off logging in noisy libraries, etc.
:ns-whitelist []
:ns-blacklist []
;; 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
:timestamp-locale nil ; A Locale object, or nil
:prefix-fn ; DEPRECATED, here for backwards comp
(fn [{:keys [level timestamp hostname ns]}]
(str timestamp " " hostname " " (-> level name str/upper-case)
" [" ns "]"))
;; Default output formatter used by built-in appenders. Custom appenders
;; may (but are not required to use) its output (:default-output).
:fmt-output-fn
(fn [{:keys [level throwable message timestamp hostname ns]}]
;; <timestamp> <hostname> <LEVEL> [<ns>] - <message> <throwable>
(format "%s %s %s [%s] - %s%s"
timestamp hostname (-> level name str/upper-case) ns (or message "")
(or (stacktrace throwable "\n") "")))
:shared-appender-config {} ; Provided to all appenders via :ap-config key
:appenders
{:standard-out
{:doc "Prints to *out*/*err*. Enabled by default."
:min-level nil :enabled? true :async? false :limit-per-msecs nil
:fn (fn [{:keys [error? default-output]}]
(binding [*out* (if error? *err* *out*)]
(str-println default-output)))}
:spit
{:doc "Spits to `(:spit-filename :shared-appender-config)` file."
:min-level nil :enabled? false :async? false :limit-per-msecs nil
:fn (fn [{:keys [ap-config default-output]}]
(when-let [filename (:spit-filename ap-config)]
(try (spit filename default-output :append true)
(catch java.io.IOException _))))}}})
(utils/defonce* config (atom example-config))
(defn set-config! [ks val] (swap! config assoc-in ks val))
(defn merge-config! [& maps] (apply swap! config utils/merge-deep maps))
;;;; Appender-fn decoration
(defn- wrap-appender-fn
"Wraps compile-time appender fn with additional runtime capabilities
controlled by compile-time config."
[{apfn :fn :keys [async? limit-per-msecs] :as appender}]
(let [limit-per-msecs (or (:max-message-per-msecs appender)
limit-per-msecs)] ; Backwards comp
(->> ; Wrapping applies per appender, bottom-to-top
apfn
;; Rate limit support
((fn [apfn]
(if-not limit-per-msecs apfn
(let [timers (atom {})] ; {:hash last-appended-time-msecs ...}
(fn [{ns :ns [x1 & _] :args :as apfn-args}]
(let [now (System/currentTimeMillis)
2013-10-22 12:58:07 +07:00
hash (str ns "/" x1) ; TODO Alternatives?
limit? (fn [last-msecs]
(and last-msecs (<= (- now last-msecs)
limit-per-msecs)))]
(when-not (limit? (@timers hash))
(apfn apfn-args)
(swap! timers assoc hash now))
(when (< (rand) 0.001) ; Occasionally garbage collect
(when-let [expired-timers (->> (keys @timers)
(remove #(limit? (@timers %)))
(seq))]
(apply swap! timers dissoc expired-timers)))))))))
;; 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."
;; Thread safe SimpleDateTime soln. from instant.clj, Ref. http://goo.gl/CEBJnQ
[^String pattern ^Locale locale]
(let [format (proxy [ThreadLocal] [] ; For thread safety
(initialValue []
(if locale
(SimpleDateFormat. pattern locale)
(SimpleDateFormat. pattern))))]
(fn [^Date instant] (.format ^SimpleDateFormat (.get format) instant))))
(comment ((make-timestamp-fn "yyyy-MMM-dd" nil) (Date.)))
(def ^:private get-hostname
(utils/memoize-ttl 60000
(fn []
(let [p (promise)]
(future ; Android doesn't like this on the main thread
(deliver p
(try (.. java.net.InetAddress getLocalHost getHostName)
(catch java.net.UnknownHostException _
"UnknownHost"))))
@p))))
(defn- wrap-appender-juxt
"Wraps compile-time appender juxt with additional runtime capabilities
2013-02-11 11:55:38 +07:00
(incl. middleware) controlled by compile-time config. Like `wrap-appender-fn`
but operates on the entire juxt at once."
[juxtfn]
2013-05-15 19:37:17 +07:00
(->> ; Wrapping applies per juxt, bottom-to-top
juxtfn
;; Post-middleware stuff
((fn [juxtfn]
;; Compile-time:
(let [{ap-config :shared-appender-config
:keys [timestamp-pattern timestamp-locale
prefix-fn fmt-output-fn]} @config
timestamp-fn (make-timestamp-fn timestamp-pattern timestamp-locale)]
(fn [juxtfn-args]
;; Runtime:
(when-let [{:keys [instant msg-type args]} juxtfn-args]
(let [juxtfn-args (if-not msg-type juxtfn-args ; tools.logging
(-> juxtfn-args
(dissoc :msg-type)
(assoc :message
(when-not (empty? args)
(case msg-type
:format (apply format args)
:print-str (apply print-str args)
:nil nil)))))]
(juxtfn
(merge juxtfn-args
{:timestamp (timestamp-fn instant)
;; DEPRECATED, here for backwards comp:
:prefix (when-let [f prefix-fn] (f juxtfn-args))
:default-output (when-let [f fmt-output-fn] (f juxtfn-args))}))))))))
2013-05-15 19:37:17 +07:00
;; Middleware transforms/filters support
((fn [juxtfn]
;; Compile-time:
(if-let [middleware (seq (:middleware @config))]
(let [composed-middleware
(apply comp (map (fn [mf] (fn [args] (when args (mf args))))
middleware))]
(fn [juxtfn-args]
;; Runtime:
(when-let [juxtfn-args (composed-middleware juxtfn-args)]
(juxtfn juxtfn-args))))
juxtfn)))
;; Pre-middleware stuff
((fn [juxtfn]
;; Compile-time:
(let [{ap-config :shared-appender-config} @config]
(fn [juxtfn-args]
;; Runtime:
(juxtfn (merge juxtfn-args {:ap-config ap-config
:hostname (get-hostname)}))))))))
;;;; Caching
;;; Appender-fns
(def appenders-juxt-cache
2013-05-15 19:37:17 +07:00
"Per-level, combined level-relevant appender-fns to allow for fast runtime
appender-fn dispatch:
{:level (wrapped-juxt wrapped-appender-fn wrapped-appender-fn ...) or nil
...}"
(atom {}))
2013-05-15 19:37:17 +07:00
(defn- relevant-appenders [level]
(->> (:appenders @config)
(filter #(let [{:keys [enabled? min-level]} (val %)]
(and enabled? (>= (levels-compare level min-level) 0))))
(into {})))
(comment (relevant-appenders :debug)
(relevant-appenders :trace))
2013-05-15 19:37:17 +07:00
(defn- cache-appenders-juxt! []
(->>
(zipmap
levels-ordered
(->> levels-ordered
(map (fn [l] (let [rel-aps (relevant-appenders l)]
;; Return nil if no relevant appenders
(when-let [ap-ids (keys rel-aps)]
(->> ap-ids
(map #(wrap-appender-fn (rel-aps %)))
(apply juxt)
(wrap-appender-juxt))))))))
(reset! appenders-juxt-cache)))
;;; Namespace filter
(def ns-filter-cache "@ns-filter-cache => (fn relevant-ns? [ns] ...)"
(atom (constantly true)))
2013-05-15 19:37:17 +07:00
(defn- ns-match? [ns match]
(-> (str "^" (-> (str match) (.replace "." "\\.") (.replace "*" "(.*)")) "$")
re-pattern (re-find (str ns)) boolean))
2013-05-15 19:37:17 +07:00
(defn- cache-ns-filter! []
(->>
(let [{:keys [ns-whitelist ns-blacklist]} @config]
(memoize
(fn relevant-ns? [ns]
(and (or (empty? ns-whitelist)
(some (partial ns-match? ns) ns-whitelist))
(or (empty? ns-blacklist)
(not-any? (partial ns-match? ns) ns-blacklist))))))
(reset! ns-filter-cache)))
;;; Prime initial caches and re-cache on config change
(cache-appenders-juxt!)
(cache-ns-filter!)
(add-watch
config "config-cache-watch"
(fn [key ref old-state new-state]
(when (not= (dissoc old-state :current-level)
(dissoc new-state :current-level))
(cache-appenders-juxt!)
(cache-ns-filter!))))
;;;; Logging macros
(defmacro logging-enabled?
"Returns true iff current logging level is sufficient and current namespace
unfiltered. The namespace test is runtime, the logging-level test compile-time
iff a compile-time logging level was specified."
[level]
(if level-compile-time
(when (level-sufficient? level)
`(@ns-filter-cache ~(str *ns*)))
`(and (level-sufficient? ~level) (@ns-filter-cache ~(str *ns*)))))
(comment (def compile-time-level :info)
(def compile-time-level nil)
(macroexpand-1 '(logging-enabled? :debug)))
2013-08-07 22:50:39 +07:00
(defn send-to-appenders! "Implementation detail - subject to change."
[;; Args provided by both Timbre, tools.logging:
level base-appender-args log-vargs ns throwable message
;; Additional args provided by Timbre only:
& [juxt-fn msg-type file line]]
2013-08-07 22:50:39 +07:00
(when-let [juxt-fn (or juxt-fn (@appenders-juxt-cache level))]
(juxt-fn
(conj (or base-appender-args {})
{:instant (Date.)
:ns ns
:file file ; No tools.logging support
:line line ; No tools.logging support
:level level
:error? (error-level? level)
:args log-vargs ; No tools.logging support
:throwable throwable
:message message ; Timbre: nil, tools.logging: nil or string
:msg-type msg-type ; Timbre: nnil, tools.logging: nil
}))
2013-08-07 22:50:39 +07:00
nil))
(defmacro log* "Implementation detail - subject to change."
[msg-type level base-appender-args & log-args]
{:pre [(#{:nil :print-str :format} msg-type)]}
`(when (logging-enabled? ~level)
2013-08-07 22:50:39 +07:00
(when-let [juxt-fn# (@appenders-juxt-cache ~level)]
(let [[x1# & xn# :as xs#] (vector ~@log-args)
has-throwable?# (instance? Throwable x1#)
log-vargs# (vec (if has-throwable?# xn# xs#))]
(send-to-appenders!
~level
~base-appender-args
log-vargs#
~(str *ns*)
(when has-throwable?# x1#)
nil ; Timbre generates msg only after middleware
2013-08-07 22:50:39 +07:00
juxt-fn#
~msg-type
2013-08-07 22:50:39 +07:00
(let [file# ~*file*] (when (not= file# "NO_SOURCE_PATH") file#))
;; TODO Waiting on http://dev.clojure.org/jira/browse/CLJ-865:
~(:line (meta &form)))))))
(defmacro log "Logs using print-style args."
{:arglists '([level & message] [level throwable & message])}
[level & sigs] `(log* :print-str ~level {} ~@sigs))
2013-08-07 22:50:39 +07:00
(defmacro logf "Logs using format-style args."
{:arglists '([level fmt & fmt-args] [level throwable fmt & fmt-args])}
[level & sigs] `(log* :format ~level {} ~@sigs))
(defmacro log-errors [& body] `(try ~@body (catch Throwable t# (error t#))))
(defmacro log-and-rethrow-errors [& body]
`(try ~@body (catch Throwable t# (error t#) (throw t#))))
(defmacro logged-future [& body] `(future (log-errors ~@body)))
(comment (log-errors (/ 0))
(log-and-rethrow-errors (/ 0))
(logged-future (/ 0)))
(defmacro spy
2012-09-21 21:02:58 +07:00
"Evaluates named expression and logs its result. Always returns the result.
Defaults to :debug logging level and unevaluated expression as name."
([expr] `(spy :debug ~expr))
2012-09-21 21:02:58 +07:00
([level expr] `(spy ~level '~expr ~expr))
([level name expr]
`(log-and-rethrow-errors
(let [result# ~expr] (log ~level ~name result#) result#))))
2013-05-15 19:37:17 +07:00
(defmacro ^:private def-logger [level]
(let [level-name (name level)]
`(do
(defmacro ~(symbol level-name)
2013-08-07 22:50:39 +07:00
~(str "Logs at " level " level using print-style args.")
~'{:arglists '([& message] [throwable & message])}
2013-07-22 22:21:07 +07:00
[& sigs#] `(log ~~level ~@sigs#))
(defmacro ~(symbol (str level-name "f"))
2013-08-07 22:50:39 +07:00
~(str "Logs at " level " level using format-style args.")
~'{:arglists '([fmt & fmt-args] [throwable fmt & fmt-args])}
[& sigs#] `(logf ~~level ~@sigs#)))))
(defmacro ^:private def-loggers []
`(do ~@(map (fn [level] `(def-logger ~level)) levels-ordered)))
(def-loggers) ; Actually define a logger for each logging level
2013-07-22 17:53:36 +07:00
(defn refer-timbre
"Shorthand for:
(require
'[taoensso.timbre :as timbre
:refer (log trace debug info warn error fatal report
logf tracef debugf infof warnf errorf fatalf reportf
spy logged-future with-log-level)])
(require '[taoensso.timbre.utils :refer (sometimes)])
(require
'[taoensso.timbre.profiling :as profiling :refer (pspy profile defnp)])"
2013-07-22 17:53:36 +07:00
[]
(require
'[taoensso.timbre :as timbre
:refer (log trace debug info warn error fatal report
logf tracef debugf infof warnf errorf fatalf reportf
spy logged-future with-log-level)])
(require '[taoensso.timbre.utils :refer (sometimes)])
(require
'[taoensso.timbre.profiling :as profiling :refer (pspy profile defnp)]))
2013-07-22 17:53:36 +07:00
2013-07-22 22:21:07 +07:00
;;;; Deprecated
(defmacro logp "DEPRECATED: Use `log` instead."
{:arglists '([level & message] [level throwable & message])}
[& sigs] `(log ~@sigs)) ; Alias
(defmacro s "DEPRECATED: Use `spy` instead."
{:arglists '([expr] [level expr] [level name expr])}
[& args] `(spy ~@args))
(def red "DEPRECATED: Use `color-str` instead." (partial color-str :red))
(def green "DEPRECATED: Use `color-str` instead." (partial color-str :green))
(def yellow "DEPRECATED: Use `color-str` instead." (partial color-str :yellow))
;;;; Dev/tests
(comment
(info)
(info "a")
(info "a" "b" "c")
(info "a" (Exception. "b") "c")
(info (Exception. "a") "b" "c")
2013-07-22 22:21:07 +07:00
(log (or nil :info) "Booya")
(info "a%s" "b")
(infof "a%s" "b")
(set-config! [:ns-blacklist] [])
(set-config! [:ns-blacklist] ["taoensso.timbre*"])
(info "foo" "bar")
(trace (Thread/sleep 5000))
(time (dotimes [n 10000] (trace "This won't log"))) ; Overhead 5ms/10ms
(time (dotimes [n 5] (info "foo" "bar")))
(spy :info (* 6 5 4 3 2 1))
(spy :info :factorial6 (* 6 5 4 3 2 1))
(info (Exception. "noes!") "bar")
(spy (/ 4 0))
(with-log-level :trace (trace "foo"))
(with-log-level :debug (trace "foo"))
;; Middleware
(info {:name "Robert Paulson" :password "Super secret"})
(set-config! [:middleware] [])
(set-config! [:middleware]
[(fn [{:keys [hostname message args] :as ap-args}]
(if (= hostname "filtered-host") nil ; Filter
(assoc ap-args :args
;; Replace :password vals in any map args:
(mapv (fn [arg] (if-not (map? arg) arg
(if-not (contains? arg :password) arg
(assoc arg :password "****"))))
args))))]))