-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The aim of this commit is to: - provide infrastructure for internal components to be able to log messages - add some log messages to the existing functionality (I am not particularly concerned by the quality or utility of these log messages at the moment) - provide a basic default logging backend so that users can see log messages in the console The choice to have a separate internal logging interface is taken from Asset Tracking and ably-cocoa, where I think it works well. I haven’t currently copied across Asset Tracking’s subsystem mechanism, a large part of the motivation for which was to provide a way to integrate the logs emitted by the AAT-instantiated Realtime client into those emitted by AAT itself; this is not currently a goal for Chat, which does not instantiate a Realtime client. I briefly toyed with the idea of writing a macro that would log the inputs and outputs of public API methods, but figuring out how to do that didn’t seem like a good use of time now. Resolves #38.
- Loading branch information
1 parent
3b7a54e
commit 548468d
Showing
13 changed files
with
241 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,92 @@ | ||
import os | ||
|
||
public typealias LogContext = [String: any Sendable] | ||
|
||
public protocol LogHandler: AnyObject, Sendable { | ||
func log(message: String, level: LogLevel, context: LogContext?) | ||
} | ||
|
||
public enum LogLevel: Sendable { | ||
public enum LogLevel: Sendable, Comparable { | ||
case trace | ||
case debug | ||
case info | ||
case warn | ||
case error | ||
case silent | ||
} | ||
|
||
/// A reference to a line within a source code file. | ||
internal struct CodeLocation: Equatable { | ||
/// A file identifier in the format used by Swift’s `#fileID` macro. For example, `"AblyChat/Room.swift"`. | ||
internal var fileID: String | ||
/// The line number in the source code file referred to by ``fileID``. | ||
internal var line: Int | ||
} | ||
|
||
/// A log handler to be used by components of the Chat SDK. | ||
/// | ||
/// This protocol exists to give internal SDK components access to a logging interface that allows them to provide rich and granular logging information, whilst giving us control over how much of this granularity we choose to expose to users of the SDK versus instead handling it for them by, say, interpolating it into a log message. It also allows us to evolve the logging interface used internally without introducing breaking changes for users of the SDK. | ||
internal protocol InternalLogger: Sendable { | ||
/// Logs a message. | ||
/// - Parameters: | ||
/// - message: The message to log. | ||
/// - level: The log level of the message. | ||
/// - codeLocation: The location in the code where the message was emitted. | ||
func log(message: String, level: LogLevel, codeLocation: CodeLocation) | ||
} | ||
|
||
extension InternalLogger { | ||
/// A convenience logging method that uses the call site’s #file and #line values. | ||
public func log(message: String, level: LogLevel, fileID: String = #fileID, line: Int = #line) { | ||
let codeLocation = CodeLocation(fileID: fileID, line: line) | ||
log(message: message, level: level, codeLocation: codeLocation) | ||
} | ||
} | ||
|
||
internal final class DefaultInternalLogger: InternalLogger { | ||
// Exposed for testing. | ||
internal let logHandler: LogHandler | ||
internal let logLevel: LogLevel | ||
|
||
internal init(logHandler: LogHandler?, logLevel: LogLevel?) { | ||
self.logHandler = logHandler ?? DefaultLogHandler() | ||
self.logLevel = logLevel ?? .info | ||
} | ||
|
||
internal func log(message: String, level: LogLevel, codeLocation: CodeLocation) { | ||
guard level >= logLevel else { | ||
return | ||
} | ||
|
||
// I don’t yet know what `context` is for (will figure out in https://github.com/ably-labs/ably-chat-swift/issues/8) so passing nil for now | ||
logHandler.log(message: "(\(codeLocation.fileID):\(codeLocation.line)) \(message)", level: level, context: nil) | ||
} | ||
} | ||
|
||
/// The logging backend used by ``DefaultInternalLogHandler`` if the user has not provided their own. Uses Swift’s `Logger` type for logging. | ||
internal final class DefaultLogHandler: LogHandler { | ||
internal func log(message: String, level: LogLevel, context _: LogContext?) { | ||
guard let osLogType = level.toOSLogType else { | ||
// Treating .silent as meaning "don’t log it", will figure out the meaning of .silent in https://github.com/ably-labs/ably-chat-swift/issues/8 | ||
return | ||
} | ||
|
||
let logger = Logger() | ||
logger.log(level: osLogType, "\(message)") | ||
} | ||
} | ||
|
||
private extension LogLevel { | ||
var toOSLogType: OSLogType? { | ||
switch self { | ||
case .debug, .trace: | ||
.debug | ||
case .info: | ||
.info | ||
case .warn, .error: | ||
.error | ||
case .silent: | ||
nil | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
@testable import AblyChat | ||
import XCTest | ||
|
||
class DefaultInternalLoggerTests: XCTestCase { | ||
func test_defaults() { | ||
let logger = DefaultInternalLogger(logHandler: nil, logLevel: nil) | ||
|
||
XCTAssertIdentical(type(of: logger.logHandler), DefaultLogHandler.self) | ||
XCTAssertEqual(logger.logLevel, .info) | ||
} | ||
|
||
func test_log() throws { | ||
// Given: A DefaultInternalLogger instance | ||
let logHandler = MockLogHandler() | ||
let logger = DefaultInternalLogger(logHandler: logHandler, logLevel: nil) | ||
|
||
// When: `log(message:level:codeLocation:)` is called on it | ||
logger.log( | ||
message: "Hello", | ||
level: .error, // arbitrary | ||
codeLocation: .init(fileID: "Ably/Room.swift", line: 123) | ||
) | ||
|
||
// Then: It calls log(…) on the underlying logger, interpolating the code location into the message and passing through the level | ||
let logArguments = try XCTUnwrap(logHandler.logArguments) | ||
XCTAssertEqual(logArguments.message, "(Ably/Room.swift:123) Hello") | ||
XCTAssertEqual(logArguments.level, .error) | ||
XCTAssertNil(logArguments.context) | ||
} | ||
|
||
func test_log_whenLogLevelArgumentIsLessSevereThanLogLevelProperty_itDoesNotLog() { | ||
// Given: A DefaultInternalLogger instance | ||
let logHandler = MockLogHandler() | ||
let logger = DefaultInternalLogger( | ||
logHandler: logHandler, | ||
logLevel: .info // arbitrary | ||
) | ||
|
||
// When: `log(message:level:codeLocation:)` is called on it, with `level` less severe than that of the instance | ||
logger.log( | ||
message: "Hello", | ||
level: .debug, | ||
codeLocation: .init(fileID: "", line: 0) | ||
) | ||
|
||
// Then: It does not call `log(…)` on the underlying logger | ||
XCTAssertNil(logHandler.logArguments) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
@testable import AblyChat | ||
|
||
struct TestLogger: InternalLogger { | ||
func log(message _: String, level _: LogLevel, codeLocation _: CodeLocation) { | ||
// No-op; currently we don’t log in tests to keep the test logs easy to read. Can reconsider if necessary. | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
@testable import AblyChat | ||
import XCTest | ||
|
||
/// A property wrapper that uses a mutex to protect its wrapped value from concurrent reads and writes. Similar to Objective-C’s `@atomic`. | ||
/// | ||
/// Don’t overestimate the abilities of this property wrapper; it won’t allow you to, for example, increment a counter in a threadsafe manner. | ||
@propertyWrapper | ||
struct SynchronizedAccess<T> { | ||
var wrappedValue: T { | ||
get { | ||
let value: T | ||
mutex.lock() | ||
value = _wrappedValue | ||
mutex.unlock() | ||
return value | ||
} | ||
|
||
set { | ||
mutex.lock() | ||
_wrappedValue = newValue | ||
mutex.unlock() | ||
} | ||
} | ||
|
||
private var _wrappedValue: T | ||
private var mutex = NSLock() | ||
|
||
init(wrappedValue: T) { | ||
_wrappedValue = wrappedValue | ||
} | ||
} | ||
|
||
class InternalLoggerTests: XCTestCase { | ||
func test_protocolExtension_logMessage_defaultArguments_populatesFileIDAndLine() throws { | ||
let logger = MockInternalLogger() | ||
|
||
let expectedLine = #line + 1 | ||
logger.log(message: "Here is a message", level: .info) | ||
|
||
let receivedArguments = try XCTUnwrap(logger.logArguments) | ||
|
||
XCTAssertEqual(receivedArguments.level, .info) | ||
XCTAssertEqual(receivedArguments.message, "Here is a message") | ||
XCTAssertEqual(receivedArguments.codeLocation, .init(fileID: #fileID, line: expectedLine)) | ||
} | ||
} |
Oops, something went wrong.