Skip to content

For reference: LoggerWithSource #143

Closed
@ktoso

Description

@ktoso

PR #135 introduces a source parameter that defaults to the module in which a log statement was made.
See there for a long discussion why that matters -- long story short: it enables us to share a logger instance with an un-changing label, yet still keep the "this was logged from sub-component X (the module)".

We also considered adding a LoggerWithSource back then, however we decided that there are few use-cases about it today and we want to take it slow adding API. This ticket is to collect interest if this type should also ship with the swift-log library or not necessarily, as we learn about usage patterns.

The LoggerWithSource allows for overriding with a hardcoded source e.g. "thread-pool-x" or something the source of the log message. We concluded however that in most situations such things can be handled with metadata. If we see that overriding a source becomes

//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Logging API open source project
//
// Copyright (c) 2020 Apple Inc. and the Swift Logging API project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of Swift Logging API project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

/// `LoggerWithSource` shares the same API as `Logger`, except that it automatically parses on the supplies `source`
/// instead of requiring the user to supply source when logging a message.
///
/// - info: Do not accept or pass `LoggerWithSource` to/from other modules. The type you use publicly should always be
///         `Logger`.
public struct LoggerWithSource {
    /// The `Logger` we are logging with.
    public var logger: Logger

    /// The source information we are supplying to `Logger`.
    public var source: String

    /// Construct a `LoggerWithSource` logging with `logger` and `source`.
    @inlinable
    public init(_ logger: Logger, source: String) {
        self.logger = logger
        self.source = source
    }
}

extension LoggerWithSource {
    /// Log a message passing the log level as a parameter.
    ///
    /// If the `logLevel` passed to this method is more severe than the `Logger`'s `logLevel`, it will be logged,
    /// otherwise nothing will happen. The `source` is the one supplied to the initializer of `LoggerWithSource`.
    ///
    /// - parameters:
    ///    - level: The log level to log `message` at. For the available log levels, see `Logger.Level`.
    ///    - message: The message to be logged. `message` can be used with any string interpolation literal.
    ///    - metadata: One-off metadata to attach to this log message
    ///    - file: The file this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#file`).
    ///    - function: The function this log message originates from (there's usually no need to pass it explicitly as
    ///                it defaults to `#function`).
    ///    - line: The line this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#line`).
    @inlinable
    public func log(level: Logger.Level,
                    _ message: @autoclosure () -> Logger.Message,
                    metadata: @autoclosure () -> Logger.Metadata? = nil,
                    file: String = #file, function: String = #function, line: UInt = #line) {
        self.logger.log(level: level,
                        message(),
                        metadata: metadata(),
                        source: self.source,
                        file: file, function: function, line: line)
    }

    /// Add, change, or remove a logging metadata item.
    ///
    /// The `source` is the one supplied to the initializer of `LoggerWithSource`.
    ///
    /// - note: Logging metadata behaves as a value that means a change to the logging metadata will only affect the
    ///         very `Logger` it was changed on.
    @inlinable
    public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? {
        get {
            return self.logger[metadataKey: metadataKey]
        }
        set {
            self.logger[metadataKey: metadataKey] = newValue
        }
    }

    /// Get or set the log level configured for this `Logger`.
    ///
    ///  The `source` is the one supplied to the initializer of `LoggerWithSource`.
    ///
    /// - note: `Logger`s treat `logLevel` as a value. This means that a change in `logLevel` will only affect this
    ///         very `Logger`. It it acceptable for logging backends to have some form of global log level override
    ///         that affects multiple or even all loggers. This means a change in `logLevel` to one `Logger` might in
    ///         certain cases have no effect.
    @inlinable
    public var logLevel: Logger.Level {
        get {
            return self.logger.logLevel
        }
        set {
            self.logger.logLevel = newValue
        }
    }
}

extension LoggerWithSource {
    /// Log a message passing with the `Logger.Level.trace` log level.
    ///
    /// The `source` is the one supplied to the initializer of `LoggerWithSource`.
    ///
    /// If `.trace` is at least as severe as the `Logger`'s `logLevel`, it will be logged,
    /// otherwise nothing will happen.
    ///
    /// - parameters:
    ///    - message: The message to be logged. `message` can be used with any string interpolation literal.
    ///    - metadata: One-off metadata to attach to this log message
    ///    - file: The file this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#file`).
    ///    - function: The function this log message originates from (there's usually no need to pass it explicitly as
    ///                it defaults to `#function`).
    ///    - line: The line this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#line`).
    @inlinable
    public func trace(_ message: @autoclosure () -> Logger.Message,
                      metadata: @autoclosure () -> Logger.Metadata? = nil,
                      file: String = #file, function: String = #function, line: UInt = #line) {
        self.logger.trace(message(),
                          metadata: metadata(),
                          source: self.source,
                          file: file,
                          function: function,
                          line: line)
    }

