Skip to content

Commit

Permalink
Expose an API for configuring basic stdio log handling (#61)
Browse files Browse the repository at this point in the history
Expose `StreamLogHandler` to the public which replaces the previously internal `StdoutLogHandlers`. Users can choose now the output stream, while `stdout` is the default.

### Motivation:

As discussed on #51 and #60

### Modifications:

Rename `StdoutLogHandler` to `StreamLogHandler` and provide 2 factory methods `standardOutput(label:)` and `standardError(label:)` for using during bootstrapping.

### Result:

Users who adopt other logging backends during `LoggingSystem` bootstrapping will still be able to use the default log handler to get very basic console output to their choice of `stdout` or `stderr`.

### Example:
```
// Log to both stderr and stdout for good measure
LoggingSystem.bootstrap { 
    MultiplexLogHandler([
        FancyLoggingBackend(label: $0),
        StreamLogHandler.standardError(label: $0), 
        StreamLogHandler.standardOutput(label: $0)
    ])
}
```
  • Loading branch information
allenhumphreys authored and weissi committed May 1, 2019
1 parent 67b2425 commit ea3eea9
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 38 deletions.
9 changes: 9 additions & 0 deletions README.md
Expand Up @@ -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?
Expand Down
96 changes: 60 additions & 36 deletions Sources/Logging/Logging.swift
Expand Up @@ -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
Expand Down Expand Up @@ -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<FILE>

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,
Expand All @@ -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? {
Expand Down
1 change: 1 addition & 0 deletions Tests/LoggingTests/LoggingTest+XCTest.swift
Expand Up @@ -41,6 +41,7 @@ extension LoggingTest {
("testLoggerWithGlobalOverride", testLoggerWithGlobalOverride),
("testLogLevelCases", testLogLevelCases),
("testLogLevelOrdering", testLogLevelOrdering),
("testStreamLogHandlerWritesToAStream", testStreamLogHandlerWritesToAStream),
]
}
}
29 changes: 28 additions & 1 deletion Tests/LoggingTests/LoggingTest.swift
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion Tests/LoggingTests/TestLogger.swift
Expand Up @@ -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
}

Expand Down

0 comments on commit ea3eea9

Please sign in to comment.