diff --git a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift index 11ea9e7b..1dc1cf13 100644 --- a/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift +++ b/ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift @@ -19,7 +19,7 @@ struct ConfidenceDemoApp: App { var body: some Scene { WindowGroup { let secret = ProcessInfo.processInfo.environment["CLIENT_SECRET"] ?? "" - let confidence = Confidence.Builder(clientSecret: secret) + let confidence = Confidence.Builder(clientSecret: secret, loggerLevel: .TRACE) .withContext(initialContext: ["targeting_key": ConfidenceValue(string: UUID.init().uuidString)]) .withRegion(region: .europe) .build() diff --git a/README.md b/README.md index 6852442f..133343b2 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,11 @@ To set context data to be appended to all tracked events, here as example: confidence.putContext(context: ["os_version": ConfidenceValue(string: "17.0")]) ``` +### Logging +By default, the Confidence SDK will log errors and warnings. You can change the preferred log level by passing a `loggerLevel` to the `Confidence.Builder` constructor. + +To turn off logging completely, you can pass `LoggingLevel.NONE` to the `Confidence.Builder`. + # OpenFeature Provider If you want to use OpenFeature, an OpenFeature Provider for the [OpenFeature SDK](https://github.com/open-feature/kotlin-swift) is also available. diff --git a/Sources/Confidence/Apply/FlagApplierWithRetries.swift b/Sources/Confidence/Apply/FlagApplierWithRetries.swift index 306f50ec..ca652ecf 100644 --- a/Sources/Confidence/Apply/FlagApplierWithRetries.swift +++ b/Sources/Confidence/Apply/FlagApplierWithRetries.swift @@ -10,6 +10,7 @@ final class FlagApplierWithRetries: FlagApplier { private let options: ConfidenceClientOptions private let cacheDataInteractor: CacheDataActor private let metadata: ConfidenceMetadata + private let debugLogger: DebugLogger? init( httpClient: HttpClient, @@ -17,12 +18,14 @@ final class FlagApplierWithRetries: FlagApplier { options: ConfidenceClientOptions, metadata: ConfidenceMetadata, cacheDataInteractor: CacheDataActor? = nil, - triggerBatch: Bool = true + triggerBatch: Bool = true, + debugLogger: DebugLogger? = nil ) { self.storage = storage self.httpClient = httpClient self.options = options self.metadata = metadata + self.debugLogger = debugLogger let storedData = try? storage.load(defaultValue: CacheData.empty()) self.cacheDataInteractor = cacheDataInteractor ?? CacheDataInteractor(cacheData: storedData ?? .empty()) @@ -48,6 +51,7 @@ final class FlagApplierWithRetries: FlagApplier { return } + debugLogger?.logFlags(action: "Apply", flag: flagName) self.writeToFile(data: data) await triggerBatch() } diff --git a/Sources/Confidence/Confidence.swift b/Sources/Confidence/Confidence.swift index 35f1d045..67c6f103 100644 --- a/Sources/Confidence/Confidence.swift +++ b/Sources/Confidence/Confidence.swift @@ -15,6 +15,7 @@ public class Confidence: ConfidenceEventSender { private var storage: Storage private var cancellables = Set() private var currentFetchTask: Task<(), Never>? + private let debugLogger: DebugLogger? // Internal for testing internal let remoteFlagResolver: ConfidenceResolveClient @@ -31,7 +32,8 @@ public class Confidence: ConfidenceEventSender { storage: Storage, context: ConfidenceStruct = [:], parent: ConfidenceEventSender? = nil, - visitorId: String? = nil + visitorId: String? = nil, + debugLogger: DebugLogger? ) { self.eventSenderEngine = eventSenderEngine self.clientSecret = clientSecret @@ -42,6 +44,7 @@ public class Confidence: ConfidenceEventSender { self.storage = storage self.flagApplier = flagApplier self.remoteFlagResolver = remoteFlagResolver + self.debugLogger = debugLogger if let visitorId { putContext(context: ["visitor_id": ConfidenceValue.init(string: visitorId)]) } @@ -57,7 +60,10 @@ public class Confidence: ConfidenceEventSender { try await self.fetchAndActivate() self.contextReconciliatedChanges.send(context.hash()) } catch { - // TODO: Log errors for debugging + debugLogger?.logMessage( + message: "\(error)", + isWarning: true + ) } } } @@ -70,6 +76,7 @@ public class Confidence: ConfidenceEventSender { public func activate() throws { let savedFlags = try storage.load(defaultValue: FlagResolution.EMPTY) self.cache = savedFlags + debugLogger?.logFlags(action: "Activate", flag: "") } /** @@ -82,7 +89,10 @@ public class Confidence: ConfidenceEventSender { do { try await internalFetch() } catch { - // TODO: Log errors for debugging + debugLogger?.logMessage( + message: "\(error)", + isWarning: true + ) } try activate() } @@ -95,6 +105,7 @@ public class Confidence: ConfidenceEventSender { flags: resolvedFlags.resolvedValues, resolveToken: resolvedFlags.resolveToken ?? "" ) + debugLogger?.logFlags(action: "Fetch", flag: "") try storage.save(data: resolution) } @@ -107,7 +118,10 @@ public class Confidence: ConfidenceEventSender { do { try await internalFetch() } catch { - // TODO: Log errors for debugging + debugLogger?.logMessage( + message: "\(error )", + isWarning: true + ) } } } @@ -209,6 +223,7 @@ public class Confidence: ConfidenceEventSender { var map = confidence.contextSubject.value map[key] = value confidence.contextSubject.value = map + confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) } } @@ -219,6 +234,7 @@ public class Confidence: ConfidenceEventSender { map.updateValue(entry.value, forKey: entry.key) } confidence.contextSubject.value = map + confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) } } @@ -232,6 +248,7 @@ public class Confidence: ConfidenceEventSender { map.updateValue(entry.value, forKey: entry.key) } confidence.contextSubject.value = map + confidence.debugLogger?.logContext(action: "PutContext", context: confidence.contextSubject.value) } } @@ -241,6 +258,7 @@ public class Confidence: ConfidenceEventSender { map.removeValue(forKey: key) confidence.contextSubject.value = map confidence.removedContextKeys.insert(key) + confidence.debugLogger?.logContext(action: "RemoveContext", context: confidence.contextSubject.value) } } @@ -256,7 +274,8 @@ public class Confidence: ConfidenceEventSender { remoteFlagResolver: remoteFlagResolver, storage: storage, context: context, - parent: self) + parent: self, + debugLogger: debugLogger) } } @@ -266,6 +285,7 @@ extension Confidence { internal let clientSecret: String internal let eventStorage: EventStorage internal let visitorId = VisitorUtil().getId() + internal let loggerLevel: LoggerLevel // Can be configured internal var region: ConfidenceRegion = .global @@ -280,13 +300,14 @@ extension Confidence { /** Initializes the builder with the given credentails. */ - public init(clientSecret: String) { + public init(clientSecret: String, loggerLevel: LoggerLevel = .WARN) { self.clientSecret = clientSecret do { eventStorage = try EventStorageImpl() } catch { eventStorage = EventStorageInMemory() } + self.loggerLevel = loggerLevel } internal func withFlagResolverClient(flagResolver: ConfidenceResolveClient) -> Builder { @@ -320,6 +341,13 @@ extension Confidence { } public func build() -> Confidence { + var debugLogger: DebugLogger? + if loggerLevel != LoggerLevel.NONE { + debugLogger = DebugLoggerImpl(loggerLevel: loggerLevel) + debugLogger?.logContext(action: "InitialContext", context: initialContext) + } else { + debugLogger = nil + } let options = ConfidenceClientOptions( credentials: ConfidenceClientCredentials.clientSecret(secret: clientSecret), region: region) @@ -335,7 +363,8 @@ extension Confidence { httpClient: httpClient, storage: DefaultStorage(filePath: "confidence.flags.apply"), options: options, - metadata: metadata + metadata: metadata, + debugLogger: debugLogger ) let flagResolver = flagResolver ?? RemoteConfidenceResolveClient( options: options, @@ -345,7 +374,9 @@ extension Confidence { let eventSenderEngine = EventSenderEngineImpl( clientSecret: clientSecret, uploader: uploader, - storage: eventStorage) + storage: eventStorage, + debugLogger: debugLogger + ) return Confidence( clientSecret: clientSecret, region: region, @@ -355,7 +386,8 @@ extension Confidence { storage: storage ?? DefaultStorage(filePath: "confidence.flags.resolve"), context: initialContext, parent: nil, - visitorId: visitorId + visitorId: visitorId, + debugLogger: debugLogger ) } } diff --git a/Sources/Confidence/DebugLogger.swift b/Sources/Confidence/DebugLogger.swift new file mode 100644 index 00000000..fe7922c2 --- /dev/null +++ b/Sources/Confidence/DebugLogger.swift @@ -0,0 +1,69 @@ +import Foundation +import OSLog + +internal protocol DebugLogger { + func logEvent(action: String, event: ConfidenceEvent?) + func logMessage(message: String, isWarning: Bool) + func logFlags(action: String, flag: String) + func logContext(action: String, context: ConfidenceStruct) +} + +private extension Logger { + private static var subsystem = Bundle.main.bundleIdentifier + + static let confidence = Logger(subsystem: subsystem ?? "", category: "confidence") +} + +internal class DebugLoggerImpl: DebugLogger { + private let loggerLevel: LoggerLevel + + init(loggerLevel: LoggerLevel) { + self.loggerLevel = loggerLevel + } + + func logMessage(message: String, isWarning: Bool = false) { + if isWarning { + log(messageLevel: .WARN, message: message) + } else { + log(messageLevel: .DEBUG, message: message) + } + } + + func logEvent(action: String, event: ConfidenceEvent?) { + log(messageLevel: .DEBUG, message: "[\(action)] \(event?.name ?? "")") + } + + func logFlags(action: String, flag: String) { + log(messageLevel: .TRACE, message: "[\(action)] \(flag)") + } + + func logContext(action: String, context: ConfidenceStruct) { + log(messageLevel: .TRACE, message: "[\(action)] \(context)") + } + + private func log(messageLevel: LoggerLevel, message: String) { + if messageLevel >= loggerLevel { + switch messageLevel { + case .TRACE: + Logger.confidence.trace("\(message)") + case .DEBUG: + Logger.confidence.debug("\(message)") + case .WARN: + Logger.confidence.warning("\(message)") + case .ERROR: + Logger.confidence.error("\(message)") + case .NONE: + // do nothing + break + } + } + } +} + +public enum LoggerLevel: Comparable { + case TRACE + case DEBUG + case WARN + case ERROR + case NONE +} diff --git a/Sources/Confidence/EventSenderEngine.swift b/Sources/Confidence/EventSenderEngine.swift index 9a83e201..dff0b232 100644 --- a/Sources/Confidence/EventSenderEngine.swift +++ b/Sources/Confidence/EventSenderEngine.swift @@ -8,7 +8,11 @@ protocol FlushPolicy { } protocol EventSenderEngine { - func emit(eventName: String, data: ConfidenceStruct, context: ConfidenceStruct) throws + func emit( + eventName: String, + data: ConfidenceStruct, + context: ConfidenceStruct + ) throws func shutdown() func flush() } @@ -25,18 +29,21 @@ final class EventSenderEngineImpl: EventSenderEngine { private let payloadMerger: PayloadMerger = PayloadMergerImpl() private let semaphore = DispatchSemaphore(value: 1) private let writeQueue: DispatchQueue + private let debugLogger: DebugLogger? convenience init( clientSecret: String, uploader: ConfidenceClient, - storage: EventStorage + storage: EventStorage, + debugLogger: DebugLogger? ) { self.init( clientSecret: clientSecret, uploader: uploader, storage: storage, flushPolicies: [SizeFlushPolicy(batchSize: 10)], - writeQueue: DispatchQueue(label: "ConfidenceWriteQueue") + writeQueue: DispatchQueue(label: "ConfidenceWriteQueue"), + debugLogger: debugLogger ) } @@ -45,13 +52,15 @@ final class EventSenderEngineImpl: EventSenderEngine { uploader: ConfidenceClient, storage: EventStorage, flushPolicies: [FlushPolicy], - writeQueue: DispatchQueue + writeQueue: DispatchQueue, + debugLogger: DebugLogger? ) { self.uploader = uploader self.clientSecret = clientSecret self.storage = storage self.flushPolicies = flushPolicies + [ManualFlushPolicy()] self.writeQueue = writeQueue + self.debugLogger = debugLogger writeReqChannel .receive(on: self.writeQueue) @@ -119,16 +128,22 @@ final class EventSenderEngineImpl: EventSenderEngine { semaphore.signal() } - func emit(eventName: String, data: ConfidenceStruct, context: ConfidenceStruct) throws { - writeReqChannel.send(ConfidenceEvent( + func emit( + eventName: String, + data: ConfidenceStruct, + context: ConfidenceStruct + ) throws { + let event = ConfidenceEvent( name: eventName, payload: try payloadMerger.merge(context: context, data: data), eventTime: Date.backport.now) - ) + writeReqChannel.send(event) + debugLogger?.logEvent(action: "Emitting event", event: event) } func flush() { writeReqChannel.send(manualFlushEvent) + debugLogger?.logEvent(action: "Event flushed", event: nil) } func shutdown() { diff --git a/Sources/Confidence/RemoteConfidenceClient.swift b/Sources/Confidence/RemoteConfidenceClient.swift index 808c96d0..15bf28c5 100644 --- a/Sources/Confidence/RemoteConfidenceClient.swift +++ b/Sources/Confidence/RemoteConfidenceClient.swift @@ -6,11 +6,13 @@ public class RemoteConfidenceClient: ConfidenceClient { private let metadata: ConfidenceMetadata private var httpClient: HttpClient private var baseUrl: String + private let debugLogger: DebugLogger? init( options: ConfidenceClientOptions, session: URLSession? = nil, - metadata: ConfidenceMetadata + metadata: ConfidenceMetadata, + debugLogger: DebugLogger? = nil ) { self.options = options switch options.region { @@ -23,6 +25,7 @@ public class RemoteConfidenceClient: ConfidenceClient { } self.httpClient = NetworkClient(session: session, baseUrl: baseUrl) self.metadata = metadata + self.debugLogger = debugLogger } func upload(events: [NetworkEvent]) async throws -> Bool { @@ -46,14 +49,30 @@ public class RemoteConfidenceClient: ConfidenceClient { switch status { case 200: // clean up in case of success + debugLogger?.logMessage( + message: "Event upload: HTTP status 200", + isWarning: false + ) return true case 429: // we shouldn't clean up for rate limiting + debugLogger?.logMessage( + message: "Event upload: HTTP status 429", + isWarning: true + ) return false case 400...499: // if batch couldn't be processed, we should clean it up + debugLogger?.logMessage( + message: "Event upload: couldn't process batch", + isWarning: true + ) return true default: + debugLogger?.logMessage( + message: "Event upload error. Status code \(status)", + isWarning: true + ) return false } case .failure(let errorData): @@ -63,6 +82,10 @@ public class RemoteConfidenceClient: ConfidenceClient { } private func handleError(error: Error) -> Error { + debugLogger?.logMessage( + message: "Event upload error: \(error.localizedDescription)", + isWarning: true + ) if error is ConfidenceError { return error } else { diff --git a/Tests/ConfidenceTests/ConfidenceContextTests.swift b/Tests/ConfidenceTests/ConfidenceContextTests.swift index cf65f4db..c41447cc 100644 --- a/Tests/ConfidenceTests/ConfidenceContextTests.swift +++ b/Tests/ConfidenceTests/ConfidenceContextTests.swift @@ -17,7 +17,8 @@ final class ConfidenceContextTests: XCTestCase { flagApplier: FlagApplierMock(), remoteFlagResolver: client, storage: StorageMock(), - context: ["k1": ConfidenceValue(string: "v1")] + context: ["k1": ConfidenceValue(string: "v1")], + debugLogger: nil ) let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( ["k2": ConfidenceValue(string: "v2")] @@ -44,7 +45,8 @@ final class ConfidenceContextTests: XCTestCase { remoteFlagResolver: client, storage: StorageMock(), context: ["k1": ConfidenceValue(string: "v1")], - parent: nil + parent: nil, + debugLogger: nil ) let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( ["k2": ConfidenceValue(string: "v2")] @@ -75,7 +77,8 @@ final class ConfidenceContextTests: XCTestCase { remoteFlagResolver: client, storage: StorageMock(), context: ["k1": ConfidenceValue(string: "v1")], - parent: nil + parent: nil, + debugLogger: nil ) confidence.putContext( key: "k1", @@ -101,7 +104,8 @@ final class ConfidenceContextTests: XCTestCase { remoteFlagResolver: client, storage: StorageMock(), context: ["k1": ConfidenceValue(string: "v1")], - parent: nil + parent: nil, + debugLogger: nil ) let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( ["k2": ConfidenceValue(string: "v2")] @@ -131,7 +135,8 @@ final class ConfidenceContextTests: XCTestCase { remoteFlagResolver: client, storage: StorageMock(), context: ["k1": ConfidenceValue(string: "v1")], - parent: nil + parent: nil, + debugLogger: nil ) let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( ["k2": ConfidenceValue(string: "v2")] @@ -161,7 +166,8 @@ final class ConfidenceContextTests: XCTestCase { remoteFlagResolver: client, storage: StorageMock(), context: ["k1": ConfidenceValue(string: "v1")], - parent: nil + parent: nil, + debugLogger: nil ) confidence.removeKey(key: "k2") let expected = [ @@ -185,7 +191,8 @@ final class ConfidenceContextTests: XCTestCase { remoteFlagResolver: client, storage: StorageMock(), context: ["k1": ConfidenceValue(string: "v1")], - parent: nil + parent: nil, + debugLogger: nil ) let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( ["k2": ConfidenceValue(string: "v2")] @@ -212,7 +219,8 @@ final class ConfidenceContextTests: XCTestCase { remoteFlagResolver: client, storage: StorageMock(), context: ["k1": ConfidenceValue(string: "v1")], - parent: nil + parent: nil, + debugLogger: nil ) let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( [ @@ -242,7 +250,8 @@ final class ConfidenceContextTests: XCTestCase { remoteFlagResolver: client, storage: StorageMock(), context: ["k1": ConfidenceValue(string: "v1")], - parent: nil + parent: nil, + debugLogger: nil ) let confidenceChild: ConfidenceEventSender = confidenceParent.withContext( [ @@ -275,7 +284,8 @@ final class ConfidenceContextTests: XCTestCase { storage: StorageMock(), context: ["k1": ConfidenceValue(string: "v1")], parent: nil, - visitorId: "uuid" + visitorId: "uuid", + debugLogger: nil ) let expected = [ "k1": ConfidenceValue(string: "v1"), diff --git a/Tests/ConfidenceTests/EventSenderEngineTest.swift b/Tests/ConfidenceTests/EventSenderEngineTest.swift index c2a78e04..7db33b67 100644 --- a/Tests/ConfidenceTests/EventSenderEngineTest.swift +++ b/Tests/ConfidenceTests/EventSenderEngineTest.swift @@ -59,7 +59,8 @@ final class EventSenderEngineTest: XCTestCase { uploader: uploaderMock, storage: storageMock, flushPolicies: [MinSizeFlushPolicy(maxSize: 1)], - writeQueue: writeQueue + writeQueue: writeQueue, + debugLogger: nil ) let expectation = XCTestExpectation(description: "Upload finished") @@ -97,7 +98,8 @@ final class EventSenderEngineTest: XCTestCase { uploader: uploaderMock, storage: storageMock, flushPolicies: [MinSizeFlushPolicy(maxSize: 5)], - writeQueue: writeQueue + writeQueue: writeQueue, + debugLogger: nil ) try eventSenderEngine.emit(eventName: "Hello", data: [:], context: [:]) @@ -117,7 +119,8 @@ final class EventSenderEngineTest: XCTestCase { uploader: badRequestUploader, storage: storageMock, flushPolicies: [ImmidiateFlushPolicy()], - writeQueue: writeQueue + writeQueue: writeQueue, + debugLogger: nil ) try eventSenderEngine.emit(eventName: "testEvent", data: ConfidenceStruct(), context: ConfidenceStruct()) let expectation = expectation(description: "events batched") @@ -141,7 +144,8 @@ final class EventSenderEngineTest: XCTestCase { uploader: retryLaterUploader, storage: storageMock, flushPolicies: [ImmidiateFlushPolicy()], - writeQueue: writeQueue + writeQueue: writeQueue, + debugLogger: nil ) try eventSenderEngine.emit(eventName: "testEvent", data: ConfidenceStruct(), context: ConfidenceStruct()) @@ -158,7 +162,8 @@ final class EventSenderEngineTest: XCTestCase { storage: storageMock, // no other flush policy is set which means that only manual flushes will trigger upload flushPolicies: [], - writeQueue: writeQueue + writeQueue: writeQueue, + debugLogger: nil ) try eventSenderEngine.emit(eventName: "Hello", data: [:], context: [:]) @@ -193,7 +198,8 @@ final class EventSenderEngineTest: XCTestCase { storage: storageMock, // no other flush policy is set which means that only manual flushes will trigger upload flushPolicies: [], - writeQueue: writeQueue + writeQueue: writeQueue, + debugLogger: nil ) eventSenderEngine.flush()