Merge branch 'dev'

This commit is contained in:
Peter Taoussanis 2015-05-26 18:08:10 +07:00
commit 16032e1e5e
25 changed files with 1304 additions and 1206 deletions

View File

@ -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.

247
README.md
View File

@ -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")
@ -76,154 +78,137 @@ java.lang.Exception: Oh noes
### 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 <appender-map>), 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
<Also, any *context* keys, which get merged into data map>
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 []
{: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}]]
;; <timestamp> <hostname> <LEVEL> [<ns>] - <message> <throwable>
(format "%s %s %s [%s] - %s%s"
timestamp hostname (-> level name str/upper-case) ns (or message "")
(or (stacktrace throwable "\n" (when nofonts? {})) "")))
: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)))}
: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 _))))}}})
{: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:
* 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! <level>)`. (Usual method).
* Via dynamic thread-level binding: `(timbre/with-log-level <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 +270,7 @@ Otherwise reach me (Peter Taoussanis) at [taoensso.com][] or on [Twitter][]. Che
## License
Copyright &copy; 2012-2014 Peter Taoussanis. Distributed under the [Eclipse Public License][], the same as Clojure.
Copyright &copy; 2012-2015 Peter Taoussanis. Distributed under the [Eclipse Public License][], the same as Clojure.
[API docs]: http://ptaoussanis.github.io/timbre/

View File

@ -1,6 +1,6 @@
(defproject com.taoensso/timbre "3.4.0"
(defproject com.taoensso/timbre "4.0.0-beta1"
:author "Peter Taoussanis <https://www.taoensso.com>"
: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,81 @@
: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.31.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
[{: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}}]}
: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"

View File

@ -1,627 +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}))))
;;;; 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))
(defmacro with-err-as-out "Evaluates body with *err* bound to *out*."
[& body] `(binding [*err* *out*] ~@body))
(defn stacktrace "Default stacktrace formatter for use by appenders, etc."
[throwable & [separator stacktrace-fonts]]
(when throwable
(str separator
(if-let [fonts stacktrace-fonts]
(binding [aviso-ex/*fonts* fonts] (aviso-ex/format-exception throwable))
(aviso-ex/format-exception throwable)))))
(comment (stacktrace (Exception. "foo") nil {}))
(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
(defn default-fmt-output-fn
[{:keys [level throwable message timestamp hostname ns]}
;; Any extra appender-specific opts:
& [{:keys [nofonts?] :as appender-fmt-output-opts}]]
;; <timestamp> <hostname> <LEVEL> [<ns>] - <message> <throwable>
(format "%s %s %s [%s] - %s%s"
timestamp hostname (-> level name str/upper-case) ns (or message "")
(or (stacktrace throwable "\n" (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`.
: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:
: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.
: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.
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!`."
{;; Prefer `level-atom` to in-config level when possible:
;; :current-logging-level :debug
;;; Control log filtering by namespace patterns (e.g. ["my-app.*"]).
;;; Useful for turning off logging in noisy libraries, etc.
:ns-whitelist []
:ns-blacklist []
;; Fns (applied right-to-left) to transform/filter appender fn args.
;; Useful for obfuscating credentials, pattern filtering, etc.
:middleware []
;;; Control :timestamp format
:timestamp-pattern "yyyy-MMM-dd HH:mm:ss ZZ" ; SimpleDateFormat pattern
:timestamp-locale nil ; A Locale object, or nil
:prefix-fn ; DEPRECATED, here for backwards comp
(fn [{:keys [level timestamp hostname ns]}]
(str timestamp " " hostname " " (-> level name str/upper-case)
" [" ns "]"))
;; 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)))}
: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]}] ; 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
"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}]
(str (or (some #(and (map? %) (:timbre/hash %)) args) ; Explicit hash given
[hostname 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]
:or {args-hash-fn default-args-hash-fn}}]
(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
((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)}))))))))
;;;; 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 {<level> <wrapped-juxt or nil>}
: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))
(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."
[;; 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]]
(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
:error? (level-error? level)
:args log-vargs ; No tools.logging support
:throwable throwable
:message message ; Timbre: nil, tools.logging: nil or string
:msg-type msg-type ; Timbre: nnil, tools.logging: nil
}))
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 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)]}
;; 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#)
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)))))))))
(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 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 {:nofonts? 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
)

582
src/taoensso/timbre.cljx Normal file
View File

@ -0,0 +1,582 @@
(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]))
;;;; Encore version check
#+clj
(let [min-encore-version 1.31]
(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
#+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
"(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!)
(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
: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 <appender-map>), 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
<Also, any *context* keys, which get merged into data map>
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!`."
{: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.*"]
: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 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 _)))))}}
#+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))))))}}})
(comment
(set-config! example-config)
(infof "Hello %s" "world :-)"))
(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))
(defn swap-config! [f]
#+cljs (set! *config* (f *config*))
#+clj (alter-var-root #'*config* f))
(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}))))
(defmacro with-level [level & body]
`(binding [*config* (merge *config* {:level ~level})] ~@body))
(comment (set-level! :info) *config*)
;;;; Levels
(def ordered-levels [:trace :debug :info :warn :error :fatal :report])
(def ^:private scored-levels (zipmap ordered-levels (next (range))))
(def ^:private valid-levels (set ordered-levels))
(def ^:private valid-level
(fn [level]
(or (valid-levels level)
(throw (ex-info (str "Invalid logging level: " level) {:level level})))))
(comment (valid-level :info))
(defn level>= [x y] (>= (long (scored-levels (valid-level x)))
(long (scored-levels (valid-level y)))))
(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 (or (env-val "TIMBRE_LEVEL")
(env-val "TIMBRE_LOG_LEVEL")))))
(defn get-active-level [& [config]] (or (:level (or config *config*)) :report))
(comment
(qb 10000 (get-active-level))
(binding [*config* {:level :trace}] (level>= :trace (get-active-level))))
;;;; ns filter
(def ^:private compile-ns-filters
"(fn [whitelist blacklist]) -> (fn [ns]) -> ?unfiltered-ns"
(let [->re-pattern
(fn [x]
(enc/cond!
(enc/re-pattern? x) x
(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) ; Might be surprising
(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*))))]
(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]
((compile-ns-filters whitelist blacklist) 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
(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
"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
(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 {}))
#+clj
(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]])))
;;;; Internal logging core
(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 ""))
true)))
(comment (log? :trace))
(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 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*
{: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_] ; For use *after* middleware, etc.
(when-not (nil? msg-type)
(when-let [vargs (have [:or nil? vector?] (force vargs_))]
(case msg-type
: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]
(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
msg_ (delay (or (msg-fn (:vargs_ data)) #_""))
output-fn (or (:output-fn appender)
(:output-fn config)
default-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))))
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_})]
(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)
(comment
(log1-fn *config* :info nil nil nil :p (delay [(do (println "hi") :x) :y]) nil))
(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)
(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))]
`(log1-fn ~config ~level ~ns-str ~?file ~?line ~msg-type
(delay [~@args]) ~?base-data)))))
;;;; API-level stuff
;;; Log using print-style 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 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")
(infof (Exception.) "hello %s" "world")
(infof (Exception.)))
(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 [~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*)]))
;;;; Misc 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") {: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)))
;;;; 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)))