    /// Log a message passing with the `Logger.Level.debug` log level.
    ///
    /// The `source` is the one supplied to the initializer of `LoggerWithSource`.
    ///
    /// If `.debug` is at least as severe as the `Logger`'s `logLevel`, it will be logged,
    /// otherwise nothing will happen.
    ///
    /// - parameters:
    ///    - message: The message to be logged. `message` can be used with any string interpolation literal.
    ///    - metadata: One-off metadata to attach to this log message
    ///    - file: The file this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#file`).
    ///    - function: The function this log message originates from (there's usually no need to pass it explicitly as
    ///                it defaults to `#function`).
    ///    - line: The line this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#line`).
    @inlinable
    public func debug(_ message: @autoclosure () -> Logger.Message,
                      metadata: @autoclosure () -> Logger.Metadata? = nil,
                      file: String = #file, function: String = #function, line: UInt = #line) {
        self.logger.debug(message(),
                          metadata: metadata(),
                          source: self.source,
                          file: file,
                          function: function,
                          line: line)
    }

    /// Log a message passing with the `Logger.Level.info` log level.
    ///
    /// The `source` is the one supplied to the initializer of `LoggerWithSource`.
    ///
    /// If `.info` is at least as severe as the `Logger`'s `logLevel`, it will be logged,
    /// otherwise nothing will happen.
    ///
    /// - parameters:
    ///    - message: The message to be logged. `message` can be used with any string interpolation literal.
    ///    - metadata: One-off metadata to attach to this log message
    ///    - file: The file this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#file`).
    ///    - function: The function this log message originates from (there's usually no need to pass it explicitly as
    ///                it defaults to `#function`).
    ///    - line: The line this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#line`).
    @inlinable
    public func info(_ message: @autoclosure () -> Logger.Message,
                     metadata: @autoclosure () -> Logger.Metadata? = nil,
                     file: String = #file, function: String = #function, line: UInt = #line) {
        self.logger.info(message(),
                         metadata: metadata(),
                         source: self.source,
                         file: file,
                         function: function,
                         line: line)
    }

    /// Log a message passing with the `Logger.Level.notice` log level.
    ///
    /// The `source` is the one supplied to the initializer of `LoggerWithSource`.
    ///
    /// If `.notice` is at least as severe as the `Logger`'s `logLevel`, it will be logged,
    /// otherwise nothing will happen.
    ///
    /// - parameters:
    ///    - message: The message to be logged. `message` can be used with any string interpolation literal.
    ///    - metadata: One-off metadata to attach to this log message
    ///    - file: The file this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#file`).
    ///    - function: The function this log message originates from (there's usually no need to pass it explicitly as
    ///                it defaults to `#function`).
    ///    - line: The line this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#line`).
    @inlinable
    public func notice(_ message: @autoclosure () -> Logger.Message,
                       metadata: @autoclosure () -> Logger.Metadata? = nil,
                       file: String = #file, function: String = #function, line: UInt = #line) {
        self.logger.notice(message(),
                           metadata: metadata(),
                           source: self.source,
                           file: file,
                           function: function,
                           line: line)
    }

    /// Log a message passing with the `Logger.Level.warning` log level.
    ///
    /// The `source` is the one supplied to the initializer of `LoggerWithSource`.
    ///
    /// If `.warning` is at least as severe as the `Logger`'s `logLevel`, it will be logged,
    /// otherwise nothing will happen.
    ///
    /// - parameters:
    ///    - message: The message to be logged. `message` can be used with any string interpolation literal.
    ///    - metadata: One-off metadata to attach to this log message
    ///    - file: The file this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#file`).
    ///    - function: The function this log message originates from (there's usually no need to pass it explicitly as
    ///                it defaults to `#function`).
    ///    - line: The line this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#line`).
    @inlinable
    public func warning(_ message: @autoclosure () -> Logger.Message,
                        metadata: @autoclosure () -> Logger.Metadata? = nil,
                        file: String = #file, function: String = #function, line: UInt = #line) {
        self.logger.warning(message(),
                            metadata: metadata(),
                            source: self.source,
                            file: file,
                            function: function,
                            line: line)
    }

