Skip to content
This repository

Design Philosophy

The design goal behind Logger4clj is to allow a logger to be defined and used in a namespace without it actively logging, with clients able to bind that logger into their own. This is similar to Log4j, where each distinct logger is created in a static context and these are accessed and their appenders, etc configured independently.

Log4j's strategy for letting users determine what should or should not be logged can be confusing; you can configure the root logger and log everything from everywhere; OR configure appenders and log levels based on package names, being sure to set the additivity correctly so as not to log entries twice.

In general, the way Log4j and other Java-based loggers are usually used, they attach significance to namespace hierarchies where there really isn't any, from a logging perspective.

Logger4clj, on the other hand, requires that whatever ought to be logged be explicity bound to (one or more) root loggers in order for that logger to be active. If each namespace has a logger called, say, logger, each can be explicitly referred to as namespace/logger without having to think about where it fits in the namespace hierarchy.

Packages for use/require

(ns mypackage
  (:use [logger4clj.logger]       ;; contains basic logger configuration functions and macros
        [logger4clj.formatters]   ;; formatter definitions
        [logger4clj.appenders]))  ;; appender definitions

Basic Logger Configuration

The simplest logger configuration is

(def-logger my-logger)

The def-logger macro creates two vars in the namespace in which it's used. In this case, it creates a map called my-logger-var and a function called my-logger. You can completely ignore my-logger-var, but be sure not to accidently redefine it.

The my-logger-var map contains nothing interesting in this case, since there are no appenders, nor is there a queue to hold log messages. All logging operations are essentially a no-op until the my-logger is explicitly 'bound' and the parent logger is started.

The above statement creates a logger that may be used, but does not actively log.

Logging

To use the logger, for example:

(my-logger :info "Hello Cruel World")

This calls the function created by def-logger called my-logger with the two arguments. An optional 3rd argument is accepted which must be of type java.lang.Throwable (i.e. an exception, usually one caught within a catch block).

(try
  (/ 0 0)
  (catch Exception e
      (my-logger :error "Something happened!" e)))

Again, these statements are no-ops unless my-logger is bound and started.

Log Levels

Accepted log levels are :trace, :debug, :info, :warning, :error, :fatal

Binding Loggers

To say that logger-A binds to logger-B is the same as saying that logger-B's logging state is determined by logger-A.

This relationship is written as follows:

  (def-logger logger-A)

  (def-logger logger-B
      (bind-logger logger-A))

  (def-logger logger-C
      (bind-logger logger-B))

Since there are no appenders defined and the loggers aren't started, then logging to these loggers is still a no-op. A client may still come along and bind to logger-C with an appender and start itself, and all three of logger-A, logger-B and logger-C will be activated and statements will be logged to the appender with which logger-C is bound.

Restrictions on Binding Hierarchies

There is only one restriction:

  1. Circular dependencies are not allowed (you'll probably end up with a stack overflow when starting the logger)

Diamond dependencies WILL work without unexpected consequences. For example, statements at the bottom won't be logged twice unless you have two differently-named appenders appending to the same resource, but this would result in problems no matter what the shape of the dependency hierarchy.

Appenders and Formatters

Introduction to Appenders

Before a logger can log anything, it needs an appender. An appender is a map of lifecycle functions:

  { :init     (fn [])        ;; called by start-logger
    :do-log   (fn [arg-map]) ;; called by the thread that waits on the blocking queue
    :clean-up (fn [])        ;; called by stop-logger
   }

As you can see, writing a custom appender would be dirt simple.

There are currently two appenders in logger4clj.appenders, a console appender and a fully-featured file appender. See documentation for specific configuration options on the file appender.

Note that while there's no real reason to call the stop-logger function in most cases (the file appender flushes the stream after all writes, so when the JVM exits, the file should close after everything has been written), it may be desirable to do so, perhaps in a JVM shutdown hook, to make sure custom appender types are accommodated, so that resources are closed effectively.

Introduction to Formatters

Formatters are functions that accept a map of logging information and return a string in the desired format of the output. Formatters must be explicitly supported by the appenders that use them. The logger4clj.formatters namespace includes a bunch of formatters for the file and console appenders, most of which aren't frequently used in logging output (xml, json, clojure data structures, yaml).

Most of the time, the line formatter will be used. The line-formatter accepts a string such as:

   "[${ts:yyyy-MM-dd HH:mm:ss.SSSZ}][${lvl}] ${msg} (${fn}:${ln}) ${ex}${n}"

Each of the ${} fields contains a key for what gets replaced at that location. For example, ${ln} is the line number where the logging statement is called. The ${ts:...} tag contains a java.text.SimpleDateFormat string that describes how to format the timestamp of the log message. The output will look something like:

 [2012-12-09 21:19:20.841-0800][ERROR] An error was encountered! (sample_logger.clj:35) 
 java.lang.IllegalStateException: Divide by zero!
         at sample_logger$eval4086.invoke(sample_logger.clj:33)
         at clojure.lang.Compiler.eval(Compiler.java:6511)
 Caused by: java.lang.ArithmeticException: Divide by zero
         at clojure.lang.Numbers.divide(Numbers.java:156)
         at clojure.lang.Numbers.divide(Numbers.java:3691)
         ... 25 more 

Using Appenders and Formatters

First, the example:

(def-logger my-logger
  (register-appender :log-file
     (create-file-appender "/home/testuser/app/log/program.log"
        :formatter (create-line-formatter 
                     "${ts} [${lvl}] - (${fn}:${ln}) ${msg} ${ex} ${n}")))
  (with-appenders
     [:log-file :debug])))      

Note there are TWO extra parameters to def-logger, register-appender and with-appenders.

The register-appender clause is where the appender is defined. Here we have a file appender named :log-file with a line formatter created in the argument to the :formatter key parameter. There may be more than one register-appender clause.

The with-appenders clause directs logger4clj to use the :log-file appender, with a log level of :debug, with the logger under definition, that is all all log statements to my-logger will log to the :log-file file appender when the log level exceeds or is equal to :debug (i.e. not :trace). Multiple such vectors may be provided to the with-appenders clause.

The reason these are separate statements is that a registered appender may also be use when binding a logger and may not necessarily be used in the logger under definition:

(def-logger my-logger
  (register-appender :console
     (create-console-appender))
  (register-appender :session-log
     (create-file-appender "/home/testuser/app/log/program.log"
        :formatter (create-line-formatter 
                     "${ts} [${lvl}] - (${fn}:${ln}) ${msg} ${ex} ${n}")))
  (bind-logger myapp.session-controller/logger
        :with-appender [:session-log :info])
  (with-appenders
     [:console :error]))

register-appender clauses MUST preceed their uses in bind-logger or with-appenders clauses, since bind-logger manipulates these registrations right away.

In the example immediately above, my-logger will use only the :console appender, while myapp.session-controller/logger will log only to the :session-log.

Starting/Stopping the Loggers

Starting the logger can be accomplished in two ways, either as the last clause of a def-logger statement, or separately:

   (start-logger my-logger)

or

(def-logger my-logger
  (register-appender :console
     (create-console-appender))
  (with-appenders
     [:console :error])
  (start-logger))

This will start all the threads that read from each logger's queue and write to the appenders. It will also trigger initialization of all of the appenders.

Stopping the logger is about the same:

    (stop-logger my-logger)

This will stop all of the logging threads by sending them a special 'stop' message and then call the clean-up routine on all of the appenders.

Something went wrong with that request. Please try again.