Specification

This document describes a common API for logging libraries.

The main goal is to allow libraries which write logs to receive an instance of Log and write messages to it in a simple and universal way. This ensures that different libraries that the application uses can write to the centralized application log.

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

The word implementor in this document is to be interpreted as someone implementing the Logger protocol in a log-related library or framework. Users of log are referred to as user.

Basics

  • The BedrockLog module exposes a Log type as a top level API for users.

  • The Log type exposes eight instance methods to write log messages with different severities according to RFC 5424:

    Severity Description
    emergency System is unusable
    alert Action must be taken immediately
    critical Critical conditions
    error Error conditions
    warning Warning conditions
    notice Normal but significant condition
    info Informational messages
    debug Debug-level messages
  • A call to any of eight methods delegates recording of a message to an injected implementation of Logger, which is provided by implementor.

Log

Log depends on instance of Logger and delegates him storing messages. Log provides eight method, one for each of severities. Each method takes four parameters (msg, function, file, line).

User MUST provide a value for msg, other parameters have actual defaults. User SHOULD NOT pass explicit values for function, file, line.

//
//  Copyright 2018 LitGroup, LLC
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

/// Represents a log for writing messages about an application events.
///
/// It should be used by anyone who wants to log messages about events inside some component.
public class Log {

    /// The logger which handles the log storing.
    private let logger: Logger

    /// Creates an instance.
    ///
    /// - Parameter logger: A logger which handles the log storing.
    public init(logger: Logger) {
        self.logger = logger
    }

    /// Logs a message about an application unusable state.
    ///
    /// - Parameter msg:
    ///   A message to be logged.
    ///
    /// - Parameter function:
    ///   A name of the function invoking the log API.
    ///   Defaults to the actual name of the function invoking this function.
    ///
    /// - Parameter file:
    ///   A file of the source code of the function invoking the log API.
    ///   Defaults to the file of the actual function invoking this function.
    ///
    /// - Parameter line:
    ///   A line in the source code of the function invoking the log API.
    ///   Defaults to the actual line of the actual function invoking this function.
    public func emergency(_ msg: @autoclosure () -> String,
                   function: String = #function, file: String = #file, line: UInt = #line) {
        logger.log(.emergency, message:
            Message(text: msg(), from: MessageOrigin(function: function, file: file, line: line)))
    }

    /// Logs a message about a requirement of immediate action.
    ///
    /// - Parameter msg:
    ///   A message to be logged.
    ///
    /// - Parameter function:
    ///   A name of the function invoking the log API.
    ///   Defaults to the actual name of the function invoking this function.
    ///
    /// - Parameter file:
    ///   A file of the source code of the function invoking the log API.
    ///   Defaults to the file of the actual function invoking this function.
    ///
    /// - Parameter line:
    ///   A line in the source code of the function invoking the log API.
    ///   Defaults to the actual line of the actual function invoking this function.
    public func alert(_ msg: @autoclosure () -> String,
               function: String = #function, file: String = #file, line: UInt = #line) {
        logger.log(.alert, message:
            Message(text: msg(), from: MessageOrigin(function: function, file: file, line: line)))
    }

    /// Logs a message about a critical condition of the application.
    ///
    /// - Parameter msg:
    ///   A message to be logged.
    ///
    /// - Parameter function:
    ///   A name of the function invoking the log API.
    ///   Defaults to the actual name of the function invoking this function.
    ///
    /// - Parameter file:
    ///   A file of the source code of the function invoking the log API.
    ///   Defaults to the file of the actual function invoking this function.
    ///
    /// - Parameter line:
    ///   A line in the source code of the function invoking the log API.
    ///   Defaults to the actual line of the actual function invoking this function.
    public func critical(_ msg: @autoclosure () -> String,
                  function: String = #function, file: String = #file, line: UInt = #line) {
        logger.log(.critical, message:
            Message(text: msg(), from: MessageOrigin(function: function, file: file, line: line)))
    }

