Nb simplify appenders config, output API

This commit is contained in:
Peter Taoussanis 2015-05-27 14:10:01 +07:00
parent 2452041296
commit 142a2d9d08
14 changed files with 592 additions and 531 deletions

View File

@ -87,24 +87,18 @@ This is the biggest win over Java logging IMO. Here's `timbre/example-config` (a
"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)
:enabled? ;
:async? ; Dispatch using agent? Useful for slow appenders
:rate-limit ; [[ncalls-limit window-ms] <...>], or nil
:opts ; Any appender-specific opts
:fn ; (fn [data-map]), with keys described below
:output-fn ; Optional override for inherited (fn [data]) -> string
:fn ; (fn [data]) -> side effects, 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 dispatching
:appender ; Entire map of appender currently dispatching
:appender-opts ; Duplicates (:opts <appender-map>) for convenience
:instant ; Platform date (java.util.Date or js/Date)
:level ; Keyword
@ -118,7 +112,7 @@ This is the biggest win over Java logging IMO. Here's `timbre/example-config` (a
:hostname_ ; Delay - string (clj only)
:msg_ ; Delay - args string
:timestamp_ ; Delay - string
:output-fn ; (fn [data & [opts]]) -> formatted output string
:output-fn ; (fn [data]) -> formatted output string
:profile-stats ; From `profile` macro
@ -141,11 +135,19 @@ This is the biggest win over Java logging IMO. Here's `timbre/example-config` (a
:middleware [] ; (fns [data]) -> ?data, applied left->right
;; Clj only:
:timestamp-opts default-timestamp-opts ; {:pattern _ :locale _ :timezone _}
:output-fn default-output-fn ; (fn [data]) -> string
:appenders
{:simple-println ; Appender id
{:example-println-appender ; Appender id
;; Appender definition (just a map):
{:min-level nil :enabled? true :async? false
{:enabled? true
:async? false
:min-level nil
:rate-limit [[1 250] [10 5000]] ; 1/250ms, 10/5s
:output-fn :inherit
:fn ; Appender's fn
(fn [data]
(let [{:keys [output-fn]} data

View File

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

View File

@ -1,21 +1,26 @@
(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?)]
[taoensso.timbre :as timbre-macros :refer ()])
#+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)]
[taoensso.timbre.appenders.core :as core-appenders])
#+cljs
(:require
[clojure.string :as str]
[taoensso.encore :as enc :refer () :refer-macros (have have?)]
[taoensso.timbre.appenders.core :as core-appenders])
#+cljs
(:require-macros [taoensso.timbre :as timbre-macros :refer ()]))
;;;; Encore version check
#+clj
(let [min-encore-version 1.31]
(let [min-encore-version 1.32]
(if-let [assert! (ns-resolve 'taoensso.encore 'assert-min-encore-version)]
(assert! min-encore-version)
(throw
@ -36,42 +41,44 @@
:timezone (java.util.TimeZone/getDefault)})
(declare stacktrace)
(defn default-output-fn "(fn [data]) -> string output."
([data] (default-output-fn nil data))
([opts data] ; Allow partials for common modified fns
(let [{:keys [level ?err_ vargs_ msg_ ?ns-str hostname_ timestamp_]} data
{:keys [no-stacktrace?]} opts]
(str
#+clj (force timestamp_) #+clj " "
#+clj (force hostname_) #+clj " "
(str/upper-case (name level)) " "
"[" (or ?ns-str "?ns") "] - "
(force msg_)
(when-not no-stacktrace?
(when-let [err (force ?err_)]
(str "\n" (stacktrace err 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 " "
#+clj (force hostname_) #+clj " "
(str/upper-case (name level))
" [" (or ?ns-str "?ns") "] - " (force msg_)
(when-let [err (force ?err_)] (str "\n" (stacktrace err opts))))))
(declare default-err default-out ensure-spit-dir-exists!)
;;; Alias core appenders here for user convenience
(declare default-err default-out)
#+clj (enc/defalias core-appenders/println-appender)
#+clj (enc/defalias core-appenders/spit-appender)
#+cljs (def println-appender core-appenders/println-appender)
#+cljs (def console-?appender core-appenders/console-?appender)
(def example-config
"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)
:enabled? ;
:async? ; Dispatch using agent? Useful for slow appenders
:rate-limit ; [[ncalls-limit window-ms] <...>], or nil
:opts ; Any appender-specific opts
:fn ; (fn [data-map]), with keys described below
:output-fn ; Optional override for inherited (fn [data]) -> string
:fn ; (fn [data]) -> side effects, 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 dispatching
:appender ; Entire map of appender currently dispatching
:appender-opts ; Duplicates (:opts <appender-map>) for convenience
:instant ; Platform date (java.util.Date or js/Date)
:level ; Keyword
@ -85,7 +92,7 @@
:hostname_ ; Delay - string (clj only)
:msg_ ; Delay - args string
:timestamp_ ; Delay - string
:output-fn ; (fn [data & [opts]]) -> formatted output string
:output-fn ; (fn [data]) -> formatted output string
:profile-stats ; From `profile` macro
@ -111,71 +118,17 @@
#+clj :timestamp-opts
#+clj default-timestamp-opts ; {:pattern _ :locale _ :timezone _}
:output-fn default-output-fn ; (fn [data & [opts]]) -> string
:output-fn default-output-fn ; (fn [data]) -> string
:appenders
#+clj
{:println ; Appender id
;; Appender map:
{:doc "Prints to (:stream <appender-opts>) 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 <stream>}
}
: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 <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 _)))))}}
{:println (println-appender {:stream :auto})
;; :spit (spit-appender {:fname "./timbre-spit.log"})
}
#+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 vargs_]} data
{:keys []} appender-opts
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))))))}}})
{;; :println (println-appender {})
:console (console-?appender {})}})
(comment
(set-config! example-config)
@ -331,6 +284,21 @@
(declare get-hostname)
(defn- inherit-over [k appender config default]
(or
(let [a (get appender k)] (when-not (enc/kw-identical? a :inherit) a))
(get config k)
default))
(defn- inherit-into [k appender config default]
(merge default
(get config k)
(let [a (get appender k)] (when-not (enc/kw-identical? a :inherit) a))))
(comment
(inherit-over :foo {:foo :inherit} {:foo :bar} nil)
(inherit-into :foo {:foo {:a :A :b :B :c :C}} {:foo {:a 1 :b 2 :c 3 :d 4}} nil))
(defn log1-fn
"Core fn-level logger. Implementation detail!"
[config level ?ns-str ?file ?line msg-type vargs_ ?base-data]
@ -372,51 +340,50 @@
(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))))))
(when (and (:enabled? appender)
(level>= level (or (:min-level appender) :trace)))
(let [{:keys [async?] apfn :fn} appender
msg_ (delay (or (msg-fn (:vargs_ data)) #_""))
output-fn (or (:output-fn appender)
(:output-fn config)
default-output-fn)
(let [rate-limit-specs (:rate-limit appender)
data-hash-fn (inherit-over :data-hash-fn appender config
default-data-hash-fn)
rate-limit-okay?
(or (empty? rate-limit-specs)
(let [rl-fn (get-rate-limiter id rate-limit-specs)
data-hash (data-hash-fn data)]
(not (rl-fn data-hash))))]
#+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))))
(when rate-limit-okay?
(let [{:keys [async?] apfn :fn} appender
msg_ (delay (or (msg-fn (:vargs_ data)) #_""))
output-fn (inherit-over :output-fn appender config
default-output-fn)
data ; Final data prep before going to appender
(merge data
{:appender-id id
:appender appender
:appender-opts (:opts appender) ; For convenience
:msg_ msg_
:msg-fn msg-fn
:output-fn output-fn
#+clj :timestamp_ #+clj timestamp_})]
#+clj timestamp_
#+clj
(delay
(let [timestamp-opts (inherit-into :timestamp-opts
appender config
default-timestamp-opts)
{:keys [pattern locale timezone]} timestamp-opts]
(.format (enc/simple-date-format pattern
{:locale locale :timezone timezone})
(:instant data))))
(if-not async?
(apfn data) ; Allow errors to throw
#+cljs (apfn data)
#+clj (send-off (get-agent id) (fn [_] (apfn data)))))))
data ; Final data prep before going to appender
(merge data
{:appender-id id
:appender appender
;; :appender-opts (:opts appender) ; For convenience
:msg_ msg_
:msg-fn msg-fn
:output-fn output-fn
:data-hash-fn data-hash-fn
#+clj :timestamp_ #+clj timestamp_})]
(if-not async?
(apfn data) ; Allow errors to throw
#+cljs (apfn data)
#+clj (send-off (get-agent id) (fn [_] (apfn data)))))))))
nil
(enc/clj1098 (:appenders config))))))
nil)
@ -554,19 +521,10 @@
(comment (stacktrace (Exception. "Boo") {:stacktrace-fonts {}}))
#+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)))
(when (< (rand) ~probability) ~@body)))
;;;; EXPERIMENTAL shutdown hook
;; Workaround for http://dev.clojure.org/jira/browse/CLJ-124

View File

@ -4,45 +4,54 @@
(:require [clojure.string :as str]
[taoensso.timbre :as timbre]))
(defn make-appender
;; TODO Test port to Timbre v4
(defn logcat-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-config make-config]]
(let [default-appender-config
{:enabled? true
:min-level :debug}]
[]
{:enabled? true
:async? false
:min-level :debug
:rate-limit nil
(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)]
:output-fn ; Drop hostname, ns, stacktrace
(fn [data]
(let [{:keys [level timestamp_ msg_]} data]
(str
(force timestamp_) " "
(str/upper-case (name level)) " "
(force msg_))))
(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))
:fn
(fn [data]
(let [{:keys [level ?ns-str ?err_ output-fn]} data
ns (str ?ns-str "")
output-str (output-fn data)]
(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)))))})))
(if-let [throwable (force ?err_)]
(case level
:trace (android.util.Log/d ns output-str throwable)
:debug (android.util.Log/d ns output-str throwable)
:info (android.util.Log/i ns output-str throwable)
:warn (android.util.Log/w ns output-str throwable)
:error (android.util.Log/e ns output-str throwable)
:fatal (android.util.Log/e ns output-str throwable)
:report (android.util.Log/i ns output-str throwable))
(case level
:trace (android.util.Log/d ns output-str)
:debug (android.util.Log/d ns output-str)
:info (android.util.Log/i ns output-str)
:warn (android.util.Log/w ns output-str)
:error (android.util.Log/e ns output-str)
:fatal (android.util.Log/e ns output-str)
:report (android.util.Log/i ns output-str)))))})
;;;; Deprecated
(def make-logcat-appender make-appender)
(defn make-logcat-appender
"DEPRECATED. Please use `logcat-appender` instead."
[& [appender-merge opts]]
(merge (logcat-appender opts) appender-merge))

