Utilities and DSL for logging on top of Timbre with support for Riemann, Logstash, and more.
Currently active-logger
supports hijacking log messages from Log4j,
java.util.logging (JUL), and Apache Commons Logging (JCL).
- Upgraded Timbre from version 5 to 6, which are incompatible due to namespace changes of the ‘community’/’3rdparty’ appenders.
- We removed
active.clojure.logger.timbre/timbre-config-section
; the client should choose its section key for the configuration itself and the library should be called with a configuration object of schemaactive.clojure.logger.timbre/timbre-config-schema
.
- We revamped the Metrics implementation: You need to adjust all your calls to
log-metric
in your code. Details see below.
- A call to
redirect-log4j!
isn’t necessary anymore. You have to remove theactive.clojure.logger.log4j
namespace from your:require
statements. You also have to remove the calls toredirect-log4j!
without substitution.
If you just want to get going, here is a minimal working example on how to start logging events.
- Add
active-logger
andactive-clojure
to yourdeps.edn
:{:deps {de.active-group/active-clojure {:mvn/version "0.42.0"} de.active-group/active-logger {:mvn/version "0.14.0"}}}
- Start logging some events:
(ns my.app (:require ... [active.clojure.logger.event :as event-logger])) (event-logger/log-event! :info "We are live!")
The default configuration logs via
println
tostdout
.See examples/ for example projects.
To adjust the configuration you can provide a Timbre configuration map:
(ns my.app
(:require [active.clojure.logger.event :as event-logger]))
(def timbre-config-map
{:min-level :debug ;; Log everything that is at least a :debug message.
:appenders
{;; Print messages to stdout/stderr.
:println '(println)
;; Write messages to a file.
:spit '(spit "/tmp/my-app.log")
;; Submit messages to a logstash instance.
:logstash '(logstash "localhost" 4660)}})
(event-logger/set-global-log-events-config-from-map! timbre-config-map)
Or use active-clojure’s active.clojure.config
facilities:
(ns my.app
(:require [active.clojure.config :as active-config]
[active.clojure.logger.event :as event-logger]
[active.clojure.logger.timbre :as timbre]))
(def config-map
{:min-level :debug ;; Log everything that is at least a :debug message.
:appenders
{;; Print messages to stdout/stderr.
:println '(println)
;; Write messages to a file.
:spit '(spit "/tmp/my-app.log")
;; Submit messages to a logstash instance.
:logstash '(logstash "localhost" 4660)}})
(def configuration (active-config/make-configuration timbre/timbre-config-schema [] config-map))
;; Initialize the event-logger.
(-> configuration
timbre/configuration->timbre-config
event-logger/set-global-log-events-config!)
For active-logger
to work as expected, please make sure to exclude
any direct or transitive dependencies on org.slf4j/slf4j-log4j12
and
other slf4j
bindings.
You compiler should give you a warning along these lines if you still depend on it:
SLF4J: Class path contains multiple SLF4J bindings.
active-logger
is intended to be used alongside active-clojure and follows
the same naming scheme (all active-logger
namespaces can be found under
active.clojure.logger
).
There are two basic ways to do logging with active-logger
: Effectful and
monadic.
In general, both of theses options are available for each logging function in
active-logger
.
Effectful logging refers to logging functions that immediately execute.
Usually they are signified by a bang (!
) at the end of the var’s name
(i. e. active.clojure.logger.event/log-event!
).
In any non-monadic execution context, use these functions for logging,
metrics, etc.
Monadic logging refers to logging functions that can be used to formulate the intent on logging at this location when executing a monadic program. The log function will only be executed when the monadic program is run.
Monadic in this context means programs defined using active.clojure.monad
.
(ns my.ns
(:require [active.clojure.config :as active-config]
[active.clojure.logger.timbre :as timbre]
[active.clojure.logger.event :as event-logger]
[active.clojure.monad :as monad :refer [monadic]]))
;; NOTE: Initialize as shown in TL;DR section.
;; Effectful logging
(let [res (+ 2 2)]
(event-logger/log-event! :trace (str "res =" res))
res)
;; 2020-07-11 15:19:02.659 host INFO [my.ns] TRACE - res = 4
;; => 4
;; Monadic logging
(def prog (monadic [res (monad/return 4)]
(event-logger/log-event :trace (str "res =" res))
(monad/return 4)))
(monad/run-free-reader-state-exception
event-logger/log-events-command-config
prog)
;; 2020-07-11 15:19:02.659 host INFO [my.ns] TRACE - res = 4
;; => 4
active-logger
includes several different entities that can be logged.
Events are reports of things that the system did that may be of interest
for later perusal by system administrators in case of failures or by
developers.
Each event carries a namespace saying where the event came from, and a
level that specifies how “important” the event is.
Functions to log events can be found in active.clojure.logger.event
.
Metrics can be used to log metrics for specified parts of your system.
Functions to log events can be found in active.clojure.logger.metric
.
Metrics use Prometheus-style metric types, for more details see https://prometheus.io/docs/concepts/metric_types/.
As for Events described above, we have an effectful logging and an monadic
login. The command config for monadic logging is
active.clojure.logger.metric/monad-command-config
. We currently implement:
- Counters with
log-counter-metric!
and the monadiclog-counter-metric
, - Gauges with
log-gauge-metric!
and the monadiclog-gauge-metric
, and - Histograms with
log-histogram-metric!
and the monadiclog-histogram-metric
that supports an arbitrary number of buckets.
Counters and histograms are state-based, which means that their next value
depends on previous values and that they accumulate over time and need to be
stored. The metric store is internal to the implementation and there are
functions for (re-)setting the store, most useful is
active.clojure.logger.metric-accumulator/reset-global-metric-store!
. This
resets the store for both the effectful and monadic implementation as both
implementations can be used concurrently and share the same store.
Since the existence of metrics, respectively the absence of metrics may hold
important information about the health of a system, the library comes with
functionality to prune stale metrics, that is to clean the metric store from
metrics that have not been updated in a while. Without pruning old metrics,
they will still be in the store and may be cause wrong conclusions about the
system. You can use
active.clojure.logger.metric-accumulator/prune-stale-metrics!
or as a
convenience start a thread that prunes stale metrics periodically with
active.clojure.logger.metric-accumulator/start-prune-stale-metrics-thread!
.
(We also implement set-counter-metric!
and the monadic set-counter-metric
for
metrics that represent counters but can be used by gauges by setting the value
instead of incrementing the value. Use this only if you know what you are
doing.)
Metrics can be emitted
- as Events and use the events configuration. This is the default and by
default it logs the events with log level
:info
. This can be changed to:debug
for example:
(active.clojure.logger.metric-emitter/set-global-log-metrics-config!
active.clojure.logger.metric-emitter/configure-metrics-loging :events :debug)
- to Riemann by passing the result of
(active.clojure.logger.metric-emitter/configure-metrics-logging riemann-config :riemann)
toactive.clojure.logger.metric-emitter/set-global-log-metrics-config
;riemann-config
is a Riemann configuration object, see below. - not at all – this can be set with
(active.clojure.logger.metric-emitter/set-global-log-metrics-config! :no-push)
Not emitting metrics at all is the recommended configuration when using Prometheus, see next section.
Prometheus scrapes metrics from its targets from their HTTP endpoints. The
namespace active.clojure.logger.metric-prometheus
provides functionality for
providing such an endpoint, the most important functions are:
(render-metrics!)
that returns the Prometheus-parseable string of all metrics in the store, and(wrap-prometheus-metrics-ring-handler handler)
which is a ring handler that returns the rendered page on the endpoint/metrics
. This is a convenient way to hook the route into your already existing webserver.
Here is an example on how to add a webserver that serves the metrics to your application using the HTTP Kit webserver:
(ns example.webserver
(:require [org.httpkit.server :as http]
[active.clojure.config :as config]
[active.clojure.logger.metric-prometheus :as metric-prometheus]
[active.clojure.logger.metric :as metrics]
[active.clojure.logger.event :as events]))
(def webserver-host
(config/setting
:host
"Where the webserver is hosted."
(config/default-string-range "0.0.0.0")))
(def webserver-port
(config/setting
:port
"The port the webserver is listening on."
(config/integer-between-range 1024 49151 8002)))
(def webserver-section
(config/section
:webserver
(config/schema "Configuration for the webserver."
webserver-host
webserver-port)))
(defn record-http-requests-total-handler
[handler]
(fn [req]
(let [res (handler req)]
(metrics/log-counter-metric! "http_requests_total"
(merge
{:uri (:uri req)}
(when-let [status (:status res)]
{:status status}))
1)
res)))
(def app
(record-http-requests-total-handler
(metric-prometheus/wrap-prometheus-metrics-ring-handler
(fn [_req]
{:status 404 :headers {"Content-Type" "text/plain"} :body "not found"}))))
(defn start-webserver!
([webserver-config]
(let [host (config/access webserver-config webserver-host)
port (config/access webserver-config webserver-port)]
(start-webserver! host port)))
([host port]
(events/log-event :info (str "Starting webserver on " host ":" port))
(let [stop-server! (http/run-server app {:ip host :port port})]
(fn []
(stop-server! :timeout 100)
(events/log-event :info "Stopped webserver.")))))
This example uses active.clojure.config
to configure the host and port of the
webserver, you can hook the webserver-section
into your already existing
active.clojure.config
setup.
It also records a metric that counts all the HTTP requests that reach the webserver. An example output when pointing a browser to the endpoint might look like:
# HELP http_requests_total http_requests_total # TYPE http_requests_total counter http_requests_total{uri="/metrics",status="200"} 5 1662025543973
Timed metrics can be used to log timing characteristics specified parts of
your system. Functions to log events can be found in
active.clojure.logger.timed-metric
. Basically syntactic sugar around
active.clojure.logger.metric
– setup and configuration of Metrics applies to
Timed metrics as well.
State changes are for monitoring the live operation of a system.
They announce for a certain system component its state, its service.
Functions to log events can be found in active.clojure.logger.state-change
.
active-logger
comes with two basic configuration sections: Timbre and Riemann.
For more information on active.clojure.config
, refer to active-clojure’s documentation on the topic.
In general, to provide a configuration means to supply a map that contains
only keys and values according to some schema defined as a
active.clojure.config/schema
and consumed by
active.clojure.config/make-configuration
.
There are lots of configuration options to define how the event logger should behave. For a full list of options, refer to the respective sources.
Here, we will cover the most important options:
default: :debug
The minimum level a message must have to be printed.
Possible values are #{:trace :debug :info :warn :error :fatal :report}
.
There is syntax for advanced settings for levels for namespaces:
[[#{\"taoensso.*\"} :error] ... [#{\"*\"} :debug]]
default: {:println (println)}
Defines how messages are printed (stdout
, to a file, …).
An appender spec is a list starting with one of
{spit, rotor, logstash, println}
,
followed by keyword parameters corresponding to the respective appender.
Multiple appenders can be configured and active at the same time. A full example of an appender configuration might looks like this:
{:appenders {:spit (spit {:fname "/tmp/app.log"})
:riemann (riemann {:host "localhost"
:port 5555})
:println (println)}}
Specifies an appender that writes to a file, specified via the :fname
setting.
Example: {:spit (spit {:fname "my.log"})}
Specifies an appender that writes to a file and rotates the file when it reaches a given file size. It accepts the following settings:
option | description | default |
---|---|---|
:path | Path to log file. Historical versions are suffixed with a 3-digit index. | "./timbre-rotor.log" |
:max-size | Maximum size of a log file in bytes. Log files are rotated when they exceed this size. | 1.048.576 bytes (1~MB) |
:backlog | Number of rotated logs to keep. | 5 |
Example:
{:rotor (rotor {:path "/tmp/project.log"
:max-size 1073741824
:backlog 999})}
Specifies an appender that will print regular log entries to stdout, errors to stderr.
Example: {:println (println)}
Specifies an appender that writes to a Logstash instance.
It takes two arguments: the host name, and the port number of the Logstash instance.
Example: {:logstash (logstash "localhost" 4660)}
Specifies an appender that writes to a Riemann instance. It accepts the following settings:
option | description | default |
---|---|---|
:host | The host Riemann is served on. | “localhost” |
:port | The port Riemann is listening on. | 5555 |
Example
{:riemann (riemann {:host "localhost"
:port 5555})}
It is possible to filter specific namespaces in the log-output. You can give a
map with keys `:allow` and `:deny` with set values that contain namespace pattern.
Example: {:ns-filter {:allow #{"*"} :deny #{"taoensso.*"}}}
Section containing three settings related to how timestamps are formatted in logs:
option | description |
---|---|
:pattern | Pattern for the timestamp (see SimpleDateFormat) |
:locale | This is an IETF BCP 47 language tag string specifying the locale such as "de-DE" or "en-US" or :jvm-default . |
:timezone | This is an ID for the time zone relative to which log entry dates should formatted. This can be a full name such as "Germany/Berlin" , or a custom ID such as "GMT-8:00" . The value may also be :jvm-default for the default time zone, and :utc for UTC. |
Here are the configuration settings for Riemann. They happen in the
:riemann
section:
option | description | default |
---|---|---|
:host | String specifying the host where Riemann runs. | "127.0.0.1 |
:port | Port where Riemann runs. | 5555 |
:tls? | Specifies whether the communication with Riemann should use TLS. It can be true or false . | false |
:key | If :tls? is true, use the specified TLS key-file. | - |
:cert | If :tls? is true, use the specified TLS cert-file. | - |
:ca-cert | If :tls? is true, use the specified TLS CA cert-file. | - |
Certain libraries and frameworks are notorious for emitting log
messages as soon as their namespaces are included. This will happen
before any configuration for active-logger
can be applied.
Therefore, you need to handle each such scenario differently. Refer
to the Jetty example below to get an idea on how such problems might
be fixed.
Jetty is one example of a bad player. It will, as soon as any of it’s
namespaces are included, emit an ‘announcement’ message that will be
printed to stdout as soon as your ns
-declaration is evaluated:
Logging to org.slf4j.impl.Log4jLoggerAdapter(org.eclipse.jetty.util.log) via org.eclipse.jetty.util.log.Slf4jLog Logging initialized @18641ms to org.eclipse.jetty.util.log.Slf4jLog
The solution to disable the message is to
- create a namespace, i.e.
my-project.disable-jetty-logs
that turns off this message in particular - include that namespace in the namespace that will be called first
(usually the one that contains your
-main
function, likelycore.clj
) as the very first dependency
Example:
;; disable_jetty_logs.clj
(ns my-project.disable-jetty-logs)
(.setProperty (org.eclipse.jetty.util.log.Log/getProperties) "org.eclipse.jetty.util.log.announce" "false")
;; core.clj
(ns my-project.core
(:require [my-project.disable-jetty-logs]
...)) ; more imports
This answer on StackOverflow led to this solution.
Copyright © 2022 Active Group GmbH
This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which is available at http://www.eclipse.org/legal/epl-2.0.
This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version, with the GNU Classpath Exception which is available at https://www.gnu.org/software/classpath/license.html.