Refactor elision utils

This commit is contained in:
Peter Taoussanis 2016-07-05 22:54:51 +07:00
parent 728dec01c7
commit d6f10795a6
3 changed files with 163 additions and 116 deletions

View File

@ -106,6 +106,7 @@
{:level :debug ; e/o #{:trace :debug :info :warn :error :fatal :report}
;; TODO Consider switching to `:ns-pattern` to match Tufte?
;; Control log filtering by namespaces/patterns. Useful for turning off
;; logging in noisy libraries, etc.:
:ns-whitelist [] #_["my-app.foo-ns"]
@ -151,84 +152,154 @@
(comment (set-level! :info) *config*)
;;;; Levels
;; Note that for historical reasons we don't make a distinction
;; between form "level"s and config "min-level"s.
(def ordered-levels [:trace :debug :info :warn :error :fatal :report])
(def ^:private scored-levels (zipmap ordered-levels (next (range))))
(def ^:private valid-levels (set ordered-levels))
(def ^:private valid-level
(fn [level]
(or (valid-levels level)
(throw (ex-info (str "Invalid logging level: " level) {:level level})))))
(def ^:const -levels-vec [:trace :debug :info :warn :error :fatal :report])
(def ^:const -levels-set (set -levels-vec))
(def ^:const -levels-map (zipmap -levels-vec (next (range))))
(comment (valid-level :info))
(defn valid-level? [x] (if (-levels-set x) true false))
(defn valid-level [x]
(or (-levels-set x)
(throw (ex-info "Invalid Timbre logging level" {:given x}))))
(defn level>= [x y] (>= ^long (scored-levels (valid-level x))
^long (scored-levels (valid-level y))))
(defn level>= [x y]
(>= ^long (-levels-map (valid-level x))
^long (-levels-map (valid-level y))))
(comment (qb 10000 (level>= :info :debug)))
(comment (qb 1e6 (level>= :info :debug))) ; 81.25
#+clj
(def ^:private compile-time-level
;; Will stack with runtime level
(have [:or nil? valid-level]
(when-let [level (keyword ; For back compatibility
(or (enc/read-sys-val "TIMBRE_LEVEL")
(enc/read-sys-val "TIMBRE_LOG_LEVEL")))]
(println (str "Compile-time (elision) Timbre level: " level))
level)))
;;;; Namespace filtering
;;;; ns filter
(def ^:private compile-ns-filters
"(fn [whitelist blacklist]) -> (fn [ns]) -> ?unfiltered-ns"
(let [->re-pattern
(fn [x]
(enc/cond!
(enc/re-pattern? x) x
;; Code shared with Tufte
(def compile-ns-filter "Returns (fn [?ns]) -> truthy."
(let [compile1
(fn [x] ; ns-pattern
(cond
(enc/re-pattern? x) (fn [ns-str] (re-find x ns-str))
(string? x)
(let [s (-> (str "^" x "$")
(str/replace "." "\\.")
(str/replace "*" "(.*)"))]
(re-pattern s))))]
(if (enc/str-contains? x "*")
(let [re
(re-pattern
(-> (str "^" x "$")
(str/replace "." "\\.")
(str/replace "*" "(.*)")))]
(fn [ns-str] (re-find re ns-str)))
(fn [ns-str] (= ns-str x)))
(enc/memoize_
(fn [whitelist blacklist]
(let [whitelist* (mapv ->re-pattern whitelist)
blacklist* (mapv ->re-pattern blacklist)
:else (throw (ex-info "Unexpected ns-pattern type"
{:given x :type (type x)}))))]
white-filter
(cond
;; (nil? whitelist) (fn [ns] false) ; Might be surprising
(empty? whitelist*) (fn [ns] true)
:else (fn [ns] (some #(re-find % ns) whitelist*)))
(fn self
([ns-pattern] ; Useful for user-level matching
(let [x ns-pattern]
(cond
(map? x) (self (:whitelist x) (:blacklist x))
(or (vector? x) (set? x)) (self x nil)
(= x "*") (fn [?ns] true)
:else
(let [match? (compile1 x)]
(fn [?ns] (if (match? (str ?ns)) true))))))
black-filter
(cond
(empty? blacklist*) (fn [ns] true)
:else (fn [ns] (not (some #(re-find % ns) blacklist*))))]
([whitelist blacklist]
(let [white
(when (seq whitelist)
(let [match-fns (mapv compile1 whitelist)
[m1 & mn] match-fns]
(if mn
(fn [ns-str] (enc/rsome #(% ns-str) match-fns))
(fn [ns-str] (m1 ns-str)))))
(fn [ns] (when (and (white-filter ns) (black-filter ns)) ns)))))))
black
(when (seq blacklist)
(let [match-fns (mapv compile1 blacklist)
[m1 & mn] match-fns]
(if mn
(fn [ns-str] (not (enc/rsome #(% ns-str) match-fns)))
(fn [ns-str] (not (m1 ns-str))))))]
(cond
(and white black)
(fn [?ns]
(let [ns-str (str ?ns)]
(if (white ns-str)
(if (black ns-str)
true))))
white (fn [?ns] (if (white (str ?ns)) true))
black (fn [?ns] (if (black (str ?ns)) true))
:else (fn [?ns] true) ; Common case
))))))
(def ^:private -compile-ns-filter (enc/memoize_ compile-ns-filter))
(def ^:private ns-filter
"(fn [whitelist blacklist ns]) -> ?unfiltered-ns"
"Returns true iff given ns passes white/black lists."
(enc/memoize_
(fn [whitelist blacklist ?ns]
{:pre [(have? [:or nil? string?] ?ns)]}
((compile-ns-filters whitelist blacklist) (or ?ns "")))))
((-compile-ns-filter whitelist blacklist) ?ns))))
(comment
(qb 10000 (ns-filter ["foo.*"] ["foo.baz"] "foo.bar"))
(qb 1e6 (ns-filter ["foo.*"] ["foo.baz"] "foo.bar")) ; 238.33
(ns-filter nil nil "")
(ns-filter nil nil nil))
;;;; Combo filtering
#+clj
(def ^:private compile-time-level
(when-let [level (or (enc/read-sys-val "TIMBRE_LEVEL")
(enc/read-sys-val "TIMBRE_LOG_LEVEL"))]
(println (str "Compile-time (elision) Timbre level: " level))
(let [;; Back compatibility
level (if (string? level) (keyword level) level)]
(valid-level level))))
#+clj
(def ^:private compile-time-ns-filter
;; Will stack with runtime ns filters
(let [whitelist (have [:or nil? vector?] (enc/read-sys-val "TIMBRE_NS_WHITELIST"))
blacklist (have [:or nil? vector?] (enc/read-sys-val "TIMBRE_NS_BLACKLIST"))]
(when whitelist (println (str "Compile-time (elision) Timbre ns whitelist: " whitelist)))
(when blacklist (println (str "Compile-time (elision) Timbre ns blacklist: " blacklist)))
(fn [ns] (ns-filter whitelist blacklist ns))))
(if-let [ns-pattern (enc/read-sys-val "TIMBRE_NS_PATTERN")]
(do
(println (str "Compile-time (elision) Timbre ns-pattern: " ns-pattern))
(-compile-ns-filter ns-pattern))
;; Back compatibility
(let [whitelist (have [:or nil? vector?] (enc/read-sys-val "TIMBRE_NS_WHITELIST"))
blacklist (have [:or nil? vector?] (enc/read-sys-val "TIMBRE_NS_BLACKLIST"))]
(when whitelist (println (str "Compile-time (elision) Timbre ns whitelist: " whitelist)))
(when blacklist (println (str "Compile-time (elision) Timbre ns blacklist: " blacklist)))
(-compile-ns-filter whitelist blacklist))))
#+clj ; Called only at macro-expansiom time
(defn -elide? [level-form ns-str-form]
(not
(and
(or ; Level okay
(nil? compile-time-level)
(not (valid-level? level-form)) ; Not a compile-time level const
(level>= level-form compile-time-level))
(or ; Namespace okay
(not (string? ns-str-form)) ; Not a compile-time ns-str const
(compile-time-ns-filter ns-str-form)))))
(defn may-log?
"Runtime check: would Timbre currently log at the given logging level?
* `?ns-str` arg required to support ns filtering
* `config` arg required to support non-global config"
([level ] (may-log? level nil nil))
([level ?ns-str ] (may-log? level ?ns-str nil))
([level ?ns-str ?config]
(let [config (or ?config *config*)
min-level (get config :level :report)]
(and
(level>= level min-level)
(ns-filter
(get config :ns-whitelist)
(get config :ns-blacklist)
?ns-str)
true))))
(comment (qb 1e5 (may-log? :info))) ; 34.13
;;;; Utils
@ -266,45 +337,11 @@
(def ^:dynamic *context*
"General-purpose dynamic logging context. Context will be included in
appender data map at logging time." nil)
appender data map at logging time."
nil)
(defmacro with-context [context & body] `(binding [*context* ~context] ~@body))
(defn log?
"Runtime check: would Timbre currently log at the given logging level?
* `?ns-str` arg required to support ns filtering
* `config` arg required to support non-global config"
([level ] (log? level nil nil))
([level ?ns-str ] (log? level ?ns-str nil))
([level ?ns-str config]
(let [config (or config *config*)
active-level (get config :level :report)]
(and
(level>= level active-level)
(ns-filter (get config :ns-whitelist) (get config :ns-blacklist) ?ns-str)
true))))
(comment
(set-level! :debug)
(log? :trace)
(with-level :trace (log? :trace))
(qb 10000
(log? :trace)
(log? :trace "foo")
(tracef "foo")
(when false "foo"))
;; [1.38 1.42 2.08 0.26]
;;; Full benchmarks
(defmacro with-sole-appender [appender & body]
`(with-config (assoc *config* :appenders {:appender ~appender}) ~@body))
(with-sole-appender {:enabled? true :fn (fn [data] nil)}
(qb 10000 (info "foo"))) ; ~88ms ; Time to delays ready
(with-sole-appender {:enabled? true :fn (fn [data] ((:output-fn data) data))}
(qb 10000 (info "foo"))) ; ~218ms ; Time to output ready
)
(defmacro with-context [context & body]
`(binding [*context* ~context] ~@body))
(defn- next-vargs [v] (if (> (count v) 1) (subvec v 1) []))
(defn- vargs->margs
@ -369,7 +406,7 @@
([config level ?ns-str ?file ?line msg-type ?err vargs_
?base-data callsite-id]
(when (log? level ?ns-str config) ; Runtime check
(when (may-log? level ?ns-str config)
(let [instant (enc/now-dt)
context *context*
vargs @vargs_
@ -504,21 +541,7 @@
(-log! *config* :info nil nil nil :p :auto
(delay [(do (println "hi") :x) :y]) nil "callsite-id"))
(defmacro -with-elision
"Implementation detail.
Executes body iff given level and ns pass compile-time elision."
[level-form ns-str-form & body]
(when (or (nil? compile-time-level)
(not (valid-levels level-form)) ; Not a compile-time level const
(level>= level-form compile-time-level))
(when (or (not (string? ns-str-form)) ; Not a compile-time ns-str const
(compile-time-ns-filter ns-str-form))
`(do ~@body))))
(comment (-with-elision :info "ns" (println "foo")))
(defn- fline [and-form] (:line (meta and-form)))
#+clj (defn- fline [and-form] (:line (meta and-form)))
(defmacro log! ; Public wrapper around `-log!`
"Core low-level log macro. Useful for tooling, etc.
@ -532,9 +555,8 @@
[level msg-type args & [opts]]
(have sequential? args) ; To allow -> (delay [~@args])
(let [{:keys [?ns-str] :or {?ns-str (str *ns*)}} opts]
(-with-elision
level ; level-form (may/not be a compile-time kw const)
?ns-str ; ns-str-form (may/not be a compile-time str const)
;; level, ns may/not be compile-time consts:
(when-not (-elide? level ?ns-str)
(let [{:keys [config ?err ?file ?line ?base-data]
:or {config 'taoensso.timbre/*config*
?err :auto ; => Extract as err-type v0
@ -559,6 +581,29 @@
(macroexpand '(log! :info :p ["foo"]))
(macroexpand '(log! :info :p ["foo"] {:?line 42})))
;;;; Benchmarking
(comment
(set-level! :debug)
(may-log? :trace)
(with-level :trace (log? :trace))
(qb 10000
(may-log? :trace)
(may-log? :trace "foo")
(tracef "foo")
(when false "foo"))
;; [1.38 1.42 2.08 0.26]
(defmacro with-sole-appender [appender & body]
`(with-config (assoc *config* :appenders {:appender ~appender}) ~@body))
(with-sole-appender {:enabled? true :fn (fn [data] nil)}
(qb 10000 (info "foo"))) ; ~74.58 ; Time to delays ready
(with-sole-appender {:enabled? true :fn (fn [data] (force (:output_ data)))}
(qb 10000 (info "foo"))) ; ~136.68 ; Time to output ready
)
;;;; Main public API-level stuff
;; TODO Have a bunch of cruft here trying to work around CLJ-865 to some extent
@ -737,7 +782,9 @@
;;;; Deprecated
#+cljs (def console-?appender core-appenders/console-appender)
(defn logging-enabled? [level compile-time-ns] (log? level (str compile-time-ns)))
(def ordered-levels -levels-vec)
(def log? may-log?)
(defn logging-enabled? [level compile-time-ns] (may-log? level (str compile-time-ns)))
(defn str-println [& xs] (str-join xs))
(defmacro with-log-level [level & body] `(with-level ~level ~@body))
(defmacro with-logging-config [config & body] `(with-config ~config ~@body))

View File

@ -238,7 +238,7 @@
(let [id (qualified-kw *ns* id)]
(if elide-profiling?
`(do ~@body)
`(if (timbre/log? ~level ~(str *ns*)) ; Runtime check
`(if (timbre/may-log? ~level ~(str *ns*)) ; Runtime check
(profiled (do ~@body) [stats# result#]
(let [stats-str# (-format-stats stats#)]
(timbre/log! ~level :p

View File

@ -15,7 +15,7 @@
(enabled? [_ level]
;; No support for per-call config
(timbre/log? level logger-ns-str timbre-config))
(timbre/may-log? level logger-ns-str timbre-config))
(write! [_ level throwable message]
(timbre/log! level :p