Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

For reference: LoggerWithSource #143

Open
ktoso opened this issue Jul 6, 2020 · 2 comments
Open

For reference: LoggerWithSource #143

ktoso opened this issue Jul 6, 2020 · 2 comments
Labels
kind/enhancement Improvements to existing feature. kind/support Adopter support requests.

Comments

@ktoso
Copy link
Member

ktoso commented Jul 6, 2020

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.

@ktoso ktoso added the kind/enhancement Improvements to existing feature. label Jul 6, 2020
@ktoso ktoso added kind/enhancement Improvements to existing feature. kind/support Adopter support requests. and removed kind/enhancement Improvements to existing feature. labels Jul 6, 2020
@glbrntt
Copy link
Contributor

glbrntt commented Aug 10, 2020

Hmm this would be useful in frameworks where they'd like to set a consistent source (i.e. module name) and have the issue highlighted in #145 (namely that you get the folder containing the source file rather than the module). However, I don't think that's reason to add LoggerWithSource, I think it's more of a reason to resolve #145.

@glbrntt
Copy link
Contributor

glbrntt commented Aug 10, 2020

I was curious though – and maybe this is the wrong place to ask – why source can't just be set on Logger (i.e. as a default value which could be overridden by specifying it in one of info, debug etc.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/enhancement Improvements to existing feature. kind/support Adopter support requests.
Projects
None yet
Development

No branches or pull requests

2 participants