View File

@ -5,19 +5,7 @@
[irclj.core :as irc]
[taoensso.timbre :as timbre]))
(defn default-fmt-output-fn
[{:keys [level ?err_ msg_]}]
(format "[%s] %s%s"
(-> level name (str/upper-case))
(or (force msg_) "")
(if-let [err (force ?err_)]
(str "\n" (timbre/stacktrace err))
"")))
(def default-appender-config
{:async? true
:enabled? true
:min-level :info})
;; TODO Test port to Timbre v4
(defn- connect [{:keys [host port pass nick user name chan]
:or {port 6667}}]
@ -29,9 +17,7 @@
(irc/join conn chan)
conn))
(defn- ensure-conn [conn conf]
(if-not @conn
(reset! conn @(connect conf))))
(defn- ensure-conn [conn conf] (if-not @conn (reset! conn @(connect conf))))
(defn- send-message [conn chan output]
(let [[fst & rst] (str/split output #"\n")]
@ -39,30 +25,50 @@
(doseq [line rst]
(irc/message conn chan ">" line))))
(defn- make-appender-fn [irc-config conn]
(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
;;; Public
(defn irc-appender
"Returns an IRC appender.
(irc-appender
{:host \"irc.example.org\" :port 6667 :nick \"logger\"
:name \"My Logger\" :chan \"#logs\"})"
(defn make-appender
"Sends IRC messages using irc.
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-config appender-config
{:conn conn
:fn (make-appender-fn irc-config conn)})))
[irc-config]
(let [conn (atom nil)
fmt-fn (or (:fmt-output-fn irc-config) default-fmt-output-fn)]
{:enabled? true
:async? true
:min-level :info
:rate-limit nil
:output-fn
(fn [data]
(let [{:keys [level ?err_ msg_]}]
(format "[%s] %s%s"
(-> level name (str/upper-case))
(or (force msg_) "")
(if-let [err (force ?err_)]
(str "\n" (timbre/stacktrace err))
""))))
:fn
(fn [data]
(let [{:keys [output-fn]} data]
(ensure-conn conn irc-config)
(send-message conn (:chan irc-config) (output-fn data))))}))
;;;; Deprecated
(defn make-irc-appender
"DEPRECATED. Please use `irc-appender` instead."
[& [appender-merge opts]]
(merge (irc-appender (:irc-config opts) (dissoc :irc-config opts))
appender-merge))
;;;;
(comment
(timbre/merge-config! {:appenders {:irc (make-appender)}})
(timbre/merge-config! {:appenders {:irc (irc-appender)}})
(timbre/merge-config!
{:appenders
{:irc
@ -73,7 +79,3 @@
:name "Lazylus Logus"
:chan "bob"}}}})
(timbre/error "A multiple\nline message\nfor you"))
;;;; Deprecated
(def make-irc-appender make-appender)

View File

@ -5,10 +5,9 @@
[taoensso.timbre :as timbre]
[taoensso.encore :as encore]))
(def conn (atom nil))
;; TODO Test port to Timbre v4
(def default-args {:host "127.0.0.1" :port 27017})
(defn connect [{:keys [db server write-concern]}]
(let [args (merge default-args server)
c (mongo/make-connection db args)]
@ -16,35 +15,33 @@
(mongo/set-write-concern c write-concern))
c))
(defn ensure-conn [config]
(swap! conn #(or % (connect config))))
(def conn (atom nil))
(defn ensure-conn [config] (swap! conn #(or % (connect config))))
(defn log-message [params {:keys [collection logged-keys]
:as config}]
(let [selected-params (if logged-keys
(select-keys params logged-keys)
(dissoc params :config :appender :appender-opts))
logged-params (encore/map-vals #(str (force %)) selected-params)]
(defn log-message [params {:keys [collection logged-keys] :as config}]
(let [entry {:instant instant
:level level
:?ns-str (str (:?ns-str data))
:hostname (str (force (:hostname_ data)))
:vargs (str (force (:vargs_ data)))
:?err (str (force (:?err_ data)))}]
(mongo/with-mongo (ensure-conn config)
(mongo/insert! collection logged-params))))
(mongo/insert! collection entry))))
(defn- make-appender-fn [make-config]
(fn [data]
(let [{:keys [appender-opts]} data]
(when-let [mongo-config appender-opts]
(log-message data mongo-config)))))
(defn congomongo-appender
"Returns a congomongo MongoDB appender.
(congomongo-appender
{:db \"logs\"
:collection \"myapp\"
:logged-keys [:instant :level :msg_]
:write-concern :acknowledged
:server {:host \"127.0.0.1\"
:port 27017}})"
(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)})))
[congo-config]
{:enabled? true
:async? true
:min-level :warn
:rate-limit [[1 1000]] ; 1/sec
:output-fn :inherit
:fn (fn [data] (log-message data congo-config))})

View File

@ -6,6 +6,8 @@
(:import [java.text SimpleDateFormat]
[java.util Calendar]))
;; TODO Test port to Timbre v4
(defn- rename-old-create-new-log [log old-log]
(.renameTo log old-log)
(.createNewFile log))
@ -23,10 +25,7 @@
(rename-old-create-new-log log index-log))))
(rename-old-create-new-log log old-log))))
(defn- log-cal [date]
(let [now (Calendar/getInstance)]
(.setTime now date)
now))
(defn- log-cal [date] (let [now (Calendar/getInstance)] (.setTime now date) now))
(defn- prev-period-end-cal [date pattern]
(let [cal (log-cal date)
@ -42,38 +41,36 @@
(.set cal Calendar/MILLISECOND 999)
cal))
(defn- make-appender-fn [path pattern]
(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
(try
(if (.exists log)
(if (<= (.lastModified log) (.getTimeInMillis prev-cal))
(shift-log-period log path prev-cal))
(.createNewFile log))
(spit path (with-out-str (println output)) :append true)
(catch java.io.IOException _))))))
(defn rolling-appender
"Returns a Rolling file appender. Opts:
:path - logfile path.
:pattern - frequency of rotation, e/o {:daily :weekly :monthly}."
[& [{:keys [path pattern]
:or {path "./timbre-rolling.log"
pattern :daily}}]]
(defn make-appender
"Returns a Rolling file appender.
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-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)})))
{:enabled? true
:async? false
:min-level nil
:rate-limit nil
:output-fn :inherit
:fn
(fn [data]
(let [{:keys [instant output-fn]} data
output-str (output-fn data)
prev-cal (prev-period-end-cal instant pattern)]
(when-let [log (io/file path)]
(try
(if (.exists log)
(if (<= (.lastModified log) (.getTimeInMillis prev-cal))
(shift-log-period log path prev-cal))
(.createNewFile log))
(spit path (with-out-str (println output-str)) :append true)
(catch java.io.IOException _)))))})
;;;; Deprecated
(def make-rolling-appender make-appender)
(defn make-rolling-appender
"DEPRECATED. Please use `rolling-appender` instead."
[& [appender-merge opts]]
(merge (rolling-appender opts) appender-merge))

