LogHandler

public protocol LogHandler

A LogHandler is an implementation of a logging backend.

This type is an implementation detail and should not normally be used, unless implementing your own logging backend. To use the SwiftLog API, please refer to the documentation of Logger.

Implementation requirements

To implement your own LogHandler you should respect a few requirements that are necessary so applications work as expected regardless of the selected LogHandler implementation.

  • The LogHandler must be a struct.
  • The metadata and logLevel properties must be implemented so that setting them on a Logger does not affect other Loggers.

Treat log level & metadata as values

When developing your LogHandler, please make sure the following test works.

LoggingSystem.bootstrap(MyLogHandler.init) // your LogHandler might have a different bootstrapping step
var logger1 = Logger(label: "first logger")
logger1.logLevel = .debug
logger1[metadataKey: "only-on"] = "first"

var logger2 = logger1
logger2.logLevel = .error                  // this must not override `logger1`'s log level
logger2[metadataKey: "only-on"] = "second" // this must not override `logger1`'s metadata

XCTAssertEqual(.debug, logger1.logLevel)
XCTAssertEqual(.error, logger2.logLevel)
XCTAssertEqual("first", logger1[metadataKey: "only-on"])
XCTAssertEqual("second", logger2[metadataKey: "only-on"])

Special cases

In certain special cases, the log level behaving like a value on Logger might not be what you want. For example, you might want to set the log level across all Loggers to .debug when say a signal (eg. SIGUSR1) is received to be able to debug special failures in production. This special case is acceptable but we urge you to create a solution specific to your LogHandler implementation to achieve that. Please find an example implementation of this behavior below, on reception of the signal you would call LogHandlerWithGlobalLogLevelOverride.overrideGlobalLogLevel = .debug, for example.

public struct LogHandlerWithGlobalLogLevelOverride: LogHandler {
    // the static properties hold the globally overridden log level (if overridden)
    private static let overrideLock = Lock()
    private static var overrideLogLevel: Logger.Level? = nil

    // this holds the log level if not overridden
    private var _logLevel: Logger.Level = .info

    // metadata storage
    public var metadata: Logger.Metadata = [:]

    public init(label: String) {
        // [...]
    }

    public var logLevel: Logger.Level {
        // when we get asked for the log level, we check if it was globally overridden or not
        get {
            return LogHandlerWithGlobalLogLevelOverride.overrideLock.withLock {
                return LogHandlerWithGlobalLogLevelOverride.overrideLogLevel
            } ?? self._logLevel
        }
        // we set the log level whenever we're asked (note: this might not have an effect if globally
        // overridden)
        set {
            self._logLevel = newValue
        }
    }

    public func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?,
             file: String, function: String, line: UInt) {
        // [...]
    }

    public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
        get {
            return self.metadata[metadataKey]
        }
        set(newValue) {
            self.metadata[metadataKey] = newValue
        }
    }

    // this is the function to globally override the log level, it is not part of the `LogHandler` protocol
    public static func overrideGlobalLogLevel(_ logLevel: Logger.Level) {
        LogHandlerWithGlobalLogLevelOverride.overrideLock.withLock {
            LogHandlerWithGlobalLogLevelOverride.overrideLogLevel = logLevel
        }
    }
}

Please note that the above LogHandler will still pass the ‘log level is a value’ test above it iff the global log level has not been overridden. And most importantly it passes the requirement listed above: A change to the log level on one Logger should not affect the log level of another Logger variable.

  • This method is called when a LogHandler must emit a log message. There is no need for the LogHandler to check if the level is above or below the configured logLevel as Logger already performed this check and determined that a message should be logged.

    Declaration

    Swift

    func log(level: Logger.Level, message: Logger.Message, metadata: Logger.Metadata?, file: String, function: String, line: UInt)

    Parameters

    level

    The log level the message was logged at.

    message

    The message to log. To obtain a String representation call message.description.

    metadata

    The metadata associated to this log message.

    file

    The file the log message was emitted from.

    function

    The function the log line was emitted from.

    line

    The line the log message was emitted from.

  • Add, remove, or change the logging metadata.

    Note

    LogHandlers must treat logging metadata as a value type. This means that the change in metadata must only affect this very LogHandler.

    Declaration

    Swift

    subscript(metadataKey _: String) -> Logger.Metadata.Value? { get set }

    Parameters

    metadataKey

    The key for the metadata item

  • Get or set the entire metadata storage as a dictionary.

    Note

    LogHandlers must treat logging metadata as a value type. This means that the change in metadata must only affect this very LogHandler.

    Declaration

    Swift

    var metadata: Logger.Metadata { get set }
  • Get or set the configured log level.

    Note

    LogHandlers must treat the log level as a value type. This means that the change in metadata must only affect this very LogHandler. It is acceptable to provide some form of global log level override that means a change in log level on a particular LogHandler might not be reflected in any LogHandler.

    Declaration

    Swift

    var logLevel: Logger.Level { get set }