mirror of https://github.com/status-im/timbre.git
Merge branch 'dev'
This commit is contained in:
commit
16032e1e5e
39
CHANGELOG.md
39
CHANGELOG.md
|
@ -1,5 +1,44 @@
|
||||||
> This project uses [Break Versioning](https://github.com/ptaoussanis/encore/blob/master/BREAK-VERSIONING.md) as of **Aug 16, 2014**.
|
> 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
|
## v3.4.0 / 2015 Feb 16
|
||||||
|
|
||||||
> This should be a **non-breaking** release that only bumps some old dependencies.
|
> This should be a **non-breaking** release that only bumps some old dependencies.
|
||||||
|
|
247
README.md
247
README.md
|
@ -1,29 +1,33 @@
|
||||||
**[API docs][]** | **[CHANGELOG][]** | [other Clojure libs][] | [Twitter][] | [contact/contrib](#contact--contributing) | current [Break Version][]:
|
**[API docs][]** | **[CHANGELOG][]** | [other Clojure libs][] | [Twitter][] | [contact/contrib](#contact--contributing) | current [Break Version][]:
|
||||||
|
|
||||||
```clojure
|
```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™?
|
## What's in the box™?
|
||||||
* [Logs as Clojure values](https://github.com/ptaoussanis/timbre/tree/dev#redis-carmine-appender-v3) (v3+).
|
* Full **Clojure** + **ClojureScript** support (v4+).
|
||||||
* Small, uncomplicated **all-Clojure** library.
|
* No XML or properties files. **One config map**, and you're set.
|
||||||
* **Super-simple map-based config**: no arcane XML or properties files!
|
* Deeply flexible **fn appender model** with **middleware**.
|
||||||
* **Low overhead** with dynamic logging level.
|
* **Fantastic performance** at any scale.
|
||||||
* **No overhead** with compile-time logging level. (v2.6+)
|
* Filter logging by levels and **namespace whitelist/blacklist patterns**.
|
||||||
* Flexible **fn-centric appender model** with **middleware**.
|
* **Zero overhead** with **complete Clj+Cljs elision** for compile-time level/ns filters.
|
||||||
* Sensible built-in appenders including simple **email appender**.
|
* Useful built-in appenders for **out-the-box** Clj+Cljs logging.
|
||||||
* Tunable **rate limit** and **asynchronous** logging support.
|
* Powerful, easy-to-configure per-appender **rate limits** and **async logging**.
|
||||||
* Robust **namespace filtering**.
|
* [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).
|
* [tools.logging](https://github.com/clojure/tools.logging) support (optional, useful when integrating with legacy logging systems).
|
||||||
* Dead-simple, logging-level-aware **logging profiler**.
|
* Logging-level-aware **logging profiler**.
|
||||||
|
* Tiny, **simple**, cross-platform codebase.
|
||||||
|
|
||||||
## 3rd-party tools, appenders, etc.
|
## 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.
|
* [log-config](https://github.com/palletops/log-config) by [Hugo Duncan](https://github.com/hugoduncan) - library to help manage Timbre logging config.
|
||||||
* Suggestions welcome!
|
* Other suggestions welcome!
|
||||||
|
|
||||||
## Getting started
|
## 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:
|
Add the necessary dependency to your [Leiningen][] `project.clj` and use the supplied ns-import helper:
|
||||||
|
|
||||||
```clojure
|
```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
|
(ns my-app ; Your ns
|
||||||
(timbre/refer-timbre) ; Provides useful Timbre aliases in this 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:
|
You can also use `timbre/refer-timbre` to setup these ns refers automatically (Clj only).
|
||||||
```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*)])
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logging
|
### 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
|
```clojure
|
||||||
(info "This will print") => nil
|
(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
|
(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
|
```clojure
|
||||||
(info (Exception. "Oh noes") "arg1" "arg2")
|
(info (Exception. "Oh noes") "arg1" "arg2")
|
||||||
|
@ -76,154 +78,137 @@ java.lang.Exception: Oh noes
|
||||||
|
|
||||||
### Configuration
|
### 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
|
```clojure
|
||||||
(def example-config
|
(def example-config
|
||||||
"APPENDERS
|
"Example (+default) Timbre v4 config map.
|
||||||
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.
|
|
||||||
|
|
||||||
An appender's fn takes a single map with keys:
|
APPENDERS
|
||||||
: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.
|
|
||||||
|
|
||||||
MIDDLEWARE
|
*** Please see the `taoensso.timbre.appenders.example-appender` ns if you
|
||||||
Middleware are fns (applied right-to-left) that transform the map
|
plan to write your own Timbre appender ***
|
||||||
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.
|
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!`."
|
See also `set-config!`, `merge-config!`, `set-level!`."
|
||||||
|
|
||||||
{;;; Control log filtering by namespace patterns (e.g. ["my-app.*"]).
|
{:level :debug ; e/o #{:trace :debug :info :warn :error :fatal :report}
|
||||||
;;; Useful for turning off logging in noisy libraries, etc.
|
|
||||||
:ns-whitelist []
|
|
||||||
:ns-blacklist []
|
|
||||||
|
|
||||||
;; Fns (applied right-to-left) to transform/filter appender fn args.
|
;; Control log filtering by namespaces/patterns. Useful for turning off
|
||||||
;; Useful for obfuscating credentials, pattern filtering, etc.
|
;; logging in noisy libraries, etc.:
|
||||||
:middleware []
|
:whitelist [] #_["my-app.foo-ns"]
|
||||||
|
:blacklist [] #_["taoensso.*"]
|
||||||
|
|
||||||
;;; Control :timestamp format
|
:middleware [] ; (fns [data]) -> ?data, applied left->right
|
||||||
:timestamp-pattern "yyyy-MMM-dd HH:mm:ss ZZ" ; SimpleDateFormat pattern
|
|
||||||
:timestamp-locale nil ; A Locale object, or nil
|
|
||||||
|
|
||||||
;; 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
|
:appenders
|
||||||
{:standard-out
|
{:simple-println ; Appender id
|
||||||
{:doc "Prints to *out*/*err*. Enabled by default."
|
;; Appender definition (just a map):
|
||||||
:min-level nil :enabled? true :async? false :rate-limit nil
|
{:min-level nil :enabled? true :async? false
|
||||||
:fn (fn [{:keys [error? output]}] ; Use any appender args
|
:rate-limit [[1 250] [10 5000]] ; 1/250ms, 10/5s
|
||||||
(binding [*out* (if error? *err* *out*)]
|
:fn ; Appender's fn
|
||||||
(str-println output)))}
|
(fn [data]
|
||||||
|
(let [{:keys [output-fn]} data]
|
||||||
:spit
|
(println (output-fn data))))}}})
|
||||||
{: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 _))))}}})
|
|
||||||
```
|
```
|
||||||
|
|
||||||
A few things to note:
|
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.
|
||||||
* 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.
|
||||||
* 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:
|
The **logging level** may be set:
|
||||||
* At compile-time: (`TIMBRE_LOG_LEVEL` environment variable).
|
* At compile-time: (`TIMBRE_LEVEL` environment variable).
|
||||||
* Via an atom: `(timbre/set-level! <level>)`. (Usual method).
|
* With `timbre/set-level!`/`timbre/merge-level!`.
|
||||||
* Via dynamic thread-level binding: `(timbre/with-log-level <level> ...)`.
|
* With `timbre/with-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**).
|
|
||||||
|
|
||||||
### Built-in appenders
|
### Built-in appenders
|
||||||
|
|
||||||
#### Redis ([Carmine](https://github.com/ptaoussanis/carmine)) appender (v3+)
|
#### Redis ([Carmine](https://github.com/ptaoussanis/carmine)) appender (v3+)
|
||||||
|
|
||||||
```clojure
|
```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
|
;; (: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:
|
This gives us a high-performance Redis appender:
|
||||||
* **All raw logging args are preserved** in serialized form (**even Throwables!**).
|
* **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).
|
* 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.
|
* 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.
|
* **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`.
|
A simple query utility is provided: `car-appender/query-entries`.
|
||||||
|
|
||||||
#### Email ([Postal](https://github.com/drewr/postal)) appender
|
#### Email ([Postal](https://github.com/drewr/postal)) appender
|
||||||
|
|
||||||
```clojure
|
```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
|
;; (:require [taoensso.timbre.appenders (postal :as postal-appender)]) ; Add to ns
|
||||||
|
|
||||||
(timbre/set-config! [:appenders :postal]
|
(timbre/merge-config!
|
||||||
(postal-appender/make-postal-appender
|
{:appenders {:postal
|
||||||
{:enabled? true
|
(postal-appender/make-appender {}
|
||||||
:rate-limit [1 60000] ; 1 msg / 60,000 msecs (1 min)
|
|
||||||
:async? true ; Don't block waiting for email to send
|
|
||||||
}
|
|
||||||
{:postal-config
|
{:postal-config
|
||||||
^{:host "mail.isp.net" :user "jsmith" :pass "sekrat!!1"}
|
^{: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
|
#### File appender
|
||||||
|
|
||||||
```clojure
|
```clojure
|
||||||
(timbre/set-config! [:appenders :spit :enabled?] true)
|
(timbre/merge-config!
|
||||||
(timbre/set-config! [:shared-appender-config :spit-filename] "/path/my-file.log")
|
{:appenders {:spit {:enabled? true :opts {:spit-finame "/path/my-file.log"}}}})
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Other included appenders
|
#### 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).
|
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
|
## License
|
||||||
|
|
||||||
Copyright © 2012-2014 Peter Taoussanis. Distributed under the [Eclipse Public License][], the same as Clojure.
|
Copyright © 2012-2015 Peter Taoussanis. Distributed under the [Eclipse Public License][], the same as Clojure.
|
||||||
|
|
||||||
|
|
||||||
[API docs]: http://ptaoussanis.github.io/timbre/
|
[API docs]: http://ptaoussanis.github.io/timbre/
|
||||||
|
|
79
project.clj
79
project.clj
|
@ -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>"
|
:author "Peter Taoussanis <https://www.taoensso.com>"
|
||||||
:description "Clojure logging & profiling library"
|
:description "Clojure/Script logging & profiling library"
|
||||||
:url "https://github.com/ptaoussanis/timbre"
|
:url "https://github.com/ptaoussanis/timbre"
|
||||||
:license {:name "Eclipse Public License"
|
:license {:name "Eclipse Public License"
|
||||||
:url "http://www.eclipse.org/legal/epl-v10.html"
|
:url "http://www.eclipse.org/legal/epl-v10.html"
|
||||||
|
@ -8,42 +8,81 @@
|
||||||
:comments "Same as Clojure"}
|
:comments "Same as Clojure"}
|
||||||
:min-lein-version "2.3.3"
|
:min-lein-version "2.3.3"
|
||||||
:global-vars {*warn-on-reflection* true
|
:global-vars {*warn-on-reflection* true
|
||||||
*assert* true}
|
*assert* true}
|
||||||
|
|
||||||
:dependencies
|
:dependencies
|
||||||
[[org.clojure/clojure "1.4.0"]
|
[[org.clojure/clojure "1.4.0"]
|
||||||
[com.taoensso/encore "1.24.1"]
|
[com.taoensso/encore "1.31.0"]
|
||||||
[io.aviso/pretty "0.1.17"]]
|
[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
|
:profiles
|
||||||
{;; :default [:base :system :user :provided :dev]
|
{;; :default [:base :system :user :provided :dev]
|
||||||
:server-jvm {:jvm-opts ^:replace ["-server"]}
|
:server-jvm {:jvm-opts ^:replace ["-server"]}
|
||||||
:1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]}
|
:1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]}
|
||||||
:1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]}
|
: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"]
|
:test {:dependencies [[expectations "2.1.0"]
|
||||||
[org.clojure/test.check "0.7.0"]
|
|
||||||
[org.clojure/tools.logging "0.3.1"]
|
[org.clojure/tools.logging "0.3.1"]
|
||||||
|
|
||||||
;; Appender dependencies
|
;; Appender deps
|
||||||
[com.taoensso/nippy "2.8.0"]
|
[com.taoensso/nippy "2.9.0-RC2"]
|
||||||
[com.taoensso/carmine "2.9.2"]
|
[com.taoensso/carmine "2.10.0"]
|
||||||
[com.draines/postal "1.11.3"]
|
[com.draines/postal "1.11.3"]
|
||||||
[irclj "0.5.0-alpha4"]]
|
[irclj "0.5.0-alpha4"]]}
|
||||||
:plugins [[lein-expectations "0.0.8"]
|
|
||||||
[lein-autoexpect "1.4.2"]]}
|
|
||||||
:dev
|
:dev
|
||||||
[:1.6 :test
|
[:1.7 :test
|
||||||
{:dependencies []
|
{:dependencies [[org.clojure/clojurescript "0.0-3297"]]
|
||||||
:plugins [[lein-ancient "0.6.4"]
|
:plugins
|
||||||
[codox "0.8.11"]]}]}
|
[;; 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
|
:aliases
|
||||||
{"test-all" ["with-profile" "default:+1.5:+1.6" "expectations"]
|
{"test-all" ["do" "clean," "cljx" "once,"
|
||||||
;; "test-all" ["with-profile" "default:+1.6" "expectations"]
|
"with-profile" "default:+1.5:+1.6:+1.7" "expectations,"
|
||||||
|
"with-profile" "+test" "cljsbuild" "test"]
|
||||||
"test-auto" ["with-profile" "+test" "autoexpect"]
|
"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"]}
|
"start-dev" ["with-profile" "+server-jvm" "repl" ":headless"]}
|
||||||
|
|
||||||
:repositories {"sonatype-oss-public"
|
:repositories {"sonatype-oss-public"
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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)))
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)})))
|
|
@ -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]
|
(:require [clojure.java.io :as io]
|
||||||
[taoensso.timbre :as timbre])
|
[taoensso.timbre :as timbre])
|
||||||
(:import [java.text SimpleDateFormat]
|
(:import [java.text SimpleDateFormat]
|
||||||
[java.util Calendar]))
|
[java.util Calendar]))
|
||||||
|
|
||||||
(defn- rename-old-create-new-log [log old-log]
|
(defn- rename-old-create-new-log [log old-log]
|
||||||
(.renameTo log old-log)
|
(.renameTo log old-log)
|
||||||
|
@ -41,9 +43,11 @@
|
||||||
cal))
|
cal))
|
||||||
|
|
||||||
(defn- make-appender-fn [path pattern]
|
(defn- make-appender-fn [path pattern]
|
||||||
(fn [{:keys [ap-config output instant]}]
|
(fn [data]
|
||||||
(let [path (or path (-> ap-config :rolling :path))
|
(let [{:keys [instant appender-opts output-fn]} data
|
||||||
pattern (or pattern (-> ap-config :rolling :pattern) :daily)
|
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)
|
prev-cal (prev-period-end-cal instant pattern)
|
||||||
log (io/file path)]
|
log (io/file path)]
|
||||||
(when log
|
(when log
|
||||||
|
@ -52,20 +56,24 @@
|
||||||
(if (<= (.lastModified log) (.getTimeInMillis prev-cal))
|
(if (<= (.lastModified log) (.getTimeInMillis prev-cal))
|
||||||
(shift-log-period log path prev-cal))
|
(shift-log-period log path prev-cal))
|
||||||
(.createNewFile log))
|
(.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 _))))))
|
(catch java.io.IOException _))))))
|
||||||
|
|
||||||
(defn make-rolling-appender
|
(defn make-appender
|
||||||
"Returns a Rolling file appender.
|
"Returns a Rolling file appender.
|
||||||
A rolling config map can be provided here as a second argument, or provided at
|
A rolling config map can be provided here as a second argument, or provided in
|
||||||
:rolling in :shared-appender-config.
|
appender's :opts map.
|
||||||
|
|
||||||
(make-rolling-appender {:enabled? true}
|
(make-rolling-appender {:enabled? true}
|
||||||
{:path \"log/app.log\"
|
{:path \"log/app.log\"
|
||||||
:pattern :daily})
|
:pattern :daily})
|
||||||
path: logfile path
|
path: logfile path
|
||||||
pattern: frequency of rotation, available values: :daily (default), :weekly, :monthly"
|
pattern: frequency of rotation, available values: :daily (default), :weekly, :monthly"
|
||||||
[& [appender-opts {:keys [path pattern]}]]
|
[& [appender-config {:keys [path pattern]}]]
|
||||||
(let [default-appender-opts {:enabled? true :min-level nil}]
|
(let [default-appender-config {:enabled? true :min-level nil}]
|
||||||
(merge default-appender-opts appender-opts
|
(merge default-appender-config appender-config
|
||||||
{:fn (make-appender-fn path pattern)})))
|
{:fn (make-appender-fn path pattern)})))
|
||||||
|
|
||||||
|
;;;; Deprecated
|
||||||
|
|
||||||
|
(def make-rolling-appender make-appender)
|
|
@ -1,10 +1,8 @@
|
||||||
(ns taoensso.timbre.appenders.rotor
|
(ns taoensso.timbre.appenders.3rd-party.rotor
|
||||||
{:author "Yutaka Matsubara"}
|
{:author "Yutaka Matsubara"}
|
||||||
(:import
|
(:require [clojure.java.io :as io]
|
||||||
[java.io File FilenameFilter])
|
[taoensso.timbre :as timbre])
|
||||||
(:require
|
(:import [java.io File FilenameFilter]))
|
||||||
[clojure.java.io :as io]
|
|
||||||
[taoensso.timbre :as t]))
|
|
||||||
|
|
||||||
(defn- ^FilenameFilter file-filter
|
(defn- ^FilenameFilter file-filter
|
||||||
"Returns a Java FilenameFilter instance which only matches
|
"Returns a Java FilenameFilter instance which only matches
|
||||||
|
@ -45,25 +43,27 @@
|
||||||
(reverse (map vector logs-to-rotate (iterate inc 1)))]
|
(reverse (map vector logs-to-rotate (iterate inc 1)))]
|
||||||
(.renameTo log-file (io/file (format "%s.%03d" abs-path n))))))
|
(.renameTo log-file (io/file (format "%s.%03d" abs-path n))))))
|
||||||
|
|
||||||
(defn appender-fn [{:keys [ap-config output]}]
|
(defn make-appender-fn [make-config]
|
||||||
(let [{:keys [path max-size backlog]
|
(fn [data]
|
||||||
:or {max-size (* 1024 1024)
|
(let [{:keys [appender-opts output-fn]} data
|
||||||
backlog 5}} (:rotor ap-config)]
|
{:keys [path max-size backlog]
|
||||||
(when path
|
:or {max-size (* 1024 1024)
|
||||||
(try
|
backlog 5}} appender-opts]
|
||||||
(when (> (.length (io/file path)) max-size)
|
(when path
|
||||||
(rotate-logs path backlog))
|
(try
|
||||||
(spit path
|
(when (> (.length (io/file path)) max-size)
|
||||||
(str output "\n")
|
(rotate-logs path backlog))
|
||||||
:append true)
|
(spit path (str (output-fn data) "\n") :append true)
|
||||||
(catch java.io.IOException _)))))
|
(catch java.io.IOException _))))))
|
||||||
|
|
||||||
(def rotor-appender
|
(defn make-appender
|
||||||
{:doc (str "Simple Rotating File Appender.\n"
|
"Simple Rotating File Appender.
|
||||||
"Needs :rotor config map in :shared-appender-config, e.g.:
|
Needs :opts map in appender, e.g.:
|
||||||
{:path \"logs/app.log\"
|
{:path \"logs/app.log\"
|
||||||
:max-size (* 512 1024)
|
:max-size (* 512 1024)
|
||||||
:backlog 5}")
|
:backlog 5}"
|
||||||
:min-level nil
|
[& [appender-config make-config]]
|
||||||
:enabled? true
|
(let [default-appender-config
|
||||||
:fn appender-fn})
|
{:min-level :warn :enabled? true}]
|
||||||
|
(merge default-appender-config appender-config
|
||||||
|
{:fn (make-appender-fn make-config)})))
|
|
@ -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)})))
|
|
@ -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)
|
|
@ -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))
|
|
|
@ -3,7 +3,8 @@
|
||||||
{:author "Peter Taoussanis"}
|
{:author "Peter Taoussanis"}
|
||||||
(:require [taoensso.carmine :as car]
|
(:require [taoensso.carmine :as car]
|
||||||
[taoensso.nippy :as nippy]
|
[taoensso.nippy :as nippy]
|
||||||
[taoensso.timbre :as timbre]))
|
[taoensso.timbre :as timbre]
|
||||||
|
[taoensso.encore :as enc :refer (have have?)]))
|
||||||
|
|
||||||
(defn- sha48
|
(defn- sha48
|
||||||
"Truncated 160bit SHA hash (48bit Long). Redis can store small collections of
|
"Truncated 160bit SHA hash (48bit Long). Redis can store small collections of
|
||||||
|
@ -15,51 +16,56 @@
|
||||||
|
|
||||||
(comment (sha48 {:key "I'm gonna get hashed!"}))
|
(comment (sha48 {:key "I'm gonna get hashed!"}))
|
||||||
|
|
||||||
(defn default-keyfn [level] {:pre [(string? level)]}
|
(defn default-keyfn [level] {:pre [(have? string? level)]}
|
||||||
(format "carmine:timbre:default:%s" level))
|
(str "carmine:timbre:default:" level))
|
||||||
|
|
||||||
(defn make-carmine-appender
|
(defn make-appender
|
||||||
"Alpha - subject to change!
|
"Returns a Carmine Redis appender (experimental, subject to change):
|
||||||
Returns a Carmine Redis appender:
|
* All raw logging args are preserved in serialized form (even Throwables!).
|
||||||
* 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
|
||||||
* Only the most recent instance of each unique entry is kept (hash fn used
|
|
||||||
to determine uniqueness is configurable).
|
to determine uniqueness is configurable).
|
||||||
* Configurable number of entries to keep per logging level.
|
* Configurable number of entries to keep per logging level.
|
||||||
* Log is just a value: a vector of Clojure maps: query+manipulate with
|
* 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
|
standard seq fns: group-by hostname, sort/filter by ns & severity, explore
|
||||||
exception stacktraces, filter by raw arguments, etc. Datomic and `core.logic`
|
exception stacktraces, filter by raw arguments, etc. Datomic and `core.logic`
|
||||||
also offer interesting opportunities here.
|
also offer interesting opportunities here.
|
||||||
|
|
||||||
See accompanying `query-entries` fn to return deserialized log entries."
|
See accompanying `query-entries` fn to return deserialized log entries."
|
||||||
[& [appender-opts {:keys [conn-opts keyfn args-hash-fn nentries-by-level]
|
[& [appender-config
|
||||||
:or {keyfn default-keyfn
|
{:keys [conn-opts keyfn data-hash-fn nentries-by-level]
|
||||||
args-hash-fn timbre/default-args-hash-fn
|
:or {keyfn default-keyfn
|
||||||
nentries-by-level {:trace 50
|
data-hash-fn timbre/default-data-hash-fn
|
||||||
:debug 50
|
nentries-by-level {:trace 50
|
||||||
:info 50
|
:debug 50
|
||||||
:warn 100
|
:info 50
|
||||||
:error 100
|
:warn 100
|
||||||
:fatal 100
|
:error 100
|
||||||
:report 100}}
|
:fatal 100
|
||||||
:as opts}]]
|
:report 100}}
|
||||||
{:pre [(string? (keyfn "test"))
|
:as make-config}]]
|
||||||
(every? #(contains? nentries-by-level %) timbre/levels-ordered)
|
{:pre [(have? string? (keyfn "test"))
|
||||||
(every? #(and (integer? %) (<= 0 % 100000)) (vals nentries-by-level))]}
|
(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}]
|
(let [default-appender-config {:enabled? true :min-level nil}]
|
||||||
(merge default-appender-opts appender-opts
|
(merge default-appender-config appender-config
|
||||||
{:fn
|
{:fn
|
||||||
(fn [{:keys [level instant] :as apfn-args}]
|
(fn [data]
|
||||||
(let [entry-hash (sha48 (args-hash-fn apfn-args))
|
(let [{:keys [level instant]} data
|
||||||
entry (select-keys apfn-args [:hostname :ns :args :throwable
|
entry-hash (sha48 (data-hash-fn data))
|
||||||
:profile-stats])
|
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-zset (keyfn (name level))
|
||||||
k-hash (str k-zset ":entries")
|
k-hash (str k-zset ":entries")
|
||||||
udt (.getTime ^java.util.Date instant) ; Use as zscore
|
udt (.getTime ^java.util.Date instant) ; Use as zscore
|
||||||
nmax-entries (nentries-by-level level)]
|
nmax-entries (nentries-by-level level)]
|
||||||
|
|
||||||
(when (> nmax-entries 0)
|
(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]
|
(binding [nippy/*final-freeze-fallback* nippy/freeze-fallback-as-str]
|
||||||
(car/hset k-hash entry-hash entry))
|
(car/hset k-hash entry-hash entry))
|
||||||
(car/zadd k-zset udt entry-hash)
|
(car/zadd k-zset udt entry-hash)
|
||||||
|
@ -91,7 +97,7 @@
|
||||||
maps. Normal sequence fns can be used to query/transform entries. Datomic and
|
maps. Normal sequence fns can be used to query/transform entries. Datomic and
|
||||||
core.logic are also useful!"
|
core.logic are also useful!"
|
||||||
[conn-opts level & [n asc? keyfn]]
|
[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)
|
(let [keyfn (or keyfn default-keyfn)
|
||||||
k-zset (keyfn (name level))
|
k-zset (keyfn (name level))
|
||||||
k-hash (str k-zset ":entries")
|
k-hash (str k-zset ":entries")
|
||||||
|
@ -125,9 +131,8 @@
|
||||||
;;;; Dev/tests
|
;;;; Dev/tests
|
||||||
|
|
||||||
(comment
|
(comment
|
||||||
(timbre/log {:timestamp-pattern "yyyy-MMM-dd HH:mm:ss ZZ"
|
(timbre/with-merged-config {:appenders {:carmine (make-appender)}}
|
||||||
:appenders {:carmine (make-carmine-appender)}}
|
(timbre/info "Hello1" "Hello2"))
|
||||||
:info "Hello1" "Hello2")
|
|
||||||
|
|
||||||
(car/wcar {} (car/keys (default-keyfn "*")))
|
(car/wcar {} (car/keys (default-keyfn "*")))
|
||||||
(count (car/wcar {} (car/hgetall (default-keyfn "info:entries"))))
|
(count (car/wcar {} (car/hgetall (default-keyfn "info:entries"))))
|
||||||
|
@ -139,3 +144,7 @@
|
||||||
|
|
||||||
(count (query-entries {} :info 2))
|
(count (query-entries {} :info 2))
|
||||||
(count (query-entries {} :info 2 :asc)))
|
(count (query-entries {} :info 2 :asc)))
|
||||||
|
|
||||||
|
;;;; Deprecated
|
||||||
|
|
||||||
|
(def make-carmine-appender make-appender)
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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"))
|
|
|
@ -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})
|
|
|
@ -3,15 +3,10 @@
|
||||||
{:author "Peter Taoussanis"}
|
{:author "Peter Taoussanis"}
|
||||||
(:require [clojure.string :as str]
|
(:require [clojure.string :as str]
|
||||||
[postal.core :as postal]
|
[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]
|
(defn make-appender
|
||||||
(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
|
|
||||||
"Returns a Postal email appender.
|
"Returns a Postal email appender.
|
||||||
A Postal config map can be provided here as an argument, or as a :postal key
|
A Postal config map can be provided here as an argument, or as a :postal key
|
||||||
in :shared-appender-config.
|
in :shared-appender-config.
|
||||||
|
@ -20,30 +15,36 @@
|
||||||
{:postal-config
|
{:postal-config
|
||||||
^{:host \"mail.isp.net\" :user \"jsmith\" :pass \"sekrat!!1\"}
|
^{:host \"mail.isp.net\" :user \"jsmith\" :pass \"sekrat!!1\"}
|
||||||
{:from \"Bob's logger <me@draines.com>\" :to \"foo@example.com\"}})"
|
{:from \"Bob's logger <me@draines.com>\" :to \"foo@example.com\"}})"
|
||||||
[& [appender-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
|
{:enabled? true
|
||||||
:min-level :warn
|
:min-level :warn
|
||||||
:async? true ; Slow!
|
:async? true ; Slow!
|
||||||
:rate-limit [5 (* 1000 60 2)] ; 5 calls / 2 mins
|
:rate-limit [[5 (enc/ms :mins 2)]
|
||||||
:fmt-output-opts {:nofonts? true} ; Disable ANSI-escaped stuff
|
[50 (enc/ms :hours 24)]]}]
|
||||||
}]
|
|
||||||
|
|
||||||
(merge default-appender-opts appender-opts
|
(merge default-appender-config appender-config
|
||||||
{:fn
|
{:fn
|
||||||
(fn [{:keys [ap-config output]}]
|
(fn [data]
|
||||||
(when-let [postal-config (or postal-config (:postal ap-config))]
|
(let [{:keys [output-fn appender-opts]} data
|
||||||
(postal/send-message
|
{:keys [no-fonts?]} appender-opts]
|
||||||
(assoc postal-config
|
(when-let [postal-config (or postal-config (:postal appender-opts))]
|
||||||
:subject (-> (str output)
|
(let [output (str (output-fn data {:stacktrace-fonts {}}))]
|
||||||
(str/trim)
|
(postal/send-message
|
||||||
(str-trunc subject-len)
|
(assoc postal-config
|
||||||
(str/replace #"\s+" " "))
|
:subject (-> output
|
||||||
:body (body-fn output)))))})))
|
(str/trim)
|
||||||
|
(str/replace #"\s+" " ")
|
||||||
|
(enc/substr 0 subject-len))
|
||||||
|
:body (body-fn output)))))))})))
|
||||||
|
|
||||||
(def postal-appender "DEPRECATED: Use `make-postal-appender` instead."
|
;;;; Deprecated
|
||||||
(make-postal-appender))
|
|
||||||
|
(def make-postal-appender make-appender)
|
||||||
|
|
|
@ -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})
|
|
|
@ -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)})))
|
|
|
@ -1,15 +1,20 @@
|
||||||
(ns taoensso.timbre.profiling
|
(ns taoensso.timbre.profiling
|
||||||
"Logging profiler for Timbre, adapted from clojure.contrib.profile."
|
"Logging profiler for Timbre, adapted from clojure.contrib.profile."
|
||||||
{:author "Peter Taoussanis"}
|
{:author "Peter Taoussanis"}
|
||||||
(:require [taoensso.encore :as encore]
|
(:require [taoensso.encore :as enc]
|
||||||
[taoensso.timbre :as timbre]))
|
[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
|
;;;; Utils
|
||||||
|
|
||||||
(defmacro fq-keyword "Returns namespaced keyword for given id."
|
(defmacro fq-keyword "Returns namespaced keyword for given id."
|
||||||
[id]
|
[id] `(if (and (keyword? ~id) (namespace ~id)) ~id
|
||||||
`(if (and (keyword? ~id) (namespace ~id)) ~id
|
(keyword ~(str *ns*) (name ~id))))
|
||||||
(keyword (timbre/get-compile-time-ns) (name ~id))))
|
|
||||||
|
|
||||||
(comment (map #(fq-keyword %) ["foo" :foo :foo/bar]))
|
(comment (map #(fq-keyword %) ["foo" :foo :foo/bar]))
|
||||||
|
|
||||||
|
@ -56,7 +61,7 @@
|
||||||
(declare ^:private format-stats)
|
(declare ^:private format-stats)
|
||||||
|
|
||||||
(defmacro with-pdata [level & body]
|
(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)}
|
{:result (do ~@body)}
|
||||||
(binding [*pdata* (atom {})]
|
(binding [*pdata* (atom {})]
|
||||||
{:result (pspy ::clock-time ~@body)
|
{:result (pspy ::clock-time ~@body)
|
||||||
|
@ -73,9 +78,9 @@
|
||||||
[level id & body]
|
[level id & body]
|
||||||
`(let [{result# :result stats# :stats} (with-pdata ~level ~@body)]
|
`(let [{result# :result stats# :stats} (with-pdata ~level ~@body)]
|
||||||
(when stats#
|
(when stats#
|
||||||
(timbre/log* {:profile-stats stats#} :format ~level
|
(timbre/log1-macro timbre/*config* ~level :f
|
||||||
"Profiling: %s\n%s" (fq-keyword ~id)
|
["Profiling: %s\n%s" (fq-keyword ~id) (format-stats stats#)]
|
||||||
(format-stats stats#)))
|
{:profile-stats stats#}))
|
||||||
result#))
|
result#))
|
||||||
|
|
||||||
(defmacro sampling-profile
|
(defmacro sampling-profile
|
||||||
|
@ -87,7 +92,7 @@
|
||||||
|
|
||||||
;;;; Data capturing & aggregation
|
;;;; Data capturing & aggregation
|
||||||
|
|
||||||
(def ^:private ^:constant stats-gc-n 111111)
|
(def ^:private stats-gc-n 111111)
|
||||||
|
|
||||||
(defn capture-time! [id t-elapsed]
|
(defn capture-time! [id t-elapsed]
|
||||||
(let [ntimes
|
(let [ntimes
|
||||||
|
@ -182,7 +187,7 @@
|
||||||
(let [nanosecs (long nanosecs) ; Truncate any fractional nanosecs
|
(let [nanosecs (long nanosecs) ; Truncate any fractional nanosecs
|
||||||
pow #(Math/pow 10 %)
|
pow #(Math/pow 10 %)
|
||||||
ok-pow? #(>= nanosecs (pow %))
|
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")
|
(cond (ok-pow? 9) (str (to-pow 9 1) "s")
|
||||||
(ok-pow? 6) (str (to-pow 6 0) "ms")
|
(ok-pow? 6) (str (to-pow 6 0) "ms")
|
||||||
(ok-pow? 3) (str (to-pow 3 0) "μs")
|
(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]
|
||||||
[name doc-string? attr-map? ([params*] prepost-map? body)+ attr-map?])}
|
[name doc-string? attr-map? ([params*] prepost-map? body)+ attr-map?])}
|
||||||
[name' & sigs]
|
[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))
|
single-arity? (vector? (first sigs))
|
||||||
[sigs func->str]
|
[sigs func->str]
|
||||||
(if single-arity?
|
(if single-arity?
|
||||||
|
|
|
@ -1,30 +1,33 @@
|
||||||
(ns taoensso.timbre.tools.logging
|
(ns taoensso.timbre.tools.logging
|
||||||
"clojure.tools.logging.impl/Logger implementation.
|
"clojure.tools.logging.impl/Logger implementation.
|
||||||
|
|
||||||
Limitations:
|
The tools.logging API has some significant limits that native Timbre does not.
|
||||||
* No support for zero-overhead compile-time logging levels (`enabled?`
|
Only use Timbre through tools.logging if you absolutely must (e.g. you're
|
||||||
called as a fn).
|
working with a legacy codebase)."
|
||||||
* 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)."
|
|
||||||
(:require [clojure.tools.logging]
|
(:require [clojure.tools.logging]
|
||||||
[taoensso.timbre :as timbre]))
|
[taoensso.timbre :as timbre]))
|
||||||
|
|
||||||
(deftype Logger [logger-ns]
|
(deftype Logger [logger-ns]
|
||||||
clojure.tools.logging.impl/Logger
|
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]
|
(write! [_ level throwable message]
|
||||||
;; tools.logging message may be a string (for `logp`/`logf` calls) or raw
|
(let [config timbre/*config* ; No support for explicit config
|
||||||
;; argument (for `log` calls). The best we can do for :args is therefore
|
?ns-str nil ; No support
|
||||||
;; `[message]`:
|
?file nil ; ''
|
||||||
(timbre/send-to-appenders! level {} [message] logger-ns throwable
|
?line nil ; ''
|
||||||
(when (string? message) message))))
|
msg-type :p ; No support for pre-msg raw args
|
||||||
|
]
|
||||||
|
(timbre/log1-fn config level ?ns-str ?file ?line msg-type [message]))))
|
||||||
|
|
||||||
(deftype LoggerFactory []
|
(deftype LoggerFactory []
|
||||||
clojure.tools.logging.impl/LoggerFactory
|
clojure.tools.logging.impl/LoggerFactory
|
||||||
(name [_] "Timbre")
|
(name [_] "Timbre")
|
||||||
(get-logger [_ logger-ns] (->Logger logger-ns)))
|
(get-logger [_ logger-ns] (Logger. logger-ns)))
|
||||||
|
|
||||||
(defn use-timbre []
|
(defn use-timbre []
|
||||||
(alter-var-root (var clojure.tools.logging/*logger-factory*)
|
(alter-var-root (var clojure.tools.logging/*logger-factory*)
|
||||||
(constantly (->LoggerFactory))))
|
(constantly (LoggerFactory.))))
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
(ns taoensso.timbre.utils
|
|
||||||
{:author "Peter Taoussanis"})
|
|
|
@ -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"))))))
|
|
|
@ -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!)
|
|
Loading…
Reference in New Issue