View File

@ -4,6 +4,8 @@
[taoensso.timbre :as timbre])
(:import [java.io File FilenameFilter]))
;; TODO Test port to Timbre v4
(defn- ^FilenameFilter file-filter
"Returns a Java FilenameFilter instance which only matches
files with the given `basename`."
@ -43,27 +45,30 @@
(reverse (map vector logs-to-rotate (iterate inc 1)))]
(.renameTo log-file (io/file (format "%s.%03d" abs-path n))))))
(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 _))))))
(defn rotor-appender
"Returns a rotating file appender."
[& [{:keys [path max-size backlog]
:or {path "./timbre-rotor.log"
max-size (* 1024 1024)
backlog 5}}]]
{:enabled? true
:async? false
:min-level :warn
:rate-limit nil
:output-fn :inherit
:fn
(fn [data]
(let [{:keys [output-fn]} data]
(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 _)))))})
(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)})))
;;;; Deprecated
(defn make-rotor-appender
"DEPRECATED. Please use `rotor-appender` instead."
[& [appender-merge opts]]
(merge (rotor-appender opts) appender-merge))

View File

@ -6,7 +6,7 @@
(:import [java.net Socket InetAddress]
[java.io BufferedReader InputStreamReader PrintWriter]))
(def conn (atom nil))
;; TODO Test port to Timbre v4
(defn listener-fun [in out]
(loop [lines (-> in
@ -21,32 +21,40 @@
(.setDaemon true)
(.start)))
(def conn (atom nil))
(defn connect [{:keys [port listen-addr]}]
(let [addr (when (not= :all listen-addr)
(InetAddress/getByName listen-addr))]
(with-redefs [server.socket/on-thread on-thread-daemon]
(create-server port listener-fun 0 ^InetAddress addr))))
(defn ensure-conn [socket-config]
(swap! conn #(or % (connect socket-config))))
(defn ensure-conn [socket-config] (swap! conn #(or % (connect socket-config))))
(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))))))))))
(defn socket-appender
"Returns a TCP socket appender.
(socket-appender {:listener-addr :all :port 9000})"
[& [socket-config]]
(let [{:keys [listener-addr port]
:or {listener-addr :all
port 9000}} socket-config]
(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)})))
{:enabled? true
:async? false
:min-level nil
:rate-limit nil
:output-fn :inherit
:fn
(fn [data]
(let [{:keys [output-fn]} data]
(let [c (ensure-conn socket-config)]
(doseq [sock @(:connections c)]
(let [out (PrintWriter. (.getOutputStream ^Socket sock))]
(binding [*out* out]
(println (output-fn data))))))))}))
;;;; Deprecated
(defn make-socket-appender
"DEPRECATED. Please use `socket-appender` instead."
[& [appender-merge opts]]
(merge (socket-appender opts) appender-merge))

