diff --git a/README.md b/README.md index 2c9e7def..2e4a0988 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,15 @@ logger.info("Hello World!") 2019-03-13T15:46:38+0000 info: Hello World! ``` +#### Default `Logger` behavior + +`SwiftLog` provides for very basic console logging out-of-the-box by way of `StreamLogHandler`. It is possible to switch the default output to `stderr` like so: +``` +LoggingSystem.bootstrap(StreamLogHandler.standardError) +``` + +`StreamLogHandler` is primarily a convenience only and does not provide any substantial customization. Library maintainers who aim to build their own logging backends for integration and consumption should implement the `LogHandler` protocol directly as laid out in the "On the implementation of a logging backend" section. + For further information, please check the [API documentation][api-docs]. ## What is an API package? diff --git a/Sources/Logging/Logging.swift b/Sources/Logging/Logging.swift index 937d4527..b3fd8a49 100644 --- a/Sources/Logging/Logging.swift +++ b/Sources/Logging/Logging.swift @@ -258,7 +258,7 @@ extension Logger { /// implementation. public enum LoggingSystem { fileprivate static let lock = ReadWriteLock() - fileprivate static var factory: (String) -> LogHandler = StdoutLogHandler.init + fileprivate static var factory: (String) -> LogHandler = StreamLogHandler.standardOutput fileprivate static var initialized = false /// `bootstrap` is a one-time configuration function which globally selects the desired logging backend @@ -505,30 +505,72 @@ public struct MultiplexLogHandler: LogHandler { } } -/// Ships with the logging module, really boring just prints something using the `print` function -internal struct StdoutLogHandler: LogHandler { - private let lock = Lock() +/// A wrapper to facilitate `print`-ing to stderr and stdio that +/// ensures access to the underlying `FILE` is locked to prevent +/// cross-thread interleaving of output. +internal struct StdioOutputStream: TextOutputStream { + internal let file: UnsafeMutablePointer + + internal func write(_ string: String) { + string.withCString { ptr in + flockfile(file) + defer { + funlockfile(file) + } + _ = fputs(ptr, file) + } + } + + internal static let stderr = StdioOutputStream(file: systemStderr) + internal static let stdout = StdioOutputStream(file: systemStdout) +} - public init(label: String) {} +// Prevent name clashes +#if os(macOS) || os(tvOS) || os(iOS) || os(watchOS) +let systemStderr = Darwin.stderr +let systemStdout = Darwin.stdout +#else +let systemStderr = Glibc.stderr! +let systemStdout = Glibc.stdout! +#endif - private var _logLevel: Logger.Level = .info +/// `StreamLogHandler` is a simple implementation of `LogHandler` for directing +/// `Logger` output to either `stderr` or `stdout` via the factory methods. +public struct StreamLogHandler: LogHandler { - public var logLevel: Logger.Level { + /// Factory that makes a `StreamLogHandler` to directs its output to `stdout` + public static func standardOutput(label: String) -> StreamLogHandler { + return StreamLogHandler(label: label, stream: StdioOutputStream.stdout) + } + + /// Factory that makes a `StreamLogHandler` to directs its output to `stderr` + public static func standardError(label: String) -> StreamLogHandler { + return StreamLogHandler(label: label, stream: StdioOutputStream.stderr) + } + + private let stream: TextOutputStream + + public var logLevel: Logger.Level = .info + + private var prettyMetadata: String? + public var metadata = Logger.Metadata() { + didSet { + prettyMetadata = prettify(metadata) + } + } + + public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { get { - return self.lock.withLock { self._logLevel } + return metadata[metadataKey] } set { - self.lock.withLock { - self._logLevel = newValue - } + metadata[metadataKey] = newValue } } - private var prettyMetadata: String? - private var _metadata = Logger.Metadata() { - didSet { - self.prettyMetadata = self.prettify(self._metadata) - } + // internal for testing only + internal init(label: String, stream: TextOutputStream) { + self.stream = stream } public func log(level: Logger.Level, @@ -538,27 +580,9 @@ internal struct StdoutLogHandler: LogHandler { let prettyMetadata = metadata?.isEmpty ?? true ? self.prettyMetadata : self.prettify(self.metadata.merging(metadata!, uniquingKeysWith: { _, new in new })) - print("\(self.timestamp()) \(level):\(prettyMetadata.map { " \($0)" } ?? "") \(message)") - } - public var metadata: Logger.Metadata { - get { - return self.lock.withLock { self._metadata } - } - set { - self.lock.withLock { self._metadata = newValue } - } - } - - public subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { - get { - return self.lock.withLock { self._metadata[metadataKey] } - } - set { - self.lock.withLock { - self._metadata[metadataKey] = newValue - } - } + var stream = self.stream + stream.write("\(timestamp()) \(level):\(prettyMetadata.map { " \($0)" } ?? "") \(message)\n") } private func prettify(_ metadata: Logger.Metadata) -> String? { diff --git a/Tests/LoggingTests/LoggingTest+XCTest.swift b/Tests/LoggingTests/LoggingTest+XCTest.swift index 8cb08ac3..1a4ee52d 100644 --- a/Tests/LoggingTests/LoggingTest+XCTest.swift +++ b/Tests/LoggingTests/LoggingTest+XCTest.swift @@ -41,6 +41,7 @@ extension LoggingTest { ("testLoggerWithGlobalOverride", testLoggerWithGlobalOverride), ("testLogLevelCases", testLogLevelCases), ("testLogLevelOrdering", testLogLevelOrdering), + ("testStreamLogHandlerWritesToAStream", testStreamLogHandlerWritesToAStream), ] } } diff --git a/Tests/LoggingTests/LoggingTest.swift b/Tests/LoggingTests/LoggingTest.swift index 8494e07c..e5cdd4bb 100644 --- a/Tests/LoggingTests/LoggingTest.swift +++ b/Tests/LoggingTests/LoggingTest.swift @@ -284,7 +284,7 @@ class LoggingTest: XCTestCase { } func testMultiplexerIsValue() { - let multi = MultiplexLogHandler([StdoutLogHandler(label: "x"), StdoutLogHandler(label: "y")]) + let multi = MultiplexLogHandler([StreamLogHandler.standardOutput(label: "x"), StreamLogHandler.standardOutput(label: "y")]) LoggingSystem.bootstrapInternal { _ in print("new multi") return multi @@ -417,4 +417,31 @@ class LoggingTest: XCTestCase { XCTAssertLessThan(Logger.Level.warning, Logger.Level.critical) XCTAssertLessThan(Logger.Level.error, Logger.Level.critical) } + + final class InterceptStream: TextOutputStream { + var interceptedText: String? + var strings = [String]() + + func write(_ string: String) { + // This is a test implementation, a real implementation would include locking + strings.append(string) + interceptedText = (interceptedText ?? "") + string + } + } + + func testStreamLogHandlerWritesToAStream() { + let interceptStream = InterceptStream() + LoggingSystem.bootstrapInternal { _ in + StreamLogHandler(label: "test", stream: interceptStream) + } + let log = Logger(label: "test") + + let testString = "my message is better than yours" + log.critical("\(testString)") + + let messageSucceeded = interceptStream.interceptedText?.trimmingCharacters(in: .whitespacesAndNewlines).hasSuffix(testString) + + XCTAssertTrue(messageSucceeded ?? false) + XCTAssertEqual(interceptStream.strings.count, 1) + } } diff --git a/Tests/LoggingTests/TestLogger.swift b/Tests/LoggingTests/TestLogger.swift index 8a9cde9b..469a8c83 100644 --- a/Tests/LoggingTests/TestLogger.swift +++ b/Tests/LoggingTests/TestLogger.swift @@ -39,7 +39,7 @@ internal struct TestLogHandler: LogHandler { self.label = label self.config = config self.recorder = recorder - self.logger = Logger(label: "test", StdoutLogHandler(label: label)) + self.logger = Logger(label: "test", StreamLogHandler.standardOutput(label: label)) self.logger.logLevel = .debug }