View File

@ -0,0 +1,48 @@
(ns taoensso.timbre.appenders.3rd-party.android
"Android LogCat appender. Requires Android runtime."
{:author "Adam Clements"}
(:require [clojure.string :as str]
[taoensso.timbre :as timbre]))
(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-config make-config]]
(let [default-appender-config
{:enabled? true
:min-level :debug}]
(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)]
(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)))))})))
;;;; Deprecated
(def make-logcat-appender make-appender)

View File

@ -0,0 +1,79 @@
(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 ?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})
(defn- connect [{:keys [host port pass nick user name chan]
:or {port 6667}}]
(let [conn (irc/connect host port nick
:username user
:real-name name
:pass pass
:callbacks {})]
(irc/join conn chan)
conn))
(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")]
(irc/message conn chan fst)
(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
(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)})))
(comment
(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"))
;;;; Deprecated
(def make-irc-appender make-appender)

View File

@ -0,0 +1,50 @@
(ns taoensso.timbre.appenders.3rd-party.mongo
"MongoDB appender. Requires on https://github.com/aboekhoff/congomongo."
{:author "Emlyn Corrin"}
(:require [somnium.congomongo :as mongo]
[taoensso.timbre :as timbre]
[taoensso.encore :as encore]))
(def conn (atom nil))
(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)]
(when write-concern
(mongo/set-write-concern c write-concern))
c))
(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)]
(mongo/with-mongo (ensure-conn config)
(mongo/insert! collection logged-params))))
(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 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)})))

View File

@ -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,24 @@
(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)})))
;;;; Deprecated
(def make-rolling-appender make-appender)

View File

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

View File

@ -0,0 +1,52 @@
(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]))
(def conn (atom nil))
(defn listener-fun [in out]
(loop [lines (-> in
(InputStreamReader.)
(BufferedReader.)
(line-seq))]
(when-not (re-find #"(?i)^quit" (first lines))
(recur (rest lines)))))
(defn on-thread-daemon [f]
(doto (Thread. ^Runnable f)
(.setDaemon true)
(.start)))
(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 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 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)})))

