diff --git a/README.md b/README.md index 07b380d..83b9a9a 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,12 @@ A very small subset of our customers will want to use a custom signal ingestion let configuration = TelemetryManagerConfiguration(appID: "", baseURL: "https://nom.telemetrydeck.com") ``` +## Custom Logging Strategy + +By default, some logs helpful for monitoring TelemetryDeck are printed out to the console. This behaviour can be customised by overriding `configuration.logHandler`. This struct accepts a minimum allows log level (any log with the same or higher log level will be accepted) and a closure. + +This allows for compatibility with other logging solutions, such as [swift-log](https://github.com/apple/swift-log), by providing your own closure. + ## Developing this SDK Your PRs on TelemetryDeck's Swift Client are very much welcome. Check out the [SwiftClientTester](https://github.com/TelemetryDeck/SwiftClientTester) project, which provides a harness you can use to work on the library and try out new things. diff --git a/Sources/TelemetryClient/LogHandler.swift b/Sources/TelemetryClient/LogHandler.swift new file mode 100644 index 0000000..d289c5e --- /dev/null +++ b/Sources/TelemetryClient/LogHandler.swift @@ -0,0 +1,35 @@ +import Foundation + +public struct LogHandler { + public enum LogLevel: Int, CustomStringConvertible { + case debug = 0 + case info = 1 + case error = 2 + + public var description: String { + switch self { + case .debug: + return "DEBUG" + case .info: + return "INFO" + case .error: + return "ERROR" + } + } + } + + let logLevel: LogLevel + let handler: (LogLevel, String) -> Void + + internal func log(_ level: LogLevel = .info, message: String) { + if level.rawValue >= logLevel.rawValue { + handler(level, message) + } + } + + public static var stdout = { logLevel in + LogHandler(logLevel: logLevel) { level, message in + print("[TelemetryDeck: \(level.description)] \(message)") + } + } +} diff --git a/Sources/TelemetryClient/SignalCache.swift b/Sources/TelemetryClient/SignalCache.swift index ec93781..aa222a2 100644 --- a/Sources/TelemetryClient/SignalCache.swift +++ b/Sources/TelemetryClient/SignalCache.swift @@ -8,7 +8,7 @@ import Foundation /// /// Currently the cache is only in-memory. This will probably change in the near future. internal class SignalCache where T: Codable { - public var showDebugLogs: Bool = false + internal var logHandler: LogHandler? private var cachedSignals: [T] = [] private let maximumNumberOfSignalsToPopAtOnce = 100 @@ -69,37 +69,32 @@ internal class SignalCache where T: Codable { if let data = try? JSONEncoder().encode(self.cachedSignals) { do { try data.write(to: fileURL()) - if showDebugLogs { - print("Saved Telemetry cache \(data) of \(self.cachedSignals.count) signals") - } + logHandler?.log(message: "Saved Telemetry cache \(data) of \(self.cachedSignals.count) signals") // After saving the cache, we need to clear our local cache otherwise // it could get merged with the cache read back from disk later if // it's still in memory self.cachedSignals = [] } catch { - print("Error saving Telemetry cache") + logHandler?.log(.error, message: "Error saving Telemetry cache") } } } } /// Loads any previous signal cache from disk - init(showDebugLogs: Bool) { - self.showDebugLogs = showDebugLogs + init(logHandler: LogHandler?) { + self.logHandler = logHandler queue.sync { - if showDebugLogs { - print("Loading Telemetry cache from: \(fileURL())") - } + logHandler?.log(message: "Loading Telemetry cache from: \(fileURL())") + if let data = try? Data(contentsOf: fileURL()) { // Loaded cache file, now delete it to stop it being loaded multiple times try? FileManager.default.removeItem(at: fileURL()) // Decode the data into a new cache if let signals = try? JSONDecoder().decode([T].self, from: data) { - if showDebugLogs { - print("Loaded \(signals.count) signals") - } + logHandler?.log(message: "Loaded \(signals.count) signals") self.cachedSignals = signals } } diff --git a/Sources/TelemetryClient/SignalManager.swift b/Sources/TelemetryClient/SignalManager.swift index 18ddd8b..d4b258c 100644 --- a/Sources/TelemetryClient/SignalManager.swift +++ b/Sources/TelemetryClient/SignalManager.swift @@ -25,7 +25,7 @@ internal class SignalManager: SignalManageable { self.configuration = configuration // We automatically load any old signals from disk on initialisation - signalCache = SignalCache(showDebugLogs: configuration.showDebugLogs) + signalCache = SignalCache(logHandler: configuration.logHandler) // Before the app terminates, we want to save any pending signals to disk // We need to monitor different notifications for different devices. @@ -83,9 +83,7 @@ internal class SignalManager: SignalManageable { isTestMode: configuration.testMode ? "true" : "false" ) - if configuration.showDebugLogs { - print("Process signal: \(signalPostBody)") - } + configuration.logHandler?.log(.debug, message: "Process signal: \(signalPostBody)") self.signalCache.push(signalPostBody) } @@ -95,21 +93,17 @@ internal class SignalManager: SignalManageable { /// If any fail to send, we put them back into the cache to send later. @objc private func checkForSignalsAndSend() { - if configuration.showDebugLogs { - print("Current signal cache count: \(signalCache.count())") - } + configuration.logHandler?.log(.debug, message: "Current signal cache count: \(signalCache.count())") let queuedSignals: [SignalPostBody] = signalCache.pop() if !queuedSignals.isEmpty { - if configuration.showDebugLogs { - print("Sending \(queuedSignals.count) signals leaving a cache of \(signalCache.count()) signals") - } + configuration.logHandler?.log(message: "Sending \(queuedSignals.count) signals leaving a cache of \(signalCache.count()) signals") + send(queuedSignals) { [configuration, signalCache] data, response, error in if let error = error { - if configuration.showDebugLogs { - print(error) - } + configuration.logHandler?.log(.error, message: "\(error)") + // The send failed, put the signal back into the queue signalCache.push(queuedSignals) return @@ -118,18 +112,14 @@ internal class SignalManager: SignalManageable { // Check for valid status code response guard response?.statusCodeError() == nil else { let statusError = response!.statusCodeError()! - if configuration.showDebugLogs { - print(statusError) - } + configuration.logHandler?.log(.error, message: "\(statusError)") // The send failed, put the signal back into the queue signalCache.push(queuedSignals) return } if let data = data { - if configuration.showDebugLogs { - print(String(data: data, encoding: .utf8)!) - } + configuration.logHandler?.log(.debug, message: String(data: data, encoding: .utf8)!) } } } @@ -140,10 +130,7 @@ internal class SignalManager: SignalManageable { private extension SignalManager { @objc func appWillTerminate() { - if configuration.showDebugLogs { - print(#function) - } - + configuration.logHandler?.log(.debug, message: #function) signalCache.backupCache() } @@ -153,24 +140,18 @@ private extension SignalManager { /// so we merge them into the new cache. #if os(watchOS) || os(tvOS) || os(iOS) @objc func didEnterForeground() { - if configuration.showDebugLogs { - print(#function) - } + configuration.logHandler?.log(.debug, message: #function) let currentCache = signalCache.pop() - if configuration.showDebugLogs { - print("current cache is \(currentCache.count) signals") - } - signalCache = SignalCache(showDebugLogs: configuration.showDebugLogs) + configuration.logHandler?.log(.debug, message: "current cache is \(currentCache.count) signals") + signalCache = SignalCache(logHandler: configuration.logHandler) signalCache.push(currentCache) startTimer() } @objc func didEnterBackground() { - if configuration.showDebugLogs { - print(#function) - } + configuration.logHandler?.log(.debug, message: #function) sendTimer?.invalidate() sendTimer = nil @@ -193,9 +174,8 @@ private extension SignalManager { urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") urlRequest.httpBody = try! JSONEncoder.telemetryEncoder.encode(signalPostBodies) - if self.configuration.showDebugLogs { - print(String(data: urlRequest.httpBody!, encoding: .utf8)!) - } + self.configuration.logHandler?.log(.debug, message: String(data: urlRequest.httpBody!, encoding: .utf8)!) + /// Wait for connectivity let config = URLSessionConfiguration.default config.waitsForConnectivity = true @@ -239,7 +219,7 @@ private extension SignalManager { } #else #if DEBUG - print("[Telemetry] On this platform, Telemetry can't generate a unique user identifier. It is recommended you supply one yourself. More info: https://telemetrydeck.com/pages/signal-reference.html") + configuration.logHandler?.log(message: "[Telemetry] On this platform, Telemetry can't generate a unique user identifier. It is recommended you supply one yourself. More info: https://telemetrydeck.com/pages/signal-reference.html") #endif return "unknown user \(SignalPayload.platform) \(SignalPayload.systemVersion) \(SignalPayload.buildNumber)" #endif diff --git a/Sources/TelemetryClient/TelemetryClient.swift b/Sources/TelemetryClient/TelemetryClient.swift index 430226e..832eb2d 100644 --- a/Sources/TelemetryClient/TelemetryClient.swift +++ b/Sources/TelemetryClient/TelemetryClient.swift @@ -110,12 +110,17 @@ public final class TelemetryManagerConfiguration { /// /// Works together with `swiftUIPreviewMode` if either of those values is `true` no analytics events are sent. /// However it won't interfere with SwiftUI Previews, when explicitly settings this value to `false`. - public var analyticsDisabled: Bool = false /// Log the current status to the signal cache to the console. + @available(*, deprecated, message: "Please use the logHandler property instead") public var showDebugLogs: Bool = false - + + /// A strategy for handling logs. + /// + /// Defaults to `print` with info/errror messages - debug messages are not outputted. Set to `nil` to disable all logging from TelemetryDeck SDK. + public var logHandler: LogHandler? = LogHandler.stdout(.info) + public init(appID: String, salt: String? = nil, baseURL: URL? = nil) { telemetryAppID = appID diff --git a/Tests/TelemetryClientTests/LogHandlerTests.swift b/Tests/TelemetryClientTests/LogHandlerTests.swift new file mode 100644 index 0000000..2a7f0bd --- /dev/null +++ b/Tests/TelemetryClientTests/LogHandlerTests.swift @@ -0,0 +1,35 @@ +@testable import TelemetryClient +import XCTest + +final class LogHandlerTests: XCTestCase { + func testLogHandler_stdoutLogLevelDefined() { + XCTAssertEqual(LogHandler.stdout(.error).logLevel, .error) + } + + func testLogHandler_logLevelRespected() { + var counter = 0 + + let handler = LogHandler(logLevel: .info) { _, _ in + counter += 1 + } + + XCTAssertEqual(counter, 0) + handler.log(.debug, message: "") + XCTAssertEqual(counter, 0) + handler.log(.info, message: "") + XCTAssertEqual(counter, 1) + handler.log(.error, message: "") + XCTAssertEqual(counter, 2) + } + + func testLogHandler_defaultLogLevel() { + var lastLevel: LogHandler.LogLevel? + + let handler = LogHandler(logLevel: .debug) { level, _ in + lastLevel = level + } + + handler.log(message: "") + XCTAssertEqual(lastLevel, .info) + } +} diff --git a/Tests/TelemetryClientTests/TelemetryClientTests.swift b/Tests/TelemetryClientTests/TelemetryClientTests.swift index 6e2ce7e..47cd7e4 100644 --- a/Tests/TelemetryClientTests/TelemetryClientTests.swift +++ b/Tests/TelemetryClientTests/TelemetryClientTests.swift @@ -14,7 +14,7 @@ final class TelemetryClientTests: XCTestCase { } func testPushAndPop() { - let signalCache = SignalCache(showDebugLogs: false) + let signalCache = SignalCache(logHandler: nil) let signals: [SignalPostBody] = [ .init(receivedAt: Date(), appID: UUID(), clientUser: "01", sessionID: "01", type: "test", floatValue: nil, payload: [], isTestMode: "true"),