diff --git a/README.md b/README.md index d1bf138..198e0bd 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ Log("This is the message to be logged.", onLevel: .info) ### Logging execution -`LoggerManager` handles execution of logging tasks in an asynchronous serial manner. Each logging task is dispatched asynchronously on a custom serial background queue, where all loggers perform their tasks serially one by one. +Each logger can choose whether it runs asynchronously via `isAsynchronous`. `LoggerManager` invokes synchronous loggers immediately on the caller's thread, which makes console output visible right away when debugging with breakpoints or when the app terminates unexpectedly. Asynchronous loggers are dispatched on a shared serial background queue. `FileLogger` uses asynchronous execution by default, while the built-in non-file loggers stay synchronous by default. ![asyncserial](https://user-images.githubusercontent.com/2511209/33495945-a2732168-d6c8-11e7-9a77-519204be448a.png) @@ -199,4 +199,3 @@ The `View` is available within `SwiftLoggerSampleApp`. ## License `Logger` is released under the [MIT License](LICENSE). - diff --git a/Sources/Logger/LoggerManager.swift b/Sources/Logger/LoggerManager.swift index fece210..caca460 100644 --- a/Sources/Logger/LoggerManager.swift +++ b/Sources/Logger/LoggerManager.swift @@ -15,8 +15,7 @@ public typealias MetaInformationBundle = (types: [MetaInformationType], bundle: /// Each of these logger classes must be subclassed from BaseLogger. The class handles logging to registered loggers /// based on levels they are set to acccept. public class LoggerManager { - // Configuration of logging mode - // SerialQueue: special DispatchQueue that is on background and uses serial configuration, that means logs will go one after each other + // Async loggers are processed serially on a background queue. let serialQueue: DispatchQueue = .defaultSerialLoggingQueue // Registered loggers private let loggers: [Logging] @@ -78,8 +77,14 @@ public class LoggerManager { logMetaInformation() } + let synchronousLoggers = availableLoggers.filter { !$0.isAsynchronous } + synchronousLoggers.forEach { $0.log(log) } + + let asynchronousLoggers = availableLoggers.filter(\.isAsynchronous) + guard !asynchronousLoggers.isEmpty else { return } + serialQueue.async { - availableLoggers.forEach { $0.log(log) } + asynchronousLoggers.forEach { $0.log(log) } } } diff --git a/Sources/Logger/Loggers/FileLogger/FileLogger.swift b/Sources/Logger/Loggers/FileLogger/FileLogger.swift index b85dccd..3753e1c 100644 --- a/Sources/Logger/Loggers/FileLogger/FileLogger.swift +++ b/Sources/Logger/Loggers/FileLogger/FileLogger.swift @@ -83,6 +83,7 @@ public class FileLogger: Logging { } public var levels: [Level] = [.info] + public let isAsynchronous: Bool = true // MARK: - Initializers diff --git a/Sources/Logger/Loggers/NativeLogger/OSLogStore/OSLogEntry.swift b/Sources/Logger/Loggers/NativeLogger/OSLogStore/OSLogEntry.swift index fa3ad2f..414176c 100644 --- a/Sources/Logger/Loggers/NativeLogger/OSLogStore/OSLogEntry.swift +++ b/Sources/Logger/Loggers/NativeLogger/OSLogStore/OSLogEntry.swift @@ -32,7 +32,7 @@ public struct OSEntryLog: Equatable { } } -extension OSLogEntry: Identifiable { +extension OSLogEntry: @retroactive Identifiable { public var id: String { UUID().uuidString } diff --git a/Sources/Logger/Logging.swift b/Sources/Logger/Logging.swift index 1b4fdc0..3e82cb9 100644 --- a/Sources/Logger/Logging.swift +++ b/Sources/Logger/Logging.swift @@ -9,6 +9,7 @@ import Foundation public protocol Logging { var levels: [Level] { get set } + var isAsynchronous: Bool { get } func configure() func log(_: LogEntry) @@ -16,6 +17,7 @@ public protocol Logging { extension Logging { public func configure() {} + public var isAsynchronous: Bool { false } func doesLog(forLevel level: Level) -> Bool { levels.contains(level) diff --git a/Tests/LoggerTests/LoggerManagerTests.swift b/Tests/LoggerTests/LoggerManagerTests.swift index 7e1266a..47a363b 100644 --- a/Tests/LoggerTests/LoggerManagerTests.swift +++ b/Tests/LoggerTests/LoggerManagerTests.swift @@ -10,8 +10,47 @@ import XCTest import Combine class LoggerManagerTests: XCTestCase { + func test_log_delivers_to_synchronous_logger_immediately() { + let logger = TestLogger(isAsynchronous: false) + let loggerManager = LoggerManager( + loggers: [logger], + applicationCallbackLoggerBundle: nil, + metaInformationLoggerBundle: nil + ) + + loggerManager.log("message", onLevel: .info) + + XCTAssertEqual(logger.loggedMessages, ["message"]) + } + + func test_log_does_not_block_on_asynchronous_logger() { + let didLog = expectation(description: "async logger called") + let shouldFinishLogging = DispatchSemaphore(value: 0) + let logger = BlockingLogger( + isAsynchronous: true, + onLog: { + shouldFinishLogging.wait() + didLog.fulfill() + } + ) + let loggerManager = LoggerManager( + loggers: [logger], + applicationCallbackLoggerBundle: nil, + metaInformationLoggerBundle: nil + ) + + loggerManager.log("message", onLevel: .info) + shouldFinishLogging.signal() + + waitForExpectations(timeout: 0.5) + } + func test_loggerManager_multithreading_delete_and_log_simultaneously() throws { - let loggerManager = LoggerManager(loggers: .init()) + let loggerManager = LoggerManager( + loggers: .init(), + applicationCallbackLoggerBundle: nil, + metaInformationLoggerBundle: nil + ) var cancellables = Set() let expectation = self.expectation(description: "") var logCount = 0 @@ -63,3 +102,32 @@ class LoggerManagerTests: XCTestCase { XCTAssertEqual(deleteCount, 50) } } + +private final class TestLogger: Logging { + let isAsynchronous: Bool + var levels: [Level] = Level.allCases + var loggedMessages: [String] = [] + + init(isAsynchronous: Bool) { + self.isAsynchronous = isAsynchronous + } + + func log(_ logEntry: LogEntry) { + loggedMessages.append(logEntry.message.description) + } +} + +private final class BlockingLogger: Logging { + let isAsynchronous: Bool + var levels: [Level] = Level.allCases + private let onLog: () -> Void + + init(isAsynchronous: Bool, onLog: @escaping () -> Void) { + self.isAsynchronous = isAsynchronous + self.onLog = onLog + } + + func log(_ logEntry: LogEntry) { + onLog() + } +}