View File

@ -8,34 +8,35 @@
(doto (zmq/socket context :push)
(zmq/connect (format "%s://%s:%d" transport address port))))
(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-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-config {:keys [transport address port]}]]
(let [default-appender-config
{:enabled? true
:min-level :error
:async? true}
context (zmq/zcontext)
(defm zmq-appender
"Returns a ØMQ appender. Opts:
:transport - string representing transport type: tcp, ipc, inproc, pgm/epgm.
:address - string containing an address to connect to.
:port - number representing the port to connect to."
[{:keys [transport address port]}]
(let [context (zmq/zcontext)
socket (make-zmq-socket context transport address port)
poller (doto (zmq/poller context)
(zmq/register socket :pollout :pollerr))]
(merge default-appender-config appender-config
{:fn (make-appender-fn socket poller)})))
{:enabled? true
:async? true
:min-level :error
:rate-limit nil
:output-fn :inherit
:fn
(fn [data]
(let [{:keys [output-fn]} data
output-str (output-fn data)]
(loop []
(zmq/poll poller 500)
(cond
(zmq/check-poller poller 0 :pollout) (zmq/send-str socket output-str)
(zmq/check-poller poller 0 :pollerr) (System/exit 1)
:else (recur)))))}))
;;;; Deprecated
(def make-zmq-appender make-appender)
(defn make-zmq-appender
"DEPRECATED. Please use `zmq-appender` instead."
[& [appender-merge opts]]
(merge (zmq-appender opts) zmq-merge))

View File

@ -19,11 +19,11 @@
(defn default-keyfn [level] {:pre [(have? string? level)]}
(str "carmine:timbre:default:" level))
(defn make-appender
(defn carmine-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).
* Only the most recent instance of each unique entry is kept (uniqueness
determined by data-hash-fn).
* 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
@ -31,63 +31,68 @@
also offer interesting opportunities here.
See accompanying `query-entries` fn to return deserialized log entries."
[& [appender-config
{:keys [conn-opts keyfn data-hash-fn nentries-by-level]
[& [{:keys [conn-opts keyfn 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}]]
:report 100}}}]]
{: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-config {:enabled? true :min-level nil}]
(merge default-appender-config appender-config
{:fn
(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)}
{:enabled? true
:async? false
:min-level nil
:rate-limit nil
:output-fn :inherit
:fn
(fn [data]
(let [{:keys [level instant data-hash-fn]} data
entry-hash (sha48 (data-hash-fn data))
entry (merge
{:instant instant
:level level
:?ns-str (:?ns-str data)
:hostname (force (:hostname_ data))
:vargs (force (:vargs_ data))
:?err (force (:?err_ data))}
(when-let [pstats (:profile-stats data)]
{:profile-stats pstats}))
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)]
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 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)
(when (> nmax-entries 0)
(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)
(when (< (rand) 0.01) ; Occasionally GC
;; This is necessary since we're doing zset->entry-hash->entry
;; rather than zset->entry. We want the former for the control
;; it gives us over what should constitute a 'unique' entry.
(car/lua
"-- -ive idx used to prune from the right (lowest score first)
local max_idx = (0 - (tonumber(_:nmax-entries)) - 1)
local entries_to_prune =
redis.call('zrange', _:k-zset, 0, max_idx)
redis.call('zremrangebyrank', _:k-zset, 0, max_idx) -- Prune zset
(when (< (rand) 0.01) ; Occasionally GC
;; This is necessary since we're doing zset->entry-hash->entry
;; rather than zset->entry. We want the former for the control
;; it gives us over what should constitute a 'unique' entry.
(car/lua
"-- -ive idx used to prune from the right (lowest score first)
local max_idx = (0 - (tonumber(_:nmax-entries)) - 1)
local entries_to_prune =
redis.call('zrange', _:k-zset, 0, max_idx)
redis.call('zremrangebyrank', _:k-zset, 0, max_idx) -- Prune zset
for i,entry in pairs(entries_to_prune) do
redis.call('hdel', _:k-hash, entry) -- Prune hash
end
return nil"
{:k-zset k-zset
:k-hash k-hash}
{:nmax-entries nmax-entries}))))))})))
for i,entry in pairs(entries_to_prune) do
redis.call('hdel', _:k-hash, entry) -- Prune hash
end
return nil"
{:k-zset k-zset
:k-hash k-hash}
{:nmax-entries nmax-entries}))))))})
;;;; Query utils
@ -128,10 +133,17 @@
(-> (merge m1 m2-or-ex) (dissoc :hash))))
entries-zset entries-hash)))
;;;; Deprecated
(defn make-carmine-appender
"DEPRECATED. Please use `carmine-appender` instead."
[& [appender-merge opts]]
(merge (carmine-appender opts) appender-merge))
;;;; Dev/tests
(comment
(timbre/with-merged-config {:appenders {:carmine (make-appender)}}
(timbre/with-merged-config {:appenders {:carmine (carmine-appender)}}
(timbre/info "Hello1" "Hello2"))
(car/wcar {} (car/keys (default-keyfn "*")))
@ -144,7 +156,3 @@
(count (query-entries {} :info 2))
(count (query-entries {} :info 2 :asc)))
;;;; Deprecated
(def make-carmine-appender make-appender)

View File

@ -0,0 +1,161 @@
(ns taoensso.timbre.appenders.core
"Core Timbre appenders without any special dependency requirements. These can
be aliased into the main Timbre ns for convenience."
{:author "Peter Taoussanis"}
#+clj
(:require
[clojure.string :as str]
[taoensso.encore :as enc :refer (have have? qb)])
#+cljs
(:require
[clojure.string :as str]
[taoensso.encore :as enc :refer () :refer-macros (have have?)]))
;;;; TODO
;; * Simple official rolling spit appender?
;;;; Example appender
#_
(defn example-appender
"Docstring to explain any special opts to influence appender construction,
etc. Returns the appender map."
[& [{:keys [] :as opts}]]
{:enabled? true ; Please enable by default
:async? false ; Use agent for appender dispatch? Useful for slow dispatch.
:min-level nil ; nil (no min level), or min logging level keyword
;; :rate-limit nil
:rate-limit [[5 (enc/ms :mins 1)] ; 5 calls/min
[100 (enc/ms :hours 1)] ; 100 calls/hour
]
:output-fn :inherit ; or (fn [data]) -> string
:fn
(fn [data]
(let [;; See `timbre/example-config` for info on all available args:
{:keys [instant level ?err_ vargs_ output-fn
config ; Entire Timbre config map in effect
appender ; Entire appender map in effect
]}
data
;;; 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 an output string with ns, timestamp, vargs, etc.
;; A (fn [data]) -> string formatter is provided 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. See
;; `taoensso.timbre/default-output-fn` source for details.
;;
output-str (output-fn data)]
(println output-str)))})
(comment (merge (example-appender) {:min-level :debug}))
;;;; Println appender (clj & cljs)
#+clj (enc/declare-remote taoensso.timbre/default-out
taoensso.timbre/default-err)
#+clj (alias 'timbre 'taoensso.timbre)
(defn println-appender
"Returns a simple `println` appender for Clojure/Script.
Use with ClojureScript requires that `cljs.core/*print-fn*` be set.
:stream (clj only) - e/o #{:auto :std-err :std-out <io-stream>}."
;; Unfortunately no easy way to check if *print-fn* is set. Metadata on the
;; default throwing fn would be nice...
[& #+clj [{:keys [stream] :or {stream :auto}}] #+cljs [_opts]]
(let [#+clj stream
#+clj (case stream
:std-err timbre/default-err
:std-out timbre/default-out
stream)]
{:enabled? true
:async? false
:min-level nil
:rate-limit nil
:output-fn :inherit
:fn
(fn [data]
(let [{:keys [output-fn]} data]
#+cljs (println (output-fn data))
#+clj
(let [stream (if (= stream :auto)
(if (:error? data) *err* *out*)
stream)]
(binding [*out* stream] (println (output-fn data))))))}))
(comment (println-appender))
;;;; Spit appender (clj only)
#+clj
(def ^:private ensure-spit-dir-exists!
(enc/memoize* (enc/ms :mins 1)
(fn [fname]
(when-not (str/blank? fname)
(let [file (java.io.File. ^String fname)
dir (.getParentFile (.getCanonicalFile file))]
(when-not (.exists dir) (.mkdirs dir)))))))
#+clj
(defn spit-appender
"Returns a simple `spit` file appender for Clojure."
[& [{:keys [fname] :or {fname "./timbre-spit.log"}}]]
{:enabled? true
:async? false
:min-level nil
:rate-limit nil
:output-fn :inherit
:fn
(fn [data]
(let [{:keys [output-fn]} data]
(try ; To allow TTL-memoization of dir creator
(ensure-spit-dir-exists! fname)
(spit fname (str (output-fn data) "\n") :append true)
(catch java.io.IOException _))))})
(comment (spit-appender))
;;;; js/console appender (cljs only)
#+cljs
(defn console-?appender
"Returns a simple js/console appender for ClojureScript, or nil if no
js/console exists."
[]
(when-let [have-logger? (and (exists? js/console) (.-log js/console))]
(let [have-warn-logger? (and have-logger? (.-warn js/console))
have-error-logger? (and have-logger? (.-error js/console))
level->logger {:fatal (if have-error-logger? :error :info)
:error (if have-error-logger? :error :info)
:warn (if have-warn-logger? :warn :info)}]
{:enabled? true
:async? false
:min-level nil
:rate-limit nil
:output-fn :inherit
:fn
(fn [data]
(let [{:keys [level output-fn vargs_]} data
vargs (force vargs_)
[v1 vnext] (enc/vsplit-first vargs)
output (if (= v1 :timbre/raw)
(into-array vnext)
(output-fn data))]
(case (level->logger level)
:error (.error js/console output)
:warn (.warn js/console output)
(.log js/console output))))})))
(comment (console-?appender))

