From 197251872a8dc73db4149e9649e78e499f7472ad Mon Sep 17 00:00:00 2001 From: Nathan Harris Date: Wed, 21 Dec 2022 22:18:05 -0600 Subject: [PATCH] Add capability to configure loggers inline --- Sources/Logging/Logging.swift | 48 +++++++++++++ Tests/LoggingTests/LoggingTest.swift | 102 +++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/Sources/Logging/Logging.swift b/Sources/Logging/Logging.swift index acc1b9a7..1b4f7555 100644 --- a/Sources/Logging/Logging.swift +++ b/Sources/Logging/Logging.swift @@ -649,6 +649,54 @@ extension Logger { #endif } +extension Logger.Metadata { + public struct MergingPolicy: Equatable { + public static var replaceValues: MergingPolicy { return .init(policy: .replaceValues) } + public static var retainExistingValues: MergingPolicy { return .init(policy: .retainExistingValues) } + + fileprivate var policy: Policy + fileprivate enum Policy { + case replaceValues + case retainExistingValues + } + + public static func ==(lhs: MergingPolicy, rhs: MergingPolicy) -> Bool { + return lhs.policy == rhs.policy + } + } +} + +extension Logger { + public func makeCopy(with metadata: Metadata, mergePolicy: Metadata.MergingPolicy = .retainExistingValues) -> Logger { + let originalMetadata = self.handler.metadata + var child = self + child.handler.metadata = originalMetadata.merging(metadata, uniquingKeysWith: { old, new in + switch mergePolicy.policy { + case .replaceValues: return new + case .retainExistingValues: return old + } + }) + return child + } + + @inlinable + public func makeCopy(_ configure: (inout Logger) -> Void) -> Logger { + var child = self + configure(&child) + return child + } + + public init(label: String, metadata: Metadata) { + self.init(label: label) + self.handler.metadata = metadata + } + + public init(label: String, _ configure: (inout Logger) -> Void) { + self.init(label: label) + configure(&self) + } +} + /// The `LoggingSystem` is a global facility where the default logging backend implementation (`LogHandler`) can be /// configured. `LoggingSystem` is set up just once in a given program to set up the desired logging backend /// implementation. diff --git a/Tests/LoggingTests/LoggingTest.swift b/Tests/LoggingTests/LoggingTest.swift index 441b6f94..09c1a547 100644 --- a/Tests/LoggingTests/LoggingTest.swift +++ b/Tests/LoggingTests/LoggingTest.swift @@ -871,6 +871,108 @@ class LoggingTest: XCTestCase { logging.history.assertExist(level: .error, message: "errorDescription") } + + func testCopyingWithFactoryMethod() { + let metadataKey = "key_under_test" + let testLogging = TestLogging() + LoggingSystem.bootstrapInternal(testLogging.make) + + var logger = Logger(label: "\(#function)") + logger.logLevel = .info + logger[metadataKey: metadataKey] = "original" + + logger.info("yes") + logger.trace("no") + + testLogging.history.assertExist(level: .info, message: "yes", metadata: [metadataKey: "original"]) + testLogging.history.assertNotExist(level: .trace, message: "no", metadata: [metadataKey: "original"]) + + let copy = logger.makeCopy { + $0.logLevel = .trace + $0[metadataKey: metadataKey] = "new" + } + copy.trace("yes") + testLogging.history.assertExist(level: .trace, message: "yes", metadata: [metadataKey: "new"]) + + // make sure we haven't changed the original logger + logger.info("no") + testLogging.history.assertNotExist(level: .info, message: "no", metadata: [metadataKey: "new"]) + } + + func testCopyingWithMetadata() { + let metadataKey = "key_under_test" + let testLogging = TestLogging() + LoggingSystem.bootstrapInternal(testLogging.make) + + var logger = Logger(label: "\(#function)") + logger.logLevel = .trace + logger[metadataKey: metadataKey] = "original" + + XCTAssertTrue(testLogging.history.entries.isEmpty) + + logger.trace("yes") + XCTAssertEqual(testLogging.history.entries.count, 1) + testLogging.history.assertExist(level: .trace, message: "yes", metadata: [metadataKey: "original"]) + + let defaultChild = logger.makeCopy(with: [metadataKey: "new"]) + defaultChild.debug("yes default") + XCTAssertEqual(testLogging.history.entries.count, 2) + testLogging.history.assertExist(level: .debug, message: "yes default", metadata: [metadataKey: "original"]) + + let retainedChild = logger.makeCopy(with: [metadataKey: "new"], mergePolicy: .retainExistingValues) + retainedChild.info("yes retain") + XCTAssertEqual(testLogging.history.entries.count, 3) + testLogging.history.assertExist(level: .info, message: "yes retain", metadata: [metadataKey: "original"]) + + let replacedChild = logger.makeCopy(with: [metadataKey: "new"], mergePolicy: .replaceValues) + replacedChild.notice("yes replace") + XCTAssertEqual(testLogging.history.entries.count, 4) + testLogging.history.assertExist(level: .notice, message: "yes replace", metadata: [metadataKey: "new"]) + + // make sure we haven't changed the original logger + logger.info("no") + testLogging.history.assertNotExist(level: .info, message: "no", metadata: [metadataKey: "new"]) + } + + func testInitWithMetadata() { + let testLogging = TestLogging() + LoggingSystem.bootstrapInternal(testLogging.make) + + let basicLogger = Logger(label: "\(#function)") + XCTAssertTrue(basicLogger.handler.metadata.isEmpty) + + let metadataLogger = Logger(label: "\(#function)", metadata: ["key_under_test": "value"]) + XCTAssertFalse(metadataLogger.handler.metadata.isEmpty) + + // make sure we haven't changed the original logger + XCTAssertTrue(basicLogger.handler.metadata.isEmpty) + } + + func testInitWithClosure() { + let testLogging = TestLogging() + LoggingSystem.bootstrapInternal(testLogging.make) + + let basicLogger = Logger(label: "\(#function)") + XCTAssertTrue(basicLogger.handler.metadata.isEmpty) + + let originalLogLevel = basicLogger.logLevel + + let closureLogger = Logger(label: "\(#function)") { + guard let newLogLevel = Logger.Level.allCases.first(where: { $0 != originalLogLevel }) else { + XCTFail("failed to find a log level for asserting conditions") + return + } + $0.logLevel = newLogLevel + $0[metadataKey: "key_under_test"] = "value" + } + XCTAssertNotEqual(closureLogger.logLevel, originalLogLevel) + XCTAssertFalse(closureLogger.handler.metadata.isEmpty) + + // make sure we haven't changed the original logger + XCTAssertTrue(basicLogger.handler.metadata.isEmpty) + XCTAssertEqual(basicLogger.logLevel, originalLogLevel) + XCTAssertTrue(basicLogger.handler.metadata.isEmpty) + } } extension Logger {