From 2dd0121352bc903828d4cc9724a464074c6d8b18 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 7 Jul 2014 16:14:14 +0700 Subject: [PATCH 01/16] *ABORTED*: old v4 refactor --- src/taoensso/timbre.clj | 359 +++++++++++++++++------ src/taoensso/timbre/appenders/postal.clj | 3 +- src/taoensso/timbre/tools/logging.clj | 12 +- 3 files changed, 276 insertions(+), 98 deletions(-) diff --git a/src/taoensso/timbre.clj b/src/taoensso/timbre.clj index 7cd7114..68d0c5e 100644 --- a/src/taoensso/timbre.clj +++ b/src/taoensso/timbre.clj @@ -19,6 +19,27 @@ min-encore-version) {:min-version min-encore-version})))) +;;;; TODO v4 +;; * Use `format`, `sprintln` from Encore. +;; * Decide on `:message` format design +;; - No delay, just require use of tool? +;; - Delay set (as wrapper) per-appender with merged `:ap-config`? +;; - :msg-type e/o #{:tools.logging :print-str :pr-str :format nil} +;; - :message key is set [only, ever] at per-appender wrapper level. +;; - Pros: great flexibility with easy config, simple. +;; - Cons: cost of per-appender delay generation. Problem? +;; * Get core working + tested. +;; * Enumerate changes from v3. +;; * Look into v3 backwards compatibility. +;; * Document changes from v3. +;; * Update bundled appenders (?). +;; * Update docs. +;; +;; * Investigate better encore/Cljs interplay: fns? +;; * Do runtime level check even if a compile-time level is in effect if the +;; provided `log` level arg is not immediately recognized (e.g. it may be a +;; runtime level form that first requires eval). + ;;;; Public utils (defn str-println @@ -37,23 +58,70 @@ (def default-out (java.io.OutputStreamWriter. System/out)) (def default-err (java.io.PrintWriter. System/err)) - (defmacro with-default-outs "Evaluates body with Clojure's default *out* and *err* bindings." - [& body] `(binding [*out* default-out *err* default-err] ~@body)) + [& body] `(binding [*out* default-out + *err* default-err] ~@body)) -(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." +(defn fmt-stacktrace "Default stacktrace formatter for use by appenders." [throwable & [separator stacktrace-fonts]] (when throwable (str separator - (if-let [fonts stacktrace-fonts] + (if-let [fonts stacktrace-fonts] ; nil (defaults), or a map (binding [aviso-ex/*fonts* fonts] (aviso-ex/format-exception throwable)) (aviso-ex/format-exception throwable))))) -(comment (stacktrace (Exception. "foo") nil {})) +(comment (println (fmt-stacktrace (Exception. "foo") nil nil)) + (println (fmt-stacktrace (Exception. "foo") nil {}))) + +(def get-hostname + ;; TODO Any way to keep future from affecting shutdown time, + ;; Ref. http://goo.gl/5hx9oK? + (encore/memoize* (encore/ms :mins 2) + (fn [] + (-> + (future ; Android doesn't like this on the main thread + (try (.. java.net.InetAddress getLocalHost getHostName) + (catch java.net.UnknownHostException _ + "UnknownHost"))) + (deref 5000 "UnknownHost"))))) + +(def ^:private default-message-timestamp-pattern "14-Jul-07 16:42:11" + "yy-MMM-dd HH:mm:ss") + +(def ^:private default-message-pattern-fn + "14-Jul-07 16:42:11 localhost INFO [my-app.foo.bar] - Hello world" + (fn [{:keys [ns ; & Any other appender args + ;; These are delays: + timestamp_ hostname_ level-name_ args-str_ stacktrace_]}] + (str @timestamp_ " " @hostname_ " " @level-name_ " " + "[" ns "] - " @args-str_ @stacktrace_))) + +(defn fmt-appender-args "Formats appender arguments as a message string." + [fmt-fn ; `(apply args)`: format, print-str, pr-str, etc. + {:as appender-args :keys [instant ns level throwable args]} & + [{:as fmt-opts :keys [timestamp-pattern timestamp-locale no-fonts? pattern-fn] + :or {timestamp-pattern default-message-timestamp-pattern + timestamp-locale nil + pattern-fn default-message-pattern-fn}}]] + + (when-not (empty? args) + (pattern-fn + (merge appender-args + ;; Delays since user pattern may/not want any of these: + {:hostname_ (delay (get-hostname)) + :timestamp_ (delay (.format (encore/simple-date-format timestamp-pattern + {:locale timestamp-locale}) instant)) + :level-name_ (delay (-> level name str/upper-case)) + :args-str_ (delay (apply fmt-fn args)) ; `args` is non-empty + :stacktrace_ (delay (fmt-stacktrace throwable "\n" (when no-fonts? {})))})))) + +(comment + (encore/qbench 1000 + (fmt-appender-args print-str + {:instant (Date.) :ns *ns* :level :info :throwable nil + :args ["Hello" "there"]})) ; ~14ms + ) (defmacro sometimes "Executes body with probability e/o [0,1]. Useful for sampled logging." @@ -77,8 +145,6 @@ (def level-atom (atom :debug)) (defn set-level! [level] (reset! level-atom level)) -;;; - (def levels-ordered [:trace :debug :info :warn :error :fatal :report]) (def levels-scored (zipmap levels-ordered (next (range)))) @@ -120,45 +186,37 @@ ;;;; Default configuration and appenders -(defn default-fmt-output-fn - [{:keys [level throwable message timestamp hostname ns]} - ;; Any extra appender-specific opts: - & [{:keys [nofonts?] :as appender-fmt-output-opts}]] - ;; [] - - (format "%s %s %s [%s] - %s%s" - timestamp hostname (-> level name str/upper-case) ns (or message "") - (or (stacktrace throwable "\n" (when nofonts? {})) ""))) - (def example-config "APPENDERS An appender is a map with keys: - :doc ; (Optional) string. - :min-level ; (Optional) keyword, or nil (no minimum level). - :enabled? ; (Optional). - :async? ; (Optional) dispatch using agent (good for slow appenders). - :rate-limit ; (Optional) [ncalls-limit window-ms]. - :fmt-output-opts ; (Optional) extra opts passed to `fmt-output-fn`. + :doc ; Optional docstring. + :min-level ; Level keyword, or nil (=> no minimum level). + :enabled? ; + :async? ; Dispatch using agent? Useful for slow appenders. + :rate-limit ; [ncalls-limit window-ms], or nil. + :args-hash-fn ; Used by rate-limiter, etc. + :appender-config ; Any appender-specific config. :fn ; (fn [appender-args-map]), with keys described below. - :args-hash-fn ; Experimental. Used by rate-limiter, etc. An appender's fn takes a single map with keys: + :instant ; java.util.Date. + :ns ; String. :level ; Keyword. :error? ; Is level an 'error' level? :throwable ; java.lang.Throwable. :args ; Raw logging macro args (as given to `info`, etc.). - :message ; Stringified logging macro args, or nil. - :output ; Output of `fmt-output-fn`, used by built-in appenders - ; as final, formatted appender output. Appenders may (but - ; are not obligated to) use this as their output. - :ap-config ; Content of config's :shared-appender-config key. + ;; + :context ; Thread-local dynamic logging context. + :ap-config ; Content of appender's own `:appender-config` merged over + ; `:shared-appender-config`. :profile-stats ; From `profile` macro. - :instant ; java.util.Date. - :timestamp ; String generated from :timestamp-pattern, :timestamp-locale. - :hostname ; String. - :ns ; String. + ;; ;; Waiting on http://dev.clojure.org/jira/browse/CLJ-865: :file ; String. :line ; Integer. + ;; + :message ; DELAYED string of formatted appender args. Appenders may + ; (but are not obligated to) use this as their output. MIDDLEWARE Middleware are fns (applied right-to-left) that transform the map @@ -168,8 +226,7 @@ The `example-config` code contains further settings and details. See also `set-config!`, `merge-config!`, `set-level!`." - {;; Prefer `level-atom` to in-config level when possible: - ;; :current-logging-level :debug + {;; :current-level :debug ; Prefer `level-atom` ;;; Control log filtering by namespace patterns (e.g. ["my-app.*"]). ;;; Useful for turning off logging in noisy libraries, etc. @@ -180,33 +237,27 @@ ;; 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 + :shared-appender-config + {:message-fmt-opts ; `:message` appender argument formatting + {:timestamp-pattern default-message-timestamp-pattern ; SimpleDateFormat + :timestamp-locale nil ; A Locale object, or nil + :pattern-fn default-message-pattern-fn}} - :prefix-fn ; DEPRECATED, here for backwards comp - (fn [{:keys [level timestamp hostname ns]}] - (str timestamp " " hostname " " (-> level name str/upper-case) - " [" ns "]")) - - ;; Output formatter used by built-in appenders. Custom appenders may (but are - ;; not required to use) its output (:output). Extra per-appender opts can be - ;; supplied as an optional second (map) arg. - :fmt-output-fn default-fmt-output-fn - - :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 :rate-limit nil - :fn (fn [{:keys [error? output]}] ; Can use any appender args - (binding [*out* (if error? *err* *out*)] - (str-println output)))} + :appender-config {:always-log-to-err? false} + :fn (fn [{:keys [ap-config error? message]}] ; Can use any appender args + (binding [*out* (if (or error? (:always-log-to-err? ap-config)) + *err* *out*)] + (str-println @message)))} :spit - {:doc "Spits to `(:spit-filename :shared-appender-config)` file." + {:doc "Spits to `(:spit-filename :ap-config)` file." :min-level nil :enabled? false :async? false :rate-limit nil - :fn (fn [{:keys [ap-config output]}] ; Can use any appender args + :appender-config {:spit-filename "timbre-spit.log"} + :fn (fn [{:keys [ap-config message]}] ; Can use any appender args (when-let [filename (:spit-filename ap-config)] (try (ensure-spit-dir-exists! filename) (spit filename (str output "\n") :append true) @@ -219,27 +270,20 @@ ;;;; Appender-fn decoration (defn default-args-hash-fn - "Alpha - subject to change!! - Returns a hash identifier for given appender arguments in such a way that - (= (hash args-A) (hash args-B)) iff arguments A and B are \"the same\" by - some reasonable-in-the-general-case definition for logging arguments. - - Useful in the context of rate limiting, deduplicating appenders, etc." - - ;; Things like dates & user ids user ids will still trip us up. - ;; `[hostname ns line]` may be another idea? - ;; Waiting on http://dev.clojure.org/jira/browse/CLJ-865. - - [{:keys [hostname ns args] :as apfn-args}] + "Returns a hash id for given appender args such that + (= (hash args-A) (hash args-B)) iff args A and B are \"the same\" by + some reasonable-in-the-general-case definition for logging args. Useful for + rate limiting, deduplicating appenders, etc." + [{:keys [ns line args] :as apfn-args}] (str (or (some #(and (map? %) (:timbre/hash %)) args) ; Explicit hash given - [hostname ns args]))) + ;; [ns line] ; TODO Waiting on http://goo.gl/cVVAYA + [ns args]))) (defn- wrap-appender-fn - "Wraps compile-time appender fn with additional runtime capabilities - controlled by compile-time config." [config {:as appender apfn :fn - :keys [async? rate-limit fmt-output-opts args-hash-fn] + :keys [async? rate-limit args-hash-fn appender-config] :or {args-hash-fn default-args-hash-fn}}] +<<<<<<< HEAD (let [rate-limit (or rate-limit ; Backwards comp: (if-let [x (:max-message-per-msecs appender)] [1 x] (when-let [x (:limit-per-msecs appender)] [1 x])))] @@ -338,13 +382,80 @@ juxtfn))) ;; Pre-middleware stuff +======= + (assert (or (nil? rate-limit) (vector? rate-limit))) + (->> ; Wrapping applies per appender, bottom-to-top + apfn + + ;; :ap-config + ((fn [apfn] + ;; Compile-time: + (if-not appender-config apfn + (let [merged-config (merge (:shared-appender-config config) + appender-config)] + (println "DEBUG! `merged-config`:" merged-config) ; TODO + (fn [apfn-args] + ;; Runtime: + (apfn (assoc apfn-args :ap-config merged-config))))))) + + ;; Rate limits + ((fn [apfn] + ;; Compile-time: + (if-not rate-limit apfn + (let [[ncalls-limit window-ms] rate-limit + limiter-any (encore/rate-limiter ncalls-limit window-ms) + ;; This is a little hand-wavy but it's a decent general + ;; strategy and helps us from making this overly complex to + ;; configure. + limiter-specific (encore/rate-limiter (quot ncalls-limit 4) + window-ms)] + (fn [{:keys [ns args] :as apfn-args}] + ;; Runtime: + (when-not (or (limiter-specific (args-hash-fn apfn-args)) + (limiter-any)) ; Test smaller limit 1st + (apfn apfn-args))))))) + + ;; Async (agents) + ((fn [apfn] + ;; Compile-time: + (if-not async? apfn + (let [agent (agent nil :error-mode :continue)] + (fn [apfn-args] ; Runtime: + (send-off agent (fn [_] (apfn apfn-args)))))))))) + +(def ^:dynamic *context* "Thread-local dynamic logging context." {}) +(defn- wrap-appender-juxt [config juxtfn] + (->> ; Wrapping applies per juxt, bottom-to-top + juxtfn + + ;; ;; Post-middleware stuff + ;; ((fn [juxtfn] + ;; ;; Compile-time: + ;; (fn [juxtfn-args] + ;; ;; Runtime: + ;; (juxtfn juxtfn-args)))) + + ;; Middleware (transforms/filters) +>>>>>>> fe51297... NB Experimental: major refactor (currently breaking, for potential Timbre v4) ((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)})))))))) + (let [middleware (:middleware config)] + (if (empty? middleware) juxtfn + (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)))))))) + + ;; ;; Pre-middleware stuff + ;; ((fn [juxtfn] + ;; ;; Compile-time: + ;; (fn [juxtfn-args] + ;; ;; Runtime: + ;; (juxtfn juxtfn-args)))) + )) ;;;; Config compilation @@ -400,6 +511,9 @@ (defn get-default-config [] (or *config-dynamic* @config)) +(defmacro with-logging-context "Thread-local dynamic logging context." + [context & body] `(binding [*context* ~context] ~@body)) + (defn ns-unfiltered? [config ns] ((:ns-filter (compile-config config)) ns)) (defn logging-enabled? "For 3rd-party utils, etc." @@ -409,25 +523,80 @@ (or (nil? compile-time-ns) (ns-unfiltered? config' compile-time-ns))))) +(defn send-to-appenders! "Implementation detail." + [{:keys [;; Args provided by both Timbre, tools.logging: + level base-appender-args log-vargs ns throwable message + ;; Additional args provided by Timbre only: + juxt-fn file line]}] + (when-let [juxt-fn (or juxt-fn (get-in (compile-config (get-default-config)) + [:appenders-juxt level]))] + (let [appender-args + (conj (or base-appender-args {}) + {;;; Passed through + :level level + :args log-vargs ; String / 1-vec raw arg for tools.logging impl + :ns ns + :throwable throwable + :file file ; Nil for tools.logging + :line line ; '' + + ;;; Generated + :instant (Date.) + :error? (level-error? level) + + ;;; Varies + :message message})] + (juxt-fn appender-args) + nil))) + +(comment ; TODO + (delay + (fmt-appender-args ; TODO + maybe merge :ap-config for fmt opts? + ;; Or just have in :shared-appender-config + (case msg-type + :format format + :print-str print-str + :pr-str pr-str)))) + +(comment ; TODO + [fmt-fn ; `(apply args)`: format, print-str, pr-str, etc. + {:as appender-args :keys [instant ns level throwable args]} & + [{:as fmt-opts :keys [timestamp-pattern timestamp-locale no-fonts? pattern-fn] + :or {timestamp-pattern default-message-timestamp-pattern + timestamp-locale nil + pattern-fn default-message-pattern-fn}}]]) + +(defn send-to-appenders! "Implementation detail." + ([level base-appender-args log-vargs ns throwable message]) + + + + + ) + (defn send-to-appenders! "Implementation detail." [;; 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]] + & [juxt-fn file line]] (when-let [juxt-fn (or juxt-fn (get-in (compile-config (get-default-config)) [:appenders-juxt 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 + {;;; Generated + :instant (Date.) :error? (level-error? level) - :args log-vargs ; No tools.logging support + + ;;; Passed through + :ns ns + :level level :throwable throwable - :message message ; Timbre: nil, tools.logging: nil or string - :msg-type msg-type ; Timbre: nnil, tools.logging: nil + :message message + + ;;; Passed through (no/limited tools.logging support) + :file file ; Nil for tools.logging impl + :line line ; '' + :args log-vargs ; String / 1-vec raw arg for tools.logging impl })) nil)) @@ -435,10 +604,9 @@ (comment (macroexpand '(get-compile-time-ns))) (defmacro log* "Implementation detail." - {:arglists '([base-appender-args msg-type level & log-args] - [base-appender-args msg-type config level & log-args])} - [base-appender-args msg-type & [s1 s2 :as sigs]] - {:pre [(#{:nil :print-str :format} msg-type)]} + {:arglists '([base-appender-args fmt-fn level & log-args] + [base-appender-args fmt-fn config level & log-args])} + [base-appender-args fmt-fn & [s1 s2 :as sigs]] ;; Compile-time: (when (or (nil? level-compile-time) (let [level (cond (levels-scored s1) s1 @@ -468,9 +636,13 @@ log-vargs# compile-time-ns# (when has-throwable?# x1#) + + + ;; TODO nil ; Timbre generates msg only after middleware + + juxt-fn# - ~msg-type (let [file# ~*file*] (when (not= file# "NO_SOURCE_PATH") file#)) ;; TODO Waiting on http://dev.clojure.org/jira/browse/CLJ-865: ~(:line (meta &form))))))))) @@ -550,6 +722,9 @@ ;;;; Deprecated +(defmacro with-err-as-out "DEPRECATED." [& body] `(binding [*err* *out*] ~@body)) +(def stacktrace "DEPREACTED. Use `fmt-stacktrace` instead." fmt-stacktrace) + (defmacro logp "DEPRECATED: Use `log` instead." {:arglists '([level & message] [level throwable & message])} [& sigs] `(log ~@sigs)) ; Alias @@ -613,7 +788,7 @@ {:appenders {:fmt-output-opts-test {:min-level :error :enabled? true - :fmt-output-opts {:nofonts? true} + :fmt-output-opts {:no-fonts? true} :fn (fn [{:keys [output]}] (str-println output))}}}) (log :report (Exception. "Oh noes") "Hello")) diff --git a/src/taoensso/timbre/appenders/postal.clj b/src/taoensso/timbre/appenders/postal.clj index 823cc10..d3483a1 100644 --- a/src/taoensso/timbre/appenders/postal.clj +++ b/src/taoensso/timbre/appenders/postal.clj @@ -30,7 +30,8 @@ :min-level :warn :async? true ; Slow! :rate-limit [5 (* 1000 60 2)] ; 5 calls / 2 mins - :fmt-output-opts {:nofonts? true} ; Disable ANSI-escaped stuff + ;; TODO These opts are deprecated! + :fmt-output-opts {:no-fonts? true} ; Disable ANSI-escaped stuff }] (merge default-appender-opts appender-opts diff --git a/src/taoensso/timbre/tools/logging.clj b/src/taoensso/timbre/tools/logging.clj index 7ac5c0d..ccad981 100644 --- a/src/taoensso/timbre/tools/logging.clj +++ b/src/taoensso/timbre/tools/logging.clj @@ -13,12 +13,14 @@ (deftype Logger [logger-ns] clojure.tools.logging.impl/Logger (enabled? [_ level] (timbre/logging-enabled? level)) - (write! [_ level throwable message] - ;; tools.logging message may be a string (for `logp`/`logf` calls) or raw - ;; argument (for `log` calls). The best we can do for :args is therefore - ;; `[message]`: + (write! [_ level throwable message] + ;; tools.logging message may be a string (for `logp`/`logf` calls) or + ;; single raw argument (for `log` calls). The best we can do for :args is + ;; therefore `[message]`: (timbre/send-to-appenders! level {} [message] logger-ns throwable - (when (string? message) message)))) + (when (string? message) + (delay ; Mimic Timbre's lazy message creation + message))))) (deftype LoggerFactory [] clojure.tools.logging.impl/LoggerFactory From 94caaa5fbc6f07942fab7ff2833fd3f938c7b189 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 25 May 2015 19:46:28 +0700 Subject: [PATCH 02/16] Update project.clj for v4 --- project.clj | 91 ++++++++++++++++++++++------- test/taoensso/timbre/tests/main.clj | 2 +- 2 files changed, 72 insertions(+), 21 deletions(-) diff --git a/project.clj b/project.clj index 85fc189..11e8748 100644 --- a/project.clj +++ b/project.clj @@ -1,6 +1,6 @@ -(defproject com.taoensso/timbre "3.4.0" +(defproject com.taoensso/timbre "4.0.0-SNAPSHOT" :author "Peter Taoussanis " - :description "Clojure logging & profiling library" + :description "Clojure/Script logging & profiling library" :url "https://github.com/ptaoussanis/timbre" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html" @@ -8,42 +8,93 @@ :comments "Same as Clojure"} :min-lein-version "2.3.3" :global-vars {*warn-on-reflection* true - *assert* true} + *assert* true} :dependencies [[org.clojure/clojure "1.4.0"] - [com.taoensso/encore "1.24.1"] - [io.aviso/pretty "0.1.17"]] + [com.taoensso/encore "1.30.0"] + [io.aviso/pretty "0.1.18"]] + + :plugins + [[lein-pprint "1.1.2"] + [lein-ancient "0.6.7"] + [lein-expectations "0.0.8"] + [lein-autoexpect "1.4.3"] + [codox "0.8.12"]] :profiles {;; :default [:base :system :user :provided :dev] :server-jvm {:jvm-opts ^:replace ["-server"]} :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} :1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} + :1.7 {:dependencies [[org.clojure/clojure "1.7.0-RC1"]]} :test {:dependencies [[expectations "2.1.0"] - [org.clojure/test.check "0.7.0"] [org.clojure/tools.logging "0.3.1"] - ;; Appender dependencies - [com.taoensso/nippy "2.8.0"] - [com.taoensso/carmine "2.9.2"] + ;; Appender deps + [com.taoensso/nippy "2.9.0-RC2"] + [com.taoensso/carmine "2.10.0"] [com.draines/postal "1.11.3"] - [irclj "0.5.0-alpha4"]] - :plugins [[lein-expectations "0.0.8"] - [lein-autoexpect "1.4.2"]]} + [irclj "0.5.0-alpha4"]]} :dev - [:1.6 :test - {:dependencies [] - :plugins [[lein-ancient "0.6.4"] - [codox "0.8.11"]]}]} + [:1.7 :test + {:dependencies [[org.clojure/clojurescript "0.0-3297"]] + :plugins + [;; These must be in :dev, Ref. https://github.com/lynaghk/cljx/issues/47: + [com.keminglabs/cljx "0.6.0"] + [lein-cljsbuild "1.0.6"]]}]} - :test-paths ["test" "src"] + ;; :jar-exclusions [#"\.cljx|\.DS_Store"] + :source-paths ["src" "target/classes"] + :test-paths ["src" "test" "target/test-classes"] + + :cljx + {:builds + [{:source-paths ["src"] :rules :clj :output-path "target/classes"} + {:source-paths ["src"] :rules :cljs :output-path "target/classes"} + {:source-paths ["src" "test"] :rules :clj :output-path "target/test-classes"} + {:source-paths ["src" "test"] :rules :cljs :output-path "target/test-classes"}]} + + :cljsbuild + {:test-commands {"node" ["node" :node-runner "target/tests.js"] + ;; "phantom" ["phantomjs" :runner "target/tests.js"] + } + :builds + ;; TODO Parallel builds currently cause issues with Expectations v2.1.0, + ;; Ref. http://goo.gl/8LDHe5 + [{:id "main" + :source-paths ["src" "target/classes"] + ;; :notify-command ["terminal-notifier" "-title" "cljsbuild" "-message"] + :compiler {:output-to "target/main.js" + :optimizations :advanced + :pretty-print false}} + {:id "tests" + :source-paths ["src" "target/classes" "test" "target/test-classes"] + :notify-command ["node" "target/tests.js"] + :compiler {:output-to "target/tests.js" + :optimizations :simple ; Necessary for node.js + :pretty-print true + :target :nodejs + :hashbang false ; Ref. http://goo.gl/vrtNDR + :main "taoensso.encore.tests"}}]} + + :auto-clean false + :prep-tasks [["cljx" "once"] "javac" "compile"] + + :codox {:language :clojure ; [:clojure :clojurescript] cljsbuild ; No support? + :sources ["target/classes"] + :src-linenum-anchor-prefix "L" + :src-dir-uri "http://github.com/ptaoussanis/encore/blob/master/src/" + :src-uri-mapping {#"target/classes" + #(.replaceFirst (str %) "(.cljs$|.clj$)" ".cljx")}} :aliases - {"test-all" ["with-profile" "default:+1.5:+1.6" "expectations"] - ;; "test-all" ["with-profile" "default:+1.6" "expectations"] + {"test-all" ["do" "clean," "cljx" "once," + "with-profile" "default:+1.5:+1.6:+1.7" "expectations," + "with-profile" "+test" "cljsbuild" "test"] "test-auto" ["with-profile" "+test" "autoexpect"] - "deploy-lib" ["do" "deploy" "clojars," "install"] + "build-once" ["do" "clean," "cljx" "once," "cljsbuild" "once" "main"] + "deploy-lib" ["do" "build-once," "deploy" "clojars," "install"] "start-dev" ["with-profile" "+server-jvm" "repl" ":headless"]} :repositories {"sonatype-oss-public" diff --git a/test/taoensso/timbre/tests/main.clj b/test/taoensso/timbre/tests/main.clj index c2bddfe..116d562 100644 --- a/test/taoensso/timbre/tests/main.clj +++ b/test/taoensso/timbre/tests/main.clj @@ -9,4 +9,4 @@ (defn- before-run {:expectations-options :before-run} []) (defn- after-run {:expectations-options :after-run} []) -(expect true) ; TODO Add tests (PRs welcome!) +(expect true) From 614d0103b34cfbd2b9246770e795fb58cb61f0f1 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 25 May 2015 19:47:25 +0700 Subject: [PATCH 03/16] Remove /test --- .../taoensso/timbre/tests/appenders/rotor.clj | 39 ------------------- test/taoensso/timbre/tests/main.clj | 12 ------ 2 files changed, 51 deletions(-) delete mode 100644 test/taoensso/timbre/tests/appenders/rotor.clj delete mode 100644 test/taoensso/timbre/tests/main.clj diff --git a/test/taoensso/timbre/tests/appenders/rotor.clj b/test/taoensso/timbre/tests/appenders/rotor.clj deleted file mode 100644 index 5c3b375..0000000 --- a/test/taoensso/timbre/tests/appenders/rotor.clj +++ /dev/null @@ -1,39 +0,0 @@ -(ns taoensso.timbre.tests.appenders.rotor - {:author "Ian Truslove, Kurt Harriger"} - (:require [taoensso.timbre.appenders.rotor :as rotor :refer :all] - [clojure.test :refer :all] - [clojure.java.io :refer [file]])) - -(defn with-temp-dir-containing-log-files - "Call f with the temp directory name, that directory having n log - files created within it" - [n f] - (let [tmp-dir (java.io.File/createTempFile "test" "") - log-file-basename "log" - log-files (into [log-file-basename] - (map #(format "%s.%03d" log-file-basename %) (range 1 n)))] - (.delete tmp-dir) - (.mkdirs tmp-dir) - (doseq [filename log-files] (.createNewFile (file tmp-dir filename))) - (try - (f (.getAbsolutePath (file tmp-dir (first log-files)))) - (finally - (doseq [filename log-files] (.delete (file tmp-dir filename))) - (.delete (file tmp-dir)))))) - -(deftest test-rotor - (testing "rotating logs" - (testing "when rotating with a full backlog of files, the last should be deleted" - (with-temp-dir-containing-log-files 5 - (fn [basepath] - (#'rotor/rotate-logs basepath 2) - (is (not (.exists (file (str basepath)))) - "log should have been rotated to log.001") - (is (.exists (file (str basepath ".001"))) - "log.001 should remain") - (is (.exists (file (str basepath ".002"))) - "log.002 should remain") - (is (not (.exists (file (str basepath ".003")))) - "log.003 should be deleted because it is past the max-count threshold") - (is (not (.exists (file (str basepath ".004")))) - "log.004 should be deleted because it is past the max-count threshold")))))) diff --git a/test/taoensso/timbre/tests/main.clj b/test/taoensso/timbre/tests/main.clj deleted file mode 100644 index 116d562..0000000 --- a/test/taoensso/timbre/tests/main.clj +++ /dev/null @@ -1,12 +0,0 @@ -(ns taoensso.timbre.tests.main - (:require [expectations :as test :refer :all] - [taoensso.timbre :as timbre])) - -;; (timbre/refer-timbre) - -(comment (test/run-tests '[taoensso.timbre.tests.main])) - -(defn- before-run {:expectations-options :before-run} []) -(defn- after-run {:expectations-options :after-run} []) - -(expect true) From 3853cd3e8fa7ffbcddeb4bcebb1b9f8c035e5920 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 25 May 2015 19:49:08 +0700 Subject: [PATCH 04/16] Remove vestigial utils ns --- src/taoensso/timbre/utils.clj | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 src/taoensso/timbre/utils.clj diff --git a/src/taoensso/timbre/utils.clj b/src/taoensso/timbre/utils.clj deleted file mode 100644 index 7440517..0000000 --- a/src/taoensso/timbre/utils.clj +++ /dev/null @@ -1,2 +0,0 @@ -(ns taoensso.timbre.utils - {:author "Peter Taoussanis"}) From d6a70873fce8292c1ed490dbaaa35a389147efcd Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 25 May 2015 19:51:37 +0700 Subject: [PATCH 05/16] Group 3rd-party appenders --- src/taoensso/timbre/appenders/{ => 3rd_party}/android.clj | 0 src/taoensso/timbre/appenders/{ => 3rd_party}/irc.clj | 0 src/taoensso/timbre/appenders/{ => 3rd_party}/mongo.clj | 0 src/taoensso/timbre/appenders/{ => 3rd_party}/rolling.clj | 0 src/taoensso/timbre/appenders/{ => 3rd_party}/rotor.clj | 0 src/taoensso/timbre/appenders/{ => 3rd_party}/socket.clj | 0 src/taoensso/timbre/appenders/{ => 3rd_party}/zmq.clj | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename src/taoensso/timbre/appenders/{ => 3rd_party}/android.clj (100%) rename src/taoensso/timbre/appenders/{ => 3rd_party}/irc.clj (100%) rename src/taoensso/timbre/appenders/{ => 3rd_party}/mongo.clj (100%) rename src/taoensso/timbre/appenders/{ => 3rd_party}/rolling.clj (100%) rename src/taoensso/timbre/appenders/{ => 3rd_party}/rotor.clj (100%) rename src/taoensso/timbre/appenders/{ => 3rd_party}/socket.clj (100%) rename src/taoensso/timbre/appenders/{ => 3rd_party}/zmq.clj (100%) diff --git a/src/taoensso/timbre/appenders/android.clj b/src/taoensso/timbre/appenders/3rd_party/android.clj similarity index 100% rename from src/taoensso/timbre/appenders/android.clj rename to src/taoensso/timbre/appenders/3rd_party/android.clj diff --git a/src/taoensso/timbre/appenders/irc.clj b/src/taoensso/timbre/appenders/3rd_party/irc.clj similarity index 100% rename from src/taoensso/timbre/appenders/irc.clj rename to src/taoensso/timbre/appenders/3rd_party/irc.clj diff --git a/src/taoensso/timbre/appenders/mongo.clj b/src/taoensso/timbre/appenders/3rd_party/mongo.clj similarity index 100% rename from src/taoensso/timbre/appenders/mongo.clj rename to src/taoensso/timbre/appenders/3rd_party/mongo.clj diff --git a/src/taoensso/timbre/appenders/rolling.clj b/src/taoensso/timbre/appenders/3rd_party/rolling.clj similarity index 100% rename from src/taoensso/timbre/appenders/rolling.clj rename to src/taoensso/timbre/appenders/3rd_party/rolling.clj diff --git a/src/taoensso/timbre/appenders/rotor.clj b/src/taoensso/timbre/appenders/3rd_party/rotor.clj similarity index 100% rename from src/taoensso/timbre/appenders/rotor.clj rename to src/taoensso/timbre/appenders/3rd_party/rotor.clj diff --git a/src/taoensso/timbre/appenders/socket.clj b/src/taoensso/timbre/appenders/3rd_party/socket.clj similarity index 100% rename from src/taoensso/timbre/appenders/socket.clj rename to src/taoensso/timbre/appenders/3rd_party/socket.clj diff --git a/src/taoensso/timbre/appenders/zmq.clj b/src/taoensso/timbre/appenders/3rd_party/zmq.clj similarity index 100% rename from src/taoensso/timbre/appenders/zmq.clj rename to src/taoensso/timbre/appenders/3rd_party/zmq.clj From f287de4cb2cfed440225574fc33879810745c274 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 25 May 2015 19:54:38 +0700 Subject: [PATCH 06/16] Initial new .cljx ns (complete rewrite) --- src/taoensso/timbre.clj | 802 -------------------------- src/taoensso/timbre.cljx | 451 +++++++++++++++ src/taoensso/timbre/profiling.clj | 27 +- src/taoensso/timbre/tools/logging.clj | 35 +- src/taoensso/v3.clj | 69 +++ 5 files changed, 554 insertions(+), 830 deletions(-) delete mode 100644 src/taoensso/timbre.clj create mode 100644 src/taoensso/timbre.cljx create mode 100644 src/taoensso/v3.clj diff --git a/src/taoensso/timbre.clj b/src/taoensso/timbre.clj deleted file mode 100644 index 68d0c5e..0000000 --- a/src/taoensso/timbre.clj +++ /dev/null @@ -1,802 +0,0 @@ -(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.encore :as enc]) - (:import [java.util Date Locale] - [java.text SimpleDateFormat] - [java.io File])) - -;;;; Encore version check - -(let [min-encore-version 1.21] ; Let's get folks on newer versions here - (if-let [assert! (ns-resolve 'taoensso.encore 'assert-min-encore-version)] - (assert! min-encore-version) - (throw - (ex-info - (format - "Insufficient com.taoensso/encore version (< %s). You may have a Leiningen dependency conflict (see http://goo.gl/qBbLvC for solution)." - min-encore-version) - {:min-version min-encore-version})))) - -;;;; TODO v4 -;; * Use `format`, `sprintln` from Encore. -;; * Decide on `:message` format design -;; - No delay, just require use of tool? -;; - Delay set (as wrapper) per-appender with merged `:ap-config`? -;; - :msg-type e/o #{:tools.logging :print-str :pr-str :format nil} -;; - :message key is set [only, ever] at per-appender wrapper level. -;; - Pros: great flexibility with easy config, simple. -;; - Cons: cost of per-appender delay generation. Problem? -;; * Get core working + tested. -;; * Enumerate changes from v3. -;; * Look into v3 backwards compatibility. -;; * Document changes from v3. -;; * Update bundled appenders (?). -;; * Update docs. -;; -;; * Investigate better encore/Cljs interplay: fns? -;; * Do runtime level check even if a compile-time level is in effect if the -;; provided `log` level arg is not immediately recognized (e.g. it may be a -;; runtime level form that first requires eval). - -;;;; 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." - [& xs] (print (str (str/join \space (filter identity xs)) \newline)) - (flush)) - -(defn color-str [color & xs] - (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"))] - (str (ansi-color color) (apply str xs) (ansi-color :reset)))) - -(def default-out (java.io.OutputStreamWriter. System/out)) -(def default-err (java.io.PrintWriter. System/err)) -(defmacro with-default-outs - "Evaluates body with Clojure's default *out* and *err* bindings." - [& body] `(binding [*out* default-out - *err* default-err] ~@body)) - -(defn fmt-stacktrace "Default stacktrace formatter for use by appenders." - [throwable & [separator stacktrace-fonts]] - (when throwable - (str separator - (if-let [fonts stacktrace-fonts] ; nil (defaults), or a map - (binding [aviso-ex/*fonts* fonts] (aviso-ex/format-exception throwable)) - (aviso-ex/format-exception throwable))))) - -(comment (println (fmt-stacktrace (Exception. "foo") nil nil)) - (println (fmt-stacktrace (Exception. "foo") nil {}))) - -(def get-hostname - ;; TODO Any way to keep future from affecting shutdown time, - ;; Ref. http://goo.gl/5hx9oK? - (encore/memoize* (encore/ms :mins 2) - (fn [] - (-> - (future ; Android doesn't like this on the main thread - (try (.. java.net.InetAddress getLocalHost getHostName) - (catch java.net.UnknownHostException _ - "UnknownHost"))) - (deref 5000 "UnknownHost"))))) - -(def ^:private default-message-timestamp-pattern "14-Jul-07 16:42:11" - "yy-MMM-dd HH:mm:ss") - -(def ^:private default-message-pattern-fn - "14-Jul-07 16:42:11 localhost INFO [my-app.foo.bar] - Hello world" - (fn [{:keys [ns ; & Any other appender args - ;; These are delays: - timestamp_ hostname_ level-name_ args-str_ stacktrace_]}] - (str @timestamp_ " " @hostname_ " " @level-name_ " " - "[" ns "] - " @args-str_ @stacktrace_))) - -(defn fmt-appender-args "Formats appender arguments as a message string." - [fmt-fn ; `(apply args)`: format, print-str, pr-str, etc. - {:as appender-args :keys [instant ns level throwable args]} & - [{:as fmt-opts :keys [timestamp-pattern timestamp-locale no-fonts? pattern-fn] - :or {timestamp-pattern default-message-timestamp-pattern - timestamp-locale nil - pattern-fn default-message-pattern-fn}}]] - - (when-not (empty? args) - (pattern-fn - (merge appender-args - ;; Delays since user pattern may/not want any of these: - {:hostname_ (delay (get-hostname)) - :timestamp_ (delay (.format (encore/simple-date-format timestamp-pattern - {:locale timestamp-locale}) instant)) - :level-name_ (delay (-> level name str/upper-case)) - :args-str_ (delay (apply fmt-fn args)) ; `args` is non-empty - :stacktrace_ (delay (fmt-stacktrace throwable "\n" (when no-fonts? {})))})))) - -(comment - (encore/qbench 1000 - (fmt-appender-args print-str - {:instant (Date.) :ns *ns* :level :info :throwable nil - :args ["Hello" "there"]})) ; ~14ms - ) - -(defmacro sometimes - "Executes body with probability e/o [0,1]. Useful for sampled logging." - [probability & body] - `(do (assert (<= 0 ~probability 1) "Probability: 0 <= p <= 1") - (when (< (rand) ~probability) ~@body))) - -;;;; Logging levels - -(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." - (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 levels-ordered [:trace :debug :info :warn :error :fatal :report]) -(def levels-scored (zipmap levels-ordered (next (range)))) - -(defn- level-error? [level] (boolean (#{:error :fatal} level))) -(defn- level-checked-score [level] - (or (when (nil? level) 0) ; < any valid level - (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))))) - -(defn level-sufficient? "Precendence: compile-time > dynamic > config > atom." - [level config] (<= 0 (levels-compare level (or level-compile-time - *level-dynamic* - (:current-level config) - @level-atom)))) - -;;;; - -(def ^:private get-hostname - (enc/memoize* 60000 - (fn [] - (-> - (future ; Android doesn't like this on the main thread - (try (.. java.net.InetAddress getLocalHost getHostName) - (catch java.net.UnknownHostException _ - "UnknownHost"))) - (deref 5000 "UnknownHost"))))) - -(def ^:private ensure-spit-dir-exists! - (enc/memoize* 60000 - (fn [fname] - (when-not (str/blank? fname) - (let [file (File. ^String fname) - dir (.getParentFile (.getCanonicalFile file))] - (when-not (.exists dir) - (.mkdirs dir))))))) - -;;;; Default configuration and appenders - -(def example-config - "APPENDERS - An appender is a map with keys: - :doc ; Optional docstring. - :min-level ; Level keyword, or nil (=> no minimum level). - :enabled? ; - :async? ; Dispatch using agent? Useful for slow appenders. - :rate-limit ; [ncalls-limit window-ms], or nil. - :args-hash-fn ; Used by rate-limiter, etc. - :appender-config ; Any appender-specific config. - :fn ; (fn [appender-args-map]), with keys described below. - - An appender's fn takes a single map with keys: - :instant ; java.util.Date. - :ns ; String. - :level ; Keyword. - :error? ; Is level an 'error' level? - :throwable ; java.lang.Throwable. - :args ; Raw logging macro args (as given to `info`, etc.). - ;; - :context ; Thread-local dynamic logging context. - :ap-config ; Content of appender's own `:appender-config` merged over - ; `:shared-appender-config`. - :profile-stats ; From `profile` macro. - ;; - ;; Waiting on http://dev.clojure.org/jira/browse/CLJ-865: - :file ; String. - :line ; Integer. - ;; - :message ; DELAYED string of formatted appender args. Appenders may - ; (but are not obligated to) use this as their output. - - 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 further settings and details. - See also `set-config!`, `merge-config!`, `set-level!`." - - {;; :current-level :debug ; Prefer `level-atom` - - ;;; 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 [] - - :shared-appender-config - {:message-fmt-opts ; `:message` appender argument formatting - {:timestamp-pattern default-message-timestamp-pattern ; SimpleDateFormat - :timestamp-locale nil ; A Locale object, or nil - :pattern-fn default-message-pattern-fn}} - - :appenders - {:standard-out - {:doc "Prints to *out*/*err*. Enabled by default." - :min-level nil :enabled? true :async? false :rate-limit nil - :appender-config {:always-log-to-err? false} - :fn (fn [{:keys [ap-config error? message]}] ; Can use any appender args - (binding [*out* (if (or error? (:always-log-to-err? ap-config)) - *err* *out*)] - (str-println @message)))} - - :spit - {:doc "Spits to `(:spit-filename :ap-config)` file." - :min-level nil :enabled? false :async? false :rate-limit nil - :appender-config {:spit-filename "timbre-spit.log"} - :fn (fn [{:keys [ap-config message]}] ; Can use any appender args - (when-let [filename (:spit-filename ap-config)] - (try (ensure-spit-dir-exists! filename) - (spit filename (str output "\n") :append true) - (catch java.io.IOException _))))}}}) - -(enc/defonce* config (atom example-config)) -(defn set-config! [ks val] (swap! config assoc-in ks val)) -(defn merge-config! [& maps] (apply swap! config enc/merge-deep maps)) - -;;;; Appender-fn decoration - -(defn default-args-hash-fn - "Returns a hash id for given appender args such that - (= (hash args-A) (hash args-B)) iff args A and B are \"the same\" by - some reasonable-in-the-general-case definition for logging args. Useful for - rate limiting, deduplicating appenders, etc." - [{:keys [ns line args] :as apfn-args}] - (str (or (some #(and (map? %) (:timbre/hash %)) args) ; Explicit hash given - ;; [ns line] ; TODO Waiting on http://goo.gl/cVVAYA - [ns args]))) - -(defn- wrap-appender-fn - [config {:as appender apfn :fn - :keys [async? rate-limit args-hash-fn appender-config] - :or {args-hash-fn default-args-hash-fn}}] -<<<<<<< HEAD - (let [rate-limit (or rate-limit ; Backwards comp: - (if-let [x (:max-message-per-msecs appender)] [1 x] - (when-let [x (:limit-per-msecs appender)] [1 x])))] - - (assert (or (nil? rate-limit) (vector? rate-limit))) - - (->> ; Wrapping applies per appender, bottom-to-top - apfn - - ;; Custom appender-level fmt-output-opts - ((fn [apfn] ; Compile-time: - (if-not fmt-output-opts apfn ; Common case (no appender-level fmt opts) - (fn [apfn-args] ; Runtime: - ;; Replace default (juxt-level) output: - (apfn (assoc apfn-args :output - ((:fmt-output-fn config) apfn-args fmt-output-opts))))))) - - ;; Rate limit support - ((fn [apfn] - ;; Compile-time: - (if-not rate-limit apfn - (let [[ncalls-limit window-ms] rate-limit - limiter-any (enc/rate-limiter ncalls-limit window-ms) - ;; This is a little hand-wavy but it's a decent general - ;; strategy and helps us from making this overly complex to - ;; configure. - limiter-specific (enc/rate-limiter (quot ncalls-limit 4) - window-ms)] - (fn [{:keys [ns args] :as apfn-args}] - ;; Runtime: (test smaller limit 1st): - (when-not (or (limiter-specific (args-hash-fn apfn-args)) - (limiter-any)) - (apfn apfn-args))))))) - - ;; Async (agent) support - ((fn [apfn] - ;; Compile-time: - (if-not async? apfn - (let [agent (agent nil :error-mode :continue)] - (fn [apfn-args] ; Runtime: - (send-off agent (fn [_] (apfn apfn-args))))))))))) - -(defn- wrap-appender-juxt - "Wraps compile-time appender juxt with additional runtime capabilities - (incl. middleware) controlled by compile-time config. Like `wrap-appender-fn` - but operates on the entire juxt at once." - [config juxtfn] - (->> ; 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 - (if-not timestamp-pattern (constantly nil) - (fn [^Date dt] - (.format (enc/simple-date-format timestamp-pattern - {:locale timestamp-locale}) dt)))] - - (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) - ;; TODO Consider a breaking change here to - ;; swap assoc'd message with a delay, as - ;; per http://goo.gl/7YVSfj: - (assoc :message - (when-not (empty? args) - (case msg-type - :format (apply format args) - :print-str (apply print-str args) - :nil nil))))) - juxtfn-args (assoc juxtfn-args :timestamp (timestamp-fn instant)) - juxtfn-args (assoc juxtfn-args - ;; DEPRECATED, here for backwards comp: - :prefix (when-let [f prefix-fn] (f juxtfn-args)) - :output (when-let [f fmt-output-fn] (f juxtfn-args)))] - (juxtfn juxtfn-args))))))) - - ;; 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 -======= - (assert (or (nil? rate-limit) (vector? rate-limit))) - (->> ; Wrapping applies per appender, bottom-to-top - apfn - - ;; :ap-config - ((fn [apfn] - ;; Compile-time: - (if-not appender-config apfn - (let [merged-config (merge (:shared-appender-config config) - appender-config)] - (println "DEBUG! `merged-config`:" merged-config) ; TODO - (fn [apfn-args] - ;; Runtime: - (apfn (assoc apfn-args :ap-config merged-config))))))) - - ;; Rate limits - ((fn [apfn] - ;; Compile-time: - (if-not rate-limit apfn - (let [[ncalls-limit window-ms] rate-limit - limiter-any (encore/rate-limiter ncalls-limit window-ms) - ;; This is a little hand-wavy but it's a decent general - ;; strategy and helps us from making this overly complex to - ;; configure. - limiter-specific (encore/rate-limiter (quot ncalls-limit 4) - window-ms)] - (fn [{:keys [ns args] :as apfn-args}] - ;; Runtime: - (when-not (or (limiter-specific (args-hash-fn apfn-args)) - (limiter-any)) ; Test smaller limit 1st - (apfn apfn-args))))))) - - ;; Async (agents) - ((fn [apfn] - ;; Compile-time: - (if-not async? apfn - (let [agent (agent nil :error-mode :continue)] - (fn [apfn-args] ; Runtime: - (send-off agent (fn [_] (apfn apfn-args)))))))))) - -(def ^:dynamic *context* "Thread-local dynamic logging context." {}) -(defn- wrap-appender-juxt [config juxtfn] - (->> ; Wrapping applies per juxt, bottom-to-top - juxtfn - - ;; ;; Post-middleware stuff - ;; ((fn [juxtfn] - ;; ;; Compile-time: - ;; (fn [juxtfn-args] - ;; ;; Runtime: - ;; (juxtfn juxtfn-args)))) - - ;; Middleware (transforms/filters) ->>>>>>> fe51297... NB Experimental: major refactor (currently breaking, for potential Timbre v4) - ((fn [juxtfn] - ;; Compile-time: - (let [middleware (:middleware config)] - (if (empty? middleware) juxtfn - (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)))))))) - - ;; ;; Pre-middleware stuff - ;; ((fn [juxtfn] - ;; ;; Compile-time: - ;; (fn [juxtfn-args] - ;; ;; Runtime: - ;; (juxtfn juxtfn-args)))) - )) - -;;;; Config compilation - -(defn- relevant-appenders [appenders level] - (->> appenders - (filter #(let [{:keys [enabled? min-level]} (val %)] - (and enabled? (>= (levels-compare level min-level) 0)))) - (into {}))) - -(defn- ns-match? [ns match] - (-> (str "^" (-> (str match) (.replace "." "\\.") (.replace "*" "(.*)")) "$") - re-pattern (re-find (str ns)) boolean)) - -(def compile-config ; Used in macros, must be public - "Implementation detail. - Returns {:appenders-juxt { } - :ns-filter (fn relevant-ns? [ns])}." - (memoize - ;; Careful. The presence of fns means that inline config's won't correctly - ;; be identified as samey. In practice not a major (?) problem since configs - ;; will usually be assigned to a var for which we have proper identity. - (fn [{:keys [appenders] :as config}] - {:appenders-juxt - (zipmap levels-ordered - (->> levels-ordered - (map (fn [l] (let [rel-aps (relevant-appenders appenders l)] - ;; Return nil if no relevant appenders - (when-let [ap-ids (keys rel-aps)] - (->> ap-ids - (map #(wrap-appender-fn config (rel-aps %))) - (apply juxt) - (wrap-appender-juxt config)))))))) - :ns-filter - (let [{:keys [ns-whitelist ns-blacklist]} config] - (if (and (empty? ns-whitelist) (empty? ns-blacklist)) - (fn relevant-ns? [ns] true) - (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)))))))}))) - -(comment (compile-config example-config) - (compile-config nil)) - -;;;; Logging macros - -(def ^:dynamic *config-dynamic* nil) -(defmacro with-logging-config - "Allows thread-local logging config override. Useful for dev & testing." - [config & body] `(binding [*config-dynamic* ~config] ~@body)) - -(defn get-default-config [] (or *config-dynamic* @config)) - -(defmacro with-logging-context "Thread-local dynamic logging context." - [context & body] `(binding [*context* ~context] ~@body)) - -(defn ns-unfiltered? [config ns] ((:ns-filter (compile-config config)) ns)) - -(defn logging-enabled? "For 3rd-party utils, etc." - [level & [compile-time-ns]] - (let [config' (get-default-config)] - (and (level-sufficient? level config') - (or (nil? compile-time-ns) - (ns-unfiltered? config' compile-time-ns))))) - -(defn send-to-appenders! "Implementation detail." - [{:keys [;; Args provided by both Timbre, tools.logging: - level base-appender-args log-vargs ns throwable message - ;; Additional args provided by Timbre only: - juxt-fn file line]}] - (when-let [juxt-fn (or juxt-fn (get-in (compile-config (get-default-config)) - [:appenders-juxt level]))] - (let [appender-args - (conj (or base-appender-args {}) - {;;; Passed through - :level level - :args log-vargs ; String / 1-vec raw arg for tools.logging impl - :ns ns - :throwable throwable - :file file ; Nil for tools.logging - :line line ; '' - - ;;; Generated - :instant (Date.) - :error? (level-error? level) - - ;;; Varies - :message message})] - (juxt-fn appender-args) - nil))) - -(comment ; TODO - (delay - (fmt-appender-args ; TODO + maybe merge :ap-config for fmt opts? - ;; Or just have in :shared-appender-config - (case msg-type - :format format - :print-str print-str - :pr-str pr-str)))) - -(comment ; TODO - [fmt-fn ; `(apply args)`: format, print-str, pr-str, etc. - {:as appender-args :keys [instant ns level throwable args]} & - [{:as fmt-opts :keys [timestamp-pattern timestamp-locale no-fonts? pattern-fn] - :or {timestamp-pattern default-message-timestamp-pattern - timestamp-locale nil - pattern-fn default-message-pattern-fn}}]]) - -(defn send-to-appenders! "Implementation detail." - ([level base-appender-args log-vargs ns throwable message]) - - - - - ) - -(defn send-to-appenders! "Implementation detail." - [;; Args provided by both Timbre, tools.logging: - level base-appender-args log-vargs ns throwable message - ;; Additional args provided by Timbre only: - & [juxt-fn file line]] - (when-let [juxt-fn (or juxt-fn (get-in (compile-config (get-default-config)) - [:appenders-juxt level]))] - (juxt-fn - (conj (or base-appender-args {}) - {;;; Generated - :instant (Date.) - :error? (level-error? level) - - ;;; Passed through - :ns ns - :level level - :throwable throwable - :message message - - ;;; Passed through (no/limited tools.logging support) - :file file ; Nil for tools.logging impl - :line line ; '' - :args log-vargs ; String / 1-vec raw arg for tools.logging impl - })) - nil)) - -(defmacro get-compile-time-ns [] (str *ns*)) ; Nb need `str` to be readable -(comment (macroexpand '(get-compile-time-ns))) - -(defmacro log* "Implementation detail." - {:arglists '([base-appender-args fmt-fn level & log-args] - [base-appender-args fmt-fn config level & log-args])} - [base-appender-args fmt-fn & [s1 s2 :as sigs]] - ;; Compile-time: - (when (or (nil? level-compile-time) - (let [level (cond (levels-scored s1) s1 - (levels-scored s2) s2)] - (or (nil? level) ; Also needs to be compile-time - (level-sufficient? level nil)))) - ;; Runtime: - `(let [;;; Support [level & log-args], [config level & log-args] sigs: - s1# ~s1 - default-config?# (levels-scored s1#) - config# (if default-config?# (get-default-config) s1#) - level# (if default-config?# s1# ~s2) - compile-time-ns# (get-compile-time-ns)] - ;; (println "DEBUG: Runtime level check") - (when (and (level-sufficient? level# config#) - (ns-unfiltered? config# compile-time-ns#)) - (when-let [juxt-fn# (get-in (compile-config config#) - [:appenders-juxt level#])] - (let [[x1# & xn# :as xs#] (if default-config?# - (vector ~@(next sigs)) - (vector ~@(nnext sigs))) - has-throwable?# (instance? Throwable x1#) - log-vargs# (vec (if has-throwable?# xn# xs#))] - (send-to-appenders! - level# - ~base-appender-args - log-vargs# - compile-time-ns# - (when has-throwable?# x1#) - - - ;; TODO - nil ; Timbre generates msg only after middleware - - - juxt-fn# - (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. Takes optional logging config (defaults to - `timbre/@config`.)" - {:arglists '([level & message] [level throwable & message] - [config level & message] [config level throwable & message])} - [& sigs] `(log* {} :print-str ~@sigs)) - -(defmacro logf - "Logs using format-style args. Takes optional logging config (defaults to - `timbre/@config`.)" - {:arglists '([level fmt & fmt-args] [level throwable fmt & fmt-args] - [config level fmt & fmt-args] [config level throwable fmt & fmt-args])} - [& sigs] `(log* {} :format ~@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 - "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)) - ([level expr] `(spy ~level '~expr ~expr)) - ([level name expr] - `(log-and-rethrow-errors - (let [result# ~expr] (log ~level ~name result#) result#)))) - -(defmacro ^:private def-logger [level] - (let [level-name (name level)] - `(do - (defmacro ~(symbol level-name) - ~(str "Logs at " level " level using print-style args.") - ~'{:arglists '([& message] [throwable & message])} - [& sigs#] `(log ~~level ~@sigs#)) - - (defmacro ~(symbol (str level-name "f")) - ~(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 - -(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 with-logging-config - sometimes)]) - (require - '[taoensso.timbre.profiling :as profiling - :refer (pspy pspy* profile defnp p p*)])" - [] - (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 with-logging-config - sometimes)]) - (require - '[taoensso.timbre.profiling :as profiling - :refer (pspy pspy* profile defnp p p*)])) - -;;;; Deprecated - -(defmacro with-err-as-out "DEPRECATED." [& body] `(binding [*err* *out*] ~@body)) -(def stacktrace "DEPREACTED. Use `fmt-stacktrace` instead." fmt-stacktrace) - -(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") - (log (or nil :info) "Booya") - - (info "a%s" "b") - (infof "a%s" "b") - - (info {} "a") - (log {} :info "a") - (log example-config :info "a") - - (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->15ms - (time (dotimes [n 10000] (when false))) - (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))))]) - - ;; fmt-output-opts - (-> (merge example-config - {:appenders - {:fmt-output-opts-test - {:min-level :error :enabled? true - :fmt-output-opts {:no-fonts? true} - :fn (fn [{:keys [output]}] (str-println output))}}}) - (log :report (Exception. "Oh noes") "Hello")) - - ;; compile-time level (enabled log* debug println) - (def level-compile-time :warn) - (debug "hello") - - (log :info "hello") ; Discarded at compile-time - (log {} :info) ; Discarded at compile-time - (log (or :info) "hello") ; Discarded at runtime - ) diff --git a/src/taoensso/timbre.cljx b/src/taoensso/timbre.cljx new file mode 100644 index 0000000..5e9f617 --- /dev/null +++ b/src/taoensso/timbre.cljx @@ -0,0 +1,451 @@ +(ns taoensso.timbre + "Simple, flexible logging for Clojure/Script. No XML." + {:author "Peter Taoussanis"} + + #+clj + (:require [clojure.string :as str] + [io.aviso.exception :as aviso-ex] + [taoensso.encore :as enc :refer (have have? qb)]) + + #+cljs + (:require [clojure.string :as str] + [taoensso.encore :as enc :refer ()]) + #+cljs + (:require-macros + [taoensso.encore :as enc :refer (have have?)]) + + #+clj + (:import [java.util Date Locale] + [java.text SimpleDateFormat] + [java.io File])) + +;;;; TODO +;; - Check for successful cljs compile +;; - Bump encore version + min version check +;; - Clj default appenders +;; - Simple config flag to log std err -> out +;; - Cljs default appenders +;; - Port profiling ns (cljs support?) +;; - Document shutdown-agents, +;; Ref. https://github.com/ptaoussanis/timbre/pull/100/files +;; - Try ease backward comp +;; - Port appenders +;; - Update README, CHANGELOG + +;;;; Encore version check + +#+clj +(let [min-encore-version 1.30] + (if-let [assert! (ns-resolve 'taoensso.encore 'assert-min-encore-version)] + (assert! min-encore-version) + (throw + (ex-info + (format + "Insufficient com.taoensso/encore version (< %s). You may have a Leiningen dependency conflict (see http://goo.gl/qBbLvC for solution)." + min-encore-version) + {:min-version min-encore-version})))) + +;;;; Config + +(def example-config + "Example (+default) Timbre config map." ; TODO + {:level :debug + :appenders ; TODO + {:println + {:min-level nil :enabled? true :async? false :rate-limit nil + :fn (fn [data] + (println ((:output-fn data) data)))}}}) + +(enc/defonce* ^:dynamic *config* example-config) +(defmacro with-config [config & body] `(binding [*config* ~config] ~@body)) +(defn swap-config! [f] + #+cljs (set! *config* (f *config*)) + #+clj (alter-var-root #'*config* f)) + +(defn set-config! [m] (swap-config! (fn [_] m))) +(defn merge-config! [m] (swap-config! (fn [old] (enc/nested-merge old m)))) + +(defn set-level! [level] (swap-config! (fn [m] (merge m {:level level})))) +(defn with-level [level & body] + `(binding [*config* (merge *config* {:level ~level})] ~@body)) + +(comment (set-level! :info) *config*) + +;;;; Levels + +(def ^:private ordered-levels [:trace :debug :info :warn :error :fatal :report]) +(def ^:private scored-levels (zipmap ordered-levels (next (range)))) +(def ^:private valid-level + (let [valid-level-set (set ordered-levels)] + (fn [level] + (or (valid-level-set level) + (throw (ex-info (str "Invalid logging level: " level) {:level level})))))) + +(comment (valid-level :info)) + +(defn level>= [x y] (>= (long (scored-levels (valid-level x))) + (long (scored-levels (valid-level y))))) + +(comment (level>= :info :debug)) + +#+clj (defn- env-val [id] (when-let [s (System/getenv id)] (enc/read-edn s))) +#+clj (def ^:private compile-time-level + (have [:or nil? valid-level] (keyword (env-val "TIMBRE_LEVEL")))) + +(defn get-active-level [& [config]] (or (:level (or config *config*)) :report)) +(comment (qb 10000 (get-active-level))) + +(comment (binding [*config* {:level :trace}] (level>= :trace (get-active-level)))) + +;;;; ns filter + +(def ^:private compile-ns-filters + (let [->re-pattern + (fn [x] + (enc/cond! + (enc/re-pattern? x) x + (string? x) + (let [s (-> (str "^" x "$") + (str/replace "." "\\.") + (str/replace "*" "(.*)"))] + (re-pattern s))))] + + (enc/memoize_ + (fn [whitelist blacklist] + (let [whitelist* (mapv ->re-pattern whitelist) + blacklist* (mapv ->re-pattern blacklist) + + white-filter + (cond + ;; (nil? whitelist) (fn [ns] false) + (empty? whitelist*) (fn [ns] true) + :else (fn [ns] (some #(re-find % ns) whitelist*))) + + black-filter + (cond + (empty? blacklist*) (fn [ns] true) + :else (fn [ns] (not (some #(re-find % ns) blacklist*))))] + + [white-filter black-filter]))))) + +(def ^:private ns-filter + (enc/memoize_ + (fn [whitelist blacklist ns] + (let [[white-filter black-filter] (compile-ns-filters whitelist blacklist)] + (when (and (white-filter ns) (black-filter ns)) ns))))) + +(comment (qb 10000 (ns-filter ["foo.*"] ["foo.baz"] "foo.bar"))) + +#+clj +(def ^:private compile-time-ns-filter + (let [whitelist (have [:or nil? vector?] (env-val "TIMBRE_NS_WHITELIST")) + blacklist (have [:or nil? vector?] (env-val "TIMBRE_NS_BLACKLIST"))] + (partial ns-filter whitelist blacklist))) + +;;;; Utils + +(defmacro delay-vec [coll] (mapv (fn [in] `(delay ~in)) coll)) +(comment + (qb 10000 (delay :foo) (fn [] :foo)) + (macroexpand '(delay-vec [(do (println "hi") :x) :y :z]))) + +(defn- vsplit-err1 [[v1 :as v]] (if-not (enc/error? v1) [nil v] (enc/vsplit-first v))) +(comment + (vsplit-err1 [:a :b :c]) + (vsplit-err1 [(Exception.) :a :b :c])) + +(declare stacktrace) + +(defn default-output-fn [data & [opts]] + (let [{:keys [level ?err_ vargs_ msg-fn ?ns-str hostname_ timestamp_]} data] + (str (force timestamp_) " " + #+clj @hostname_ #+clj " " + (str/upper-case (name level)) + " [" ?ns-str "] - " (msg-fn vargs_) + (when-let [err (force ?err_)] (str "\n" (stacktrace err)))))) + +(comment (infof (Exception.) "Hello %s" "Steve")) + +(defn default-data-hash-fn [data] + (let [{:keys [?ns-str ?line vargs_]} data + vargs (force vargs_)] + (str + (or (some #(and (map? %) (:timbre/hash %)) vargs) ; Explicit hash given + #_[?ns-str ?line] ; TODO Waiting on http://goo.gl/cVVAYA + [?ns-str vargs])))) + +(comment (default-data-hash-fn {})) + +(enc/defonce* ^:private get-agent + (enc/memoize_ (fn [appender-id] (agent nil :error-mode :continue)))) + +(comment (get-agent :my-appender)) + +(enc/defonce* ^:private get-rate-limiter + (enc/memoize_ (fn [appender-id specs] (enc/rate-limiter* specs)))) + +(comment (def rf (get-rate-limiter :my-appender [[10 5000]]))) + +;;;; Logging core + +(defn log? [level & [?ns-str config]] + (let [config (or config *config*)] + (and (level>= level (get-active-level config)) + (ns-filter (:whitelist config) (:blacklist config) (or ?ns-str "")) + true))) + +(comment (log? :trace)) + +(def ^:dynamic *context* "General-purpose dynamic logging context." nil) +(defmacro with-context [context & body] `(binding [*context* ~context] ~@body)) + +(declare get-hostname) + +;;;; TODO Temp, work on timestamps +;; want a simple, pattern-based + +(def ^:private default-timestamp-pattern "14-Jul-07 16:42:11" + "yy-MMM-dd HH:mm:ss") + +(.format + (enc/simple-date-format default-timestamp-pattern + {:locale (Locale. "en") + ;; :timezone "foo" + }) (enc/now-dt)) + + + +;;;; TODO + + +(defn log* "Core fn-level logger. Implementation detail." + [config level ?ns-str ?file ?line msg-type dvargs & [base-data]] + (when (log? level ?ns-str config) + (let [instant (enc/now-dt) + vargs*_ (delay (vsplit-err1 (mapv force dvargs))) + ?err_ (delay (get @vargs*_ 0)) + vargs_ (delay (get @vargs*_ 1)) + msg-fn (fn [vargs_] ; Post-middleware vargs, etc. + (when-not (nil? msg-type) + (when-let [vargs (have [:or nil? vector?] (force vargs_))] + (case msg-type + :print (enc/spaced-str vargs) + :format (let [[fmt args] (enc/vsplit-first vargs)] + (enc/format* fmt args)))))) + data + (merge base-data *context* + {:config config ; Entire config! + :instant instant + :level level + :?ns-str ?ns-str + :?file ?file + :?line ?line + :?err_ ?err_ + :vargs_ vargs_ + :msg-fn msg-fn + #+clj :hostname_ #+clj (delay (get-hostname)) + :error-level? (#{:error :fatal} level)}) + + ?data + (reduce ; Apply middleware: data->?data + (fn [acc mf] + (let [result (mf acc)] + (if (nil? result) + (reduced nil) + result))) + data + (:middleware config))] + + (when-let [data ?data] ; Not filtered by middleware + (reduce-kv + (fn [_ id appender] + (when + (and + (:enabled? appender) + (level>= level (or (:min-level appender) :trace)) + (let [rate-limit-specs (:rate-limit appender)] + (if (empty? rate-limit-specs) + true + (let [rl-fn (get-rate-limiter id rate-limit-specs) + hash-fn (or (:data-hash-fn appender) + (:data-hash-fn config) + default-data-hash-fn) + data-hash (hash-fn data)] + (not (rl-fn data-hash)))))) + + (let [{:keys [async?] apfn :fn} appender + output-fn (or (:output-fn appender) + (:output-fn config) + default-output-fn) + + ;; TODO Grab config (tz, pattern, locale, etc.) from + timestamp_ (delay "TODO") + + + data (assoc data :output-fn + )] + + :timestamp_ timestamp_ + ;; :output-fn output-fn + + ;; :timestamp_ (delay "maybe?") ; TODO Nix? + + + + + + (if-not async? + (apfn data) + (send-off (get-agent id) (fn [_] (apfn data))))))) + nil + (enc/clj1098 (:appenders config)))))) + nil) + +(comment + (log* *config* :info + nil nil nil :print (delay-vec [(do (println "hi") :x) :y]))) + +;;;; Logging macros + +(defmacro log "Core macro-level logger." + [config level msg-type args & [base-data]] + + ;; Compile-time elision: + (when (or (nil? compile-time-level) (level>= level compile-time-level)) + (when (compile-time-ns-filter (str *ns*)) + + (let [ns-str (str *ns*) + ?file (let [f *file*] (when (not= f "NO_SOURCE_PATH") f)) + ;; TODO Waiting on http://dev.clojure.org/jira/browse/CLJ-865: + ?line (:line (meta &form))] + `(log* ~config ~level ~ns-str ~?file ~?line ~msg-type + (delay-vec ~args) ~base-data))))) + +(defmacro ^:private def-logger [level] + (let [level-name (name level)] + `(do + (defmacro ~(symbol (str level-name #_"p")) + ~(str "Logs at " level " level using print-style args.") + ~'{:arglists '([& message] [error & message])} + [& sigs#] `(log *config* ~~level :print ~sigs#)) + + (defmacro ~(symbol (str level-name "f")) + ~(str "Logs at " level " level using format-style args.") + ~'{:arglists '([fmt & fmt-args] [error fmt & fmt-args])} + [& sigs#] `(log *config* ~~level :format ~sigs#))))) + +(defmacro def-loggers [] + `(do ~@(map (fn [level] `(def-logger ~level)) ordered-levels))) + +(def-loggers) + +(comment (infof "hello %s" "world")) + +(defmacro log-errors [& body] + `(let [[?result# ?error#] (enc/catch-errors ~@body)] + (when-let [e# ?error#] (error e#)) + ?result#)) + +(defmacro log-and-rethrow-errors [& body] + `(let [[?result# ?error#] (enc/catch-errors ~@body)] + (when-let [e# ?error#] (error e#) (throw e#)) + ?result#)) + +(defmacro logged-future [& body] `(future (log-errors ~@body))) + +(comment + (log-errors (/ 0)) + (log-and-rethrow-errors (/ 0)) + (logged-future (/ 0))) + +(defmacro spy + "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)) + ([ level expr] `(spy ~level '~expr ~expr)) + ([ level name expr] `(spy *config* ~level ~name ~expr)) + ([config level name expr] + `(log-and-rethrow-errors + (let [result# ~expr] + (log ~config ~level :print [~name "=>" result#]) + result#)))) + +#+clj +(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)]) + (require '[taoensso.timbre.profiling :as profiling + :refer (pspy pspy* profile defnp p p*)])" + [] + (require '[taoensso.timbre :as timbre + :refer (log trace debug info warn error fatal report + logf tracef debugf infof warnf errorf fatalf reportf + spy)]) + (require '[taoensso.timbre.profiling :as profiling + :refer (pspy pspy* profile defnp p p*)])) + +;;;; Public utils + +#+clj +(defn color-str [color & xs] + (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"))] + (str (ansi-color color) (apply str xs) (ansi-color :reset)))) + +#+clj (def default-out (java.io.OutputStreamWriter. System/out)) +#+clj (def default-err (java.io.PrintWriter. System/err)) + +(defmacro with-default-outs [& body] + `(binding [*out* default-out, *err* default-err] ~@body)) + +#+clj +(def get-hostname + ;; Note that this triggers slow shutdown, Ref. http://goo.gl/5hx9oK: + (enc/memoize* (enc/ms :mins 1) + (fn [] + (let [f_ (future ; Android doesn't like this on the main thread + (try (.. java.net.InetAddress getLocalHost getHostName) + (catch java.net.UnknownHostException _ "UnknownHost")))] + (deref f_ 5000 "UnknownHost"))))) + +(comment (get-hostname)) + +(defn stacktrace [err & [opts]] + #+cljs (str err) ; TODO Alternatives? + #+clj + (if-let [fonts (:stacktrace-fonts opts)] + (binding [aviso-ex/*fonts* fonts] (aviso-ex/format-exception err)) + (aviso-ex/format-exception err))) + +(comment (stacktrace (Exception. "Boo"))) + +#+clj +(def ^:private ensure-spit-dir-exists! + (enc/memoize* (enc/ms :mins 1) + (fn [fname] + (when-not (str/blank? fname) + (let [file (File. ^String fname) + dir (.getParentFile (.getCanonicalFile file))] + (when-not (.exists dir) (.mkdirs dir))))))) + +(defmacro sometimes "Handy for sampled logging, etc." + [probability & body] + `(do (assert (<= 0 ~probability 1) "Probability: 0 <= p <= 1") + (when (< (rand) ~probability) ~@body))) + + + +;;;; TODO Scratch +;; :keys [timestamp-pattern timestamp-locale +;; prefix-fn fmt-output-fn]} config +;; timestamp-fn +;; (if-not timestamp-pattern (constantly nil) +;; (fn [^Date dt] +;; (.format (enc/simple-date-format timestamp-pattern +;; {:locale timestamp-locale}) dt)))] diff --git a/src/taoensso/timbre/profiling.clj b/src/taoensso/timbre/profiling.clj index 9f30adf..ec4ef23 100644 --- a/src/taoensso/timbre/profiling.clj +++ b/src/taoensso/timbre/profiling.clj @@ -1,15 +1,20 @@ (ns taoensso.timbre.profiling "Logging profiler for Timbre, adapted from clojure.contrib.profile." {:author "Peter Taoussanis"} - (:require [taoensso.encore :as encore] + (:require [taoensso.encore :as enc] [taoensso.timbre :as timbre])) +;;;; TODO ns could use some housekeeping +;; * Boxed math optimizations +;; * Possible porting to .cljx (any point?) +;; * Support for explicit `config` args? +;; * General housekeeping, perf work + ;;;; Utils (defmacro fq-keyword "Returns namespaced keyword for given id." - [id] - `(if (and (keyword? ~id) (namespace ~id)) ~id - (keyword (timbre/get-compile-time-ns) (name ~id)))) + [id] `(if (and (keyword? ~id) (namespace ~id)) ~id + (keyword ~(str *ns*) (name ~id)))) (comment (map #(fq-keyword %) ["foo" :foo :foo/bar])) @@ -56,7 +61,7 @@ (declare ^:private format-stats) (defmacro with-pdata [level & body] - `(if-not (timbre/logging-enabled? ~level (timbre/get-compile-time-ns)) + `(if-not (timbre/log? ~level ~(str *ns*)) {:result (do ~@body)} (binding [*pdata* (atom {})] {:result (pspy ::clock-time ~@body) @@ -73,9 +78,9 @@ [level id & body] `(let [{result# :result stats# :stats} (with-pdata ~level ~@body)] (when stats# - (timbre/log* {:profile-stats stats#} :format ~level - "Profiling: %s\n%s" (fq-keyword ~id) - (format-stats stats#))) + (timbre/log timbre/*config* ~level :format + ["Profiling: %s\n%s" (fq-keyword ~id) (format-stats stats#)] + {:profile-stats stats#})) result#)) (defmacro sampling-profile @@ -87,7 +92,7 @@ ;;;; Data capturing & aggregation -(def ^:private ^:constant stats-gc-n 111111) +(def ^:private stats-gc-n 111111) (defn capture-time! [id t-elapsed] (let [ntimes @@ -182,7 +187,7 @@ (let [nanosecs (long nanosecs) ; Truncate any fractional nanosecs pow #(Math/pow 10 %) ok-pow? #(>= nanosecs (pow %)) - to-pow #(encore/round (/ nanosecs (pow %1)) :round %2)] + to-pow #(enc/round (/ nanosecs (pow %1)) :round %2)] (cond (ok-pow? 9) (str (to-pow 9 1) "s") (ok-pow? 6) (str (to-pow 6 0) "ms") (ok-pow? 3) (str (to-pow 3 0) "μs") @@ -207,7 +212,7 @@ '([name doc-string? attr-map? [params*] prepost-map? body] [name doc-string? attr-map? ([params*] prepost-map? body)+ attr-map?])} [name' & sigs] - (let [[name' sigs] (encore/name-with-attrs name' sigs) + (let [[name' sigs] (enc/name-with-attrs name' sigs) single-arity? (vector? (first sigs)) [sigs func->str] (if single-arity? diff --git a/src/taoensso/timbre/tools/logging.clj b/src/taoensso/timbre/tools/logging.clj index ccad981..9dee248 100644 --- a/src/taoensso/timbre/tools/logging.clj +++ b/src/taoensso/timbre/tools/logging.clj @@ -1,32 +1,33 @@ (ns taoensso.timbre.tools.logging "clojure.tools.logging.impl/Logger implementation. - Limitations: - * No support for zero-overhead compile-time logging levels (`enabled?` - called as a fn). - * No support for ns filtering (`write!` called as a fn and w/o compile-time - ns info). - * Limited raw `:args` support (`write!` called w/o raw args)." + The tools.logging API has some significant limits that native Timbre does not. + Only use Timbre through tools.logging if you absolutely must (e.g. you're + working with a legacy codebase)." (:require [clojure.tools.logging] [taoensso.timbre :as timbre])) (deftype Logger [logger-ns] clojure.tools.logging.impl/Logger - (enabled? [_ level] (timbre/logging-enabled? level)) - (write! [_ level throwable message] - ;; tools.logging message may be a string (for `logp`/`logf` calls) or - ;; single raw argument (for `log` calls). The best we can do for :args is - ;; therefore `[message]`: - (timbre/send-to-appenders! level {} [message] logger-ns throwable - (when (string? message) - (delay ; Mimic Timbre's lazy message creation - message))))) + + ;; Limitations: no support for explicit config, or ns filtering + (enabled? [_ level] (timbre/log? level)) + + ;; Limitations inline + (write! [_ level throwable message] + (let [config *config* ; No support for explicit config + ?ns-str nil ; No support + ?file nil ; '' + ?line nil ; '' + msg-type :print ; No support for pre-msg raw args + ] + (timbre/log* config level ?ns-str ?file ?line msg-type [message])))) (deftype LoggerFactory [] clojure.tools.logging.impl/LoggerFactory (name [_] "Timbre") - (get-logger [_ logger-ns] (->Logger logger-ns))) + (get-logger [_ logger-ns] (Logger. logger-ns))) (defn use-timbre [] (alter-var-root (var clojure.tools.logging/*logger-factory*) - (constantly (->LoggerFactory)))) + (constantly (LoggerFactory.)))) diff --git a/src/taoensso/v3.clj b/src/taoensso/v3.clj new file mode 100644 index 0000000..6b49fa3 --- /dev/null +++ b/src/taoensso/v3.clj @@ -0,0 +1,69 @@ +;;;; Default configuration and appenders + +(def example-config + "APPENDERS + An appender is a map with keys: + :doc ; Optional docstring. + :min-level ; Level keyword, or nil (=> no minimum level). + :enabled? ; + :async? ; Dispatch using agent? Useful for slow appenders. + :rate-limit ; [ncalls-limit window-ms], or nil. + :args-hash-fn ; Used by rate-limiter, etc. + :appender-config ; Any appender-specific config. + :fn ; (fn [appender-args-map]), with keys described below. + + An appender's fn takes a single map with keys: + :instant ; java.util.Date. + :ns ; String. + :level ; Keyword. + :error? ; Is level an 'error' level? + :throwable ; java.lang.Throwable. + :args ; Raw logging macro args (as given to `info`, etc.). + ;; + :context ; Thread-local dynamic logging context. + :ap-config ; Content of appender's own `:appender-config` merged over + ; `:shared-appender-config`. + :profile-stats ; From `profile` macro. + ;; + ;; Waiting on http://dev.clojure.org/jira/browse/CLJ-865: + :file ; String. + :line ; Integer. + ;; + :message ; DELAYED string of formatted appender args. Appenders may + ; (but are not obligated to) use this as their output. + + 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 further settings and details. + See also `set-config!`, `merge-config!`, `set-level!`." + + { + + :shared-appender-config + {:message-fmt-opts ; `:message` appender argument formatting + {:timestamp-pattern default-message-timestamp-pattern ; SimpleDateFormat + :timestamp-locale nil ; A Locale object, or nil + :pattern-fn default-message-pattern-fn}} + + :appenders + {:standard-out + {:doc "Prints to *out*/*err*. Enabled by default." + :min-level nil :enabled? true :async? false :rate-limit nil + :appender-config {:always-log-to-err? false} + :fn (fn [{:keys [ap-config error? message]}] ; Can use any appender args + (binding [*out* (if (or error? (:always-log-to-err? ap-config)) + *err* *out*)] + (str-println @message)))} + + :spit + {:doc "Spits to `(:spit-filename :ap-config)` file." + :min-level nil :enabled? false :async? false :rate-limit nil + :appender-config {:spit-filename "timbre-spit.log"} + :fn (fn [{:keys [ap-config message]}] ; Can use any appender args + (when-let [filename (:spit-filename ap-config)] + (try (ensure-spit-dir-exists! filename) + (spit filename (str output "\n") :append true) + (catch java.io.IOException _))))}}}) From 17c6986087a8e6ce62fe26619b4007de082fe303 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Mon, 25 May 2015 22:16:59 +0700 Subject: [PATCH 07/16] Finish up initial new .cljx ns Wrote whole ns in one sitting; have likely missed some bugs, etc. --- project.clj | 2 +- src/taoensso/timbre.cljx | 295 +++++++++++++++++++++++---------------- src/taoensso/v3.clj | 69 --------- 3 files changed, 179 insertions(+), 187 deletions(-) delete mode 100644 src/taoensso/v3.clj diff --git a/project.clj b/project.clj index 11e8748..afd28f8 100644 --- a/project.clj +++ b/project.clj @@ -12,7 +12,7 @@ :dependencies [[org.clojure/clojure "1.4.0"] - [com.taoensso/encore "1.30.0"] + [com.taoensso/encore "1.31.0"] [io.aviso/pretty "0.1.18"]] :plugins diff --git a/src/taoensso/timbre.cljx b/src/taoensso/timbre.cljx index 5e9f617..421f9fb 100644 --- a/src/taoensso/timbre.cljx +++ b/src/taoensso/timbre.cljx @@ -1,41 +1,28 @@ (ns taoensso.timbre "Simple, flexible logging for Clojure/Script. No XML." {:author "Peter Taoussanis"} - - #+clj - (:require [clojure.string :as str] - [io.aviso.exception :as aviso-ex] - [taoensso.encore :as enc :refer (have have? qb)]) - - #+cljs - (:require [clojure.string :as str] - [taoensso.encore :as enc :refer ()]) - #+cljs - (:require-macros - [taoensso.encore :as enc :refer (have have?)]) - - #+clj - (:import [java.util Date Locale] - [java.text SimpleDateFormat] - [java.io File])) + #+clj (:require [clojure.string :as str] + [io.aviso.exception :as aviso-ex] + [taoensso.encore :as enc :refer (have have? qb)]) + #+cljs (:require [clojure.string :as str] + [taoensso.encore :as enc :refer ()]) + #+cljs (:require-macros [taoensso.encore :as enc :refer (have have?)]) + #+clj (:import [java.util Date Locale] + [java.text SimpleDateFormat] + [java.io File])) ;;;; TODO ;; - Check for successful cljs compile -;; - Bump encore version + min version check -;; - Clj default appenders -;; - Simple config flag to log std err -> out ;; - Cljs default appenders -;; - Port profiling ns (cljs support?) +;; - Port appenders +;; - Try ease backward comp, update README, CHANGELOG ;; - Document shutdown-agents, ;; Ref. https://github.com/ptaoussanis/timbre/pull/100/files -;; - Try ease backward comp -;; - Port appenders -;; - Update README, CHANGELOG ;;;; Encore version check #+clj -(let [min-encore-version 1.30] +(let [min-encore-version 1.31] (if-let [assert! (ns-resolve 'taoensso.encore 'assert-min-encore-version)] (assert! min-encore-version) (throw @@ -47,14 +34,119 @@ ;;;; Config +#+clj +(def default-timestamp-opts + {:pattern "yy-MMM-dd HH:mm:ss" + :locale (java.util.Locale. "en") + ;; :timezone (java.util.TimeZone/getTimeZone "UTC") + :timezone (java.util.TimeZone/getDefault)}) + +(declare stacktrace) +(defn default-output-fn [data & [opts]] + (let [{:keys [level ?err_ vargs_ msg_ ?ns-str hostname_ timestamp_]} data] + (str + #+clj (force timestamp_) #+clj " " + #+clj (force hostname_) #+clj " " + (str/upper-case (name level)) + " [" (or ?ns-str "?ns") "] - " (force msg_) + (when-let [err (force ?err_)] (str "\n" (stacktrace err)))))) + +(declare default-err default-out ensure-spit-dir-exists!) + (def example-config - "Example (+default) Timbre config map." ; TODO - {:level :debug - :appenders ; TODO - {:println - {:min-level nil :enabled? true :async? false :rate-limit nil - :fn (fn [data] - (println ((:output-fn data) data)))}}}) + "Example (+default) Timbre config map. + + APPENDERS + An appender is a map with keys: + :doc ; Optional docstring. + :min-level ; Level keyword, or nil (=> no minimum level). + :enabled? ; + :async? ; Dispatch using agent? Useful for slow appenders. + :rate-limit ; [[ncalls-limit window-ms] <...>], or nil. + :data-hash-fn ; Used by rate-limiter, etc. + :opts ; Any appender-specific opts + :fn ; (fn [data-map]), with keys described below. + + An appender's fn takes a single data map with keys: + :config ; Entire config map (this map) + :appender-id ; Id of appender currently being dispatched to + :appender ; Entire appender map currently being dispatched to + :appender-opts ; (:opts (:appender )), for convenience + + :instant ; java.util.Date or js/Date + :level ; Keyword + :error-level? ; Is level :error or :fatal? + :?ns-str ; String, or nil + :?file ; String, or nil (waiting on CLJ-865) + :?line ; Integer, or nil ('') + + :?err_ ; Delay - first-argument error + :vargs_ ; Delay - raw args vector + :hostname_ ; Delay - string (clj only) + :msg_ ; Delay - args string + :timestamp_ ; Delay - string + :output-fn ; (fn [data & [opts]]) -> formatted output string + + :profile-stats ; From `profile` macro + + + + DELAYS + As a matter of middleware hygiene, prefer using `force` to @/`deref` + when retrieving getting delayed values. + + MIDDLEWARE + Middleware are fns (applied left->right) that transform the data map + dispatched to appender fns. If any middleware returns nil, NO dispatching + will occur (i.e. the event will be filtered). + + The `example-config` source code contains further settings and details. + See also `set-config!`, `merge-config!`, `set-level!`." + + (merge + {:level :debug + + :whitelist [] ; "my-ns.*", etc. + :blacklist [] ; + :middleware [] ; (fns [data])->?data, applied left->right + + :output-fn default-output-fn + #+clj :timestamp-opts #+clj default-timestamp-opts + + :appenders + #+clj + {:println + {:doc "Prints to (:stream ) IO stream. Enabled by default." + :min-level nil :enabled? true :async? false :rate-limit nil + :opts {;; e/o #{:std-err :std-out :auto }: + :stream :auto} + :fn + (fn [data] + (let [{:keys [output-fn error? appender-opts]} data + {:keys [stream]} appender-opts + out (case stream + (nil :auto) (if error? default-err *out*) + :std-err default-err + :std-out default-out + stream)] + (binding [*out* out] (println (output-fn data)))))} + + :spit + {:doc "Spits to (:spit-filename ) file." + :min-level nil :enabled? false :async? false :rate-limit nil + :opts {:spit-filename "timbre-spit.log"} + :fn + (fn [data] + (let [{:keys [output-fn appender-opts]} data + {:keys [spit-filename]} appender-opts] + (when-let [fname (enc/as-?nblank spit-filename)] + (try (ensure-spit-dir-exists! fname) + (spit fname (str (output-fn data) "\n") :append true) + (catch java.io.IOException _)))))}}})) + +(comment + (set-config! example-config) + (infof "Hello %s" "world :-)")) (enc/defonce* ^:dynamic *config* example-config) (defmacro with-config [config & body] `(binding [*config* ~config] ~@body)) @@ -86,16 +178,17 @@ (defn level>= [x y] (>= (long (scored-levels (valid-level x))) (long (scored-levels (valid-level y))))) -(comment (level>= :info :debug)) +(comment (qb 10000 (level>= :info :debug))) #+clj (defn- env-val [id] (when-let [s (System/getenv id)] (enc/read-edn s))) #+clj (def ^:private compile-time-level (have [:or nil? valid-level] (keyword (env-val "TIMBRE_LEVEL")))) (defn get-active-level [& [config]] (or (:level (or config *config*)) :report)) -(comment (qb 10000 (get-active-level))) -(comment (binding [*config* {:level :trace}] (level>= :trace (get-active-level)))) +(comment + (qb 10000 (get-active-level)) + (binding [*config* {:level :trace}] (level>= :trace (get-active-level)))) ;;;; ns filter @@ -154,25 +247,13 @@ (vsplit-err1 [:a :b :c]) (vsplit-err1 [(Exception.) :a :b :c])) -(declare stacktrace) - -(defn default-output-fn [data & [opts]] - (let [{:keys [level ?err_ vargs_ msg-fn ?ns-str hostname_ timestamp_]} data] - (str (force timestamp_) " " - #+clj @hostname_ #+clj " " - (str/upper-case (name level)) - " [" ?ns-str "] - " (msg-fn vargs_) - (when-let [err (force ?err_)] (str "\n" (stacktrace err)))))) - -(comment (infof (Exception.) "Hello %s" "Steve")) - (defn default-data-hash-fn [data] (let [{:keys [?ns-str ?line vargs_]} data vargs (force vargs_)] (str (or (some #(and (map? %) (:timbre/hash %)) vargs) ; Explicit hash given - #_[?ns-str ?line] ; TODO Waiting on http://goo.gl/cVVAYA - [?ns-str vargs])))) + #_[?ns-str ?line] ; TODO Waiting on http://goo.gl/cVVAYA + [?ns-str vargs])))) (comment (default-data-hash-fn {})) @@ -201,51 +282,33 @@ (declare get-hostname) -;;;; TODO Temp, work on timestamps -;; want a simple, pattern-based - -(def ^:private default-timestamp-pattern "14-Jul-07 16:42:11" - "yy-MMM-dd HH:mm:ss") - -(.format - (enc/simple-date-format default-timestamp-pattern - {:locale (Locale. "en") - ;; :timezone "foo" - }) (enc/now-dt)) - - - -;;;; TODO - - (defn log* "Core fn-level logger. Implementation detail." [config level ?ns-str ?file ?line msg-type dvargs & [base-data]] (when (log? level ?ns-str config) - (let [instant (enc/now-dt) - vargs*_ (delay (vsplit-err1 (mapv force dvargs))) - ?err_ (delay (get @vargs*_ 0)) - vargs_ (delay (get @vargs*_ 1)) - msg-fn (fn [vargs_] ; Post-middleware vargs, etc. - (when-not (nil? msg-type) - (when-let [vargs (have [:or nil? vector?] (force vargs_))] - (case msg-type - :print (enc/spaced-str vargs) - :format (let [[fmt args] (enc/vsplit-first vargs)] - (enc/format* fmt args)))))) - data - (merge base-data *context* - {:config config ; Entire config! - :instant instant - :level level - :?ns-str ?ns-str - :?file ?file - :?line ?line - :?err_ ?err_ - :vargs_ vargs_ - :msg-fn msg-fn - #+clj :hostname_ #+clj (delay (get-hostname)) - :error-level? (#{:error :fatal} level)}) - + (let [instant (enc/now-dt) + vargs*_ (delay (vsplit-err1 (mapv force dvargs))) + ?err_ (delay (get @vargs*_ 0)) + vargs_ (delay (get @vargs*_ 1)) + data (merge base-data *context* + {:config config ; Entire config! + ;; :context *context* ; Extra destructure's a nuisance + :instant instant + :level level + :?ns-str ?ns-str + :?file ?file + :?line ?line + :?err_ ?err_ + :vargs_ vargs_ + #+clj :hostname_ #+clj (delay (get-hostname)) + :error-level? (#{:error :fatal} level)}) + msg-fn + (fn [vargs_] ; *After* middleware, etc. + (when-not (nil? msg-type) + (when-let [vargs (have [:or nil? vector?] (force vargs_))] + (case msg-type + :print (enc/spaced-str vargs) + :format (let [[fmt args] (enc/vsplit-first vargs)] + (enc/format* fmt args)))))) ?data (reduce ; Apply middleware: data->?data (fn [acc mf] @@ -274,26 +337,32 @@ (not (rl-fn data-hash)))))) (let [{:keys [async?] apfn :fn} appender + msg_ (delay (msg-fn (:vargs_ data))) output-fn (or (:output-fn appender) (:output-fn config) default-output-fn) - ;; TODO Grab config (tz, pattern, locale, etc.) from - timestamp_ (delay "TODO") - - - data (assoc data :output-fn - )] + #+clj timestamp_ + #+clj + (delay + (let [timestamp-opts (merge default-timestamp-opts + (:timestamp-opts config) + (:timestamp-opts appender)) + {:keys [pattern locale timezone]} timestamp-opts] + (.format (enc/simple-date-format pattern + {:locale locale :timezone timezone}) + (:instant data)))) - :timestamp_ timestamp_ - ;; :output-fn output-fn + data + (merge data + {:appender-id id + :appender appender + :appender-opts (:opts appender) + :msg_ msg_ + :msg-fn msg-fn + :output-fn output-fn + #+clj :timestamp_ #+clj timestamp_})] - ;; :timestamp_ (delay "maybe?") ; TODO Nix? - - - - - (if-not async? (apfn data) (send-off (get-agent id) (fn [_] (apfn data))))))) @@ -339,7 +408,10 @@ (def-loggers) -(comment (infof "hello %s" "world")) +(comment + (infof "hello %s" "world") + (infof (Exception.) "hello %s" "world") + (infof (Exception.))) (defmacro log-errors [& body] `(let [[?result# ?error#] (enc/catch-errors ~@body)] @@ -438,14 +510,3 @@ [probability & body] `(do (assert (<= 0 ~probability 1) "Probability: 0 <= p <= 1") (when (< (rand) ~probability) ~@body))) - - - -;;;; TODO Scratch -;; :keys [timestamp-pattern timestamp-locale -;; prefix-fn fmt-output-fn]} config -;; timestamp-fn -;; (if-not timestamp-pattern (constantly nil) -;; (fn [^Date dt] -;; (.format (enc/simple-date-format timestamp-pattern -;; {:locale timestamp-locale}) dt)))] diff --git a/src/taoensso/v3.clj b/src/taoensso/v3.clj deleted file mode 100644 index 6b49fa3..0000000 --- a/src/taoensso/v3.clj +++ /dev/null @@ -1,69 +0,0 @@ -;;;; Default configuration and appenders - -(def example-config - "APPENDERS - An appender is a map with keys: - :doc ; Optional docstring. - :min-level ; Level keyword, or nil (=> no minimum level). - :enabled? ; - :async? ; Dispatch using agent? Useful for slow appenders. - :rate-limit ; [ncalls-limit window-ms], or nil. - :args-hash-fn ; Used by rate-limiter, etc. - :appender-config ; Any appender-specific config. - :fn ; (fn [appender-args-map]), with keys described below. - - An appender's fn takes a single map with keys: - :instant ; java.util.Date. - :ns ; String. - :level ; Keyword. - :error? ; Is level an 'error' level? - :throwable ; java.lang.Throwable. - :args ; Raw logging macro args (as given to `info`, etc.). - ;; - :context ; Thread-local dynamic logging context. - :ap-config ; Content of appender's own `:appender-config` merged over - ; `:shared-appender-config`. - :profile-stats ; From `profile` macro. - ;; - ;; Waiting on http://dev.clojure.org/jira/browse/CLJ-865: - :file ; String. - :line ; Integer. - ;; - :message ; DELAYED string of formatted appender args. Appenders may - ; (but are not obligated to) use this as their output. - - 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 further settings and details. - See also `set-config!`, `merge-config!`, `set-level!`." - - { - - :shared-appender-config - {:message-fmt-opts ; `:message` appender argument formatting - {:timestamp-pattern default-message-timestamp-pattern ; SimpleDateFormat - :timestamp-locale nil ; A Locale object, or nil - :pattern-fn default-message-pattern-fn}} - - :appenders - {:standard-out - {:doc "Prints to *out*/*err*. Enabled by default." - :min-level nil :enabled? true :async? false :rate-limit nil - :appender-config {:always-log-to-err? false} - :fn (fn [{:keys [ap-config error? message]}] ; Can use any appender args - (binding [*out* (if (or error? (:always-log-to-err? ap-config)) - *err* *out*)] - (str-println @message)))} - - :spit - {:doc "Spits to `(:spit-filename :ap-config)` file." - :min-level nil :enabled? false :async? false :rate-limit nil - :appender-config {:spit-filename "timbre-spit.log"} - :fn (fn [{:keys [ap-config message]}] ; Can use any appender args - (when-let [filename (:spit-filename ap-config)] - (try (ensure-spit-dir-exists! filename) - (spit filename (str output "\n") :append true) - (catch java.io.IOException _))))}}}) From 014faa7bab6b25c34d52e3239a8151ae425901c3 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Tue, 26 May 2015 01:04:45 +0700 Subject: [PATCH 08/16] Update official appenders --- src/taoensso/timbre.cljx | 9 ++- src/taoensso/timbre/appenders/carmine.clj | 81 ++++++++++++----------- src/taoensso/timbre/appenders/postal.clj | 54 +++++++-------- 3 files changed, 74 insertions(+), 70 deletions(-) diff --git a/src/taoensso/timbre.cljx b/src/taoensso/timbre.cljx index 421f9fb..a25e0f2 100644 --- a/src/taoensso/timbre.cljx +++ b/src/taoensso/timbre.cljx @@ -49,7 +49,7 @@ #+clj (force hostname_) #+clj " " (str/upper-case (name level)) " [" (or ?ns-str "?ns") "] - " (force msg_) - (when-let [err (force ?err_)] (str "\n" (stacktrace err)))))) + (when-let [err (force ?err_)] (str "\n" (stacktrace err opts)))))) (declare default-err default-out ensure-spit-dir-exists!) @@ -149,7 +149,10 @@ (infof "Hello %s" "world :-)")) (enc/defonce* ^:dynamic *config* example-config) -(defmacro with-config [config & body] `(binding [*config* ~config] ~@body)) +(defmacro with-config [config & body] `(binding [*config* ~config] ~@body)) +(defmacro with-merged-config [config & body] + `(binding [*config* (enc/nested-merge *config* ~config)] ~@body)) + (defn swap-config! [f] #+cljs (set! *config* (f *config*)) #+clj (alter-var-root #'*config* f)) @@ -165,7 +168,7 @@ ;;;; Levels -(def ^:private ordered-levels [:trace :debug :info :warn :error :fatal :report]) +(def ordered-levels [:trace :debug :info :warn :error :fatal :report]) (def ^:private scored-levels (zipmap ordered-levels (next (range)))) (def ^:private valid-level (let [valid-level-set (set ordered-levels)] diff --git a/src/taoensso/timbre/appenders/carmine.clj b/src/taoensso/timbre/appenders/carmine.clj index 650e94f..be99f3d 100644 --- a/src/taoensso/timbre/appenders/carmine.clj +++ b/src/taoensso/timbre/appenders/carmine.clj @@ -3,7 +3,8 @@ {:author "Peter Taoussanis"} (:require [taoensso.carmine :as car] [taoensso.nippy :as nippy] - [taoensso.timbre :as timbre])) + [taoensso.timbre :as timbre] + [taoensso.encore :as enc :refer (have have?)])) (defn- sha48 "Truncated 160bit SHA hash (48bit Long). Redis can store small collections of @@ -15,51 +16,56 @@ (comment (sha48 {:key "I'm gonna get hashed!"})) -(defn default-keyfn [level] {:pre [(string? level)]} - (format "carmine:timbre:default:%s" level)) +(defn default-keyfn [level] {:pre [(have? string? level)]} + (str "carmine:timbre:default:" level)) -(defn make-carmine-appender - "Alpha - subject to change! - Returns a Carmine Redis appender: - * All raw logging args are preserved in serialized form (even Throwables!). - * Only the most recent instance of each unique entry is kept (hash fn used +(defn make-appender + "Returns a Carmine Redis appender (experimental, subject to change): + * All raw logging args are preserved in serialized form (even Throwables!). + * Only the most recent instance of each unique entry is kept (hash fn used to determine uniqueness is configurable). - * Configurable number of entries to keep per logging level. - * Log is just a value: a vector of Clojure maps: query+manipulate with - standard seq fns: group-by hostname, sort/filter by ns & severity, explore - exception stacktraces, filter by raw arguments, etc. Datomic and `core.logic` - also offer interesting opportunities here. + * Configurable number of entries to keep per logging level. + * Log is just a value: a vector of Clojure maps: query+manipulate with + standard seq fns: group-by hostname, sort/filter by ns & severity, explore + exception stacktraces, filter by raw arguments, etc. Datomic and `core.logic` + also offer interesting opportunities here. See accompanying `query-entries` fn to return deserialized log entries." - [& [appender-opts {:keys [conn-opts keyfn args-hash-fn nentries-by-level] - :or {keyfn default-keyfn - args-hash-fn timbre/default-args-hash-fn - nentries-by-level {:trace 50 - :debug 50 - :info 50 - :warn 100 - :error 100 - :fatal 100 - :report 100}} - :as opts}]] - {:pre [(string? (keyfn "test")) - (every? #(contains? nentries-by-level %) timbre/levels-ordered) - (every? #(and (integer? %) (<= 0 % 100000)) (vals nentries-by-level))]} + [& [appender-config + {:keys [conn-opts keyfn data-hash-fn nentries-by-level] + :or {keyfn default-keyfn + data-hash-fn timbre/default-data-hash-fn + nentries-by-level {:trace 50 + :debug 50 + :info 50 + :warn 100 + :error 100 + :fatal 100 + :report 100}} + :as make-config}]] + {:pre [(have? string? (keyfn "test")) + (have? [:ks>= timbre/ordered-levels] nentries-by-level) + (have? [:and integer? #(<= 0 % 100000)] :in (vals nentries-by-level))]} - (let [default-appender-opts {:enabled? true :min-level nil}] - (merge default-appender-opts appender-opts + (let [default-appender-config {:enabled? true :min-level nil}] + (merge default-appender-config appender-config {:fn - (fn [{:keys [level instant] :as apfn-args}] - (let [entry-hash (sha48 (args-hash-fn apfn-args)) - entry (select-keys apfn-args [:hostname :ns :args :throwable - :profile-stats]) + (fn [data] + (let [{:keys [level instant]} data + entry-hash (sha48 (data-hash-fn data)) + entry {:?ns-str (:?ns-str data) + :hostname (force (:hostname_ data)) + :vargs (force (:vargs_ data)) + :?err (force (:?err_ data)) + :profile-stats (:profile-stats data)} + k-zset (keyfn (name level)) k-hash (str k-zset ":entries") udt (.getTime ^java.util.Date instant) ; Use as zscore nmax-entries (nentries-by-level level)] (when (> nmax-entries 0) - (car/wcar (or conn-opts (:conn opts)) ; :conn is Deprecated + (car/wcar conn-opts (binding [nippy/*final-freeze-fallback* nippy/freeze-fallback-as-str] (car/hset k-hash entry-hash entry)) (car/zadd k-zset udt entry-hash) @@ -91,7 +97,7 @@ maps. Normal sequence fns can be used to query/transform entries. Datomic and core.logic are also useful!" [conn-opts level & [n asc? keyfn]] - {:pre [(or (nil? n) (and (integer? n) (<= 1 n 100000)))]} + {:pre [(have? [:or nil? [:and integer? #(<= 1 % 100000)]] n)]} (let [keyfn (or keyfn default-keyfn) k-zset (keyfn (name level)) k-hash (str k-zset ":entries") @@ -125,9 +131,8 @@ ;;;; Dev/tests (comment - (timbre/log {:timestamp-pattern "yyyy-MMM-dd HH:mm:ss ZZ" - :appenders {:carmine (make-carmine-appender)}} - :info "Hello1" "Hello2") + (timbre/with-merged-config {:appenders {:carmine (make-appender)}} + (timbre/info "Hello1" "Hello2")) (car/wcar {} (car/keys (default-keyfn "*"))) (count (car/wcar {} (car/hgetall (default-keyfn "info:entries")))) diff --git a/src/taoensso/timbre/appenders/postal.clj b/src/taoensso/timbre/appenders/postal.clj index d3483a1..0097f2b 100644 --- a/src/taoensso/timbre/appenders/postal.clj +++ b/src/taoensso/timbre/appenders/postal.clj @@ -3,13 +3,8 @@ {:author "Peter Taoussanis"} (:require [clojure.string :as str] [postal.core :as postal] - [taoensso.timbre :as timbre])) - -(defn- str-trunc [^String s max-len] - (if (<= (.length s) max-len) s - (.substring s 0 max-len))) - -(comment (str-trunc "Hello this is a long string" 5)) + [taoensso.timbre :as timbre] + [taoensso.encore :as enc :refer (have have?)])) (defn make-postal-appender "Returns a Postal email appender. @@ -20,31 +15,32 @@ {:postal-config ^{:host \"mail.isp.net\" :user \"jsmith\" :pass \"sekrat!!1\"} {:from \"Bob's logger \" :to \"foo@example.com\"}})" - [& [appender-opts {:keys [postal-config subject-len body-fn] - :or {subject-len 150 - body-fn (fn [output] [{:type "text/plain; charset=utf-8" - :content output}])}}]] - (let [default-appender-opts + [& [appender-config make-config]] + (let [{:keys [postal-config subject-len body-fn] + :or {subject-len 150 + body-fn (fn [output] [{:type "text/plain; charset=utf-8" + :content output}])}} + make-config + + default-appender-config {:enabled? true :min-level :warn :async? true ; Slow! - :rate-limit [5 (* 1000 60 2)] ; 5 calls / 2 mins - ;; TODO These opts are deprecated! - :fmt-output-opts {:no-fonts? true} ; Disable ANSI-escaped stuff - }] + :rate-limit [[5 (enc/ms :mins 2)] + [50 (enc/ms :hours 24)]]}] - (merge default-appender-opts appender-opts + (merge default-appender-config appender-config {:fn - (fn [{:keys [ap-config output]}] - (when-let [postal-config (or postal-config (:postal ap-config))] - (postal/send-message - (assoc postal-config - :subject (-> (str output) - (str/trim) - (str-trunc subject-len) - (str/replace #"\s+" " ")) - :body (body-fn output)))))}))) - -(def postal-appender "DEPRECATED: Use `make-postal-appender` instead." - (make-postal-appender)) + (fn [data] + (let [{:keys [output-fn appender-opts]} data + {:keys [no-fonts?]} appender-opts] + (when-let [postal-config (or postal-config (:postal appender-opts))] + (let [output (str (output-fn data {:stacktrace-fonts {}}))] + (postal/send-message + (assoc postal-config + :subject (-> output + (str/trim) + (str/replace #"\s+" " ") + (enc/substr 0 subject-len)) + :body (body-fn output)))))))}))) From 3c824d31da7658938611827fc93706efb7f66570 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Tue, 26 May 2015 01:28:32 +0700 Subject: [PATCH 09/16] Update 3rd-party appenders (rough) --- .../timbre/appenders/3rd_party/android.clj | 71 ++++++++++--------- .../timbre/appenders/3rd_party/irc.clj | 70 +++++++++--------- .../timbre/appenders/3rd_party/mongo.clj | 48 +++++++------ .../timbre/appenders/3rd_party/rolling.clj | 30 ++++---- .../timbre/appenders/3rd_party/rotor.clj | 54 +++++++------- .../timbre/appenders/3rd_party/socket.clj | 42 ++++++----- .../timbre/appenders/3rd_party/zmq.clj | 37 +++++----- 7 files changed, 185 insertions(+), 167 deletions(-) diff --git a/src/taoensso/timbre/appenders/3rd_party/android.clj b/src/taoensso/timbre/appenders/3rd_party/android.clj index 4382a42..04c90c7 100644 --- a/src/taoensso/timbre/appenders/3rd_party/android.clj +++ b/src/taoensso/timbre/appenders/3rd_party/android.clj @@ -1,41 +1,44 @@ -(ns taoensso.timbre.appenders.android - "Android LogCat appender. Depends on the android runtime. This is a - configuration for the timbre logging library" +(ns taoensso.timbre.appenders.3rd-party.android + "Android LogCat appender. Requires Android runtime." {:author "Adam Clements"} - (:require [taoensso.timbre :as timbre] - clojure.string)) + (:require [clojure.string :as str] + [taoensso.timbre :as timbre])) -(defn make-logcat-appender +(defn make-appender "Returns an appender that writes to Android LogCat. Obviously only works if running within the Android runtime (device or emulator). You may want to disable std-out to prevent printing nested timestamps, etc." - [& [appender-opts make-opts]] - (let [default-appender-opts {:enabled? true - :min-level :debug}] - (merge default-appender-opts appender-opts - {:fn (fn [{:keys [level ns throwable message]}] - (let [output (format "%s %s - %s" timestamp - (-> level name clojure.string/upper-case) - (or message ""))] - (if throwable - (case level - :trace (android.util.Log/d ns output throwable) - :debug (android.util.Log/d ns output throwable) - :info (android.util.Log/i ns output throwable) - :warn (android.util.Log/w ns output throwable) - :error (android.util.Log/e ns output throwable) - :fatal (android.util.Log/e ns output throwable) - :report (android.util.Log/i ns output throwable)) + [& [appender-config make-config]] + (let [default-appender-config + {:enabled? true + :min-level :debug}] - (case level - :trace (android.util.Log/d ns output) - :debug (android.util.Log/d ns output) - :info (android.util.Log/i ns output) - :warn (android.util.Log/w ns output) - :error (android.util.Log/e ns output) - :fatal (android.util.Log/e ns output) - :report (android.util.Log/i ns output)))))}))) + (merge default-appender-config appender-config + {:fn + (fn [data] + (let [{:keys [level ?ns-str ?err_ msg_ timestamp_]} data + msg (or (force msg_) "") + timestamp (force timestamp_) + ns (or ?ns-str "") + output (format "%s %s - %s" timestamp + (-> level name str/upper-case) + msg)] -(def logcat-appender - "DEPRECATED: Use `make-logcat-appender` instead." - (make-logcat-appender)) + (if-let [throwable (force ?err_)] + (case level + :trace (android.util.Log/d ns output throwable) + :debug (android.util.Log/d ns output throwable) + :info (android.util.Log/i ns output throwable) + :warn (android.util.Log/w ns output throwable) + :error (android.util.Log/e ns output throwable) + :fatal (android.util.Log/e ns output throwable) + :report (android.util.Log/i ns output throwable)) + + (case level + :trace (android.util.Log/d ns output) + :debug (android.util.Log/d ns output) + :info (android.util.Log/i ns output) + :warn (android.util.Log/w ns output) + :error (android.util.Log/e ns output) + :fatal (android.util.Log/e ns output) + :report (android.util.Log/i ns output)))))}))) diff --git a/src/taoensso/timbre/appenders/3rd_party/irc.clj b/src/taoensso/timbre/appenders/3rd_party/irc.clj index 4813e1a..1b8b9e2 100644 --- a/src/taoensso/timbre/appenders/3rd_party/irc.clj +++ b/src/taoensso/timbre/appenders/3rd_party/irc.clj @@ -1,18 +1,20 @@ -(ns taoensso.timbre.appenders.irc - "IRC appender. Depends on https://github.com/flatland/irclj." +(ns taoensso.timbre.appenders.3rd-party.irc + "IRC appender. Requires https://github.com/flatland/irclj." {:author "Emlyn Corrin"} (:require [clojure.string :as str] [irclj.core :as irc] [taoensso.timbre :as timbre])) (defn default-fmt-output-fn - [{:keys [level throwable message]}] + [{:keys [level ?err_ msg_]}] (format "[%s] %s%s" - (-> level name (str/upper-case)) - (or message "") - (or (timbre/stacktrace throwable "\n") ""))) + (-> level name (str/upper-case)) + (or (force msg_) "") + (if-let [err (force ?err_)] + (str "\n" (timbre/stacktrace err)) + ""))) -(def default-appender-opts +(def default-appender-config {:async? true :enabled? true :min-level :info}) @@ -38,38 +40,36 @@ (irc/message conn chan ">" line)))) (defn- make-appender-fn [irc-config conn] - (fn [{:keys [ap-config] :as args}] - (when-let [irc-config (or irc-config (:irc ap-config))] - (ensure-conn conn irc-config) - (let [fmt-fn (or (:fmt-output-fn irc-config) - default-fmt-output-fn)] - (send-message conn (:chan irc-config) (fmt-fn args)))))) + (fn [data] + (let [{:keys [appender-opts]} data] + (when-let [irc-config (or irc-config appender-opts)] + (ensure-conn conn irc-config) + (let [fmt-fn (or (:fmt-output-fn irc-config) + default-fmt-output-fn)] + (send-message conn (:chan irc-config) (fmt-fn data))))))) ;;; Public -(defn make-irc-appender +(defn make-appender "Sends IRC messages using irc. - Needs :irc config map in :shared-appender-config, e.g.: - {:host \"irc.example.org\" :port 6667 :nick \"logger\" - :name \"My Logger\" :chan \"#logs\"}" - [& [appender-opts {:keys [irc-config]}]] + Needs :opts map in appender, e.g.: + {:host \"irc.example.org\" :port 6667 :nick \"logger\" + :name \"My Logger\" :chan \"#logs\"}" + [& [appender-config {:keys [irc-config]}]] (let [conn (atom nil)] - (merge default-appender-opts - appender-opts - {:conn conn - :doc (:doc (meta #'make-irc-appender)) - :fn (make-appender-fn irc-config conn)}))) - -(def irc-appender "DEPRECATED: Use `make-irc-appender` instead." - (make-irc-appender)) + (merge default-appender-config appender-config + {:conn conn + :fn (make-appender-fn irc-config conn)}))) (comment - (timbre/set-config! - [:shared-appender-config :irc] - {:host "127.0.0.1" - :nick "lazylog" - :user "lazare" - :name "Lazylus Logus" - :chan "bob"}) - (timbre/set-config! [:appenders :irc] (make-irc-appender)) - (timbre/log :error "A multiple\nline message\nfor you")) + (timbre/merge-config! {:appenders {:irc (make-appender)}}) + (timbre/merge-config! + {:appenders + {:irc + {:opts + {:host "127.0.0.1" + :nick "lazylog" + :user "lazare" + :name "Lazylus Logus" + :chan "bob"}}}}) + (timbre/error "A multiple\nline message\nfor you")) diff --git a/src/taoensso/timbre/appenders/3rd_party/mongo.clj b/src/taoensso/timbre/appenders/3rd_party/mongo.clj index bde6431..8d6a029 100644 --- a/src/taoensso/timbre/appenders/3rd_party/mongo.clj +++ b/src/taoensso/timbre/appenders/3rd_party/mongo.clj @@ -1,7 +1,9 @@ -(ns taoensso.timbre.appenders.mongo - "MongoDB appender. Depends on https://github.com/aboekhoff/congomongo." +(ns taoensso.timbre.appenders.3rd-party.mongo + "MongoDB appender. Requires on https://github.com/aboekhoff/congomongo." {:author "Emlyn Corrin"} - (:require [somnium.congomongo :as mongo])) + (:require [somnium.congomongo :as mongo] + [taoensso.timbre :as timbre] + [taoensso.encore :as encore])) (def conn (atom nil)) @@ -21,26 +23,28 @@ :as config}] (let [selected-params (if logged-keys (select-keys params logged-keys) - (dissoc params :ap-config)) - logged-params (if-let [t (:throwable selected-params)] - (assoc selected-params :throwable (str t)) - selected-params)] + (dissoc params :config :appender :appender-opts)) + logged-params (encore/map-vals #(str (force %)) selected-params)] (mongo/with-mongo (ensure-conn config) (mongo/insert! collection logged-params)))) -(defn appender-fn [{:keys [ap-config] :as params}] - (when-let [mongo-config (:mongo ap-config)] - (log-message params mongo-config))) +(defn- make-appender-fn [make-config] + (fn [data] + (let [{:keys [appender-opts]} data] + (when-let [mongo-config appender-opts] + (log-message data mongo-config))))) -(def mongo-appender - {:doc (str "Logs to MongoDB using congomongo.\n" - "Needs :mongo config map in :shared-appender-config, e.g.: - {:db \"logs\" - :collection \"myapp\" - :logged-keys [:instant :level :message] - :write-concern :acknowledged - :server {:host \"127.0.0.1\" - :port 27017}}") - :min-level :warn :enabled? true :async? true - :rate-limit [1 1000] ; 1 entry / sec - :fn appender-fn}) +(defn make-appender + "Logs to MongoDB using congomongo. Needs :opts map in appender, e.g.: + {:db \"logs\" + :collection \"myapp\" + :logged-keys [:instant :level :msg_] + :write-concern :acknowledged + :server {:host \"127.0.0.1\" + :port 27017}}" + [& [appender-config make-config]] + (let [default-appender-config + {:min-level :warn :enabled? true :async? true + :rate-limit [[1 1000]]}] + (merge default-appender-config appender-config + {:fn (make-appender-fn make-config)}))) diff --git a/src/taoensso/timbre/appenders/3rd_party/rolling.clj b/src/taoensso/timbre/appenders/3rd_party/rolling.clj index 0f80808..6d93874 100644 --- a/src/taoensso/timbre/appenders/3rd_party/rolling.clj +++ b/src/taoensso/timbre/appenders/3rd_party/rolling.clj @@ -1,8 +1,10 @@ -(ns taoensso.timbre.appenders.rolling "Rolling file appender." +(ns taoensso.timbre.appenders.3rd-party.rolling + "Rolling file appender." + {:author "Unknown - please let me know?"} (:require [clojure.java.io :as io] [taoensso.timbre :as timbre]) - (:import [java.text SimpleDateFormat] - [java.util Calendar])) + (:import [java.text SimpleDateFormat] + [java.util Calendar])) (defn- rename-old-create-new-log [log old-log] (.renameTo log old-log) @@ -41,9 +43,11 @@ cal)) (defn- make-appender-fn [path pattern] - (fn [{:keys [ap-config output instant]}] - (let [path (or path (-> ap-config :rolling :path)) - pattern (or pattern (-> ap-config :rolling :pattern) :daily) + (fn [data] + (let [{:keys [instant appender-opts output-fn]} data + output (output-fn data) + path (or path (-> appender-opts :path)) + pattern (or pattern (-> appender-opts :pattern) :daily) prev-cal (prev-period-end-cal instant pattern) log (io/file path)] (when log @@ -52,20 +56,20 @@ (if (<= (.lastModified log) (.getTimeInMillis prev-cal)) (shift-log-period log path prev-cal)) (.createNewFile log)) - (spit path (with-out-str (timbre/str-println output)) :append true) + (spit path (with-out-str (println output)) :append true) (catch java.io.IOException _)))))) -(defn make-rolling-appender +(defn make-appender "Returns a Rolling file appender. - A rolling config map can be provided here as a second argument, or provided at - :rolling in :shared-appender-config. + A rolling config map can be provided here as a second argument, or provided in + appender's :opts map. (make-rolling-appender {:enabled? true} {:path \"log/app.log\" :pattern :daily}) path: logfile path pattern: frequency of rotation, available values: :daily (default), :weekly, :monthly" - [& [appender-opts {:keys [path pattern]}]] - (let [default-appender-opts {:enabled? true :min-level nil}] - (merge default-appender-opts appender-opts + [& [appender-config {:keys [path pattern]}]] + (let [default-appender-config {:enabled? true :min-level nil}] + (merge default-appender-config appender-config {:fn (make-appender-fn path pattern)}))) diff --git a/src/taoensso/timbre/appenders/3rd_party/rotor.clj b/src/taoensso/timbre/appenders/3rd_party/rotor.clj index 25451b3..a3c744d 100644 --- a/src/taoensso/timbre/appenders/3rd_party/rotor.clj +++ b/src/taoensso/timbre/appenders/3rd_party/rotor.clj @@ -1,10 +1,8 @@ -(ns taoensso.timbre.appenders.rotor +(ns taoensso.timbre.appenders.3rd-party.rotor {:author "Yutaka Matsubara"} - (:import - [java.io File FilenameFilter]) - (:require - [clojure.java.io :as io] - [taoensso.timbre :as t])) + (:require [clojure.java.io :as io] + [taoensso.timbre :as timbre]) + (:import [java.io File FilenameFilter])) (defn- ^FilenameFilter file-filter "Returns a Java FilenameFilter instance which only matches @@ -45,25 +43,27 @@ (reverse (map vector logs-to-rotate (iterate inc 1)))] (.renameTo log-file (io/file (format "%s.%03d" abs-path n)))))) -(defn appender-fn [{:keys [ap-config output]}] - (let [{:keys [path max-size backlog] - :or {max-size (* 1024 1024) - backlog 5}} (:rotor ap-config)] - (when path - (try - (when (> (.length (io/file path)) max-size) - (rotate-logs path backlog)) - (spit path - (str output "\n") - :append true) - (catch java.io.IOException _))))) +(defn make-appender-fn [make-config] + (fn [data] + (let [{:keys [appender-opts output-fn]} data + {:keys [path max-size backlog] + :or {max-size (* 1024 1024) + backlog 5}} appender-opts] + (when path + (try + (when (> (.length (io/file path)) max-size) + (rotate-logs path backlog)) + (spit path (str (output-fn data) "\n") :append true) + (catch java.io.IOException _)))))) -(def rotor-appender - {:doc (str "Simple Rotating File Appender.\n" - "Needs :rotor config map in :shared-appender-config, e.g.: - {:path \"logs/app.log\" - :max-size (* 512 1024) - :backlog 5}") - :min-level nil - :enabled? true - :fn appender-fn}) +(defn make-appender + "Simple Rotating File Appender. + Needs :opts map in appender, e.g.: + {:path \"logs/app.log\" + :max-size (* 512 1024) + :backlog 5}" + [& [appender-config make-config]] + (let [default-appender-config + {:min-level :warn :enabled? true}] + (merge default-appender-config appender-config + {:fn (make-appender-fn make-config)}))) diff --git a/src/taoensso/timbre/appenders/3rd_party/socket.clj b/src/taoensso/timbre/appenders/3rd_party/socket.clj index 60443af..03d5817 100644 --- a/src/taoensso/timbre/appenders/3rd_party/socket.clj +++ b/src/taoensso/timbre/appenders/3rd_party/socket.clj @@ -1,10 +1,10 @@ -(ns taoensso.timbre.appenders.socket - "TCP Socket appender. Depends on https://github.com/technomancy/server-socket." +(ns taoensso.timbre.appenders.3rd-party.socket + "TCP socket appender. Requires https://github.com/technomancy/server-socket." {:author "Emlyn Corrin"} (:require [server.socket :refer [create-server]] [taoensso.timbre :refer [stacktrace]]) - (:import [java.net Socket InetAddress] - [java.io BufferedReader InputStreamReader PrintWriter])) + (:import [java.net Socket InetAddress] + [java.io BufferedReader InputStreamReader PrintWriter])) (def conn (atom nil)) @@ -30,19 +30,23 @@ (defn ensure-conn [socket-config] (swap! conn #(or % (connect socket-config)))) -(defn appender-fn [{:keys [ap-config prefix message throwable] :as params}] - (when-let [socket-config (:socket ap-config)] - (let [c (ensure-conn socket-config)] - (doseq [sock @(:connections c)] - (let [out (PrintWriter. (.getOutputStream ^Socket sock))] - (binding [*out* out] - (println prefix message - (stacktrace throwable)))))))) +(defn make-appender-fn [make-config] + (fn [data] + (let [{:keys [appender-opts output-fn ?err_]} data] + (when-let [socket-config appender-opts] + (let [c (ensure-conn socket-config)] + (doseq [sock @(:connections c)] + (let [out (PrintWriter. (.getOutputStream ^Socket sock))] + (binding [*out* out] + (println (output-fn data)))))))))) -(def socket-appender - {:doc (str "Logs to a listening socket.\n" - "Needs :socket config map in :shared-appender-config, e.g.: - {:listen-addr :all - :port 9000}") - :min-level :trace :enabled? true - :fn appender-fn}) +(defn make-appender + "Logs to a listening socket. + Needs :opts map in appender, e.g.: + {:listen-addr :all + :port 9000}" + [& [appender-config make-config]] + (let [default-appender-config + {:min-level :trace :enabled? true}] + (merge default-appender-config appender-config + {:fn (make-appender-fn make-config)}))) diff --git a/src/taoensso/timbre/appenders/3rd_party/zmq.clj b/src/taoensso/timbre/appenders/3rd_party/zmq.clj index abfc628..e2e43a2 100644 --- a/src/taoensso/timbre/appenders/3rd_party/zmq.clj +++ b/src/taoensso/timbre/appenders/3rd_party/zmq.clj @@ -1,34 +1,37 @@ -(ns taoensso.timbre.appenders.zmq +(ns taoensso.timbre.appenders.3rd-party.zmq "ØMQ appender. Requires https://github.com/zeromq/cljzmq" {:author "Angus Fletcher"} - (:require [zeromq.zmq :as zmq] + (:require [zeromq.zmq :as zmq] [taoensso.timbre :as timbre])) (defn make-zmq-socket [context transport address port] (doto (zmq/socket context :push) (zmq/connect (format "%s://%s:%d" transport address port)))) -(defn appender-fn [socket poller {:keys [ap-config output]}] - (loop [] - (zmq/poll poller 500) - (cond - (zmq/check-poller poller 0 :pollout) (zmq/send-str socket output) - (zmq/check-poller poller 0 :pollerr) (System/exit 1) - :else (recur)))) +(defn make-appender-fn [socket poller] + (fn [data] + (let [{:keys [appender-opts output-fn]} data + output (output-fn data)] + (loop [] + (zmq/poll poller 500) + (cond + (zmq/check-poller poller 0 :pollout) (zmq/send-str socket output) + (zmq/check-poller poller 0 :pollerr) (System/exit 1) + :else (recur)))))) -(defn make-zmq-appender +(defn make-appender "Returns a ØMQ appender. Takes appender options and a map consisting of: transport: a string representing transport type: tcp, ipc, inproc, pgm/epgm address: a string containing an address to connect to. port: a number representing the port to connect to." - [& [appender-opts {:keys [transport address port]}]] - (let [default-appender-opts {:enabled? true - :min-level :error - :async? true} + [& [appender-config {:keys [transport address port]}]] + (let [default-appender-config + {:enabled? true + :min-level :error + :async? true} context (zmq/zcontext) socket (make-zmq-socket context transport address port) poller (doto (zmq/poller context) (zmq/register socket :pollout :pollerr))] - (merge default-appender-opts - appender-opts - {:fn (partial appender-fn socket poller)}))) + (merge default-appender-config appender-config + {:fn (make-appender-fn socket poller)}))) From beec0c9fdd908460e55b7462c38537000de972a6 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Tue, 26 May 2015 09:22:06 +0700 Subject: [PATCH 10/16] Housekeeping --- src/taoensso/timbre.cljx | 133 ++++++++++-------- .../timbre/appenders/example_appender.clj | 81 +++++++++++ src/taoensso/timbre/tools/logging.clj | 2 +- 3 files changed, 155 insertions(+), 61 deletions(-) create mode 100644 src/taoensso/timbre/appenders/example_appender.clj diff --git a/src/taoensso/timbre.cljx b/src/taoensso/timbre.cljx index a25e0f2..54476e5 100644 --- a/src/taoensso/timbre.cljx +++ b/src/taoensso/timbre.cljx @@ -14,7 +14,6 @@ ;;;; TODO ;; - Check for successful cljs compile ;; - Cljs default appenders -;; - Port appenders ;; - Try ease backward comp, update README, CHANGELOG ;; - Document shutdown-agents, ;; Ref. https://github.com/ptaoussanis/timbre/pull/100/files @@ -36,13 +35,17 @@ #+clj (def default-timestamp-opts + "Controls (:timestamp_ data)." {:pattern "yy-MMM-dd HH:mm:ss" :locale (java.util.Locale. "en") ;; :timezone (java.util.TimeZone/getTimeZone "UTC") :timezone (java.util.TimeZone/getDefault)}) (declare stacktrace) -(defn default-output-fn [data & [opts]] + +(defn default-output-fn + "(fn [data & [opts]]) -> string output." + [data & [opts]] (let [{:keys [level ?err_ vargs_ msg_ ?ns-str hostname_ timestamp_]} data] (str #+clj (force timestamp_) #+clj " " @@ -54,33 +57,37 @@ (declare default-err default-out ensure-spit-dir-exists!) (def example-config - "Example (+default) Timbre config map. + "Example (+default) Timbre v4 config map. APPENDERS + + *** Please see the `taoensso.timbre.appenders.example-appender` ns if you + plan to write your own Timbre appender *** + An appender is a map with keys: - :doc ; Optional docstring. - :min-level ; Level keyword, or nil (=> no minimum level). + :doc ; Optional docstring + :min-level ; Level keyword, or nil (=> no minimum level) :enabled? ; - :async? ; Dispatch using agent? Useful for slow appenders. - :rate-limit ; [[ncalls-limit window-ms] <...>], or nil. + :async? ; Dispatch using agent? Useful for slow appenders + :rate-limit ; [[ncalls-limit window-ms] <...>], or nil :data-hash-fn ; Used by rate-limiter, etc. :opts ; Any appender-specific opts - :fn ; (fn [data-map]), with keys described below. + :fn ; (fn [data-map]), with keys described below An appender's fn takes a single data map with keys: - :config ; Entire config map (this map) + :config ; Entire config map (this map, etc.) :appender-id ; Id of appender currently being dispatched to :appender ; Entire appender map currently being dispatched to - :appender-opts ; (:opts (:appender )), for convenience + :appender-opts ; Duplicates (:opts ), for convenience - :instant ; java.util.Date or js/Date + :instant ; Platform date (java.util.Date or js/Date) :level ; Keyword :error-level? ; Is level :error or :fatal? :?ns-str ; String, or nil - :?file ; String, or nil (waiting on CLJ-865) - :?line ; Integer, or nil ('') + :?file ; String, or nil ; Waiting on CLJ-865 + :?line ; Integer, or nil ; Waiting on CLJ-865 - :?err_ ; Delay - first-argument error + :?err_ ; Delay - first-argument platform error :vargs_ ; Delay - raw args vector :hostname_ ; Delay - string (clj only) :msg_ ; Delay - args string @@ -91,45 +98,46 @@ - DELAYS - As a matter of middleware hygiene, prefer using `force` to @/`deref` - when retrieving getting delayed values. - MIDDLEWARE - Middleware are fns (applied left->right) that transform the data map - dispatched to appender fns. If any middleware returns nil, NO dispatching - will occur (i.e. the event will be filtered). + Middleware are simple (fn [data]) -> ?data fns (applied left->right) that + transform the data map dispatched to appender fns. If any middleware returns + nil, NO dispatching will occur (i.e. the event will be filtered). The `example-config` source code contains further settings and details. See also `set-config!`, `merge-config!`, `set-level!`." (merge - {:level :debug + {:level :debug ; e/o #{:trace :debug :info :warn :error :fatal :report} :whitelist [] ; "my-ns.*", etc. :blacklist [] ; - :middleware [] ; (fns [data])->?data, applied left->right + :middleware [] ; (fns [data]) -> ?data, applied left->right - :output-fn default-output-fn - #+clj :timestamp-opts #+clj default-timestamp-opts + :output-fn default-output-fn ; (fn [data]) -> string + #+clj :timestamp-opts + #+clj default-timestamp-opts ; {:pattern _ :locale _ :timezone _} :appenders #+clj - {:println + {:println ; Appender id + ;; Appender : {:doc "Prints to (:stream ) IO stream. Enabled by default." :min-level nil :enabled? true :async? false :rate-limit nil - :opts {;; e/o #{:std-err :std-out :auto }: - :stream :auto} + + ;; Any custom appender opts: + :opts {:stream :auto ; e/o #{:std-err :std-out :auto } + } + :fn (fn [data] (let [{:keys [output-fn error? appender-opts]} data {:keys [stream]} appender-opts - out (case stream - (nil :auto) (if error? default-err *out*) - :std-err default-err - :std-out default-out - stream)] - (binding [*out* out] (println (output-fn data)))))} + stream (case stream + (nil :auto) (if error? default-err *out*) + :std-err default-err + :std-out default-out + stream)] + (binding [*out* stream] (println (output-fn data)))))} :spit {:doc "Spits to (:spit-filename ) file." @@ -148,7 +156,7 @@ (set-config! example-config) (infof "Hello %s" "world :-)")) -(enc/defonce* ^:dynamic *config* example-config) +(enc/defonce* ^:dynamic *config* "See `example-config` for info." example-config) (defmacro with-config [config & body] `(binding [*config* ~config] ~@body)) (defmacro with-merged-config [config & body] `(binding [*config* (enc/nested-merge *config* ~config)] ~@body)) @@ -157,7 +165,7 @@ #+cljs (set! *config* (f *config*)) #+clj (alter-var-root #'*config* f)) -(defn set-config! [m] (swap-config! (fn [_] m))) +(defn set-config! [m] (swap-config! (fn [_old] m))) (defn merge-config! [m] (swap-config! (fn [old] (enc/nested-merge old m)))) (defn set-level! [level] (swap-config! (fn [m] (merge m {:level level})))) @@ -196,6 +204,7 @@ ;;;; ns filter (def ^:private compile-ns-filters + "(fn [whitelist blacklist]) -> (fn [ns]) -> ?unfiltered-ns" (let [->re-pattern (fn [x] (enc/cond! @@ -213,7 +222,7 @@ white-filter (cond - ;; (nil? whitelist) (fn [ns] false) + ;; (nil? whitelist) (fn [ns] false) ; Might be surprising (empty? whitelist*) (fn [ns] true) :else (fn [ns] (some #(re-find % ns) whitelist*))) @@ -222,13 +231,13 @@ (empty? blacklist*) (fn [ns] true) :else (fn [ns] (not (some #(re-find % ns) blacklist*))))] - [white-filter black-filter]))))) + (fn [ns] (when (and (white-filter ns) (black-filter ns)) ns))))))) (def ^:private ns-filter + "(fn [whitelist blacklist ns]) -> ?unfiltered-ns" (enc/memoize_ (fn [whitelist blacklist ns] - (let [[white-filter black-filter] (compile-ns-filters whitelist blacklist)] - (when (and (white-filter ns) (black-filter ns)) ns))))) + ((compile-ns-filters whitelist blacklist) ns)))) (comment (qb 10000 (ns-filter ["foo.*"] ["foo.baz"] "foo.bar"))) @@ -240,17 +249,17 @@ ;;;; Utils -(defmacro delay-vec [coll] (mapv (fn [in] `(delay ~in)) coll)) -(comment - (qb 10000 (delay :foo) (fn [] :foo)) - (macroexpand '(delay-vec [(do (println "hi") :x) :y :z]))) - +(defn- ->delay [x] (if (delay? x) x (delay x))) (defn- vsplit-err1 [[v1 :as v]] (if-not (enc/error? v1) [nil v] (enc/vsplit-first v))) (comment (vsplit-err1 [:a :b :c]) (vsplit-err1 [(Exception.) :a :b :c])) -(defn default-data-hash-fn [data] +(defn default-data-hash-fn + "Used for rate limiters, some appenders (e.g. Carmine), etc. + Goal: (hash data-1) = (hash data-2) iff data-1 \"the same\" as data-2 for + rate-limiting purposes, etc." + [data] (let [{:keys [?ns-str ?line vargs_]} data vargs (force vargs_)] (str @@ -272,7 +281,11 @@ ;;;; Logging core -(defn log? [level & [?ns-str config]] +(defn log? + "Would Timbre currently log at the given logging level? + * ns filtering requires a compile-time `?ns-str` to be provided. + * Non-global config requires an explicit `config` to be provided." + [level & [?ns-str config]] (let [config (or config *config*)] (and (level>= level (get-active-level config)) (ns-filter (:whitelist config) (:blacklist config) (or ?ns-str "")) @@ -280,16 +293,18 @@ (comment (log? :trace)) -(def ^:dynamic *context* "General-purpose dynamic logging context." nil) +(def ^:dynamic *context* + "General-purpose dynamic logging context. Context will be merged into + appender data map at logging time." nil) (defmacro with-context [context & body] `(binding [*context* ~context] ~@body)) (declare get-hostname) (defn log* "Core fn-level logger. Implementation detail." - [config level ?ns-str ?file ?line msg-type dvargs & [base-data]] + [config level ?ns-str ?file ?line msg-type vargs_ & [base-data]] (when (log? level ?ns-str config) (let [instant (enc/now-dt) - vargs*_ (delay (vsplit-err1 (mapv force dvargs))) + vargs*_ (delay (vsplit-err1 (force vargs_))) ?err_ (delay (get @vargs*_ 0)) vargs_ (delay (get @vargs*_ 1)) data (merge base-data *context* @@ -305,7 +320,7 @@ #+clj :hostname_ #+clj (delay (get-hostname)) :error-level? (#{:error :fatal} level)}) msg-fn - (fn [vargs_] ; *After* middleware, etc. + (fn [vargs_] ; For use *after* middleware, etc. (when-not (nil? msg-type) (when-let [vargs (have [:or nil? vector?] (force vargs_))] (case msg-type @@ -340,7 +355,7 @@ (not (rl-fn data-hash)))))) (let [{:keys [async?] apfn :fn} appender - msg_ (delay (msg-fn (:vargs_ data))) + msg_ (delay (or (msg-fn (:vargs_ data)) #_"")) output-fn (or (:output-fn appender) (:output-fn config) default-output-fn) @@ -356,26 +371,25 @@ {:locale locale :timezone timezone}) (:instant data)))) - data + data ; Final data prep before going to appender (merge data {:appender-id id :appender appender - :appender-opts (:opts appender) + :appender-opts (:opts appender) ; For convenience :msg_ msg_ :msg-fn msg-fn :output-fn output-fn #+clj :timestamp_ #+clj timestamp_})] (if-not async? - (apfn data) + (apfn data) ; Allow errors to throw (send-off (get-agent id) (fn [_] (apfn data))))))) nil (enc/clj1098 (:appenders config)))))) nil) (comment - (log* *config* :info - nil nil nil :print (delay-vec [(do (println "hi") :x) :y]))) + (log* *config* :info nil nil nil :print (delay [(do (println "hi") :x) :y]))) ;;;; Logging macros @@ -391,7 +405,7 @@ ;; TODO Waiting on http://dev.clojure.org/jira/browse/CLJ-865: ?line (:line (meta &form))] `(log* ~config ~level ~ns-str ~?file ~?line ~msg-type - (delay-vec ~args) ~base-data))))) + (delay ~(vec args)) ~base-data))))) (defmacro ^:private def-logger [level] (let [level-name (name level)] @@ -462,7 +476,7 @@ (require '[taoensso.timbre.profiling :as profiling :refer (pspy pspy* profile defnp p p*)])) -;;;; Public utils +;;;; Misc public utils #+clj (defn color-str [color & xs] @@ -475,7 +489,6 @@ #+clj (def default-out (java.io.OutputStreamWriter. System/out)) #+clj (def default-err (java.io.PrintWriter. System/err)) - (defmacro with-default-outs [& body] `(binding [*out* default-out, *err* default-err] ~@body)) diff --git a/src/taoensso/timbre/appenders/example_appender.clj b/src/taoensso/timbre/appenders/example_appender.clj new file mode 100644 index 0000000..083f21b --- /dev/null +++ b/src/taoensso/timbre/appenders/example_appender.clj @@ -0,0 +1,81 @@ +(ns taoensso.timbre.appenders.example-appender + "An example of how Timbre appenders should be written. + Please mention any requirements/dependencies in this docstring." + {:author "Peter Taoussanis"} ; <- Your name here + (:require [clojure.string :as str] + [taoensso.timbre :as timbre] + [taoensso.encore :as encore])) + +;;;; Any private util fns, etc. + +;; ... + +;;;; + +(defn make-appender-fn + "(fn [make-config-map]) -> (fn [appender-data-map]) -> logging side effects." + [make-config] ; Any config that can influence the appender-fn construction + (let [{:keys []} make-config] + + (fn [data] ; Data map as provided to middleware + appenders + (let [{:keys [instant level ?err_ vargs_ output-fn + + config ; Entire config map in effect + appender ; Entire appender map in effect + + ;; = (:opts ), for convenience. You'll + ;; usually want to store+access runtime appender config + ;; stuff here to let users change config without recreating + ;; their appender fn: + appender-opts + + ;; <...> + ;; See `timbre/example-config` for info on all available args + ]} + data + + {:keys [my-arbitrary-appender-opt1]} appender-opts + + ;;; Use `force` to realise possibly-delayed args: + ?err (force ?err_) ; ?err non-nil iff first given arg was an error + vargs (force vargs_) ; Vector of raw args (excl. possible first error) + + ;; You'll often want a formatted string with ns, timestamp, vargs, etc. + ;; A formatter (fn [logging-data-map & [opts]]) -> string is + ;; provided for you under the :output-fn key. Prefer using this fn + ;; to your own formatter when possible, since the user can + ;; configure the :output-fn formatter in a standard way that'll + ;; influence all participating appenders. It may help to look at + ;; the source code for `taoensso.timbre/default-output-fn`! + ;; + any-special-output-fn-opts {} ; Output-fn can use these opts + output-string (output-fn data any-special-output-fn-opts)] + + (println (str my-arbitrary-appender-opt1 output-string)))))) + +(defn make-appender ; Prefer generic name to `make-foo-appender`, etc. + "Your docstring describing the appender, its options, etc." + [& [appender-config make-appender-config]] + (let [default-appender-config + {:doc "My appender docstring" + :enabled? true ; Please enable your appender by default + :min-level :debug + :rate-limit [[5 (encore/ms :mins 1)] ; 5 calls/min + [100 (encore/ms :hours 1)] ; 100 calls/hour + ] + + ;; Any default appender-specific opts. These'll be accessible to your + ;; appender fn under the :appender-opts key for convenience: + :opts {:my-arbitrary-appender-opt1 "hello world, "}} + + ;;; Here we'll prepare the final appender map as described in + ;;; `timbre/example-config`: + appender-config (merge default-appender-config appender-config) + appender-fn (make-appender-fn make-appender-config) + appender (merge appender-config {:fn appender-fn})] + + appender)) + +(comment + ;; Your examples, tests, etc. here + ) diff --git a/src/taoensso/timbre/tools/logging.clj b/src/taoensso/timbre/tools/logging.clj index 9dee248..48162a8 100644 --- a/src/taoensso/timbre/tools/logging.clj +++ b/src/taoensso/timbre/tools/logging.clj @@ -15,7 +15,7 @@ ;; Limitations inline (write! [_ level throwable message] - (let [config *config* ; No support for explicit config + (let [config timbre/*config* ; No support for explicit config ?ns-str nil ; No support ?file nil ; '' ?line nil ; '' From 502ff97d26c9a0258e4320967730e4e9488fd881 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Tue, 26 May 2015 10:52:18 +0700 Subject: [PATCH 11/16] Add default ClojureScript console appender --- project.clj | 18 ++-------- src/taoensso/timbre.cljx | 71 +++++++++++++++++++++++++++------------- 2 files changed, 51 insertions(+), 38 deletions(-) diff --git a/project.clj b/project.clj index afd28f8..3d10364 100644 --- a/project.clj +++ b/project.clj @@ -56,27 +56,15 @@ {:source-paths ["src" "test"] :rules :cljs :output-path "target/test-classes"}]} :cljsbuild - {:test-commands {"node" ["node" :node-runner "target/tests.js"] - ;; "phantom" ["phantomjs" :runner "target/tests.js"] - } + {:test-commands {"node" ["node" :node-runner "target/tests.js"] + "phantom" ["phantomjs" :runner "target/tests.js"]} :builds - ;; TODO Parallel builds currently cause issues with Expectations v2.1.0, - ;; Ref. http://goo.gl/8LDHe5 [{:id "main" :source-paths ["src" "target/classes"] ;; :notify-command ["terminal-notifier" "-title" "cljsbuild" "-message"] :compiler {:output-to "target/main.js" :optimizations :advanced - :pretty-print false}} - {:id "tests" - :source-paths ["src" "target/classes" "test" "target/test-classes"] - :notify-command ["node" "target/tests.js"] - :compiler {:output-to "target/tests.js" - :optimizations :simple ; Necessary for node.js - :pretty-print true - :target :nodejs - :hashbang false ; Ref. http://goo.gl/vrtNDR - :main "taoensso.encore.tests"}}]} + :pretty-print false}}]} :auto-clean false :prep-tasks [["cljx" "once"] "javac" "compile"] diff --git a/src/taoensso/timbre.cljx b/src/taoensso/timbre.cljx index 54476e5..c5df845 100644 --- a/src/taoensso/timbre.cljx +++ b/src/taoensso/timbre.cljx @@ -6,14 +6,13 @@ [taoensso.encore :as enc :refer (have have? qb)]) #+cljs (:require [clojure.string :as str] [taoensso.encore :as enc :refer ()]) - #+cljs (:require-macros [taoensso.encore :as enc :refer (have have?)]) + #+cljs (:require-macros [taoensso.encore :as enc :refer (have have?)] + [taoensso.timbre :as timbre-macros :refer ()]) #+clj (:import [java.util Date Locale] [java.text SimpleDateFormat] [java.io File])) ;;;; TODO -;; - Check for successful cljs compile -;; - Cljs default appenders ;; - Try ease backward comp, update README, CHANGELOG ;; - Document shutdown-agents, ;; Ref. https://github.com/ptaoussanis/timbre/pull/100/files @@ -150,7 +149,31 @@ (when-let [fname (enc/as-?nblank spit-filename)] (try (ensure-spit-dir-exists! fname) (spit fname (str (output-fn data) "\n") :append true) - (catch java.io.IOException _)))))}}})) + (catch java.io.IOException _)))))}} + + #+cljs + {:console + {:doc "Logs to js/console when it exists. Enabled by default." + :min-level nil :enabled? true :async? false :rate-limit nil + :opts {} + :fn + (let [have-logger? (and (exists? js/console) (.-log js/console)) + have-warn-logger? (and have-logger? (.-warn js/console)) + have-error-logger? (and have-logger? (.-error js/console)) + adjust-level {:fatal (if have-error-logger? :error :info) + :error (if have-error-logger? :error :info) + :warn (if have-warn-logger? :warn :info)}] + (if-not have-logger? + (fn [data] nil) + (fn [data] + (let [{:keys [level appender-opts output-fn]} data + {:keys []} appender-opts + output (output-fn data)] + + (case (adjust-level level) + :error (.error js/console output) + :warn (.warn js/console output) + (.log js/console output))))))}}})) (comment (set-config! example-config) @@ -269,6 +292,7 @@ (comment (default-data-hash-fn {})) +#+clj (enc/defonce* ^:private get-agent (enc/memoize_ (fn [appender-id] (agent nil :error-mode :continue)))) @@ -383,7 +407,8 @@ (if-not async? (apfn data) ; Allow errors to throw - (send-off (get-agent id) (fn [_] (apfn data))))))) + #+cljs (apfn data) + #+clj (send-off (get-agent id) (fn [_] (apfn data))))))) nil (enc/clj1098 (:appenders config)))))) nil) @@ -405,25 +430,25 @@ ;; TODO Waiting on http://dev.clojure.org/jira/browse/CLJ-865: ?line (:line (meta &form))] `(log* ~config ~level ~ns-str ~?file ~?line ~msg-type - (delay ~(vec args)) ~base-data))))) + (delay [~@args]) ~base-data))))) -(defmacro ^:private def-logger [level] - (let [level-name (name level)] - `(do - (defmacro ~(symbol (str level-name #_"p")) - ~(str "Logs at " level " level using print-style args.") - ~'{:arglists '([& message] [error & message])} - [& sigs#] `(log *config* ~~level :print ~sigs#)) +;;; Log using print-style args +(defmacro trace [& args] `(log *config* :trace :print ~args)) +(defmacro debug [& args] `(log *config* :debug :print ~args)) +(defmacro info [& args] `(log *config* :info :print ~args)) +(defmacro warn [& args] `(log *config* :warn :print ~args)) +(defmacro error [& args] `(log *config* :error :print ~args)) +(defmacro fatal [& args] `(log *config* :fatal :print ~args)) +(defmacro report [& args] `(log *config* :report :print ~args)) - (defmacro ~(symbol (str level-name "f")) - ~(str "Logs at " level " level using format-style args.") - ~'{:arglists '([fmt & fmt-args] [error fmt & fmt-args])} - [& sigs#] `(log *config* ~~level :format ~sigs#))))) - -(defmacro def-loggers [] - `(do ~@(map (fn [level] `(def-logger ~level)) ordered-levels))) - -(def-loggers) +;;; Log using format-style args +(defmacro tracef [& args] `(log *config* :trace :format ~args)) +(defmacro debugf [& args] `(log *config* :debug :format ~args)) +(defmacro infof [& args] `(log *config* :info :format ~args)) +(defmacro warnf [& args] `(log *config* :warn :format ~args)) +(defmacro errorf [& args] `(log *config* :error :format ~args)) +(defmacro fatalf [& args] `(log *config* :fatal :format ~args)) +(defmacro reportf [& args] `(log *config* :report :format ~args)) (comment (infof "hello %s" "world") @@ -524,5 +549,5 @@ (defmacro sometimes "Handy for sampled logging, etc." [probability & body] - `(do (assert (<= 0 ~probability 1) "Probability: 0 <= p <= 1") + `(do (assert (<= 0 ~probability 1) "Probability: 0 <= p <= 1") (when (< (rand) ~probability) ~@body))) From 371fb24d3fdc8195f7444b8618958492acc16192 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Tue, 26 May 2015 12:43:22 +0700 Subject: [PATCH 12/16] Update README, etc. --- README.md | 308 ++++++++++++++++++++++----------------- src/taoensso/timbre.cljx | 12 +- 2 files changed, 186 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index 5e258e8..a5cc4c0 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,33 @@ **[API docs][]** | **[CHANGELOG][]** | [other Clojure libs][] | [Twitter][] | [contact/contrib](#contact--contributing) | current [Break Version][]: ```clojure -[com.taoensso/timbre "3.4.0"] +[com.taoensso/timbre "3.4.0"] ; Stable +[com.taoensso/timbre "4.0.0-beta1"] ; BREAKING, please see CHANGELOG for details ``` -# Timbre, a (sane) Clojure logging & profiling library +# Timbre, a (sane) Clojure/Script logging & profiling library -Logging with Java can be maddeningly, unnecessarily hard. Particularly if all you want is something *simple that works out-the-box*. Timbre brings functional, Clojure-y goodness to all your logging needs. **No XML!** +Java logging is a tragic comedy full of crazy, unnecessary complexity that buys you _nothing_. It can be maddeningly, unnecessarily hard to get even the simplest logging working. We can do **so** much better with Clojure/Script. + +Timbre brings functional, Clojure-y goodness to all your logging needs. It's fast, deeply flexible, and easy to configure. **No XML**! ## What's in the box™? - * [Logs as Clojure values](https://github.com/ptaoussanis/timbre/tree/dev#redis-carmine-appender-v3) (v3+). - * Small, uncomplicated **all-Clojure** library. - * **Super-simple map-based config**: no arcane XML or properties files! - * **Low overhead** with dynamic logging level. - * **No overhead** with compile-time logging level. (v2.6+) - * Flexible **fn-centric appender model** with **middleware**. - * Sensible built-in appenders including simple **email appender**. - * Tunable **rate limit** and **asynchronous** logging support. - * Robust **namespace filtering**. - * [tools.logging](https://github.com/clojure/tools.logging) support (optional, useful when integrating with legacy logging systems). - * Dead-simple, logging-level-aware **logging profiler**. + * Full **Clojure** + **ClojureScript** support (v4+). + * No XML or properties files. **One config map**, and you're set. + * Deeply flexible **fn appender model** with **middleware**. + * **Fantastic performance** at any scale. + * Filter logging by levels and **namespace whitelist/blacklist patterns**. + * **Zero overhead** with **complete Clj+Cljs elision** for compile-time level/ns filters. + * Useful built-in appenders for **out-the-box** Clj+Cljs logging. + * Powerful, easy-to-configure per-appender **rate limits** and **async logging**. + * [Logs as Clojure values](#redis-carmine-appender-v3) (v3+). + * [tools.logging](https://github.com/clojure/tools.logging) support (optional, useful when integrating with legacy logging systems). + * Logging-level-aware **logging profiler**. + * Tiny, **simple**, cross-platform codebase. ## 3rd-party tools, appenders, etc. - * [log-config](https://github.com/palletops/log-config) by [Hugo Duncan](https://github.com/hugoduncan) - library to help manage Timbre logging config. - * Suggestions welcome! + * [log-config](https://github.com/palletops/log-config) by [Hugo Duncan](https://github.com/hugoduncan) - library to help manage Timbre logging config. + * Other suggestions welcome! ## Getting started @@ -32,26 +36,24 @@ Logging with Java can be maddeningly, unnecessarily hard. Particularly if all yo Add the necessary dependency to your [Leiningen][] `project.clj` and use the supplied ns-import helper: ```clojure -[com.taoensso/timbre "3.4.0"] ; project.clj +[com.taoensso/timbre "4.0.0-beta1"] ; Add to your project.clj :dependencies -(ns my-app (:require [taoensso.timbre :as timbre])) ; Your ns -(timbre/refer-timbre) ; Provides useful Timbre aliases in this ns +(ns my-app ; Your ns + (:require [taoensso.timbre :as timbre + :refer (log trace debug info warn error fatal report + logf tracef debugf infof warnf errorf fatalf reportf + spy)] + + ;; Clj only: + [taoensso.timbre.profiling :as profiling + :refer (pspy pspy* profile defnp p p*)])) ``` -The `refer-timbre` call is a convenience fn that executes: -```clojure -(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 with-logging-config - sometimes)]) -(require '[taoensso.timbre.profiling :as profiling - :refer (pspy pspy* profile defnp p p*)]) -``` +You can also use `timbre/refer-timbre` to setup these ns refers automatically (Clj only). ### Logging -By default, Timbre gives you basic print output to `*out*`/`*err*` at a `debug` logging level: +By default, Timbre gives you basic print stream or `js/console` (v4+) output at a `debug` logging level: ```clojure (info "This will print") => nil @@ -63,7 +65,7 @@ By default, Timbre gives you basic print output to `*out*`/`*err*` at a `debug` (trace "This won't print due to insufficient logging level") => nil ``` -First-argument exceptions generate a nicely cleaned-up stack trace using [io.aviso.exception](https://github.com/AvisoNovate/pretty): +First-argument exceptions generate a nicely cleaned-up stack trace using [io.aviso.exception](https://github.com/AvisoNovate/pretty) (Clj only): ```clojure (info (Exception. "Oh noes") "arg1" "arg2") @@ -74,156 +76,202 @@ java.lang.Exception: Oh noes <...> ``` +### Timbre slowing down your Clojure app shutdown? + +This is due to an outstanding [issue](http://dev.clojure.org/jira/browse/CLJ-124) in Clojure. As a workaround, add the following to your application's startup routine: + +```clojure +(.addShutdownHook (Runtime/getRuntime) (Thread. (fn [] (shutdown-agents)))) +``` + ### Configuration -This is the biggest win over Java logging utilities IMO. Here's `timbre/example-config` (also Timbre's default config): +This is the biggest win over Java logging IMO. Here's `timbre/example-config` (also Timbre's default config): + +> The example here shows config for **Timbre v4**. See [here](https://github.com/ptaoussanis/timbre/tree/v3.4.0#configuration) for an example of **Timbre v3** config. ```clojure (def example-config - "APPENDERS - An appender is a map with keys: - :doc ; (Optional) string. - :min-level ; (Optional) keyword, or nil (no minimum level). - :enabled? ; (Optional). - :async? ; (Optional) dispatch using agent (good for slow appenders). - :rate-limit ; (Optional) [ncalls-limit window-ms]. - :fmt-output-opts ; (Optional) extra opts passed to `fmt-output-fn`. - :fn ; (fn [appender-args-map]), with keys described below. + "Example (+default) Timbre v4 config map. - An appender's fn takes a single map with keys: - :level ; Keyword. - :error? ; Is level an 'error' level?. - :throwable ; java.lang.Throwable. - :args ; Raw logging macro args (as given to `info`, etc.). - :message ; Stringified logging macro args, or nil. - :output ; Output of `fmt-output-fn`, used by built-in appenders - ; as final, formatted appender output. Appenders may (but - ; are not obligated to) use this as their output. - :ap-config ; Contents of config's :shared-appender-config key. - :profile-stats ; From `profile` macro. - :instant ; java.util.Date. - :timestamp ; String generated from :timestamp-pattern, :timestamp-locale. - :hostname ; String. - :ns ; String. - ;; Waiting on http://dev.clojure.org/jira/browse/CLJ-865: - :file ; String. - :line ; Integer. + APPENDERS - 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). + *** Please see the `taoensso.timbre.appenders.example-appender` ns if you + plan to write your own Timbre appender *** - The `example-config` code contains further settings and details. + An appender is a map with keys: + :doc ; Optional docstring + :min-level ; Level keyword, or nil (=> no minimum level) + :enabled? ; + :async? ; Dispatch using agent? Useful for slow appenders + :rate-limit ; [[ncalls-limit window-ms] <...>], or nil + :data-hash-fn ; Used by rate-limiter, etc. + :opts ; Any appender-specific opts + :fn ; (fn [data-map]), with keys described below + + An appender's fn takes a single data map with keys: + :config ; Entire config map (this map, etc.) + :appender-id ; Id of appender currently being dispatched to + :appender ; Entire appender map currently being dispatched to + :appender-opts ; Duplicates (:opts ), for convenience + + :instant ; Platform date (java.util.Date or js/Date) + :level ; Keyword + :error-level? ; Is level :error or :fatal? + :?ns-str ; String, or nil + :?file ; String, or nil ; Waiting on CLJ-865 + :?line ; Integer, or nil ; Waiting on CLJ-865 + + :?err_ ; Delay - first-argument platform error, or nil + :vargs_ ; Delay - raw args vector + :hostname_ ; Delay - string (clj only) + :msg_ ; Delay - args string + :timestamp_ ; Delay - string + :output-fn ; (fn [data & [opts]]) -> formatted output string + + :profile-stats ; From `profile` macro + + + + MIDDLEWARE + Middleware are simple (fn [data]) -> ?data fns (applied left->right) that + transform the data map dispatched to appender fns. If any middleware returns + nil, NO dispatching will occur (i.e. the event will be filtered). + + The `example-config` source code contains further settings and 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 [] + (merge + {:level :debug ; e/o #{:trace :debug :info :warn :error :fatal :report} - ;; Fns (applied right-to-left) to transform/filter appender fn args. - ;; Useful for obfuscating credentials, pattern filtering, etc. - :middleware [] + ;; Control log filtering by namespaces/patterns. Useful for turning off + ;; logging in noisy libraries, etc.: + :whitelist [] #_["my-app.foo-ns"] + :blacklist [] #_["taoensso.*"] - ;;; Control :timestamp format - :timestamp-pattern "yyyy-MMM-dd HH:mm:ss ZZ" ; SimpleDateFormat pattern - :timestamp-locale nil ; A Locale object, or nil + :middleware [] ; (fns [data]) -> ?data, applied left->right - ;; Output formatter used by built-in appenders. Custom appenders may (but are - ;; not required to use) its output (:output). Extra per-appender opts can be - ;; supplied as an optional second (map) arg. - :fmt-output-fn - (fn [{:keys [level throwable message timestamp hostname ns]} - ;; Any extra appender-specific opts: - & [{:keys [nofonts?] :as appender-fmt-output-opts}]] - ;; [] - - (format "%s %s %s [%s] - %s%s" - timestamp hostname (-> level name str/upper-case) ns (or message "") - (or (stacktrace throwable "\n" (when nofonts? {})) ""))) + #+clj :timestamp-opts + #+clj default-timestamp-opts ; {:pattern _ :locale _ :timezone _} - :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 :rate-limit nil - :fn (fn [{:keys [error? output]}] ; Use any appender args - (binding [*out* (if error? *err* *out*)] - (str-println output)))} + :output-fn default-output-fn ; (fn [data]) -> string - :spit - {:doc "Spits to `(:spit-filename :shared-appender-config)` file." - :min-level nil :enabled? false :async? false :rate-limit nil - :fn (fn [{:keys [ap-config output]}] ; Use any appender args - (when-let [filename (:spit-filename ap-config)] - (try (spit filename output :append true) - (catch java.io.IOException _))))}}}) + :appenders + #+clj + {:println ; Appender id + ;; Appender : + {:doc "Prints to (:stream ) IO stream. Enabled by default." + :min-level nil :enabled? true :async? false :rate-limit nil + + ;; Any custom appender opts: + :opts {:stream :auto ; e/o #{:std-err :std-out :auto } + } + + :fn + (fn [data] + (let [{:keys [output-fn error? appender-opts]} data + {:keys [stream]} appender-opts + stream (case stream + (nil :auto) (if error? default-err *out*) + :std-err default-err + :std-out default-out + stream)] + (binding [*out* stream] (println (output-fn data)))))} + + :spit + {:doc "Spits to (:spit-filename ) file." + :min-level nil :enabled? false :async? false :rate-limit nil + :opts {:spit-filename "timbre-spit.log"} + :fn + (fn [data] + (let [{:keys [output-fn appender-opts]} data + {:keys [spit-filename]} appender-opts] + (when-let [fname (enc/as-?nblank spit-filename)] + (try (ensure-spit-dir-exists! fname) + (spit fname (str (output-fn data) "\n") :append true) + (catch java.io.IOException _)))))}} + + #+cljs + {:console + {:doc "Logs to js/console when it exists. Enabled by default." + :min-level nil :enabled? true :async? false :rate-limit nil + :opts {} + :fn + (let [have-logger? (and (exists? js/console) (.-log js/console)) + have-warn-logger? (and have-logger? (.-warn js/console)) + have-error-logger? (and have-logger? (.-error js/console)) + adjust-level {:fatal (if have-error-logger? :error :info) + :error (if have-error-logger? :error :info) + :warn (if have-warn-logger? :warn :info)}] + (if-not have-logger? + (fn [data] nil) + (fn [data] + (let [{:keys [level appender-opts output-fn]} data + {:keys []} appender-opts + output (output-fn data)] + + (case (adjust-level level) + :error (.error js/console output) + :warn (.warn js/console output) + (.log js/console output))))))}}})) ``` A few things to note: - - * Appenders are trivial to write & configure - **they're just fns**. It's Timbre's job to dispatch useful args to appenders when appropriate, it's their job to do something interesting with them. - * Being 'just fns', appenders have basically limitless potential: write to your database, send a message over the network, check some other state (e.g. environment config) before making a choice, etc. + * Appenders are _trivial_ to write & configure - **they're just fns**. It's Timbre's job to dispatch useful args to appenders when appropriate, it's their job to do something interesting with them. + * Being 'just fns', appenders have basically limitless potential: write to your database, send a message over the network, check some other state (e.g. environment config) before making a choice, etc. The **logging level** may be set: - * At compile-time: (`TIMBRE_LOG_LEVEL` environment variable). - * Via an atom: `(timbre/set-level! )`. (Usual method). - * Via dynamic thread-level binding: `(timbre/with-log-level ...)`. - -A compile-time level offers _zero-overhead_ performance since it'll cause insufficient logging calls to disappear completely at compile-time. Usually you won't need/want to bother: Timbre offers very decent performance with runtime level checks (~15msecs/10k checks on my Macbook Air). - -For common-case ease-of-use, **all logging utils use a global atom for their config**. This is configurable with `timbre/set-config!`, `timbre/merge-config!`. The lower-level `log` and `logf` macros also take an optional first-arg config map for greater flexibility (e.g. **during testing**). + * At compile-time: (`TIMBRE_LEVEL` environment variable). + * With `timbre/set-level!`/`timbre/merge-level!`. + * With `timbre/with-level`. ### Built-in appenders #### Redis ([Carmine](https://github.com/ptaoussanis/carmine)) appender (v3+) ```clojure -;; [com.taoensso/carmine "2.4.0"] ; Add to project.clj deps +;; [com.taoensso/carmine "2.10.0"] ; Add to project.clj deps ;; (:require [taoensso.timbre.appenders (carmine :as car-appender)]) ; Add to ns -(timbre/set-config! [:appenders :carmine] (car-appender/make-carmine-appender)) +(timbre/merge-config! {:appenders {:carmine (car-appender/make-appender)}}) ``` This gives us a high-performance Redis appender: - * **All raw logging args are preserved** in serialized form (**even Throwables!**). - * Only the most recent instance of each **unique entry** is kept (hash fn used to determine uniqueness is configurable). - * Configurable number of entries to keep per logging level. - * **Log is just a value**: a vector of Clojure maps: **query+manipulate with standard seq fns**: group-by hostname, sort/filter by ns & severity, explore exception stacktraces, filter by raw arguments, etc. **Datomic and `core.logic`** also offer interesting opportunities here. + * **All raw logging args are preserved** in serialized form (**even errors!**). + * Only the most recent instance of each **unique entry** is kept (hash fn used to determine uniqueness is configurable). + * Configurable number of entries to keep per logging level. + * **Log is just a value**: a vector of Clojure maps: **query+manipulate with standard seq fns**: group-by hostname, sort/filter by ns & severity, explore exception stacktraces, filter by raw arguments, stick into or query with **Datomic**, etc. A simple query utility is provided: `car-appender/query-entries`. #### Email ([Postal](https://github.com/drewr/postal)) appender ```clojure -;; [com.draines/postal "1.9.2"] ; Add to project.clj deps +;; [com.draines/postal "1.11.3"] ; Add to project.clj deps ;; (:require [taoensso.timbre.appenders (postal :as postal-appender)]) ; Add to ns -(timbre/set-config! [:appenders :postal] - (postal-appender/make-postal-appender - {:enabled? true - :rate-limit [1 60000] ; 1 msg / 60,000 msecs (1 min) - :async? true ; Don't block waiting for email to send - } +(timbre/merge-config! + {:appenders {:postal + (postal-appender/make-appender {} {:postal-config ^{:host "mail.isp.net" :user "jsmith" :pass "sekrat!!1"} - {:from "me@draines.com" :to "foo@example.com"}})) + {:from "me@draines.com" :to "foo@example.com"}})}}) ``` #### File appender ```clojure -(timbre/set-config! [:appenders :spit :enabled?] true) -(timbre/set-config! [:shared-appender-config :spit-filename] "/path/my-file.log") +(timbre/merge-config! + {:appenders {:spit {:enabled? true :opts {:spit-finame "/path/my-file.log"}}}}) ``` #### Other included appenders -A number of 3rd-party appenders are included out-the-box for: Android, IRC, sockets, MongoDB, and rotating files. These are all located in the `taoensso.timbre.appenders.x` namespaces - **please see the relevant docstrings for details**. +A number of 3rd-party appenders are included out-the-box [here](https://github.com/ptaoussanis/timbre/src/taoensso/timbre/appenders/3rd_party). **Please see the relevant docstring for details**. -Thanks to their respective authors! Just give me a shout if you've got an appender you'd like to have added. +Thanks to the respective authors! Just give me a shout if you've got an appender you'd like to have added. -## Profiling +## Profiling (currently Clj only) The usual recommendation for Clojure profiling is: use a good **JVM profiler** like [YourKit](http://www.yourkit.com/), [JProfiler](http://www.ej-technologies.com/products/jprofiler/overview.html), or [VisualVM](http://docs.oracle.com/javase/6/docs/technotes/guides/visualvm/index.html). @@ -285,7 +333,7 @@ Otherwise reach me (Peter Taoussanis) at [taoensso.com][] or on [Twitter][]. Che ## License -Copyright © 2012-2014 Peter Taoussanis. Distributed under the [Eclipse Public License][], the same as Clojure. +Copyright © 2012-2015 Peter Taoussanis. Distributed under the [Eclipse Public License][], the same as Clojure. [API docs]: http://ptaoussanis.github.io/timbre/ diff --git a/src/taoensso/timbre.cljx b/src/taoensso/timbre.cljx index c5df845..dda9f6d 100644 --- a/src/taoensso/timbre.cljx +++ b/src/taoensso/timbre.cljx @@ -86,7 +86,7 @@ :?file ; String, or nil ; Waiting on CLJ-865 :?line ; Integer, or nil ; Waiting on CLJ-865 - :?err_ ; Delay - first-argument platform error + :?err_ ; Delay - first-argument platform error, or nil :vargs_ ; Delay - raw args vector :hostname_ ; Delay - string (clj only) :msg_ ; Delay - args string @@ -108,14 +108,18 @@ (merge {:level :debug ; e/o #{:trace :debug :info :warn :error :fatal :report} - :whitelist [] ; "my-ns.*", etc. - :blacklist [] ; + ;; Control log filtering by namespaces/patterns. Useful for turning off + ;; logging in noisy libraries, etc.: + :whitelist [] #_["my-app.foo-ns"] + :blacklist [] #_["taoensso.*"] + :middleware [] ; (fns [data]) -> ?data, applied left->right - :output-fn default-output-fn ; (fn [data]) -> string #+clj :timestamp-opts #+clj default-timestamp-opts ; {:pattern _ :locale _ :timezone _} + :output-fn default-output-fn ; (fn [data]) -> string + :appenders #+clj {:println ; Appender id From 6ab9c4ac59a69da7375a14b5ea27b272f7a050b9 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Tue, 26 May 2015 14:20:39 +0700 Subject: [PATCH 13/16] Attempted auto workaround for CLJ-124 --- README.md | 8 -------- src/taoensso/timbre.cljx | 7 +++++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a5cc4c0..c5a3ca6 100644 --- a/README.md +++ b/README.md @@ -76,14 +76,6 @@ java.lang.Exception: Oh noes <...> ``` -### Timbre slowing down your Clojure app shutdown? - -This is due to an outstanding [issue](http://dev.clojure.org/jira/browse/CLJ-124) in Clojure. As a workaround, add the following to your application's startup routine: - -```clojure -(.addShutdownHook (Runtime/getRuntime) (Thread. (fn [] (shutdown-agents)))) -``` - ### Configuration This is the biggest win over Java logging IMO. Here's `timbre/example-config` (also Timbre's default config): diff --git a/src/taoensso/timbre.cljx b/src/taoensso/timbre.cljx index dda9f6d..46fe859 100644 --- a/src/taoensso/timbre.cljx +++ b/src/taoensso/timbre.cljx @@ -555,3 +555,10 @@ [probability & body] `(do (assert (<= 0 ~probability 1) "Probability: 0 <= p <= 1") (when (< (rand) ~probability) ~@body))) + +;;;; Shutdown hook ; Workaround for http://dev.clojure.org/jira/browse/CLJ-124 + +#+clj +(defonce ^:private shutdown-hook + (.addShutdownHook (Runtime/getRuntime) + (Thread. (fn [] (shutdown-agents))))) From 7c969ca0e6cc09e13420b213945224fbb7e7a8ac Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Tue, 26 May 2015 14:28:07 +0700 Subject: [PATCH 14/16] Final hk? --- CHANGELOG.md | 39 +++ README.md | 85 ++----- src/taoensso/timbre.cljx | 227 +++++++++--------- .../timbre/appenders/3rd_party/android.clj | 4 + .../timbre/appenders/3rd_party/irc.clj | 4 + .../timbre/appenders/3rd_party/rolling.clj | 4 + .../timbre/appenders/3rd_party/zmq.clj | 4 + src/taoensso/timbre/appenders/carmine.clj | 4 + src/taoensso/timbre/appenders/postal.clj | 6 +- src/taoensso/timbre/profiling.clj | 2 +- src/taoensso/timbre/tools/logging.clj | 4 +- 11 files changed, 202 insertions(+), 181 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df2f0d..6b5930f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ > This project uses [Break Versioning](https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md) as of **Aug 16, 2014**. +## v4.0.0-beta1 / 2015 May 26 + +> This is a **MAJOR** update. Your custom appenders **WILL BREAK**. Your configuration **MIGHT BREAK**. Your call sites should be fine. I've updated all included appenders, but **haven't tested** any 3rd-party appenders. + +* **New**: full **ClojureScript** support, including a default js/console appender [#51] +* **New**: support for compile-time ns filtering + elision (both Clj+Cljs) +* **New**: support for MDC-like contexts [#42] +* **New**: default :println appender has picked up a :stream opt [#49] +* **New**: create necessary spit appender paths [#93] +* **New**: full-power fn-level `log*` util [#99] +* **New**: added a reference appender example [here](https://github.com/ptaoussanis/timbre/blob/master/src/taoensso/timbre/appenders/example_appender.clj) +* **Implementation**: modernized + simplified codebase +* **Implementation**: significant performance improvements across the board +* **Implementation**: use delays to avoid unnecessarily producing unused arg msgs [#71] +* **Fix**: auto shutdown agents to prevent slow app shutdown [#61] + +```clojure +[com.taoensso/timbre "4.0.0-beta1"] +``` + +### Migration checklist + +* Removed vars: `timbre/config`, `timbre/level-atom`, `default-fmt-output-fn` +* The fn signature for `set-config!` has changed: `[ks val]` -> `[config]` +* Middleware now apply left->right, not right->left +* Renamed default appender: `:standard-out` -> `:println` +* Renamed config opts: `:timestamp-pattern`, `:timestamp-locale` -> `:timestamp-opts {:pattern _ :locale _ :timezone _}` +* Appender :rate-limit format has changed: `[ncalls ms]` -> `[[ncalls ms] <...>]` +* Renamed appender args: `:ns`->`:?ns-str`, `:file`->`:?file`, `:line`->`:?line` +* Appender args now wrapped with delays: `:throwable`->`:?err_`, `:message`->`:msg_`, `:timestamp`->`:timestamp_`, `:hostname`->`:hostname_`, `:args`->`:vargs_` +* Appender args removed: `:output`, `:ap-config` +* Appender args added: `:output-fn (fn [data])`, `:appender-opts` +* `stacktrace` util fn signature changed: `[throwable & [sep fonts]` -> `[err & [opts]]` +* All bundles 3rd-party appenders have moved to a new `3rd-party` ns + +Apologies for the hassle in migrating. The changes made here all bring serious benefits (performance, simplicity, future extensibility, cross-platform support) and I'm confident that v4's the last time I'll need to touch the core design. Future work will be focused on polish, stability, and better+more bundled appenders. + +/ Peter Taoussanis + ## v3.4.0 / 2015 Feb 16 > This should be a **non-breaking** release that only bumps some old dependencies. diff --git a/README.md b/README.md index c5a3ca6..3f66ac1 100644 --- a/README.md +++ b/README.md @@ -133,79 +133,24 @@ This is the biggest win over Java logging IMO. Here's `timbre/example-config` (a The `example-config` source code contains further settings and details. See also `set-config!`, `merge-config!`, `set-level!`." - (merge - {:level :debug ; e/o #{:trace :debug :info :warn :error :fatal :report} + {:level :debug ; e/o #{:trace :debug :info :warn :error :fatal :report} - ;; Control log filtering by namespaces/patterns. Useful for turning off - ;; logging in noisy libraries, etc.: - :whitelist [] #_["my-app.foo-ns"] - :blacklist [] #_["taoensso.*"] + ;; Control log filtering by namespaces/patterns. Useful for turning off + ;; logging in noisy libraries, etc.: + :whitelist [] #_["my-app.foo-ns"] + :blacklist [] #_["taoensso.*"] - :middleware [] ; (fns [data]) -> ?data, applied left->right + :middleware [] ; (fns [data]) -> ?data, applied left->right - #+clj :timestamp-opts - #+clj default-timestamp-opts ; {:pattern _ :locale _ :timezone _} - - :output-fn default-output-fn ; (fn [data]) -> string - - :appenders - #+clj - {:println ; Appender id - ;; Appender : - {:doc "Prints to (:stream ) IO stream. Enabled by default." - :min-level nil :enabled? true :async? false :rate-limit nil - - ;; Any custom appender opts: - :opts {:stream :auto ; e/o #{:std-err :std-out :auto } - } - - :fn - (fn [data] - (let [{:keys [output-fn error? appender-opts]} data - {:keys [stream]} appender-opts - stream (case stream - (nil :auto) (if error? default-err *out*) - :std-err default-err - :std-out default-out - stream)] - (binding [*out* stream] (println (output-fn data)))))} - - :spit - {:doc "Spits to (:spit-filename ) file." - :min-level nil :enabled? false :async? false :rate-limit nil - :opts {:spit-filename "timbre-spit.log"} - :fn - (fn [data] - (let [{:keys [output-fn appender-opts]} data - {:keys [spit-filename]} appender-opts] - (when-let [fname (enc/as-?nblank spit-filename)] - (try (ensure-spit-dir-exists! fname) - (spit fname (str (output-fn data) "\n") :append true) - (catch java.io.IOException _)))))}} - - #+cljs - {:console - {:doc "Logs to js/console when it exists. Enabled by default." - :min-level nil :enabled? true :async? false :rate-limit nil - :opts {} - :fn - (let [have-logger? (and (exists? js/console) (.-log js/console)) - have-warn-logger? (and have-logger? (.-warn js/console)) - have-error-logger? (and have-logger? (.-error js/console)) - adjust-level {:fatal (if have-error-logger? :error :info) - :error (if have-error-logger? :error :info) - :warn (if have-warn-logger? :warn :info)}] - (if-not have-logger? - (fn [data] nil) - (fn [data] - (let [{:keys [level appender-opts output-fn]} data - {:keys []} appender-opts - output (output-fn data)] - - (case (adjust-level level) - :error (.error js/console output) - :warn (.warn js/console output) - (.log js/console output))))))}}})) + :appenders + {:simple-println ; Appender id + ;; Appender definition (just a map): + {:min-level nil :enabled? true :async? false + :rate-limit [[1 250] [10 5000]] ; 1/250ms, 10/5s + :fn ; Appender's fn + (fn [data] + (let [{:keys [output-fn]} data] + (println (output-fn data))))}}}) ``` A few things to note: diff --git a/src/taoensso/timbre.cljx b/src/taoensso/timbre.cljx index 46fe859..9e21528 100644 --- a/src/taoensso/timbre.cljx +++ b/src/taoensso/timbre.cljx @@ -12,11 +12,6 @@ [java.text SimpleDateFormat] [java.io File])) -;;;; TODO -;; - Try ease backward comp, update README, CHANGELOG -;; - Document shutdown-agents, -;; Ref. https://github.com/ptaoussanis/timbre/pull/100/files - ;;;; Encore version check #+clj @@ -105,79 +100,78 @@ The `example-config` source code contains further settings and details. See also `set-config!`, `merge-config!`, `set-level!`." - (merge - {:level :debug ; e/o #{:trace :debug :info :warn :error :fatal :report} + {:level :debug ; e/o #{:trace :debug :info :warn :error :fatal :report} - ;; Control log filtering by namespaces/patterns. Useful for turning off - ;; logging in noisy libraries, etc.: - :whitelist [] #_["my-app.foo-ns"] - :blacklist [] #_["taoensso.*"] + ;; Control log filtering by namespaces/patterns. Useful for turning off + ;; logging in noisy libraries, etc.: + :whitelist [] #_["my-app.foo-ns"] + :blacklist [] #_["taoensso.*"] - :middleware [] ; (fns [data]) -> ?data, applied left->right + :middleware [] ; (fns [data]) -> ?data, applied left->right - #+clj :timestamp-opts - #+clj default-timestamp-opts ; {:pattern _ :locale _ :timezone _} + #+clj :timestamp-opts + #+clj default-timestamp-opts ; {:pattern _ :locale _ :timezone _} - :output-fn default-output-fn ; (fn [data]) -> string + :output-fn default-output-fn ; (fn [data]) -> string - :appenders - #+clj - {:println ; Appender id - ;; Appender : - {:doc "Prints to (:stream ) IO stream. Enabled by default." - :min-level nil :enabled? true :async? false :rate-limit nil + :appenders + #+clj + {:println ; Appender id + ;; Appender map: + {:doc "Prints to (:stream ) IO stream. Enabled by default." + :min-level nil :enabled? true :async? false :rate-limit nil - ;; Any custom appender opts: - :opts {:stream :auto ; e/o #{:std-err :std-out :auto } - } + ;; Any custom appender opts: + :opts {:stream :auto ; e/o #{:std-err :std-out :auto } + } - :fn - (fn [data] - (let [{:keys [output-fn error? appender-opts]} data - {:keys [stream]} appender-opts - stream (case stream - (nil :auto) (if error? default-err *out*) - :std-err default-err - :std-out default-out - stream)] - (binding [*out* stream] (println (output-fn data)))))} + :fn + (fn [data] + (let [{:keys [output-fn error? appender-opts]} data + {:keys [stream]} appender-opts + stream (case stream + (nil :auto) (if error? default-err *out*) + :std-err default-err + :std-out default-out + stream)] + (binding [*out* stream] (println (output-fn data)))))} - :spit - {:doc "Spits to (:spit-filename ) file." - :min-level nil :enabled? false :async? false :rate-limit nil - :opts {:spit-filename "timbre-spit.log"} - :fn - (fn [data] - (let [{:keys [output-fn appender-opts]} data - {:keys [spit-filename]} appender-opts] - (when-let [fname (enc/as-?nblank spit-filename)] - (try (ensure-spit-dir-exists! fname) - (spit fname (str (output-fn data) "\n") :append true) - (catch java.io.IOException _)))))}} + :spit + {:doc "Spits to (:spit-filename ) file." + :min-level nil :enabled? false :async? false :rate-limit nil + :opts {:spit-filename "timbre-spit.log"} + :fn + (fn [data] + (let [{:keys [output-fn appender-opts]} data + {:keys [spit-filename]} appender-opts] + (when-let [fname (enc/as-?nblank spit-filename)] + (try (ensure-spit-dir-exists! fname) + (spit fname (str (output-fn data) "\n") :append true) + (catch java.io.IOException _)))))}} - #+cljs - {:console - {:doc "Logs to js/console when it exists. Enabled by default." - :min-level nil :enabled? true :async? false :rate-limit nil - :opts {} - :fn - (let [have-logger? (and (exists? js/console) (.-log js/console)) - have-warn-logger? (and have-logger? (.-warn js/console)) - have-error-logger? (and have-logger? (.-error js/console)) - adjust-level {:fatal (if have-error-logger? :error :info) - :error (if have-error-logger? :error :info) - :warn (if have-warn-logger? :warn :info)}] - (if-not have-logger? - (fn [data] nil) - (fn [data] - (let [{:keys [level appender-opts output-fn]} data - {:keys []} appender-opts - output (output-fn data)] + #+cljs + {:console + {:doc "Logs to js/console when it exists. Enabled by default." + :min-level nil :enabled? true :async? false :rate-limit nil + :opts {} + :fn + (let [have-logger? (and (exists? js/console) (.-log js/console)) + have-warn-logger? (and have-logger? (.-warn js/console)) + have-error-logger? (and have-logger? (.-error js/console)) + adjust-level {:fatal (if have-error-logger? :error :info) + :error (if have-error-logger? :error :info) + :warn (if have-warn-logger? :warn :info)}] + (if-not have-logger? + (fn [data] nil) + (fn [data] + (let [{:keys [level appender-opts output-fn]} data + {:keys []} appender-opts + output (output-fn data)] - (case (adjust-level level) - :error (.error js/console output) - :warn (.warn js/console output) - (.log js/console output))))))}}})) + (case (adjust-level level) + :error (.error js/console output) + :warn (.warn js/console output) + (.log js/console output))))))}}}) (comment (set-config! example-config) @@ -195,8 +189,8 @@ (defn set-config! [m] (swap-config! (fn [_old] m))) (defn merge-config! [m] (swap-config! (fn [old] (enc/nested-merge old m)))) -(defn set-level! [level] (swap-config! (fn [m] (merge m {:level level})))) -(defn with-level [level & body] +(defn set-level! [level] (swap-config! (fn [m] (merge m {:level level})))) +(defmacro with-level [level & body] `(binding [*config* (merge *config* {:level ~level})] ~@body)) (comment (set-level! :info) *config*) @@ -205,11 +199,11 @@ (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 - (let [valid-level-set (set ordered-levels)] - (fn [level] - (or (valid-level-set level) - (throw (ex-info (str "Invalid logging level: " level) {:level level})))))) + (fn [level] + (or (valid-levels level) + (throw (ex-info (str "Invalid logging level: " level) {:level level}))))) (comment (valid-level :info)) @@ -220,7 +214,9 @@ #+clj (defn- env-val [id] (when-let [s (System/getenv id)] (enc/read-edn s))) #+clj (def ^:private compile-time-level - (have [:or nil? valid-level] (keyword (env-val "TIMBRE_LEVEL")))) + (have [:or nil? valid-level] + (keyword (or (env-val "TIMBRE_LEVEL") + (env-val "TIMBRE_LOG_LEVEL"))))) (defn get-active-level [& [config]] (or (:level (or config *config*)) :report)) @@ -307,7 +303,7 @@ (comment (def rf (get-rate-limiter :my-appender [[10 5000]]))) -;;;; Logging core +;;;; Internal logging core (defn log? "Would Timbre currently log at the given logging level? @@ -328,14 +324,15 @@ (declare get-hostname) -(defn log* "Core fn-level logger. Implementation detail." - [config level ?ns-str ?file ?line msg-type vargs_ & [base-data]] +(defn log1-fn + "Core fn-level logger. Implementation detail!" + [config level ?ns-str ?file ?line msg-type vargs_ & [?base-data]] (when (log? level ?ns-str config) (let [instant (enc/now-dt) vargs*_ (delay (vsplit-err1 (force vargs_))) ?err_ (delay (get @vargs*_ 0)) vargs_ (delay (get @vargs*_ 1)) - data (merge base-data *context* + data (merge ?base-data *context* {:config config ; Entire config! ;; :context *context* ; Extra destructure's a nuisance :instant instant @@ -352,9 +349,9 @@ (when-not (nil? msg-type) (when-let [vargs (have [:or nil? vector?] (force vargs_))] (case msg-type - :print (enc/spaced-str vargs) - :format (let [[fmt args] (enc/vsplit-first vargs)] - (enc/format* fmt args)))))) + :p (enc/spaced-str vargs) + :f (let [[fmt args] (enc/vsplit-first vargs)] + (enc/format* fmt args)))))) ?data (reduce ; Apply middleware: data->?data (fn [acc mf] @@ -418,41 +415,49 @@ nil) (comment - (log* *config* :info nil nil nil :print (delay [(do (println "hi") :x) :y]))) + (log1-fn *config* :info nil nil nil :p (delay [(do (println "hi") :x) :y]) nil)) -;;;; Logging macros - -(defmacro log "Core macro-level logger." - [config level msg-type args & [base-data]] +(defmacro log1-macro + "Core macro-level logger. Implementation detail!" + [config level msg-type args & [?base-data]] ;; Compile-time elision: - (when (or (nil? compile-time-level) (level>= level compile-time-level)) + (when (or (nil? compile-time-level) + (not (valid-levels level)) ; Not a compile-time level + (level>= level compile-time-level)) + (when (compile-time-ns-filter (str *ns*)) (let [ns-str (str *ns*) ?file (let [f *file*] (when (not= f "NO_SOURCE_PATH") f)) ;; TODO Waiting on http://dev.clojure.org/jira/browse/CLJ-865: ?line (:line (meta &form))] - `(log* ~config ~level ~ns-str ~?file ~?line ~msg-type - (delay [~@args]) ~base-data))))) + `(log1-fn ~config ~level ~ns-str ~?file ~?line ~msg-type + (delay [~@args]) ~?base-data))))) + +;;;; API-level stuff ;;; Log using print-style args -(defmacro trace [& args] `(log *config* :trace :print ~args)) -(defmacro debug [& args] `(log *config* :debug :print ~args)) -(defmacro info [& args] `(log *config* :info :print ~args)) -(defmacro warn [& args] `(log *config* :warn :print ~args)) -(defmacro error [& args] `(log *config* :error :print ~args)) -(defmacro fatal [& args] `(log *config* :fatal :print ~args)) -(defmacro report [& args] `(log *config* :report :print ~args)) +(defmacro log* [config level & args] `(log1-macro ~config ~level :p ~args)) +(defmacro log [level & args] `(log1-macro *config* ~level :p ~args)) +(defmacro trace [& args] `(log1-macro *config* :trace :p ~args)) +(defmacro debug [& args] `(log1-macro *config* :debug :p ~args)) +(defmacro info [& args] `(log1-macro *config* :info :p ~args)) +(defmacro warn [& args] `(log1-macro *config* :warn :p ~args)) +(defmacro error [& args] `(log1-macro *config* :error :p ~args)) +(defmacro fatal [& args] `(log1-macro *config* :fatal :p ~args)) +(defmacro report [& args] `(log1-macro *config* :report :p ~args)) ;;; Log using format-style args -(defmacro tracef [& args] `(log *config* :trace :format ~args)) -(defmacro debugf [& args] `(log *config* :debug :format ~args)) -(defmacro infof [& args] `(log *config* :info :format ~args)) -(defmacro warnf [& args] `(log *config* :warn :format ~args)) -(defmacro errorf [& args] `(log *config* :error :format ~args)) -(defmacro fatalf [& args] `(log *config* :fatal :format ~args)) -(defmacro reportf [& args] `(log *config* :report :format ~args)) +(defmacro logf* [config level & args] `(log1-macro ~config ~level :f ~args)) +(defmacro logf [level & args] `(log1-macro *config* ~level :f ~args)) +(defmacro tracef [& args] `(log1-macro *config* :trace :f ~args)) +(defmacro debugf [& args] `(log1-macro *config* :debug :f ~args)) +(defmacro infof [& args] `(log1-macro *config* :info :f ~args)) +(defmacro warnf [& args] `(log1-macro *config* :warn :f ~args)) +(defmacro errorf [& args] `(log1-macro *config* :error :f ~args)) +(defmacro fatalf [& args] `(log1-macro *config* :fatal :f ~args)) +(defmacro reportf [& args] `(log1-macro *config* :report :f ~args)) (comment (infof "hello %s" "world") @@ -485,7 +490,7 @@ ([config level name expr] `(log-and-rethrow-errors (let [result# ~expr] - (log ~config ~level :print [~name "=>" result#]) + (log* ~config ~level [~name "=>" result#]) result#)))) #+clj @@ -540,7 +545,7 @@ (binding [aviso-ex/*fonts* fonts] (aviso-ex/format-exception err)) (aviso-ex/format-exception err))) -(comment (stacktrace (Exception. "Boo"))) +(comment (stacktrace (Exception. "Boo") {:stacktrace-fonts {}})) #+clj (def ^:private ensure-spit-dir-exists! @@ -556,9 +561,17 @@ `(do (assert (<= 0 ~probability 1) "Probability: 0 <= p <= 1") (when (< (rand) ~probability) ~@body))) -;;;; Shutdown hook ; Workaround for http://dev.clojure.org/jira/browse/CLJ-124 +;;;; EXPERIMENTAL shutdown hook +;; Workaround for http://dev.clojure.org/jira/browse/CLJ-124 #+clj (defonce ^:private shutdown-hook (.addShutdownHook (Runtime/getRuntime) (Thread. (fn [] (shutdown-agents))))) + +;;;; Deprecated + +(defn str-println [& xs] (enc/spaced-str xs)) +(defmacro with-log-level [level & body] `(with-level ~level ~@body)) +(defmacro with-logging-config [config & body] `(with-config ~config ~@body)) +(defn logging-enabled? [level compile-time-ns] (log? level (str compile-time-ns))) diff --git a/src/taoensso/timbre/appenders/3rd_party/android.clj b/src/taoensso/timbre/appenders/3rd_party/android.clj index 04c90c7..df26a5c 100644 --- a/src/taoensso/timbre/appenders/3rd_party/android.clj +++ b/src/taoensso/timbre/appenders/3rd_party/android.clj @@ -42,3 +42,7 @@ :error (android.util.Log/e ns output) :fatal (android.util.Log/e ns output) :report (android.util.Log/i ns output)))))}))) + +;;;; Deprecated + +(def make-logcat-appender make-appender) diff --git a/src/taoensso/timbre/appenders/3rd_party/irc.clj b/src/taoensso/timbre/appenders/3rd_party/irc.clj index 1b8b9e2..fb0b483 100644 --- a/src/taoensso/timbre/appenders/3rd_party/irc.clj +++ b/src/taoensso/timbre/appenders/3rd_party/irc.clj @@ -73,3 +73,7 @@ :name "Lazylus Logus" :chan "bob"}}}}) (timbre/error "A multiple\nline message\nfor you")) + +;;;; Deprecated + +(def make-irc-appender make-appender) diff --git a/src/taoensso/timbre/appenders/3rd_party/rolling.clj b/src/taoensso/timbre/appenders/3rd_party/rolling.clj index 6d93874..5e1365e 100644 --- a/src/taoensso/timbre/appenders/3rd_party/rolling.clj +++ b/src/taoensso/timbre/appenders/3rd_party/rolling.clj @@ -73,3 +73,7 @@ (let [default-appender-config {:enabled? true :min-level nil}] (merge default-appender-config appender-config {:fn (make-appender-fn path pattern)}))) + +;;;; Deprecated + +(def make-rolling-appender make-appender) diff --git a/src/taoensso/timbre/appenders/3rd_party/zmq.clj b/src/taoensso/timbre/appenders/3rd_party/zmq.clj index e2e43a2..5a76fc4 100644 --- a/src/taoensso/timbre/appenders/3rd_party/zmq.clj +++ b/src/taoensso/timbre/appenders/3rd_party/zmq.clj @@ -35,3 +35,7 @@ (zmq/register socket :pollout :pollerr))] (merge default-appender-config appender-config {:fn (make-appender-fn socket poller)}))) + +;;;; Deprecated + +(def make-zmq-appender make-appender) diff --git a/src/taoensso/timbre/appenders/carmine.clj b/src/taoensso/timbre/appenders/carmine.clj index be99f3d..b06659e 100644 --- a/src/taoensso/timbre/appenders/carmine.clj +++ b/src/taoensso/timbre/appenders/carmine.clj @@ -144,3 +144,7 @@ (count (query-entries {} :info 2)) (count (query-entries {} :info 2 :asc))) + +;;;; Deprecated + +(def make-carmine-appender make-appender) diff --git a/src/taoensso/timbre/appenders/postal.clj b/src/taoensso/timbre/appenders/postal.clj index 0097f2b..8585740 100644 --- a/src/taoensso/timbre/appenders/postal.clj +++ b/src/taoensso/timbre/appenders/postal.clj @@ -6,7 +6,7 @@ [taoensso.timbre :as timbre] [taoensso.encore :as enc :refer (have have?)])) -(defn make-postal-appender +(defn make-appender "Returns a Postal email appender. A Postal config map can be provided here as an argument, or as a :postal key in :shared-appender-config. @@ -44,3 +44,7 @@ (str/replace #"\s+" " ") (enc/substr 0 subject-len)) :body (body-fn output)))))))}))) + +;;;; Deprecated + +(def make-postal-appender make-appender) diff --git a/src/taoensso/timbre/profiling.clj b/src/taoensso/timbre/profiling.clj index ec4ef23..ae125e3 100644 --- a/src/taoensso/timbre/profiling.clj +++ b/src/taoensso/timbre/profiling.clj @@ -78,7 +78,7 @@ [level id & body] `(let [{result# :result stats# :stats} (with-pdata ~level ~@body)] (when stats# - (timbre/log timbre/*config* ~level :format + (timbre/log1-macro timbre/*config* ~level :f ["Profiling: %s\n%s" (fq-keyword ~id) (format-stats stats#)] {:profile-stats stats#})) result#)) diff --git a/src/taoensso/timbre/tools/logging.clj b/src/taoensso/timbre/tools/logging.clj index 48162a8..a1b51af 100644 --- a/src/taoensso/timbre/tools/logging.clj +++ b/src/taoensso/timbre/tools/logging.clj @@ -19,9 +19,9 @@ ?ns-str nil ; No support ?file nil ; '' ?line nil ; '' - msg-type :print ; No support for pre-msg raw args + msg-type :p ; No support for pre-msg raw args ] - (timbre/log* config level ?ns-str ?file ?line msg-type [message])))) + (timbre/log1-fn config level ?ns-str ?file ?line msg-type [message])))) (deftype LoggerFactory [] clojure.tools.logging.impl/LoggerFactory From 745d10d4ce5efab77bb3b6ddf20887d43f0042c0 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Tue, 26 May 2015 18:04:49 +0700 Subject: [PATCH 15/16] js console appender: allow raw arg switch --- src/taoensso/timbre.cljx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/taoensso/timbre.cljx b/src/taoensso/timbre.cljx index 9e21528..7334e4d 100644 --- a/src/taoensso/timbre.cljx +++ b/src/taoensso/timbre.cljx @@ -164,14 +164,19 @@ (if-not have-logger? (fn [data] nil) (fn [data] - (let [{:keys [level appender-opts output-fn]} data + (let [{:keys [level appender-opts output-fn vargs_]} data {:keys []} appender-opts - output (output-fn data)] + + vargs (force vargs_) + [v1 vnext] (enc/vsplit-first vargs) + output (if (= v1 :timbre/raw) + (into-array vnext) + (output-fn data))] (case (adjust-level level) :error (.error js/console output) :warn (.warn js/console output) - (.log js/console output))))))}}}) + (.log js/console output))))))}}}) (comment (set-config! example-config) From b3e41a1bbfcd2de88c0773b4b9bd603f58959842 Mon Sep 17 00:00:00 2001 From: Peter Taoussanis Date: Tue, 26 May 2015 18:05:10 +0700 Subject: [PATCH 16/16] v4.0.0-beta1 --- project.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 3d10364..07716af 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject com.taoensso/timbre "4.0.0-SNAPSHOT" +(defproject com.taoensso/timbre "4.0.0-beta1" :author "Peter Taoussanis " :description "Clojure/Script logging & profiling library" :url "https://github.com/ptaoussanis/timbre"