Description
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.