    /// Logs a message about an error in the application work.
    ///
    /// - Parameter msg:
    ///   A message to be logged.
    ///
    /// - Parameter function:
    ///   A name of the function invoking the log API.
    ///   Defaults to the actual name of the function invoking this function.
    ///
    /// - Parameter file:
    ///   A file of the source code of the function invoking the log API.
    ///   Defaults to the file of the actual function invoking this function.
    ///
    /// - Parameter line:
    ///   A line in the source code of the function invoking the log API.
    ///   Defaults to the actual line of the actual function invoking this function.
    public func error(_ msg: @autoclosure () -> String,
               function: String = #function, file: String = #file, line: UInt = #line) {
        logger.log(.error, message:
            Message(text: msg(), from: MessageOrigin(function: function, file: file, line: line)))
    }

    /// Logs a warning message.
    ///
    /// - Parameter msg:
    ///   A message to be logged.
    ///
    /// - Parameter function:
    ///   A name of the function invoking the log API.
    ///   Defaults to the actual name of the function invoking this function.
    ///
    /// - Parameter file:
    ///   A file of the source code of the function invoking the log API.
    ///   Defaults to the file of the actual function invoking this function.
    ///
    /// - Parameter line:
    ///   A line in the source code of the function invoking the log API.
    ///   Defaults to the actual line of the actual function invoking this function.
    public func warning(_ msg: @autoclosure () -> String,
                 function: String = #function, file: String = #file, line: UInt = #line) {
        logger.log(.warning, message:
            Message(text: msg(), from: MessageOrigin(function: function, file: file, line: line)))
    }

    /// Logs a notice.
    ///
    /// - Parameter msg:
    ///   A message to be logged.
    ///
    /// - Parameter function:
    ///   A name of the function invoking the log API.
    ///   Defaults to the actual name of the function invoking this function.
    ///
    /// - Parameter file:
    ///   A file of the source code of the function invoking the log API.
    ///   Defaults to the file of the actual function invoking this function.
    ///
    /// - Parameter line:
    ///   A line in the source code of the function invoking the log API.
    ///   Defaults to the actual line of the actual function invoking this function.
    public func notice(_ msg: @autoclosure () -> String,
                function: String = #function, file: String = #file, line: UInt = #line) {
        logger.log(.notice, message:
            Message(text: msg(), from: MessageOrigin(function: function, file: file, line: line)))
    }

    /// Logs an informational message.
    ///
    /// - Parameter msg:
    ///   A message to be logged.
    ///
    /// - Parameter function:
    ///   A name of the function invoking the log API.
    ///   Defaults to the actual name of the function invoking this function.
    ///
    /// - Parameter file:
    ///   A file of the source code of the function invoking the log API.
    ///   Defaults to the file of the actual function invoking this function.
    ///
    /// - Parameter line:
    ///   A line in the source code of the function invoking the log API.
    ///   Defaults to the actual line of the actual function invoking this function.
    public func info(_ msg: @autoclosure () -> String,
              function: String = #function, file: String = #file, line: UInt = #line) {
        logger.log(.info, message:
            Message(text: msg(), from: MessageOrigin(function: function, file: file, line: line)))
    }

    /// Logs a debug message.
    ///
    /// - Parameter msg:
    ///   A message to be logged.
    ///
    /// - Parameter function:
    ///   A name of the function invoking the log API.
    ///   Defaults to the actual name of the function invoking this function.
    ///
    /// - Parameter file:
    ///   A file of the source code of the function invoking the log API.
    ///   Defaults to the file of the actual function invoking this function.
    ///
    /// - Parameter line:
    ///   A line in the source code of the function invoking the log API.
    ///   Defaults to the actual line of the actual function invoking this function.
    public func debug(_ msg: @autoclosure () -> String,
               function: String = #function, file: String = #file, line: UInt = #line) {
        logger.log(.debug, message:
            Message(text: msg(), from: MessageOrigin(function: function, file: file, line: line)))
    }
}

Logger

The Logger protocol describes an API of a component responsible for storing or streaming log messages.

//
//  Copyright 2018 LitGroup, LLC
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

/// A protocol of logger; logger is responsible for storing/streaming messages.
public protocol Logger: AnyObject {

    /// Writes a message with a given severity `level`.
    ///
    /// - Parameter level:
    ///   A severity level of the message.
    ///
    /// - Parameter message:
    ///   A message to be logged.
    func log(_ severity: Severity, message: @autoclosure () -> Message)
}

