Skip to content

Commit

Permalink
Support customisable LogHandler (#92)
Browse files Browse the repository at this point in the history
* Support customisable LogHandler

By default, this will increase slightly the amount of logging that end clients see - however it is now far more configurable.

`showDebugLogs` has been deprecated with users being encouraged to use `logHandler`. The debug logs parameter is no longer used.

* Update docs
  • Loading branch information
Sherlouk committed Feb 22, 2023
1 parent cb34730 commit 03ac392
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 53 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ A very small subset of our customers will want to use a custom signal ingestion
let configuration = TelemetryManagerConfiguration(appID: "<YOUR-APP-ID>", 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.
Expand Down
35 changes: 35 additions & 0 deletions Sources/TelemetryClient/LogHandler.swift
Original file line number Diff line number Diff line change
@@ -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)")
}
}
}
21 changes: 8 additions & 13 deletions Sources/TelemetryClient/SignalCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Foundation
///
/// Currently the cache is only in-memory. This will probably change in the near future.
internal class SignalCache<T> where T: Codable {
public var showDebugLogs: Bool = false
internal var logHandler: LogHandler?

private var cachedSignals: [T] = []
private let maximumNumberOfSignalsToPopAtOnce = 100
Expand Down Expand Up @@ -69,37 +69,32 @@ internal class SignalCache<T> 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
}
}
Expand Down
54 changes: 17 additions & 37 deletions Sources/TelemetryClient/SignalManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand All @@ -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)!)
}
}
}
Expand All @@ -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()
}

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions Sources/TelemetryClient/TelemetryClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 35 additions & 0 deletions Tests/TelemetryClientTests/LogHandlerTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
2 changes: 1 addition & 1 deletion Tests/TelemetryClientTests/TelemetryClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final class TelemetryClientTests: XCTestCase {
}

func testPushAndPop() {
let signalCache = SignalCache<SignalPostBody>(showDebugLogs: false)
let signalCache = SignalCache<SignalPostBody>(logHandler: nil)

let signals: [SignalPostBody] = [
.init(receivedAt: Date(), appID: UUID(), clientUser: "01", sessionID: "01", type: "test", floatValue: nil, payload: [], isTestMode: "true"),
Expand Down

0 comments on commit 03ac392

Please sign in to comment.