Finish up initial new .cljx ns

Wrote whole ns in one sitting; have likely missed some bugs, etc.
This commit is contained in:
Peter Taoussanis 2015-05-25 22:16:59 +07:00
parent f287de4cb2
commit 17c6986087
3 changed files with 179 additions and 187 deletions

View File

@ -12,7 +12,7 @@
:dependencies :dependencies
[[org.clojure/clojure "1.4.0"] [[org.clojure/clojure "1.4.0"]
[com.taoensso/encore "1.30.0"] [com.taoensso/encore "1.31.0"]
[io.aviso/pretty "0.1.18"]] [io.aviso/pretty "0.1.18"]]
:plugins :plugins

View File

@ -1,41 +1,28 @@
(ns taoensso.timbre (ns taoensso.timbre
"Simple, flexible logging for Clojure/Script. No XML." "Simple, flexible logging for Clojure/Script. No XML."
{:author "Peter Taoussanis"} {:author "Peter Taoussanis"}
#+clj (:require [clojure.string :as str]
#+clj [io.aviso.exception :as aviso-ex]
(:require [clojure.string :as str] [taoensso.encore :as enc :refer (have have? qb)])
[io.aviso.exception :as aviso-ex] #+cljs (:require [clojure.string :as str]
[taoensso.encore :as enc :refer (have have? qb)]) [taoensso.encore :as enc :refer ()])
#+cljs (:require-macros [taoensso.encore :as enc :refer (have have?)])
#+cljs #+clj (:import [java.util Date Locale]
(:require [clojure.string :as str] [java.text SimpleDateFormat]
[taoensso.encore :as enc :refer ()]) [java.io File]))
#+cljs
(:require-macros
[taoensso.encore :as enc :refer (have have?)])
#+clj
(:import [java.util Date Locale]
[java.text SimpleDateFormat]
[java.io File]))
;;;; TODO ;;;; TODO
;; - Check for successful cljs compile ;; - 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 ;; - Cljs default appenders
;; - Port profiling ns (cljs support?) ;; - Port appenders
;; - Try ease backward comp, update README, CHANGELOG
;; - Document shutdown-agents, ;; - Document shutdown-agents,
;; Ref. https://github.com/ptaoussanis/timbre/pull/100/files ;; Ref. https://github.com/ptaoussanis/timbre/pull/100/files
;; - Try ease backward comp
;; - Port appenders
;; - Update README, CHANGELOG
;;;; Encore version check ;;;; Encore version check
#+clj #+clj
(let [min-encore-version 1.30] (let [min-encore-version 1.31]
(if-let [assert! (ns-resolve 'taoensso.encore 'assert-min-encore-version)] (if-let [assert! (ns-resolve 'taoensso.encore 'assert-min-encore-version)]
(assert! min-encore-version) (assert! min-encore-version)
(throw (throw
@ -47,14 +34,119 @@
;;;; Config ;;;; 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 (def example-config
"Example (+default) Timbre config map." ; TODO "Example (+default) Timbre config map.
{:level :debug
:appenders ; TODO APPENDERS
{:println An appender is a map with keys:
{:min-level nil :enabled? true :async? false :rate-limit nil :doc ; Optional docstring.
:fn (fn [data] :min-level ; Level keyword, or nil (=> no minimum level).
(println ((:output-fn data) data)))}}}) :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 <data-map>)), 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
<Also, any *context* keys, which get merged into data map>
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 <appender-opts>) IO stream. Enabled by default."
:min-level nil :enabled? true :async? false :rate-limit nil
:opts {;; e/o #{:std-err :std-out :auto <stream>}:
: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 <appender-opts>) 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) (enc/defonce* ^:dynamic *config* example-config)
(defmacro with-config [config & body] `(binding [*config* ~config] ~@body)) (defmacro with-config [config & body] `(binding [*config* ~config] ~@body))
@ -86,16 +178,17 @@
(defn level>= [x y] (>= (long (scored-levels (valid-level x))) (defn level>= [x y] (>= (long (scored-levels (valid-level x)))
(long (scored-levels (valid-level y))))) (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 (defn- env-val [id] (when-let [s (System/getenv id)] (enc/read-edn s)))
#+clj (def ^:private compile-time-level #+clj (def ^:private compile-time-level
(have [:or nil? valid-level] (keyword (env-val "TIMBRE_LEVEL")))) (have [:or nil? valid-level] (keyword (env-val "TIMBRE_LEVEL"))))
(defn get-active-level [& [config]] (or (:level (or config *config*)) :report)) (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 ;;;; ns filter
@ -154,25 +247,13 @@
(vsplit-err1 [:a :b :c]) (vsplit-err1 [:a :b :c])
(vsplit-err1 [(Exception.) :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] (defn default-data-hash-fn [data]
(let [{:keys [?ns-str ?line vargs_]} data (let [{:keys [?ns-str ?line vargs_]} data
vargs (force vargs_)] vargs (force vargs_)]
(str (str
(or (some #(and (map? %) (:timbre/hash %)) vargs) ; Explicit hash given (or (some #(and (map? %) (:timbre/hash %)) vargs) ; Explicit hash given
#_[?ns-str ?line] ; TODO Waiting on http://goo.gl/cVVAYA #_[?ns-str ?line] ; TODO Waiting on http://goo.gl/cVVAYA
[?ns-str vargs])))) [?ns-str vargs]))))
(comment (default-data-hash-fn {})) (comment (default-data-hash-fn {}))
@ -201,51 +282,33 @@
(declare get-hostname) (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." (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 dvargs & [base-data]]
(when (log? level ?ns-str config) (when (log? level ?ns-str config)
(let [instant (enc/now-dt) (let [instant (enc/now-dt)
vargs*_ (delay (vsplit-err1 (mapv force dvargs))) vargs*_ (delay (vsplit-err1 (mapv force dvargs)))
?err_ (delay (get @vargs*_ 0)) ?err_ (delay (get @vargs*_ 0))
vargs_ (delay (get @vargs*_ 1)) vargs_ (delay (get @vargs*_ 1))
msg-fn (fn [vargs_] ; Post-middleware vargs, etc. data (merge base-data *context*
(when-not (nil? msg-type) {:config config ; Entire config!
(when-let [vargs (have [:or nil? vector?] (force vargs_))] ;; :context *context* ; Extra destructure's a nuisance
(case msg-type :instant instant
:print (enc/spaced-str vargs) :level level
:format (let [[fmt args] (enc/vsplit-first vargs)] :?ns-str ?ns-str
(enc/format* fmt args)))))) :?file ?file
data :?line ?line
(merge base-data *context* :?err_ ?err_
{:config config ; Entire config! :vargs_ vargs_
:instant instant #+clj :hostname_ #+clj (delay (get-hostname))
:level level :error-level? (#{:error :fatal} level)})
:?ns-str ?ns-str msg-fn
:?file ?file (fn [vargs_] ; *After* middleware, etc.
:?line ?line (when-not (nil? msg-type)
:?err_ ?err_ (when-let [vargs (have [:or nil? vector?] (force vargs_))]
:vargs_ vargs_ (case msg-type
:msg-fn msg-fn :print (enc/spaced-str vargs)
#+clj :hostname_ #+clj (delay (get-hostname)) :format (let [[fmt args] (enc/vsplit-first vargs)]
:error-level? (#{:error :fatal} level)}) (enc/format* fmt args))))))
?data ?data
(reduce ; Apply middleware: data->?data (reduce ; Apply middleware: data->?data
(fn [acc mf] (fn [acc mf]
@ -274,25 +337,31 @@
(not (rl-fn data-hash)))))) (not (rl-fn data-hash))))))
(let [{:keys [async?] apfn :fn} appender (let [{:keys [async?] apfn :fn} appender
msg_ (delay (msg-fn (:vargs_ data)))
output-fn (or (:output-fn appender) output-fn (or (:output-fn appender)
(:output-fn config) (:output-fn config)
default-output-fn) default-output-fn)
;; TODO Grab config (tz, pattern, locale, etc.) from #+clj timestamp_
timestamp_ (delay "TODO") #+clj
(delay
(let [timestamp-opts (merge default-timestamp-opts
data (assoc data :output-fn (:timestamp-opts config)
)] (:timestamp-opts appender))
{:keys [pattern locale timezone]} timestamp-opts]
:timestamp_ timestamp_ (.format (enc/simple-date-format pattern
;; :output-fn output-fn {:locale locale :timezone timezone})
(:instant data))))
;; :timestamp_ (delay "maybe?") ; TODO Nix?
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_})]
(if-not async? (if-not async?
(apfn data) (apfn data)
@ -339,7 +408,10 @@
(def-loggers) (def-loggers)
(comment (infof "hello %s" "world")) (comment
(infof "hello %s" "world")
(infof (Exception.) "hello %s" "world")
(infof (Exception.)))
(defmacro log-errors [& body] (defmacro log-errors [& body]
`(let [[?result# ?error#] (enc/catch-errors ~@body)] `(let [[?result# ?error#] (enc/catch-errors ~@body)]
@ -438,14 +510,3 @@
[probability & body] [probability & body]
`(do (assert (<= 0 ~probability 1) "Probability: 0 <= p <= 1") `(do (assert (<= 0 ~probability 1) "Probability: 0 <= p <= 1")
(when (< (rand) ~probability) ~@body))) (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)))]

View File

@ -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 _))))}}})