Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -199,4 +199,3 @@ The `View` is available within `SwiftLoggerSampleApp`.
## License

`Logger` is released under the [MIT License](LICENSE).

11 changes: 8 additions & 3 deletions Sources/Logger/LoggerManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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) }
}
}

Expand Down
1 change: 1 addition & 0 deletions Sources/Logger/Loggers/FileLogger/FileLogger.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public class FileLogger: Logging {
}

public var levels: [Level] = [.info]
public let isAsynchronous: Bool = true

// MARK: - Initializers

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public struct OSEntryLog: Equatable {
}
}

extension OSLogEntry: Identifiable {
extension OSLogEntry: @retroactive Identifiable {
public var id: String {
UUID().uuidString
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/Logger/Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import Foundation

public protocol Logging {
var levels: [Level] { get set }
var isAsynchronous: Bool { get }

func configure()
func log(_: LogEntry)
}

extension Logging {
public func configure() {}
public var isAsynchronous: Bool { false }

func doesLog(forLevel level: Level) -> Bool {
levels.contains(level)
Expand Down
70 changes: 69 additions & 1 deletion Tests/LoggerTests/LoggerManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnyCancellable>()
let expectation = self.expectation(description: "")
var logCount = 0
Expand Down Expand Up @@ -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()
}
}
Loading