Logger receives a Message through closure message and severity which is associated with that message. Implementor SHOULD NOT call message() without an intention to handle a message which will be returned.

Severity

//
//  Copyright 2018 LitGroup, LLC
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

/// A severity of an event which log message is about.
///
/// Eight severity levels are used according to [RFC 5424](https://tools.ietf.org/html/rfc5424).
public enum Severity: Int {

    /// System is unusable.
    case emergency

    /// Action must be taken immediately.
    case alert

    /// Critical condition.
    case critical

    /// Error condition.
    case error

    /// Warning condition.
    case warning

    /// Normal but significant condition.
    case notice

    /// Informational message.
    case info

    /// Debug-level message.
    case debug
}

extension Severity: Comparable {

    /// Returns `true` if a `lhs` severity is less than `rhs`.
    ///
    /// - Example:
    ///   ```
    ///   Severity.alert < .emergency // => true
    ///   ```
    public static func <(lhs: Severity, rhs: Severity) -> Bool {
        return lhs.rawValue > rhs.rawValue
    }
}

Severity of event which a log message is about determines a priority of the event. Severity is represented as an Int based enum. It defines eight cases with rawValue according to RFC 5424.

Severity conforms to the Comparable protocol. Operators ==, <, >, <=, >= MUST be used for comparison of two severities.

Message

Message contains a text of message, timestamp, and origin of the message.

Implementor MUST convert a message to required representation by call converted(to:with:) with appropriate MessageFormat and a Severity sent to Logger with the message.

//
//  Copyright 2018 LitGroup, LLC
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

import Foundation

/// Represents a loggable message.
public struct Message {

    /// The text of the message.
    private let text: String

    /// Date and time this message was created.
    private let timestamp = Date()

    /// The origin of the message.
    private let origin: MessageOrigin
    
    /// Creates `self`.
    ///
    /// - Parameter text:
    ///   A text of the message.
    ///
    /// - Parameter origin:
    ///   An origin of the message.
    public init(text: String, from origin: MessageOrigin) {
        assert(!text.isEmpty, "Log message text must not be empty (logged at \(origin.file):\(origin.line).")
        
        self.text = text
        self.origin = origin
    }
    
    /// Returns representation of the message in the specified format.
    ///
    /// - Parameter format:
    ///   An implementation of `MessageFormat` to be applied to the message.
    ///
    /// - Parameter severity:
    ///   A severity assigned to the message.
    public func converted<F: MessageFormat>(to format: F, with severity: Severity) -> F.FormattedMessage {
        return format.appliedTo(severity: severity, timestamp: timestamp, text: text, origin: origin)
    }
}

MessageOrigin

//
//  Copyright 2018 LitGroup, LLC
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

/// Represents a place in the source code which a message originated from.
public struct MessageOrigin {
    
    /// The name of function which a message originated from.
    public let function: String
    
    /// The name of file which a message originated from.
    public let file: String
    
    /// The number of line in the file which a message originated from.
    public let line: UInt
    
    /// Creates `self`.
    ///
    /// - Parameter function:
    ///   A name of function which a message originated from.
    ///
    /// - Parameter file:
    ///   A name of file which a message originated from.
    ///
    /// - Parameter line:
    ///   A number of line in the file which a message originated from.
    public init(function: String, file: String, line: UInt) {
        assert(line > 0, "Line numeration starts from 1.")
        assert(!function.isEmpty, "Name of the function where message was logged must not be empty.")
        assert(!file.isEmpty, "Name of the file where the message was logged must not be empty.")
        
        self.function = function
        self.file = file
        self.line = line
    }
}

MessageFormat

//
//  Copyright 2018 LitGroup, LLC
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

import Foundation

/// Represents a format of message.
///
/// Implementation of logger must apply specialized format to messages acceptable for storing.
public protocol MessageFormat {

    /// A type of formatted representation of the message.
    associatedtype FormattedMessage

    /// Returns formatted representation of a log message with a given data.
    func appliedTo(severity: Severity, timestamp: Date, text: String, origin: MessageOrigin) -> FormattedMessage
}