    /// Log a message passing with the `Logger.Level.error` log level.
    ///
    /// The `source` is the one supplied to the initializer of `LoggerWithSource`.
    ///
    /// If `.error` is at least as severe as the `Logger`'s `logLevel`, it will be logged,
    /// otherwise nothing will happen.
    ///
    /// - parameters:
    ///    - message: The message to be logged. `message` can be used with any string interpolation literal.
    ///    - metadata: One-off metadata to attach to this log message
    ///    - file: The file this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#file`).
    ///    - function: The function this log message originates from (there's usually no need to pass it explicitly as
    ///                it defaults to `#function`).
    ///    - line: The line this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#line`).
    @inlinable
    public func error(_ message: @autoclosure () -> Logger.Message,
                      metadata: @autoclosure () -> Logger.Metadata? = nil,
                      file: String = #file, function: String = #function, line: UInt = #line) {
        self.logger.error(message(),
                          metadata: metadata(),
                          source: self.source,
                          file: file,
                          function: function,
                          line: line)
    }

    /// Log a message passing with the `Logger.Level.critical` log level.
    ///
    /// The `source` is the one supplied to the initializer of `LoggerWithSource`.
    ///
    /// `.critical` messages will always be logged.
    ///
    /// - parameters:
    ///    - message: The message to be logged. `message` can be used with any string interpolation literal.
    ///    - metadata: One-off metadata to attach to this log message
    ///    - file: The file this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#file`).
    ///    - function: The function this log message originates from (there's usually no need to pass it explicitly as
    ///                it defaults to `#function`).
    ///    - line: The line this log message originates from (there's usually no need to pass it explicitly as it
    ///            defaults to `#line`).
    @inlinable
    public func critical(_ message: @autoclosure () -> Logger.Message,
                         metadata: @autoclosure () -> Logger.Metadata? = nil,
                         file: String = #file, function: String = #function, line: UInt = #line) {
        self.logger.critical(message(),
                             metadata: metadata(),
                             source: self.source,
                             file: file,
                             function: function,
                             line: line)
    }
}
    func testAllLogLevelsWorkWithOldSchoolLogHandlerButSourceIsNotPropagated() {
        let testLogging = OldSchoolTestLogging()

        var logger = LoggerWithSource(Logger(label: "\(#function)",
                                             factory: testLogging.make),
                                      source: "my-fancy-source")
        logger.logLevel = .trace

        logger.trace("yes: trace")
        logger.debug("yes: debug")
        logger.info("yes: info")
        logger.notice("yes: notice")
        logger.warning("yes: warning")
        logger.error("yes: error")
        logger.critical("yes: critical")

        // Please note that the source is _not_ propagated (because the backend doesn't support it).
        testLogging.history.assertExist(level: .trace, message: "yes: trace", source: "no source")
        testLogging.history.assertExist(level: .debug, message: "yes: debug", source: "no source")
        testLogging.history.assertExist(level: .info, message: "yes: info", source: "no source")
        testLogging.history.assertExist(level: .notice, message: "yes: notice", source: "no source")
        testLogging.history.assertExist(level: .warning, message: "yes: warning", source: "no source")
        testLogging.history.assertExist(level: .error, message: "yes: error", source: "no source")
        testLogging.history.assertExist(level: .critical, message: "yes: critical", source: "no source")
    }

    func testAllLogLevelsWorkOnLoggerWithSource() {
        let testLogging = TestLogging()
        LoggingSystem.bootstrapInternal(testLogging.make)

        var logger = LoggerWithSource(Logger(label: "\(#function)"), source: "my-fancy-source")
        logger.logLevel = .trace

        logger.trace("yes: trace")
        logger.debug("yes: debug")
        logger.info("yes: info")
        logger.notice("yes: notice")
        logger.warning("yes: warning")
        logger.error("yes: error")
        logger.critical("yes: critical")

        testLogging.history.assertExist(level: .trace, message: "yes: trace", source: "my-fancy-source")
        testLogging.history.assertExist(level: .debug, message: "yes: debug", source: "my-fancy-source")
        testLogging.history.assertExist(level: .info, message: "yes: info", source: "my-fancy-source")
        testLogging.history.assertExist(level: .notice, message: "yes: notice", source: "my-fancy-source")
        testLogging.history.assertExist(level: .warning, message: "yes: warning", source: "my-fancy-source")
        testLogging.history.assertExist(level: .error, message: "yes: error", source: "my-fancy-source")
        testLogging.history.assertExist(level: .critical, message: "yes: critical", source: "my-fancy-source")
    }

    func testLoggerWithSource() {
        let testLogging = TestLogging()
        LoggingSystem.bootstrapInternal(testLogging.make)

        var logger = Logger(label: "\(#function)").withSource("source")
        logger.logLevel = .trace

        logger.critical("yes: critical")

        testLogging.history.assertExist(level: .critical, message: "yes: critical", source: "source")
    }

snippets above are from the impl by @weissi.

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/enhancementImprovements to existing feature.kind/supportAdopter support requests.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions