Logging - coolsamson7/inject GitHub Wiki
Before describing the api we need to elaborate on the different concepts
Log
A log is an endpoint where log messages are sent to ( console, file, etc. ). Also knowns as Appender ( never liked that name :-) )
Log Formatter
Every log can reference a log formatter that is used to generate a string representation of a log entry
Logger
A logger is a named object that provides the public api to emit log messages. Every logger references a list of log's and is specified by a log level which filters log entries by comparing the levels.
Loggers form a hierarchy whenever two - "." separated - names share the same prefix. Child loggers inherit the logs of its parent.
Log Manager
A log manager collects different loggers and is used to retrieve an appropriate logger given a specific name. If a direct match is not found, it will return the the logger with the longest matching prefix.
Let's look at a complete example:
// a formatter is a composition of the different possible log entry constituents
let formatter = LogFormatter.timestamp("dd/M/yyyy, H:mm:s") + " [" + LogFormatter.logger() + "] " + LogFormatter.level() + " - " + LogFormatter.message()
let consoleLog = ConsoleLog(name: "console", formatter: formatter)
// configure loggers
LogManager()
.registerLogger("", level : .OFF, logs: [consoleLog)]) // root logger
.registerLogger("Inject", level : .WARN) // will inherit all log destinations
.registerLogger("Inject.Environment", level : .ALL)
// retrieve a logger
let logger = LogManager.getLogger(forClass: Environment.self) // -> "Inject.Environment"
logger.warn("ouch")Thai will produce something like
23/8/2016, 10:29:22 [Inject.Environment] WARN - ouch
A log level - LogManager.Level - is an enum with the following values
ALLDEBUGINFOWARNERRORFATALOFF
The values have a int raw value in order to enable level comparisons. So an ERROR level is greater than the DEBUG level.
The central class is LogManager wich stores all loggers. A static variable stores a singleton object. Whenever a new instance is created, it will overwrite the singleton. Internally a default manager is created on startup with a root logger without a log.
Retrieving the singleton is done by calling
LogManager.getSingleton()Registering logs is done with the - fluent - function
registerLogger(path : String, level : Level, logs: [Log] = [], inherit : Bool = true) -> Self
The parameters are
-
path: the path, a "." separated string -
level: the level which defines the minimum level of log requests which will be accepted by this logger -
logs: a list of associatedLog's that will be notified about log requests -
inherit: iftrue, the logger will inherit all logs of its parent
if not explicitly registered, every log manager will define a root logger ( path="" ) with level .OFF and no associated logs.
The class LogFormatter is used to compose an object that specifies the layout of a log entry.
The class offers a number of functions that reference individual aspects of a log entry:
-
func level(): the level of the logger that emitted the log request -
func logger(): the name of the logger -
func thread(): the name of the current thread -
func file(): the file name of the log request -
func function(): the current function of the log request -
func line(): the line number -
func column(): the column number -
func timestamp(pattern : String = "dd/M/yyyy, H:mm:s"): the timestamp of the log request -
func message(): the log message
Loggers are composed by simply using the + operator with a second formatter or a plain string.
Example:
The default log formatter is created like this:
LogFormatter.timestamp() + " [" + LogFormatter.logger() + "] " + LogFormatter.level() + " - " + LogFormatter.message()
All logs derive from the base class Logger.Log which offers the constructor
init(name : String, formatter : LogFormatter? = nil, colorize: Bool = false)
-
name: the name of the log -
formatter: the optional formatter -
colorize: iftrue, the output will take advantage of the Xcode terminal coloring.
the method
func log(entry : LogManager.LogEntry) -> Void
must be implemented by subclasses. In order to get a string representation, the function
func format(entry : LogManager.LogEntry) -> String
can be called that applies the defined format to the payload data.
Implemented subclasses are:
A log that simply calls print. The constructor is
init(name : String, formatter: LogFormatter, synchronize : Bool = true) with parameters
-
name: the name -
formatter: the associated formatter -
synchronize: iftrue, the log entry is synchronized with aMutex
A log that write to a file. The constructor is:
init(name : String, fileName : String, formatter: LogFormatter, synchronize : Bool = true) throws with parameters
-
name: the name -
fileName: the file name -
formatter: the associated formatter -
synchronize: iftrue, the log entry is synchronized with aMutex
A delegating log that writes on a file and moves log files every day. The constructor is
init(name : String, directory : String, baseName : String, formatter: LogFormatter? = nil, synchronize : Bool = true, keepDays : Int) throws with parameters
-
namethe name -
directorythe base directory for log files -
baseNamethe base name of a log file -
fomatterthe formatter -
synchronizeiftrue, logging is synchronized -
keepDaysthe number of days that historic logfiles will be kept. After the period files will be deleted.
The current log file is located in the directory directory and named
<baseName>.log
Historical logs are names
<baseName>-<date>.log
where date is yyyy-MM-dd
A log that uses NSLog(...). The constructor is:
init(name : String, formatter: LogFormatter, synchronize : Bool = true) with parameters
-
name: the name -
formatter: the associated formatter -
synchronize: iftrue, the log entry is synchronized with aMutex
This log is a delegating log the delegates all requests to another log. The constructor is
init(name : String, delegate : LogManager.Log, queue : dispatch_queue_t? = nil) with parameters
-
name: the name -
delegate: the delegate log -
queue: adispatch_queue_tthat is used to asynchronously trigger the delegate logging. If not supplied an internal serial dispatch queue is created.
Example:
let logManager = ...
logManager.registerLogger("", level : .OFF, logs: [QueuedLog(name: "async-console", delegate: ConsoleLog(name: "console", synchronize: false))])Loggers are retrieved from a manager with the static functions
func getLogger(forName name : String) -> Logger or
func getLogger(forClass clazz : AnyClass) -> Logger
The second function will do the lookup with the fully qualified name of the class ( which is the bundle name + "." + class name )
The getLogger()functions will either return a direct match, or return an inherited logger with the longest matching prefix.
Example:
Declared loggers are
-
""( the root logger ) "com""com.foo"
When asking for logger named
"com.bar"
it will return "com"
Asking for
"foo.bar" will return the root logger.
Actually when an inherited logger is returned, a new instance will be created that inherits all aspects of this specific logger but with a different name!
Log requests are executed by one the following functions:
func info<T>(@autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Voidfunc warn<T>(@autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Voidfunc debug<T>(@autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Voidfunc error<T>(error: ErrorType? = nil, stackTrace : Stacktrace = Stacktrace(frames: NSThread.callStackSymbols()), @autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Voidfunc fatal<T>(error: ErrorType? = nil, stackTrace : Stacktrace = Stacktrace(frames: NSThread.callStackSymbols()), @autoclosure message: () -> T, file: String = #file, function: String = #function, line: Int = #line, column: Int = #column) -> Void
As you can see the functions error and fatal pickup the current stack trace and accept an additional error parameter.
The requests will fetch the best matching logger and execute the log request if the level of the logger equal or smaller than the requested level.
Since the message is declared as an auto closure, the following expression is valid:
logger.warn("ouch!")
The typical scenario is to declare a logger as a constant within a class that applies logging ,e.g.
class ImportantClass {
static let LOGGER = LogManager.getLogger(forClass: ImportantClass.self)
public func foo() -> Void {
LOGGER.info("important things happen...")
}
}