├── .gitignore ├── .github └── workflows │ └── clojure.yml ├── test └── unilog │ ├── context_test.clj │ └── config_test.clj ├── LICENSE ├── src └── unilog │ ├── context.clj │ └── config.clj ├── project.clj └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /doc 2 | /target 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | /.lsp 12 | /.clj-kondo/.cache -------------------------------------------------------------------------------- /.github/workflows/clojure.yml: -------------------------------------------------------------------------------- 1 | name: Clojure CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | -------------------------------------------------------------------------------- /test/unilog/context_test.clj: -------------------------------------------------------------------------------- 1 | (ns unilog.context-test 2 | (:require [unilog.context :refer [with-context]] 3 | [clojure.test :refer [deftest is testing]])) 4 | 5 | (deftest context-is-stacked 6 | (testing "Context stacking works as expected" 7 | (with-context {"a" "1"} 8 | (is (= "1" (get (org.slf4j.MDC/getCopyOfContextMap) "a"))) 9 | (with-context {:a 2} 10 | (is (= "2" (get (org.slf4j.MDC/getCopyOfContextMap) "a")))) 11 | (is (= "1" (get (org.slf4j.MDC/getCopyOfContextMap) "a")))))) 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Pierre-Yves Ritschard 2 | 3 | Permission to use, copy, modify, and distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /src/unilog/context.clj: -------------------------------------------------------------------------------- 1 | (ns unilog.context 2 | "Provide a way to interact with Mapped Diagnostic Context") 3 | 4 | (defn push-context 5 | "Add a key to the current Mapped Diagnostic Context" 6 | [k v] 7 | (org.slf4j.MDC/put (name k) (str v))) 8 | 9 | (defn pull-context 10 | "Remove a key to the current Mapped Diagnostic Context" 11 | [k] 12 | (org.slf4j.MDC/remove (name k))) 13 | 14 | (defn set-context 15 | "Sets the current Mapped Diagnostic Context" 16 | [ctx] 17 | (org.slf4j.MDC/setContextMap ctx)) 18 | 19 | (defmacro with-context 20 | "Execute body with the Mapped Diagnostic Context updated from 21 | keys found in the ctx map." 22 | [ctx & body] 23 | `(if-not (map? ~ctx) 24 | (throw (ex-info "with-context expects a map" {})) 25 | (let [copy# (org.slf4j.MDC/getCopyOfContextMap)] 26 | (try 27 | (doseq [[k# v#] ~ctx] 28 | (push-context k# v#)) 29 | ~@body 30 | (finally 31 | (set-context (or copy# {}))))))) 32 | 33 | (defn mdc-fn* 34 | "Returns a function, which will install the same MDC context map in effect as in 35 | the thread at the time mdc-fn* was called and then call f with any given 36 | arguments. This may be used to define a helper function which runs on a 37 | different thread, but needs to preserve the MDC context." 38 | [f] 39 | (let [mdc (org.slf4j.MDC/getCopyOfContextMap)] 40 | (fn [& args] 41 | (when (some? mdc) 42 | (org.slf4j.MDC/setContextMap mdc)) 43 | (apply f args)))) 44 | 45 | (defmacro mdc-fn 46 | "Returns a function defined by the given fntail, which will install the 47 | same MDC context map in effect as in the thread at the time mdc-fn was called. 48 | This may be used to define a helper function which runs on a different 49 | thread, but needs to preserve the MDC context." 50 | [& fntail] 51 | `(mdc-fn* (fn ~@fntail))) 52 | 53 | (comment 54 | 55 | ;; Example usage, you'll need a pattern that shows MDC values 56 | (require '[clojure.tools.logging :refer [info]]) 57 | 58 | (let [f (mdc-fn [] (with-context {:mdcval "bar"} (info "something")))] 59 | @(future 60 | (f)))) 61 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (def slf4j-version "2.0.12") 2 | (def logback-version "1.5.6") 3 | (def jackson-version "2.16.1") 4 | 5 | (defproject spootnik/unilog "0.7.33-SNAPSHOT" 6 | :description "logging should be easy!" 7 | :url "https://github.com/pyr/unilog" 8 | :license {:name "MIT License" 9 | :url "https://github.com/pyr/unilog/tree/master/LICENSE"} 10 | :plugins [[lein-ancient "1.0.0-RC3"]] 11 | :pedantic? :abort 12 | :dependencies [[org.clojure/clojure "1.11.1"] 13 | [net.logstash.logback/logstash-logback-encoder "7.4"] 14 | [org.slf4j/slf4j-api ~slf4j-version] 15 | [org.slf4j/log4j-over-slf4j ~slf4j-version] 16 | [org.slf4j/jul-to-slf4j ~slf4j-version] 17 | [org.slf4j/jcl-over-slf4j ~slf4j-version] 18 | [ch.qos.logback/logback-classic ~logback-version] 19 | [ch.qos.logback/logback-core ~logback-version] 20 | [com.fasterxml.jackson.core/jackson-databind ~jackson-version] 21 | [com.fasterxml.jackson.core/jackson-annotations ~jackson-version] 22 | [com.fasterxml.jackson.core/jackson-core ~jackson-version]] 23 | :deploy-repositories [["releases" :clojars] ["snapshots" :clojars]] 24 | :profiles {:dev {:dependencies [[org.clojure/tools.logging "1.3.0"] 25 | [metosin/jsonista "0.3.8" 26 | :exclusions [com.fasterxml.jackson.core/*]]] 27 | :pedantic? :ignore 28 | :plugins [[lein-ancient "0.7.0"]] 29 | :global-vars {*warn-on-reflection* true}} 30 | :test {:dependencies [[org.clojure/tools.logging "1.3.0"] 31 | [metosin/jsonista "0.3.8" 32 | :exclusions [com.fasterxml.jackson.core/*]]] 33 | :plugins [[lein-difftest "2.0.0"] 34 | [lein-cljfmt "0.9.0"]] 35 | :pedantic? :abort}}) 36 | -------------------------------------------------------------------------------- /test/unilog/config_test.clj: -------------------------------------------------------------------------------- 1 | (ns unilog.config-test 2 | (:require [unilog.config :refer [start-logging!]] 3 | [clojure.test :refer [deftest is testing]] 4 | [clojure.instant :as instant] 5 | [unilog.context :refer [with-context]] 6 | [jsonista.core :as j] 7 | [clojure.java.io :as io] 8 | [clojure.tools.logging :refer [debug info warn error]])) 9 | 10 | (defn- temp-file 11 | "Temp file which gets deleted when the JVM stops" 12 | [prefix suffix] 13 | (doto (java.io.File/createTempFile prefix suffix) 14 | (.deleteOnExit))) 15 | 16 | (defn- parse-lines 17 | "Returns a seq of JSON records parsed from a file, line by line." 18 | [path] 19 | (for [line (line-seq (io/reader path))] 20 | (j/read-value line j/keyword-keys-object-mapper))) 21 | 22 | (defn- minus-seconds 23 | [^java.util.Date d ^Long seconds] 24 | (java.util.Date/from 25 | ^java.time.Instant 26 | (-> d .toInstant (.minusSeconds ^Long seconds)))) 27 | 28 | (defn- plus-seconds 29 | [^java.util.Date d ^Long seconds] 30 | (java.util.Date/from 31 | ^java.time.Instant 32 | (-> d .toInstant (.plusSeconds ^Long seconds)))) 33 | 34 | (defn- check-interval 35 | [date-str] 36 | (let [dt ^java.util.Date (instant/read-instant-date date-str) 37 | ts (java.util.Date.) 38 | ceiling ^java.util.Date (plus-seconds ts 5) 39 | floor ^java.util.Date (minus-seconds ts 5)] 40 | (and (.after dt floor) (.before dt ceiling)))) 41 | 42 | (def ^:private get-version (keyword "@version")) 43 | (def ^:private get-timestamp (keyword "@timestamp")) 44 | 45 | (deftest logging 46 | 47 | (testing "Text logging to console" 48 | (start-logging! {:level :info}) 49 | (is (nil? (debug "debug"))) 50 | (is (nil? (info "info"))) 51 | (is (nil? (warn "warn"))) 52 | (is (nil? (error "error")))) 53 | 54 | (testing "JSON logging to console" 55 | (start-logging! {:level :info 56 | :appenders [{:appender :console :encoder :json}]}) 57 | (is (nil? (debug "debug"))) 58 | (is (nil? (info "info"))) 59 | (is (nil? (warn "warn"))) 60 | (is (nil? (error "error")))) 61 | 62 | (testing "JSON logging to file" 63 | (let [path (temp-file "unilog.config" "log")] 64 | (start-logging! {:level :info :appenders [{:appender :file 65 | :file (str path) 66 | :encoder :json}]}) 67 | (debug "debug") 68 | (info "info") 69 | (warn "warn") 70 | (error "error") 71 | 72 | (let [records (parse-lines path)] 73 | (is (every? true? (map (comp check-interval get-timestamp) records))) 74 | (is (= #{"unilog.config-test"} (reduce conj #{} (map :logger_name records)))) 75 | (is (= #{"main"} (reduce conj #{} (map :thread_name records)))) 76 | (is (= #{"1"} (reduce conj #{} (map get-version records)))) 77 | (is (= ["info" "warn" "error"] (map :message records))) 78 | (is (= ["INFO" "WARN" "ERROR"] (map :level records)))))) 79 | 80 | (testing "JSON logging to file with MDC" 81 | (let [path (temp-file "unilog.config" "log")] 82 | (start-logging! {:level :info :appenders [{:appender :file 83 | :file (str path) 84 | :encoder :json}]}) 85 | (with-context {:foo :bar} 86 | 87 | (debug "debug") 88 | (info "info") 89 | (warn "warn") 90 | (error "error") 91 | 92 | (let [records (parse-lines path)] 93 | (is (every? true? (map (comp check-interval get-timestamp) records))) 94 | (is (= #{"unilog.config-test"} (reduce conj #{} (map :logger_name records)))) 95 | (is (= #{"main"} (reduce conj #{} (map :thread_name records)))) 96 | (is (= #{"1"} (reduce conj #{} (map get-version records)))) 97 | (is (= ["info" "warn" "error"] (map :message records))) 98 | (is (= ["INFO" "WARN" "ERROR"] (map :level records))) 99 | (is (= #{":bar"} (reduce conj #{} (map :foo records))))))))) 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unilog: logging should be easy! 2 | 3 | 4 | 5 | 6 | [![cljdoc badge](https://cljdoc.org/badge/spootnik/unilog)](https://cljdoc.org/d/spootnik/unilog/CURRENT) 7 | 8 | [clojure.tools.logging](https://github.com/clojure/tools.logging) is 9 | a great library to perform logging. It walks through several available 10 | options such as [slf4j](http://www.slf4j.org), 11 | [commons-logging](http://commons.apache.org/logging), 12 | [log4j](http://logging.apache.org/log4j/), 13 | and [logback](http://logback.qos.ch). 14 | 15 | While the logging itself is simple and straightforward, navigating the 16 | many ways to configure logging can be a bit daunting. The above logging 17 | frameworks which 18 | [clojure.tools.logging](https://github.com/clojure/tools.logging) 19 | relies on expect logging configuration to happen in separate configuration file. 20 | 21 | Unilog provides an extendable data format for configuration the 22 | [logback](http://logback.qos.ch/) framework. 23 | 24 | Unilog also provides facilities to attach metadata to logs. 25 | 26 | ## Coordinates 27 | 28 | [![Clojars Project](https://img.shields.io/clojars/v/spootnik/unilog)](https://clojars.org/spootnik/unilog) 29 | 30 | ## Usage 31 | 32 | Let's pretend you have an application, which reads its initial 33 | configuration in a YAML file: 34 | 35 | ```yaml 36 | other-config: 37 | foo: bar 38 | logging: 39 | level: info 40 | console: true 41 | files: 42 | - "/var/log/program.log" 43 | - file: "/var/log/program-json.log" 44 | encoder: json 45 | overrides: 46 | some.namespace: debug 47 | ``` 48 | You would supply configuration by parsing the YAML and then 49 | calling `start-logging!` 50 | 51 | ```clojure 52 | (require '[clj-yaml.core :refer [parse-string]] 53 | '[unilog.config :refer [start-logging!]]) 54 | 55 | (let [default-logging {:level "info" :console true} 56 | config (parse-string (slurp "my-config.yml"))] 57 | (start-logging! (merge default-logging (:logging config))) 58 | ;; rest of program startup) 59 | ``` 60 | 61 | ## Configuration details 62 | 63 | The configuration, given as a map to `start-logging!` understands 64 | a number of keys. 65 | 66 | ### Global Options 67 | 68 | * `:level`: Default logging level 69 | * any of `:all`, `:trace`, `:debug`, `:info`, `:warn`, `:error`, `:off` 70 | * `:external` 71 | * If it is `true`, do not try to configure logging. An external configuration is supplied. 72 | * `:overrides` 73 | * Provide a map of namespace to level, overriding the provided default level. 74 | 75 | ### Console 76 | 77 | If the `:console` key is present in the configuration map, it may be any of: 78 | 79 | * `false` 80 | * Do not log to the console. 81 | * `true` 82 | * Log to the console, using a pattern encoder and the default pattern. 83 | * A string 84 | * Log to the console, using a pattern encoder and the supplied pattern string. 85 | * A map 86 | * Log to the console, other attributes are taken from the map. 87 | * For instance: `{:console {:encoder :json}}`. 88 | 89 | ### File 90 | 91 | If the `:file` key is present in the configuration map, it may be any of: 92 | 93 | * A string: Log to the provided file, using a pattern encoder and the default pattern. 94 | * A map: Log to a file, taking configuration attributes from the map. 95 | * For instance: `{:file {:file "/var/log/foo.log" :encoder :json}}` 96 | 97 | ### Files 98 | 99 | Expects a sequence of valid configurations for `File`. 100 | 101 | ### Appenders 102 | 103 | As for `Files`, but do not assume a specific appender, expect it to be supplied in the configuration map. 104 | 105 | ## Example configuration map 106 | 107 | ```clojure 108 | {:level :info 109 | :console false 110 | :files ["/var/log/standard.log" 111 | {:file "/var/log/standard-json.log" :encoder :json}] 112 | :file {:file "/var/log/file.log" :encoder :json} 113 | :appenders [{:appender :file 114 | :encoder :json 115 | :file "/var/log/other-json.log"} 116 | 117 | {:appender :file 118 | :encoder :pattern 119 | :pattern "%p [%d] %t - %c %m%n" 120 | :file "/var/log/other-pattern.log"} 121 | 122 | {:appender :rolling-file 123 | :file "/var/log/rolling-file.log"} 124 | 125 | {:appender :rolling-file 126 | :rolling-policy :fixed-window 127 | :triggering-policy :size-based 128 | :file "/var/log/rolling-file.log"} 129 | 130 | {:appender :rolling-file 131 | :rolling-policy {:type :fixed-window 132 | :max-index 5} 133 | :triggering-policy {:type :size-based 134 | :max-size 5120} 135 | :file "/var/log/rolling-file.log"}] 136 | :overrides {"org.apache.http" :debug 137 | "org.apache.http.wire" :error}} 138 | ``` 139 | 140 | ## Encoders 141 | 142 | You could specify encoder arguments in some appenders. Not every appender supports encoders. 143 | The following encoders are currently supported in `:appenders`. 144 | 145 | `PatternLayoutEncoder` uses a default pattern of `"%p [%d] %t - %c %m%n"`. 146 | 147 | ```clojure 148 | {:appender :file 149 | :file "/var/log/file.log" 150 | ;; PatternLayoutEncoder 151 | ;; Without :pattern argument in an appender config, the default pattern is used. 152 | :encoder :pattern} 153 | 154 | {:appender :file 155 | :file "/var/log/file2.log" 156 | :encoder :pattern 157 | :pattern "%p [%d] %t - %c %m%n"} 158 | ``` 159 | 160 | `LogstashEncoder` formats messages for logstash. 161 | 162 | ```clojure 163 | {:appender :file 164 | :file "/var/log/file3.log" 165 | ;; LogstashEncoder 166 | :encoder :json} 167 | ``` 168 | 169 | ## Appenders 170 | 171 | The following appenders are currently supported: 172 | 173 | ### `:console` appender 174 | 175 | * Optional Arguments 176 | * `:encoder` 177 | * `:pattern` 178 | 179 | ```clojure 180 | {:appender :console} 181 | 182 | {:appender :console 183 | :encoder :pattern} 184 | 185 | {:appender :console 186 | :encoder :pattern 187 | :pattern "%p [%d] %t - %c %m%n"} 188 | 189 | {:appender :console 190 | :encoder :json} 191 | ``` 192 | 193 | ### `:file` appender 194 | 195 | * Mandatory Arguments 196 | * `:file` 197 | * Optional Arguments 198 | * `:encoder` 199 | * `:pattern` 200 | 201 | ```clojure 202 | {:appender :file 203 | :file "/var/log/file.log"} 204 | 205 | {:appender :file 206 | :file "/var/log/file.log" 207 | :encoder :pattern} 208 | 209 | {:appender :file 210 | :file "/var/log/file.log" 211 | :encoder :pattern 212 | :pattern "%p [%d] %t - %c %m%n"} 213 | 214 | {:appender :file 215 | :file "/var/log/file.log" 216 | :encoder :json} 217 | ``` 218 | 219 | ### `:rolling-file` appender 220 | 221 | * Mandatory Arguments 222 | * `:file` 223 | * Optional Arguments 224 | * `:rolling-policy` 225 | * `:triggering-policy` 226 | * `:encoder` 227 | * `:pattern` 228 | 229 | There are two rolling policies. 230 | 231 | * `:fixed-window` 232 | * Renames files according to a fixed window algorithm. 233 | * `:time-based` 234 | * Defines a rollover based on time. 235 | 236 | Don't use a triggering policy with `:time-based` rolling policy since `:time-based` rolling policy is its own triggering policy as well. 237 | You can specify a rolling policy by the keyword. 238 | 239 | ```clojure 240 | {:appender :rolling-file 241 | :rolling-policy :fixed-window 242 | :file "/var/log/rolling-file.log" 243 | :encoder :pattern} 244 | 245 | {:appender :rolling-file 246 | :rolling-policy :time-based 247 | :file "/var/log/rolling-file2.log" 248 | :encoder :pattern 249 | :pattern "%p [%d] %t - %c %m%n"} 250 | ``` 251 | 252 | If you want to specify arguments for a rolling policy, you can pass a map to `:rolling-policy` as below. every argument to a rolling policy except `:type` is optional. 253 | 254 | ```clojure 255 | {:appender :rolling-file 256 | :file "rolling-file.log" 257 | :rolling-policy {:type :fixed-window 258 | :min-index 1 259 | :max-index 5 260 | ;; :pattern combines with :file to make the name of a rolled log file. 261 | ;; For example, "rolling-file.log.%i.gz" 262 | ;; %i is index. 263 | :pattern ".%i.gz"} 264 | :encoder :json} 265 | 266 | {:appender :rolling-file 267 | :file "rolling-file2.log" 268 | ;; If you use this rolling policy, don't use a triggering policy 269 | :rolling-policy {:type :time-based 270 | ;; log files are kept for :max-history periods. 271 | ;; periods can be hours, days, months, and so on. 272 | :max-history 5 273 | ;; Before a period ends, if a log file reaches :max-size, it is rolled. 274 | ;; :max-size adds %i to :pattern. Without :max-size, you shouldn't 275 | ;; specify %i in :pattern. 276 | ;; Refer to http://logback.qos.ch/manual/appenders.html#SizeAndTimeBasedFNATP 277 | ;; for elaborate description of :max-size 278 | :max-size 51200 ; bytes 279 | ;; :pattern combines with :file 280 | ;; The rolling period is defined by :pattern. 281 | ;; Refer to http://logback.qos.ch/manual/appenders.html#tbrpFileNamePattern 282 | :pattern ".%d{yyyy-MM-dd}.%i"} 283 | :encoder :pattern 284 | :pattern "%p [%d] %t - %c %m%n"} 285 | ``` 286 | 287 | There is only one triggering policy, `:size-based`. 288 | 289 | ```clojure 290 | {:appender :rolling-file 291 | :rolling-policy :fixed-window 292 | ;; If you don't pass any argument to :size-based triggering policy, it triggers a rollover 293 | ;; when a log file grow beyond SizeBasedTriggeringPolicy/DEFAULT_MAX_FILE_SIZE. 294 | :triggering-policy :size-based 295 | :file "rolling-file.log"} 296 | 297 | {:appender :rolling-file 298 | :rolling-policy :fixed-window 299 | :triggering-policy {:type :size-based 300 | ;; Refer to 301 | ;; http://logback.qos.ch/manual/appenders.html#SizeBasedTriggeringPolicy 302 | :max-size 51200}} ; 51200 bytes 303 | ``` 304 | 305 | ### `:socket` appender 306 | 307 | * Optional Arguments 308 | * `:remote-host` 309 | * `:port` 310 | * `:queue-size` 311 | * `:reconnection-delay` 312 | * `:event-delay-limit` 313 | 314 | ```clojure 315 | {:appender :socket 316 | :remote-host "localhost" 317 | :port 2004 318 | :queue-size 500 319 | :reconnection-delay "10 seconds" 320 | :event-delay-limit "10 seconds"} 321 | ``` 322 | 323 | ### `:syslog` appender 324 | 325 | * Optional Arguments 326 | * `:host` 327 | * `:port` 328 | 329 | ```clojure 330 | {:appender :syslog 331 | :host "localhost" 332 | :port 514} 333 | ``` 334 | 335 | ## Extending 336 | 337 | If you wish to supply your own configuration functions for appenders or encoders, you may do so by 338 | adding multi-methods for `build-appender` and `build-encoder`. `build-appender` dispatches 339 | on the `:appender` key in a configuration map while `build-encoder` dispatches on the `:encoder` key. 340 | 341 | These functions receive the provided configuration map and may thus expect specific keys to be present 342 | to perform their configuration. 343 | 344 | You may need to add a multimethod for `start-appender!` if your appender needs a specialized initialization procedure. 345 | 346 | ## API documentation 347 | 348 | Full API documentation is available at http://pyr.github.io/unilog 349 | 350 | ## Releases 351 | 352 | ### 0.7.29 353 | 354 | - Iizuka Masashi (https://github.com/liquidz) bumped dependencies 355 | 356 | ### 0.7.28 357 | 358 | - Arnaud Geiser (https://github.com/arnaudgeiser) added support for the commons-logging bridge 359 | - Dependency upgrades 360 | - Unilog now depends on Clojure 1.10.3 361 | 362 | ### 0.7.26 363 | 364 | - Dependency upgrades 365 | - Unilog now depends on Clojure 1.10.1 366 | 367 | ### 0.7.24 368 | 369 | - Introduce mdc-fn and mdc-fn* which preserve MDC context across threads 370 | 371 | ### 0.7.22 372 | 373 | - Dependency upgrades 374 | - Switch to clojure 1.9, paving the way for specs 375 | 376 | ### 0.7.21 377 | 378 | - Dependency upgrades 379 | 380 | ### 0.7.20 381 | 382 | - Upgrade to logback 1.2.0 383 | 384 | ### 0.7.19 385 | 386 | - Add tests to ensure base functionality is preserved. 387 | - Hold-off on upgrading to logback 1.2.0 until logstash-encoder is compatible. 388 | 389 | 390 | ### 0.7.17 391 | 392 | - Coda Hale (https://github.com/codahale) updated dependencies. 393 | 394 | ### 0.7.15 395 | 396 | - Coda Hale (https://github.com/codahale) added a `java.util.logging` bridge for applications relying on this logging method. 397 | 398 | ## License 399 | 400 | Copyright © 2014 Pierre-Yves Ritschard 401 | MIT/ISC License, See LICENSE file. 402 | -------------------------------------------------------------------------------- /src/unilog/config.clj: -------------------------------------------------------------------------------- 1 | (ns unilog.config 2 | "Small veneer on top of logback. 3 | Originally based on the logging initialization in riemann. 4 | Now diverged quite a bit. 5 | 6 | For configuration, a single public function is exposed: `start-logging!` which 7 | takes care of configuring logback, later logging is done through 8 | standard facilities, such as [clojure.tools.logging](https://github.com/clojure/tools.logging). 9 | 10 | Two extension mechanism are provided to add support for more appenders and encoders, 11 | see `build-appender` and `build-encoder` respectively" 12 | (:import org.slf4j.LoggerFactory 13 | org.slf4j.bridge.SLF4JBridgeHandler 14 | ch.qos.logback.classic.net.SocketAppender 15 | ch.qos.logback.classic.encoder.PatternLayoutEncoder 16 | ch.qos.logback.classic.Logger 17 | ch.qos.logback.classic.LoggerContext 18 | ch.qos.logback.classic.BasicConfigurator 19 | ch.qos.logback.classic.Level 20 | ch.qos.logback.core.spi.ContextAware 21 | ch.qos.logback.core.rolling.TriggeringPolicy 22 | ch.qos.logback.core.rolling.RollingPolicy 23 | ch.qos.logback.core.spi.LifeCycle 24 | ch.qos.logback.core.Appender 25 | ch.qos.logback.core.encoder.Encoder 26 | ch.qos.logback.core.ConsoleAppender 27 | ch.qos.logback.core.FileAppender 28 | ch.qos.logback.core.OutputStreamAppender 29 | ch.qos.logback.core.rolling.TimeBasedRollingPolicy 30 | ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP 31 | ch.qos.logback.core.rolling.FixedWindowRollingPolicy 32 | ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy 33 | ch.qos.logback.core.rolling.RollingFileAppender 34 | ch.qos.logback.core.util.Duration 35 | ch.qos.logback.core.util.FileSize 36 | ch.qos.logback.core.net.SyslogOutputStream 37 | net.logstash.logback.encoder.LogstashEncoder)) 38 | 39 | ;; Configuration constants 40 | ;; ======================= 41 | 42 | (def levels 43 | "Logging level names to logback level association" 44 | {:all Level/ALL 45 | :trace Level/TRACE 46 | :debug Level/DEBUG 47 | :info Level/INFO 48 | :warn Level/WARN 49 | :error Level/ERROR 50 | :off Level/OFF}) 51 | 52 | (def default-pattern 53 | "Default pattern for PatternLayoutEncoder" 54 | "%p [%d] %t - %c %m%n") 55 | 56 | (def default-encoder 57 | "Default encoder and pattern configuration" 58 | {:encoder :pattern 59 | :pattern default-pattern}) 60 | 61 | (def default-configuration 62 | "A simple default logging configuration" 63 | {:pattern "%p [%d] %t - %c - %m%n" 64 | :external false 65 | :console true 66 | :files [] 67 | :level "info"}) 68 | 69 | ;; Open dispatch method to build appender configuration 70 | ;; ==================================================== 71 | 72 | (defmulti appender-config 73 | "Called by walking through each key/val pair in the main configuration 74 | map. This allows for early transformation of quick access keys such as: 75 | `:console`, `:file`, and `:files`" 76 | first) 77 | 78 | (defmethod appender-config :default 79 | [_] 80 | nil) 81 | 82 | (defmethod appender-config :console 83 | [[_ val]] 84 | (when (boolean val) 85 | (cond (string? val) {:appender :console 86 | :encoder :pattern 87 | :pattern val} 88 | (map? val) (-> (merge default-encoder val) 89 | (update-in [:encoder] keyword) 90 | (assoc :appender :console)) 91 | :else {:appender :console 92 | :encoder :pattern 93 | :pattern default-pattern}))) 94 | 95 | (defmethod appender-config :file 96 | [[_ val]] 97 | (cond (string? val) (-> default-encoder 98 | (assoc :appender :file) 99 | (assoc :file val)) 100 | (map? val) (-> (merge default-encoder val) 101 | (update-in [:encoder] keyword) 102 | (assoc :appender :file)) 103 | :else (throw (ex-info "invalid file appender config" 104 | {:config val})))) 105 | 106 | (defmethod appender-config :files 107 | [[_ files]] 108 | (for [file files] 109 | (appender-config [:file file]))) 110 | 111 | (defmethod appender-config :appenders 112 | [[_ appenders]] 113 | (for [appender appenders 114 | :when (map? appender)] 115 | (-> appender 116 | (update-in [:encoder] keyword) 117 | (update-in [:appender] keyword)))) 118 | 119 | ;; Open dispatch method to build encoders based on configuration 120 | ;; ============================================================= 121 | 122 | (defmulti build-encoder 123 | "Given a prepared configuration map, associate a prepared encoder 124 | to the `:encoder` key." 125 | :encoder) 126 | 127 | (defmethod build-encoder :pattern 128 | [{:keys [pattern] :as config}] 129 | (let [encoder (doto (PatternLayoutEncoder.) 130 | (.setPattern (or pattern default-pattern)))] 131 | (assoc config :encoder encoder))) 132 | 133 | (defmethod build-encoder :json 134 | [config] 135 | (assoc config :encoder (doto (LogstashEncoder.) (.setIncludeMdc true)))) 136 | 137 | (defmethod build-encoder :default 138 | [{:keys [appender] :as config}] 139 | (cond-> config 140 | (instance? OutputStreamAppender appender) 141 | (assoc :encoder (doto (PatternLayoutEncoder.) 142 | (.setPattern default-pattern))))) 143 | 144 | ;; 145 | ;; Open dispatch to build a file rolling policy 146 | ;; ============================================ 147 | 148 | (defmulti build-rolling-policy 149 | "Given a configuration map, build a RollingPolicy instance." 150 | :type) 151 | 152 | (defmethod build-rolling-policy :fixed-window 153 | [{:keys [file pattern max-index min-index] 154 | :or {max-index 5 155 | min-index 1 156 | pattern ".%i.gz"}}] 157 | (doto (FixedWindowRollingPolicy.) 158 | (.setFileNamePattern (str file pattern)) 159 | (.setMinIndex (int min-index)) 160 | (.setMaxIndex (int max-index)))) 161 | 162 | (defmethod build-rolling-policy :time-based 163 | [{:keys [file pattern max-history max-size] 164 | :or {max-history 5}}] 165 | (let [tbrp (TimeBasedRollingPolicy.) 166 | pattern (if pattern 167 | pattern 168 | (if max-size 169 | ;; TimeBasedRollingPolicy has a compression issue 170 | ;; http://jira.qos.ch/browse/LOGBACK-992 171 | ".%d{yyyy-MM-dd}.%i" 172 | ".%d{yyyy-MM-dd}"))] 173 | (when max-size 174 | (->> (doto (SizeAndTimeBasedFNATP.) 175 | (.setMaxFileSize (FileSize/valueOf (str max-size)))) 176 | (.setTimeBasedFileNamingAndTriggeringPolicy tbrp))) 177 | (doto tbrp 178 | (.setFileNamePattern (str file pattern)) 179 | (.setMaxHistory max-history)))) 180 | 181 | (defmethod build-rolling-policy :default 182 | [config] 183 | (throw (ex-info "Invalid rolling policy" {:config config}))) 184 | 185 | ;; 186 | ;; Open dispatch to build a triggering policy for rolling files 187 | ;; ============================================================ 188 | 189 | (defmulti build-triggering-policy 190 | "Given a configuration map, build a TriggeringPolicy instance." 191 | :type) 192 | 193 | (defmethod build-triggering-policy :size-based 194 | [{:keys [max-size] 195 | :or {max-size SizeBasedTriggeringPolicy/DEFAULT_MAX_FILE_SIZE}}] 196 | (doto (SizeBasedTriggeringPolicy.) 197 | (.setMaxFileSize (FileSize/valueOf (str max-size))))) 198 | 199 | ;; Open dispatch method to build appenders 200 | ;; ======================================= 201 | 202 | (defmulti build-appender 203 | "Given a prepared configuration map, associate a prepared appender 204 | to the `:appender` key." 205 | :appender) 206 | 207 | (defmethod build-appender :console 208 | [config] 209 | (assoc config :appender (ConsoleAppender.))) 210 | 211 | (defmethod build-appender :file 212 | [{:keys [file] :as config}] 213 | (assoc config :appender (doto (FileAppender.) 214 | (.setFile file)))) 215 | 216 | (defmethod build-appender :socket 217 | [{:keys [remote-host port queue-size reconnection-delay event-delay-limit] 218 | :or {remote-host "localhost" 219 | port 2004 220 | queue-size 500 221 | reconnection-delay "10 seconds" 222 | event-delay-limit "10 seconds"} 223 | :as config}] 224 | (let [appender (SocketAppender.)] 225 | (.setRemoteHost appender remote-host) 226 | (.setPort appender (int port)) 227 | (when queue-size 228 | (.setQueueSize appender (int queue-size))) 229 | (when reconnection-delay 230 | (.setReconnectionDelay appender (Duration/valueOf reconnection-delay))) 231 | (when event-delay-limit 232 | (.setEventDelayLimit appender (Duration/valueOf event-delay-limit))) 233 | (assoc config :appender appender))) 234 | 235 | (defmethod build-appender :syslog 236 | [{:keys [host port] :or {host "localhost" port 514} :as config}] 237 | (assoc config :appender (doto (OutputStreamAppender.) 238 | (.setOutputStream 239 | (SyslogOutputStream. host (int port)))))) 240 | 241 | (defmethod build-appender :rolling-file 242 | [{:keys [rolling-policy triggering-policy file] 243 | :or {rolling-policy :fixed-window 244 | triggering-policy :size-based} 245 | :as config}] 246 | (let [appender (RollingFileAppender.) 247 | format-policy (fn [type policy] 248 | (cond 249 | (keyword? policy) {:type policy} 250 | (string? policy) {:type policy} 251 | (map? policy) (update-in policy [:type] keyword) 252 | :else 253 | (throw (ex-info (format "invalid %s policy" type) 254 | {:config policy})))) 255 | format-policy (comp #(assoc % :file file) format-policy) 256 | rolling-policy (format-policy "rolling" rolling-policy) 257 | triggering-policy (format-policy "triggering" triggering-policy)] 258 | (.setFile appender file) 259 | (when-not (= :time-based (:type rolling-policy)) 260 | (.setTriggeringPolicy appender 261 | (build-triggering-policy triggering-policy))) 262 | (.setRollingPolicy appender (build-rolling-policy rolling-policy)) 263 | (assoc config :appender appender))) 264 | 265 | (defmethod build-appender :default 266 | [val] 267 | (throw (ex-info "invalid log appender configuration" {:config val}))) 268 | 269 | ;;; start-appender! 270 | ;;; ======================================= 271 | (defmulti start-appender! 272 | "Start an appender according to appender type" 273 | (fn [appender context] 274 | (type appender))) 275 | 276 | (defmethod start-appender! RollingFileAppender 277 | [^RollingFileAppender appender ^LoggerContext context] 278 | ;; The order of operations is important. If you change it, errors will occur. 279 | (.setContext appender context) 280 | (let [^RollingPolicy rp (.getRollingPolicy appender)] 281 | (.setParent rp appender) 282 | (when (instance? ContextAware rp) 283 | (.setContext ^ContextAware rp context)) 284 | (.start rp)) 285 | (when-let [tp ^TriggeringPolicy (.getTriggeringPolicy appender)] 286 | ;; Since TimeBasedRollingPolicy can serve as a triggering policy, 287 | ;; start triggering policy only if it is not started already. 288 | (when-not (.isStarted tp) 289 | (when (instance? ContextAware tp) 290 | (.setContext ^ContextAware tp context)) 291 | (.start tp))) 292 | (.start appender)) 293 | 294 | (defmethod start-appender! :default 295 | [appender context] 296 | (.setContext ^ContextAware appender ^LoggerContext context) 297 | (.start ^LifeCycle appender) 298 | appender) 299 | 300 | (defn start-logging! 301 | "Initialize logback logging from a map. 302 | 303 | The map accepts the following keys as keywords 304 | - `:level`: Default level at which to log. 305 | - `:pattern`: The pattern to use for logging text messages 306 | - `:console`: Append messages to the console using a simple pattern 307 | layout. If value is a boolean, treat it as such and use a default 308 | encoder. If value is a string, treat it as a pattern and use 309 | a pattern encoder. If value is a map, expect encoder configuration 310 | in the map. 311 | - `:file`: A file to log to. May either be a string, the log file, or 312 | a map which accepts optional encoder configuration. 313 | - `:files`: A list of either strings or maps. strings will create 314 | text files, maps are expected to contain a `:path` key as well 315 | as an optional `:json` which when present and true will switch 316 | the layout to a JSONEventLayout for the logger. 317 | - `:overrides`: A map of namespace or class-name to log level, 318 | this will supersede the global level. 319 | - `:external`: Do not proceed with configuration, this 320 | is useful when logging configuration is provided 321 | in a different manner (by supplying your own logback config file 322 | for instance). 323 | 324 | When called with no arguments, assume an empty map 325 | 326 | example: 327 | 328 | ```clojure 329 | {:console true 330 | :level \"info\" 331 | :files [\"/var/log/app.log\" 332 | {:file \"/var/log/app-json.log\" 333 | :encoder json}] 334 | :overrides {\"some.namespace\" \"debug\"}} 335 | ``` 336 | " 337 | ([raw-config] 338 | (let [config (merge default-configuration raw-config) 339 | {:keys [external level overrides]} config] 340 | (when-not external 341 | (SLF4JBridgeHandler/removeHandlersForRootLogger) 342 | (SLF4JBridgeHandler/install) 343 | (let [get-level #(get levels (some-> % keyword) Level/INFO) 344 | level ^Level (get-level level) 345 | root ^Logger (LoggerFactory/getLogger Logger/ROOT_LOGGER_NAME) 346 | context ^LoggerContext (LoggerFactory/getILoggerFactory) 347 | configs (->> (merge {:console true} config) 348 | (map appender-config) 349 | (flatten) 350 | (remove nil?) 351 | (map build-appender) 352 | (map build-encoder))] 353 | 354 | (.detachAndStopAllAppenders root) 355 | 356 | (doseq [{:keys [^Encoder encoder ^Appender appender]} configs] 357 | (when (and (instance? OutputStreamAppender appender) encoder) 358 | (.setContext encoder context) 359 | (.start encoder) 360 | (.setEncoder ^OutputStreamAppender appender encoder)) 361 | (start-appender! appender context) 362 | (.addAppender root appender)) 363 | 364 | (.setLevel root level) 365 | (doseq [[logger-name level] overrides 366 | :let [logger (LoggerFactory/getLogger (name logger-name)) 367 | level (get-level level)]] 368 | (.setLevel ^Logger logger ^Level level)) 369 | root)))) 370 | ([] 371 | (start-logging! default-configuration))) 372 | 373 | (comment 374 | 375 | (start-logging! (assoc default-configuration :console "%p [%d] %t ->>> %X{bim} %c - %m%n")) 376 | 377 | (require '[clojure.tools.logging :as log]) 378 | (require '[unilog.context :refer [with-context]]) 379 | 380 | (with-context {"bim" "bam"} 381 | (log/info "hello"))) 382 | --------------------------------------------------------------------------------