Skip to content
This repository has been archived by the owner on Aug 12, 2022. It is now read-only.

nilsvu/Evergreen

Repository files navigation

Evergreen

Most natural Swift logging

Build Status CocoaPods Compatible Platform Gitter

Evergreen is a logging framework written in Swift. It is designed to work just as you would expect, yet so versatile you can make it work however you wish.

Integrate Evergreen logging into your Swift project to replace those plain print() statements with calls to Evergreen's versatile logging functions that make it easy to adjust the verbosity of the output, log to multiple destinations (e.g. a file) with custom formatting and even measure time.

import Evergreen

log("Hello World!", forLevel: .info)
[AppDelegate.swift|INFO] Hello World!

Evergreen logging is great to use in any Swift project, but particularly useful when developing a framework. Give the users of your framework the opportunity to easily adjust the verbosity of the output your framework generates.

For Swift 4 development use the swift4 branch.

About Logging

Logging is a means of tracking events that happen when some software runs. The software’s developer adds logging calls to their code to indicate that certain events have occurred. An event is described by a descriptive message which can optionally contain variable data (i.e. data that is potentially different for each occurrence of the event). Events also have an importance which the developer ascribes to the event; the importance can also be called the level or severity.

— From the Python documentation.

That's not what logging is for everyone, though. There are people who would say:

Logging is the cutting, skidding, on-site processing, and loading of trees or logs onto trucks or skeleton cars.

— From Wikipedia.

For now, let's focus on what the Python people tell us. They seem to know what they are talking about.

Installation

CocoaPods

The easiest way to integrate Evergreen into your project is via CocoaPods:

  1. Install CocoaPods:

    $ gem install cocoapods
  2. Create a Podfile in you project's directory or add Evergreen to your existing Podfile:

    source 'https://github.com/CocoaPods/Specs.git'
    platform :ios, '8.0'
    use_frameworks!
    
    pod 'Evergreen'
    
  3. Let CocoaPods do its magic:

    $ pod install

As usual with CocoaPods, make sure to use the *.xcworkspace instead of the *.xcodeproj.

Swift Package Manager

You can also use the Swift Package Manager included in the Swift developer releases. Just add Evergreen as a dependency to your package description, like this:

import PackageDescription

let package = Package(
    name: "HelloWorld",
    dependencies: [
        .Package(url: "https://github.com/knly/Evergreen.git", majorVersion: 0),
        // ...
    ]
)

Manually

You can also integrate Evergreen into you project manually:

  1. Add Evergreen as a submodule:

    $ git submodule add https://github.com/knly/Evergreen.git
  2. Drag Evergreen.xcodeproj into the file navigator of your project.

  3. In the target configuration under General, add Evergreen.framework to the Embedded Binaries


Usage

Note: Open the Evergreen.xcworkspace and have a look at the Evergreen.playground for a more interactive tour through Evergreen's functionality. You can also create a Playground in your project's workspace and import Evergreen to try for yourself.

Logging without configuration

import Evergreen

log("Hello World!")

You can log events without any configuration and see a nicely formatted message show up in the console. This is for very quick and basic use only. Read on to learn about Evergreen's more sophisticated logging options.

Using Log Levels

You can assign an importance or severity to an event corresponding to one of the following log levels:

  • Critical: Events that are unexpected and can cause serious problems. You would want to be called in the middle of the night to deal with these.
  • Error: Events that are unexpected and not handled by your software. Someone should tell you about these ASAP.
  • Warning: Events that are unexpected, but will probably not affect the runtime of your software. You would want to investigate these eventually.
  • Info: General events that document the software's lifecycle.
  • Debug: Events to give you an understanding about the flow through your software, mainly for debugging purposes.
  • Verbose: Detailed information about the environment to provide additional context when needed.

The logger that handles the event has a log level as well. If the event's log level is lower than the logger's, it will not be logged.

In addition to the log levels above, a logger can have one of the following log levels. Assigning these to events only makes sense in specific use cases.

  • All: All events will be logged.
  • Off: No events will be logged.

With log levels, you can control the verbosity of your software. A common use case is to use a low log level during development and increase it in a release environment, or to adjust it for specific parts of your software and different logging destinations, such as the console or a file.

For a basic configuration, you can adjust Evergreen's default log level. Read about the logger hierarchy below to learn how to control the log level more granually.

Evergreen.logLevel = .debug

// These events will be logged, because their log level is >= .debug
log("Debug", forLevel: .debug)
log("Info", forLevel: .info)
log("Warning", forLevel: .warning)
log("Error", forLevel: .error)
log("Critical", forLevel: .critical)

// These events will not be logged, because their log level is < .debug
log("Verbose", forLevel: .verbose)

Every log level has a corresponding log function for convenience:

debug("Debug") // equivalent to log("Debug", forLevel: .debug)
info("Info") // equivalent to log("Info", forLevel: .info)
// ...

Using the Logger Hierarchy

You usually want to use loggers to log events instead of the global functions. A logger is always part of a hierarchy and inherits attributes, such as the log level, from its parent. This way, you can provide a default configuration and adjust it for specific parts of your software.