View File

@ -1,82 +0,0 @@
(ns taoensso.timbre.appenders.example-appender
"An example of how Timbre library-style appenders should be written for
bundling with Timbre. Please mention any requirements/dependencies in this
docstring, thanks!"
{:author "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 <appender-map>), 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. Take a look at the
;; `taoensso.timbre/default-output-fn` source for details.
;;
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
)

View File

@ -1,50 +1,45 @@
(ns taoensso.timbre.appenders.postal
"Email appender. Requires https://github.com/drewr/postal."
"Email (Postal) appender. Requires https://github.com/drewr/postal."
{:author "Peter Taoussanis"}
(:require [clojure.string :as str]
[postal.core :as postal]
[taoensso.timbre :as timbre]
[taoensso.encore :as enc :refer (have have?)]))
(defn make-appender
(defn postal-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.
(make-postal-appender {:enabled? true}
{:postal-config
(postal-appender
^{:host \"mail.isp.net\" :user \"jsmith\" :pass \"sekrat!!1\"}
{:from \"Bob's logger <me@draines.com>\" :to \"foo@example.com\"}})"
{:from \"Bob's logger <me@draines.com>\" :to \"foo@example.com\"})"
[& [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
[postal-config &
[{:keys [subject-len body-fn]
:or {subject-len 150
body-fn (fn [output-str] [{:type "text/plain; charset=utf-8"
:content output-str}])}}]]
{:enabled? true
:async? true ; Slow!
:min-level :warn ; Elevated
:rate-limit [[5 (enc/ms :mins 2)]
[50 (enc/ms :hours 24)]]
default-appender-config
{:enabled? true
:min-level :warn
:async? true ; Slow!
:rate-limit [[5 (enc/ms :mins 2)]
[50 (enc/ms :hours 24)]]}]
(merge default-appender-config appender-config
{:fn
(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)))))))})))
:output-fn (fn [data] (timbre/default-output-fn {:stacktrace-fonts {}} data))
:fn
(fn [data]
(let [{:keys [output-fn]} data
output-str (output-fn data)]
(postal/send-message
(assoc postal-config
:subject (-> output-str
(str/trim)
(str/replace #"\s+" " ")
(enc/substr 0 subject-len))
:body (body-fn output-str)))))})
;;;; Deprecated
(def make-postal-appender make-appender)
(defn make-postal-appender
"DEPRECATED. Please use `postal-appender` instead."
[& [appender-merge opts]]
(merge (postal-appender (:postal-config opts) (dissoc opts :postal-config))
appender-merge))