View File

@ -0,0 +1,41 @@
(ns taoensso.timbre.appenders.3rd-party.zmq
"ØMQ appender. Requires https://github.com/zeromq/cljzmq"
{:author "Angus Fletcher"}
(: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 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)
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)})))
;;;; Deprecated
(def make-zmq-appender make-appender)

View File

@ -1,41 +0,0 @@
(ns taoensso.timbre.appenders.android
"Android LogCat appender. Depends on the android runtime. This is a
configuration for the timbre logging library"
{:author "Adam Clements"}
(:require [taoensso.timbre :as timbre]
clojure.string))
(defn make-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-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))
(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)))))})))
(def logcat-appender
"DEPRECATED: Use `make-logcat-appender` instead."
(make-logcat-appender))

View File

@ -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"))))
@ -139,3 +144,7 @@
(count (query-entries {} :info 2))
(count (query-entries {} :info 2 :asc)))
;;;; Deprecated
(def make-carmine-appender make-appender)

View File

@ -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 <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. 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
)

View File

@ -1,75 +0,0 @@
(ns taoensso.timbre.appenders.irc
"IRC appender. Depends on 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]}]
(format "[%s] %s%s"
(-> level name (str/upper-case))
(or message "")
(or (timbre/stacktrace throwable "\n") "")))
(def default-appender-opts
{:async? true
:enabled? true
:min-level :info})
(defn- connect [{:keys [host port pass nick user name chan]
:or {port 6667}}]
(let [conn (irc/connect host port nick
:username user
:real-name name
:pass pass
:callbacks {})]
(irc/join conn chan)
conn))
(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")]
(irc/message conn chan fst)
(doseq [line rst]
(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))))))
;;; Public
(defn make-irc-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]}]]
(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))
(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"))

View File

@ -1,46 +0,0 @@
(ns taoensso.timbre.appenders.mongo
"MongoDB appender. Depends on https://github.com/aboekhoff/congomongo."
{:author "Emlyn Corrin"}
(:require [somnium.congomongo :as mongo]))
(def conn (atom nil))
(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)]
(when write-concern
(mongo/set-write-concern c write-concern))
c))
(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 :ap-config))
logged-params (if-let [t (:throwable selected-params)]
(assoc selected-params :throwable (str t))
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)))
(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})

View File

@ -3,15 +3,10 @@
{:author "Peter Taoussanis"}
(:require [clojure.string :as str]
[postal.core :as postal]
[taoensso.timbre :as timbre]))
[taoensso.timbre :as timbre]
[taoensso.encore :as enc :refer (have have?)]))
(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))
(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.
@ -20,30 +15,36 @@
{:postal-config
^{:host \"mail.isp.net\" :user \"jsmith\" :pass \"sekrat!!1\"}
{:from \"Bob's logger <me@draines.com>\" :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
:fmt-output-opts {:nofonts? 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)))))})))
(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)))))))})))
(def postal-appender "DEPRECATED: Use `make-postal-appender` instead."
(make-postal-appender))
;;;; Deprecated
(def make-postal-appender make-appender)

View File

@ -1,48 +0,0 @@
(ns taoensso.timbre.appenders.socket
"TCP Socket appender. Depends on 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]))
(def conn (atom nil))
(defn listener-fun [in out]
(loop [lines (-> in
(InputStreamReader.)
(BufferedReader.)
(line-seq))]
(when-not (re-find #"(?i)^quit" (first lines))
(recur (rest lines)))))
(defn on-thread-daemon [f]
(doto (Thread. ^Runnable f)
(.setDaemon true)
(.start)))
(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 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))))))))
(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})

View File

@ -1,34 +0,0 @@
(ns taoensso.timbre.appenders.zmq
"ØMQ appender. Requires https://github.com/zeromq/cljzmq"
{:author "Angus Fletcher"}
(: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-zmq-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}
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)})))

View File

@ -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/log1-macro timbre/*config* ~level :f
["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?

View File

@ -1,30 +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))
;; Limitations: no support for explicit config, or ns filtering
(enabled? [_ level] (timbre/log? level))
;; Limitations inline
(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]`:
(timbre/send-to-appenders! level {} [message] logger-ns throwable
(when (string? message) message))))
(let [config timbre/*config* ; No support for explicit config
?ns-str nil ; No support
?file nil ; ''
?line nil ; ''
msg-type :p ; No support for pre-msg raw args
]
(timbre/log1-fn 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.))))

View File

@ -1,2 +0,0 @@
(ns taoensso.timbre.utils
{:author "Peter Taoussanis"})

View File

@ -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"))))))

View File

@ -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) ; TODO Add tests (PRs welcome!)