This is particularly useful during development to lower the log level of the part of your software you are currently working on.

Every logger has a key to identify the source of any given event. In its hierarchy, the key expands to a dot-separated key path, such as "Parent.Child".

You can manually build your own hierarchy, of course, but Evergreen provides a convenient way for you to utilize this powerful feature:

  • The default logger is the root of the logger hierarchy and can be retrieved using the Evergreen.defaultLogger constant. Use it to set a default log level. The global variable Evergreen.logLevel also refers to the default logger.
  • Retrieve an appropriate logger using the global Evergreen.getLogger function or the Logger.child instance method. Provide a key path that describes the part of your software the event is relevant for, such as "MyModule.MyType". These methods will always return the same logger instance for a given key path and establish the logger hierarchy, if it does not yet exist.

It is convenient to use a constant stored attribute to make an appropriate logger available for a given type:

import Evergreen

class MyType {

	let logger = Evergreen.getLogger("MyModule.MyType")

	init() {
		self.logger.debug("Initializing...")
	}

}

Having established a logger hierarchy, you can adjust the logging configuration for parts of it:

Evergreen.logLevel = .warning // Set the `defaultLogger`'s log level to .warning
let logger = Evergreen.getLogger("MyModule") // Retrieve the logger with key "MyModule" directly descending from the default logger
logger.logLevel = .debug // We are working on this part of the software, so set its log level to .debug

Note: A good place to do this configuration for production is in the AppDelegate's application:didFinishLaunchingWithOptions: method. Temporary log level adjustments are best configured as environment variables as described in the following section.

Using Environment Variables for Configuration

The preferred way to temporarily configure the logger hierarchy is using environment variables. This way, you can conveniently enable more verbose logging for the parts of your software you are currently working on. In Xcode, choose your target from the dropdown in the toolbar, select Edit Scheme... > Run > Arguments and add environment variables to the list. Then call:

Evergreen.configureFromEnvironment()

Every environment variable prefixed Evergreen is evaluated as a logger key path and assigned a log level corresponding to the variable's value. Values should match the log level descriptions, e.g. Debug or Warning.

Valid environment variable declarations would be e.g. Evergreen = Debug or Evergreen.MyLogger = Verbose.

Logging Errors alongside your Events

You can pass any error conforming to Swift's Error type (such as NSError) to Evergreen's logging functions, either as the message or in the separate error: argument:

let error: Error // some error
debug("Something unexpected happened here!", error: error)

Measuring Time

Easily measure the time between two events with tic and toc:

tic(andLog: "Starting expensive operation...", forLevel: .debug)
// ...
toc(andLog: "Completed expensive operation!", forLevel: .info)
[Default|DEBUG] Starting expensive operation...
[Default|INFO] Completed expensive operation! [ELAPSED TIME: 0.0435580015182495s]

You can also use the timerKey argument for nested timing:

tic(andLog: "Starting expensive operation...", forLevel: .debug, timerKey: "expensiveOperation")
for var i=0; i<10; i++ {
	tic(andLog: "\(i+1). iteration...", forLevel: .verbose, timerKey: "iteration")
	// ...
	toc(andLog: "Done!", forLevel: .verbose, timerKey: "iteration")
}
toc(andLog: "Completed expensive operation!", forLevel: .info, timerKey: "expensiveOperation")

Logging Events only once

You can keep similar events from being logged in excessive amounts by associating a key with them in any logging call, e.g.:

debug("Announcing this once!", onceForKey: "announcement")

Advanced Usage

Using Handlers

When a logger determines that an event should be handled, it will pass it to its handlers. A handler uses its formatter to retrieve a human-readable record from the event and then emits the record. Subclasses of Handler emit records in different ways:

  • A ConsoleHandler prints the record to the console.
  • A FileHandler writes the records to a file.
  • A StenographyHandler appends the record to an array in memory.

You can override emit in you own subclass to implement any custom behaviour you like, e.g. send it to a server.

Evergreen's defaultLogger has a ConsoleHandler attached by default, so every event in its hierarchy will be logged to the console. You can easily add additional handlers, by appending them to an appropriate logger's handlers array:

let logger = Evergreen.defaultLogger
let stenographyHandler = StenographyHandler()
logger.handlers.append(stenographyHandler)

You can also set a handler's logLevel, to add an additional level of filtering.

Formatting

Evergreen's Formatter class implements a convenient way for you to adjust the format of log records.

The default implementation of the string(from event: Event<M>) method, that you can also override in a subclass, uses a list components: [Formatter.Component] to construct a record from an event using instances of the Formatter.Component enumeration:

let simpleFormatter = Formatter(components: [ .Text("["), .Logger, .Text("|"), .LogLevel, .Text("] "), .Message ])
let consoleHandler = ConsoleHandler(formatter: simpleFormatter)
Evergreen.defaultLogger.handlers = [ consoleHandler ]

Contact

Evergreen was created and is maintained by Nils Leif Fischer.

I would greatly appreciate some professional opinions on the API and architecture. Please let me know any suggestions via Email ([email protected]), on Gitter or by opening an issue.

License

Evergreen is released under the MIT license. See LICENSE.md for details.