diff --git a/Package.resolved b/Package.resolved index a4e00c4e..1df556d7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "d87ac3bfcdef05674d1d54c62277ab77a7f1a74d2b106a3e0a8eb5567e2ff1ed", + "originHash" : "89322b5f0f685d676bc5ca645de68fe1ef7ffddb230cde1f79d6a72190e68ae7", "pins" : [ { "identity" : "combine-schedulers", @@ -91,6 +91,15 @@ "version" : "1.1.1" } }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log", + "state" : { + "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2", + "version" : "1.6.4" + } + }, { "identity" : "swift-perception", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 6e452941..a35e5b54 100644 --- a/Package.swift +++ b/Package.swift @@ -33,6 +33,10 @@ let package = Package( name: "SharingGRDBTagged", description: "Introduce SharingGRDB conformances to the swift-tagged package." ), + .trait( + name: "SharingGRDBSwiftLog", + description: "Use swift-log instead of OSLog for logging." + ), .default(enabledTraits: ["SharingGRDBTagged"]), ], dependencies: [ @@ -50,6 +54,7 @@ let package = Package( ), .package(url: "https://github.com/pointfreeco/swift-tagged", from: "0.10.0"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.5.0"), + .package(url: "https://github.com/apple/swift-log", from: "1.6.4"), ], targets: [ .target( @@ -72,6 +77,11 @@ let package = Package( package: "swift-tagged", condition: .when(traits: ["SharingGRDBTagged"]) ), + .product( + name: "Logging", + package: "swift-log", + condition: .when(traits: ["SharingGRDBSwiftLog"]) + ) ] ), .testTarget( diff --git a/Sources/SharingGRDBCore/CloudKit/Logging.swift b/Sources/SharingGRDBCore/CloudKit/Logging.swift index b62caf4c..b5e333bd 100644 --- a/Sources/SharingGRDBCore/CloudKit/Logging.swift +++ b/Sources/SharingGRDBCore/CloudKit/Logging.swift @@ -2,8 +2,26 @@ import CloudKit import os +#if SharingGRDBSwiftLog + import Logging +#endif + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension Logger { + func log(_ event: SyncEngine.Event, syncEngine: any SyncEngineProtocol) { + switch self { + case .osLogger(let logger): + logger.log(event, syncEngine: syncEngine) + #if SharingGRDBSwiftLog + case .swiftLogger(let logger): + logger.log(event, syncEngine: syncEngine) + #endif + } + } +} + +@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) +extension os.Logger { func log(_ event: SyncEngine.Event, syncEngine: any SyncEngineProtocol) { let prefix = "[\(syncEngine.database.databaseScope.label)] handleEvent:" switch event { @@ -37,44 +55,17 @@ extension Logger { debug("unknown") } case .fetchedDatabaseChanges(_, let deletions): - let deletions = - deletions.isEmpty - ? "⚪️ No deletions" - : "✅ Zones deleted (\(deletions.count)): " - + deletions - .map { $0.zoneID.zoneName + ":" + $0.zoneID.ownerName } - .sorted() - .joined(separator: ", ") debug( """ \(prefix) fetchedDatabaseChanges - \(deletions) + \(deletedZones(ids: deletions.map(\.zoneID))) """ ) case .fetchedRecordZoneChanges(let modifications, let deletions): - let deletionsByRecordType = Dictionary( - grouping: deletions, - by: \.recordType + let (modifications, deletions) = fetchedRecordZoneChanges( + modifications: modifications, + deletions: deletions ) - let recordTypeDeletions = deletionsByRecordType.keys.sorted() - .map { recordType in "\(recordType) (\(deletionsByRecordType[recordType]!.count))" } - .joined(separator: ", ") - let deletions = - deletions.isEmpty - ? "⚪️ No deletions" : "✅ Records deleted (\(deletions.count)): \(recordTypeDeletions)" - - let modificationsByRecordType = Dictionary( - grouping: modifications, - by: \.recordType - ) - let recordTypeModifications = modificationsByRecordType.keys.sorted() - .map { recordType in "\(recordType) (\(modificationsByRecordType[recordType]!.count))" } - .joined(separator: ", ") - let modifications = - modifications.isEmpty - ? "⚪️ No modifications" - : "✅ Records modified (\(modifications.count)): \(recordTypeModifications)" - debug( """ \(prefix) fetchedRecordZoneChanges @@ -88,47 +79,17 @@ extension Logger { let deletedZoneIDs, let failedZoneDeletes ): - let savedZoneNames = savedZones - .map { $0.zoneID.zoneName + ":" + $0.zoneID.ownerName } - .sorted() - .joined(separator: ", ") - let savedZones = - savedZones.isEmpty - ? "⚪️ No saved zones" : "✅ Saved zones (\(savedZones.count)): \(savedZoneNames)" - - let deletedZoneNames = deletedZoneIDs - .map { $0.zoneName } - .sorted() - .joined(separator: ", ") - let deletedZones = - deletedZoneIDs.isEmpty - ? "⚪️ No deleted zones" - : "✅ Deleted zones (\(deletedZoneIDs.count)): \(deletedZoneNames)" - - let failedZoneSaveNames = failedZoneSaves - .map { $0.zone.zoneID.zoneName + ":" + $0.zone.zoneID.ownerName } - .sorted() - .joined(separator: ", ") - let failedZoneSaves = - failedZoneSaves.isEmpty - ? "⚪️ No failed saved zones" - : "🛑 Failed zone saves (\(failedZoneSaves.count)): \(failedZoneSaveNames)" - - let failedZoneDeleteNames = failedZoneDeletes - .keys - .map { $0.zoneName } - .sorted() - .joined(separator: ", ") - let failedZoneDeletes = - failedZoneDeletes.isEmpty - ? "⚪️ No failed deleted zones" - : "🛑 Failed zone delete (\(failedZoneDeletes.count)): \(failedZoneDeleteNames)" - + let (savedZones, deletedZones, failedZoneSaves, failedZoneDeletes) = sentDatabaseChanges( + savedZones: savedZones, + failedZoneSaves: failedZoneSaves, + deletedZoneIDs: deletedZoneIDs, + failedZoneDeletes: failedZoneDeletes + ) debug( """ \(prefix) sentDatabaseChanges \(savedZones) - \(deletedZones) + \(deletedZones) \(failedZoneSaves) \(failedZoneDeletes) """ @@ -139,31 +100,24 @@ extension Logger { let deletedRecordIDs, let failedRecordDeletes ): - let savedRecordsByRecordType = Dictionary( - grouping: savedRecords, - by: \.recordType + let ( + savedRecords, + deletedRecords, + failedRecordSaves, + failedRecordDeletes + ) = sentRecordZoneChanges( + savedRecords: savedRecords, + failedRecordSaves: failedRecordSaves, + deletedRecordIDs: deletedRecordIDs, + failedRecordDeletes: failedRecordDeletes ) - let savedRecords = savedRecordsByRecordType.keys - .sorted() - .map { "\($0) (\(savedRecordsByRecordType[$0]!.count))" } - .joined(separator: ", ") - - let failedRecordSavesByZoneName = Dictionary( - grouping: failedRecordSaves, - by: { $0.record.recordID.zoneID.zoneName + ":" + $0.record.recordID.zoneID.ownerName } - ) - let failedRecordSaves = failedRecordSavesByZoneName.keys - .sorted() - .map { "\($0) (\(failedRecordSavesByZoneName[$0]!.count))" } - .joined(separator: ", ") - debug( """ \(prefix) sentRecordZoneChanges - \(savedRecordsByRecordType.isEmpty ? "⚪️ No records saved" : "✅ Saved records: \(savedRecords)") - \(deletedRecordIDs.isEmpty ? "⚪️ No records deleted" : "✅ Deleted records (\(deletedRecordIDs.count))") - \(failedRecordSavesByZoneName.isEmpty ? "⚪️ No records failed save" : "🛑 Records failed save: \(failedRecordSaves)") - \(failedRecordDeletes.isEmpty ? "⚪️ No records failed delete" : "🛑 Records failed delete (\(failedRecordDeletes.count))") + \(savedRecords) + \(deletedRecords) + \(failedRecordSaves) + \(failedRecordDeletes) """ ) case .willFetchChanges: @@ -171,48 +125,7 @@ extension Logger { case .willFetchRecordZoneChanges(let zoneID): debug("\(prefix) willFetchRecordZoneChanges: \(zoneID.zoneName)") case .didFetchRecordZoneChanges(let zoneID, let error): - let errorType = error.map { - switch $0.code { - case .internalError: "internalError" - case .partialFailure: "partialFailure" - case .networkUnavailable: "networkUnavailable" - case .networkFailure: "networkFailure" - case .badContainer: "badContainer" - case .serviceUnavailable: "serviceUnavailable" - case .requestRateLimited: "requestRateLimited" - case .missingEntitlement: "missingEntitlement" - case .notAuthenticated: "notAuthenticated" - case .permissionFailure: "permissionFailure" - case .unknownItem: "unknownItem" - case .invalidArguments: "invalidArguments" - case .resultsTruncated: "resultsTruncated" - case .serverRecordChanged: "serverRecordChanged" - case .serverRejectedRequest: "serverRejectedRequest" - case .assetFileNotFound: "assetFileNotFound" - case .assetFileModified: "assetFileModified" - case .incompatibleVersion: "incompatibleVersion" - case .constraintViolation: "constraintViolation" - case .operationCancelled: "operationCancelled" - case .changeTokenExpired: "changeTokenExpired" - case .batchRequestFailed: "batchRequestFailed" - case .zoneBusy: "zoneBusy" - case .badDatabase: "badDatabase" - case .quotaExceeded: "quotaExceeded" - case .zoneNotFound: "zoneNotFound" - case .limitExceeded: "limitExceeded" - case .userDeletedZone: "userDeletedZone" - case .tooManyParticipants: "tooManyParticipants" - case .alreadyShared: "alreadyShared" - case .referenceViolation: "referenceViolation" - case .managedAccountRestricted: "managedAccountRestricted" - case .participantMayNeedVerification: "participantMayNeedVerification" - case .serverResponseLost: "serverResponseLost" - case .assetNotAvailable: "assetNotAvailable" - case .accountTemporarilyUnavailable: "accountTemporarilyUnavailable" - @unknown default: "unknown" - } - } - let error = errorType.map { "\n ❌ \($0)" } ?? "" + let error = error.map(\.code.type).map { "\n ❌ \($0)" } ?? "" debug( """ \(prefix) willFetchRecordZoneChanges @@ -231,6 +144,279 @@ extension Logger { } } +#if SharingGRDBSwiftLog + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + extension Logging.Logger { + func log(_ event: SyncEngine.Event, syncEngine: any SyncEngineProtocol) { + var metadata: Logging.Logger.Metadata = [ + "databaseScope.label": "\(syncEngine.database.databaseScope.label)" + ] + switch event { + case .stateUpdate: + debug("stateUpdate", metadata: metadata) + case .accountChange(let changeType): + switch changeType { + case .signIn(let currentUser): + metadata["currentUser"] = "\(currentUser.recordName).\(currentUser.zoneID.ownerName).\(currentUser.zoneID.zoneName)" + debug("signIn", metadata: metadata) + case .signOut(let previousUser): + metadata["previousUser"] = "\(previousUser.recordName).\(previousUser.zoneID.ownerName).\(previousUser.zoneID.zoneName)" + debug("signOut", metadata: metadata) + case .switchAccounts(let previousUser, let currentUser): + metadata["currentUser"] = "\(currentUser.recordName).\(currentUser.zoneID.ownerName).\(currentUser.zoneID.zoneName)" + metadata["previousUser"] = "\(previousUser.recordName).\(previousUser.zoneID.ownerName).\(previousUser.zoneID.zoneName)" + debug("switchAccounts", metadata: metadata) + @unknown default: + debug("unknown", metadata: metadata) + } + case .fetchedDatabaseChanges(_, let deletions): + metadata["zones.deleted"] = "\(deletedZones(ids: deletions.map(\.zoneID)))" + debug("fetchedDatabaseChanges", metadata: metadata) + case .fetchedRecordZoneChanges(let modifications, let deletions): + let (modifications, deletions) = fetchedRecordZoneChanges( + modifications: modifications, + deletions: deletions + ) + metadata["records.modifications"] = "\(modifications)" + metadata["records.deleted"] = "\(deletions)" + debug("fetchedRecordZoneChanges", metadata: metadata) + case .sentDatabaseChanges( + let savedZones, + let failedZoneSaves, + let deletedZoneIDs, + let failedZoneDeletes + ): + let (savedZones, deletedZones, failedZoneSaves, failedZoneDeletes) = sentDatabaseChanges( + savedZones: savedZones, + failedZoneSaves: failedZoneSaves, + deletedZoneIDs: deletedZoneIDs, + failedZoneDeletes: failedZoneDeletes + ) + metadata["zones.saved"] = "\(savedZones)" + metadata["zones.deleted"] = "\(deletedZones)" + metadata["zones.failed.saves"] = "\(failedZoneSaves)" + metadata["zones.failed.deletes"] = "\(failedZoneDeletes)" + debug("sentDatabaseChanges", metadata: metadata) + case .sentRecordZoneChanges( + let savedRecords, + let failedRecordSaves, + let deletedRecordIDs, + let failedRecordDeletes + ): + let ( + savedRecords, + deletedRecords, + failedRecordSaves, + failedRecordDeletes + ) = sentRecordZoneChanges( + savedRecords: savedRecords, + failedRecordSaves: failedRecordSaves, + deletedRecordIDs: deletedRecordIDs, + failedRecordDeletes: failedRecordDeletes + ) + metadata["records.saves"] = "\(savedRecords)" + metadata["records.deletes"] = "\(deletedRecords)" + metadata["records.failed.saves"] = "\(failedRecordSaves)" + metadata["records.failed.deletes"] = "\(failedRecordDeletes)" + debug("sentRecordZoneChanges", metadata: metadata) + case .willFetchChanges: + debug("willFetchChanges", metadata: defaultMetadata) + case .willFetchRecordZoneChanges(let zoneID): + metadata["zone"] = "\(zoneID.zoneName):\(zoneID.ownerName)" + debug("willFetchRecordZoneChanges", metadata: metadata) + case .didFetchRecordZoneChanges(let zoneID, let error): + metadata["zone"] = "\(zoneID.zoneName):\(zoneID.ownerName)" + if let error { + metadata["error"] = "\(error.code.type)" + } + debug("willFetchRecordZoneChanges", metadata: metadata) + case .didFetchChanges: + debug("didFetchChanges", metadata: defaultMetadata) + case .willSendChanges(let context): + metadata["context.reason"] = "\(context.reason.description)" + debug("willSendChanges", metadata: metadata) + case .didSendChanges(let context): + metadata["context.reason"] = "\(context.reason.description)" + debug("didSendChanges", metadata: metadata) + @unknown default: + metadata["event"] = "\(event.description)" + warning("⚠️ unknown event", metadata: metadata) + } + } + } +#endif + +private func deletedZones(ids: [CKRecordZone.ID]) -> String { + ids.isEmpty + ? "⚪️ No deletions" + : "✅ Zones deleted (\(ids.count)): " + + ids + .map { $0.zoneName + ":" + $0.ownerName } + .sorted() + .joined(separator: ", ") +} + +private func fetchedRecordZoneChanges( + modifications: [CKRecord], + deletions: [(recordID: CKRecord.ID, recordType: CKRecord.RecordType)] +) -> (modifications: String, deletions: String) { + let deletionsByRecordType = Dictionary( + grouping: deletions, + by: \.recordType + ) + let recordTypeDeletions = deletionsByRecordType.keys.sorted() + .map { recordType in "\(recordType) (\(deletionsByRecordType[recordType]!.count))" } + .joined(separator: ", ") + let deletions = + deletions.isEmpty + ? "⚪️ No deletions" : "✅ Records deleted (\(deletions.count)): \(recordTypeDeletions)" + + let modificationsByRecordType = Dictionary( + grouping: modifications, + by: \.recordType + ) + let recordTypeModifications = modificationsByRecordType.keys.sorted() + .map { recordType in "\(recordType) (\(modificationsByRecordType[recordType]!.count))" } + .joined(separator: ", ") + let modifications = + modifications.isEmpty + ? "⚪️ No modifications" + : "✅ Records modified (\(modifications.count)): \(recordTypeModifications)" + return (modifications, deletions) +} + +private func sentDatabaseChanges( + savedZones: [CKRecordZone], + failedZoneSaves: [(zone: CKRecordZone, error: CKError)], + deletedZoneIDs: [CKRecordZone.ID], + failedZoneDeletes: [CKRecordZone.ID: CKError] +) -> ( + savedZones: String, + deletedZones: String, + failedZoneSaves: String, + failedZoneDeletes: String +) { + let savedZoneNames = savedZones + .map { $0.zoneID.zoneName + ":" + $0.zoneID.ownerName } + .sorted() + .joined(separator: ", ") + let savedZones = + savedZones.isEmpty + ? "⚪️ No saved zones" : "✅ Saved zones (\(savedZones.count)): \(savedZoneNames)" + + let deletedZoneNames = deletedZoneIDs + .map { $0.zoneName } + .sorted() + .joined(separator: ", ") + let deletedZones = + deletedZoneIDs.isEmpty + ? "⚪️ No deleted zones" + : "✅ Deleted zones (\(deletedZoneIDs.count)): \(deletedZoneNames)" + + let failedZoneSaveNames = failedZoneSaves + .map { $0.zone.zoneID.zoneName + ":" + $0.zone.zoneID.ownerName } + .sorted() + .joined(separator: ", ") + let failedZoneSaves = + failedZoneSaves.isEmpty + ? "⚪️ No failed saved zones" + : "🛑 Failed zone saves (\(failedZoneSaves.count)): \(failedZoneSaveNames)" + + let failedZoneDeleteNames = failedZoneDeletes + .keys + .map { $0.zoneName } + .sorted() + .joined(separator: ", ") + let failedZoneDeletes = + failedZoneDeletes.isEmpty + ? "⚪️ No failed deleted zones" + : "🛑 Failed zone delete (\(failedZoneDeletes.count)): \(failedZoneDeleteNames)" + return (savedZones, deletedZones, failedZoneSaves, failedZoneDeletes) +} + +private func sentRecordZoneChanges( + savedRecords: [CKRecord], + failedRecordSaves: [(record: CKRecord, error: CKError)], + deletedRecordIDs: [CKRecord.ID], + failedRecordDeletes: [CKRecord.ID: CKError] +) -> ( + savedRecords: String, + deletedRecords: String, + failedRecordSaves: String, + failedRecordDeletes: String +) { + let savedRecordsByRecordType = Dictionary( + grouping: savedRecords, + by: \.recordType + ) + let savedRecords = savedRecordsByRecordType.keys + .sorted() + .map { "\($0) (\(savedRecordsByRecordType[$0]!.count))" } + .joined(separator: ", ") + + let failedRecordSavesByZoneName = Dictionary( + grouping: failedRecordSaves, + by: { $0.record.recordID.zoneID.zoneName + ":" + $0.record.recordID.zoneID.ownerName } + ) + let failedRecordSaves = failedRecordSavesByZoneName.keys + .sorted() + .map { "\($0) (\(failedRecordSavesByZoneName[$0]!.count))" } + .joined(separator: ", ") + let savedRecordsMessage = savedRecordsByRecordType.isEmpty + ? "⚪️ No records saved" : "✅ Saved records: \(savedRecords)" + let deletedRecords = deletedRecordIDs.isEmpty + ? "⚪️ No records deleted" : "✅ Deleted records (\(deletedRecordIDs.count))" + let failedRecordSavesMessage = failedRecordSavesByZoneName.isEmpty + ? "⚪️ No records failed save" : "🛑 Records failed save: \(failedRecordSaves)" + let failedRecordDeletes = failedRecordDeletes.isEmpty + ? "⚪️ No records failed delete" : "🛑 Records failed delete (\(failedRecordDeletes.count))" + return (savedRecordsMessage, deletedRecords, failedRecordSavesMessage, failedRecordDeletes) +} + +extension CKError.Code { + fileprivate var type: String { + switch self { + case .internalError: "internalError" + case .partialFailure: "partialFailure" + case .networkUnavailable: "networkUnavailable" + case .networkFailure: "networkFailure" + case .badContainer: "badContainer" + case .serviceUnavailable: "serviceUnavailable" + case .requestRateLimited: "requestRateLimited" + case .missingEntitlement: "missingEntitlement" + case .notAuthenticated: "notAuthenticated" + case .permissionFailure: "permissionFailure" + case .unknownItem: "unknownItem" + case .invalidArguments: "invalidArguments" + case .resultsTruncated: "resultsTruncated" + case .serverRecordChanged: "serverRecordChanged" + case .serverRejectedRequest: "serverRejectedRequest" + case .assetFileNotFound: "assetFileNotFound" + case .assetFileModified: "assetFileModified" + case .incompatibleVersion: "incompatibleVersion" + case .constraintViolation: "constraintViolation" + case .operationCancelled: "operationCancelled" + case .changeTokenExpired: "changeTokenExpired" + case .batchRequestFailed: "batchRequestFailed" + case .zoneBusy: "zoneBusy" + case .badDatabase: "badDatabase" + case .quotaExceeded: "quotaExceeded" + case .zoneNotFound: "zoneNotFound" + case .limitExceeded: "limitExceeded" + case .userDeletedZone: "userDeletedZone" + case .tooManyParticipants: "tooManyParticipants" + case .alreadyShared: "alreadyShared" + case .referenceViolation: "referenceViolation" + case .managedAccountRestricted: "managedAccountRestricted" + case .participantMayNeedVerification: "participantMayNeedVerification" + case .serverResponseLost: "serverResponseLost" + case .assetNotAvailable: "assetNotAvailable" + case .accountTemporarilyUnavailable: "accountTemporarilyUnavailable" + @unknown default: "unknown" + } + } +} + extension CKDatabase.Scope { var label: String { switch self { diff --git a/Sources/SharingGRDBCore/CloudKit/Metadatabase.swift b/Sources/SharingGRDBCore/CloudKit/Metadatabase.swift index fc4e2e90..2ad33c65 100644 --- a/Sources/SharingGRDBCore/CloudKit/Metadatabase.swift +++ b/Sources/SharingGRDBCore/CloudKit/Metadatabase.swift @@ -2,6 +2,10 @@ import Foundation import os +#if SharingGRDBSwiftLog + import Logging +#endif + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) func defaultMetadatabase( logger: Logger, @@ -9,16 +13,35 @@ func defaultMetadatabase( ) throws -> any DatabaseReader { var configuration = Configuration() configuration.prepareDatabase { [logger] db in - db.trace { - logger.trace("\($0.expandedDescription)") + db.trace { event in + switch logger { + case .osLogger(let logger): + logger.trace("\(event.expandedDescription)") + #if SharingGRDBSwiftLog + case .swiftLogger(let logger): + logger.trace("\(event.expandedDescription)") + #endif + } } } - logger.debug( - """ - Metadatabase connection: - open "\(url.path(percentEncoded: false))" - """ - ) + + switch logger { + case .osLogger(let logger): + logger.debug( + """ + Metadatabase connection: + open "\(url.path(percentEncoded: false))" + """ + ) + #if SharingGRDBSwiftLog + case .swiftLogger(let logger): + logger.debug( + "Metadatabase connection: Open", + metadata: ["connection.url": "\(url.path(percentEncoded: false))"] + ) + #endif + } + try FileManager.default.createDirectory( at: .applicationSupportDirectory, withIntermediateDirectories: true diff --git a/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift b/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift index b0d8ea73..a55ce4ad 100644 --- a/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift +++ b/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift @@ -8,8 +8,22 @@ import StructuredQueriesCore import SwiftData + #if SharingGRDBSwiftLog + import Logging + #endif + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) public final class SyncEngine: Sendable { + #if SharingGRDBSwiftLog + public static let defaultLogger: Logger = isTesting + ? .swiftLogger(Logging.Logger(label: "disabled") { _ in SwiftLogNoOpLogHandler() }) + : .swiftLogger(Logging.Logger(label: "cloudkit.sqlite.data")) + #else + public static let defaultLogger: Logger = isTesting + ? .osLogger(os.Logger(.disabled)) + : .osLogger(os.Logger(subsystem: "SQLiteData", category: "CloudKit")) + #endif + package let userDatabase: UserDatabase package let logger: Logger package let metadatabase: any DatabaseReader @@ -26,15 +40,14 @@ package let container: any CloudContainer let dataManager = Dependency(\.dataManager) public static let writePermissionError = "co.pointfree.sqlitedata-icloud.write-permission-error" - + public convenience init( for database: any DatabaseWriter, tables: repeat (each T1).Type, privateTables: repeat (each T2).Type, containerIdentifier: String? = nil, defaultZone: CKRecordZone = CKRecordZone(zoneName: "co.pointfree.SQLiteData.defaultZone"), - logger: Logger = isTesting - ? Logger(.disabled) : Logger(subsystem: "SQLiteData", category: "CloudKit") + logger: Logger = defaultLogger ) throws where repeat (each T1).PrimaryKey.QueryOutput: IdentifierStringConvertible, @@ -652,14 +665,31 @@ keyValue in strings += ["\(keyValue.key) (\(keyValue.value.count))"] } .joined(separator: ", ") - logger.debug( - """ - [\(syncEngine.database.databaseScope.label)] nextRecordZoneChangeBatch: \(reason) - \(state.missingTables.isEmpty ? "⚪️ No missing tables" : "⚠️ Missing tables: \(missingTables)") - \(state.missingRecords.isEmpty ? "⚪️ No missing records" : "⚠️ Missing records: \(missingRecords)") - \(state.sentRecords.isEmpty ? "⚪️ No sent records" : "✅ Sent records: \(sentRecords)") - """ - ) + + switch logger { + case .osLogger(let logger): + logger.debug( + """ + [\(syncEngine.database.databaseScope.label)] nextRecordZoneChangeBatch: \(reason) + \(state.missingTables.isEmpty ? "⚪️ No missing tables" : "⚠️ Missing tables: \(missingTables)") + \(state.missingRecords.isEmpty ? "⚪️ No missing records" : "⚠️ Missing records: \(missingRecords)") + \(state.sentRecords.isEmpty ? "⚪️ No sent records" : "✅ Sent records: \(sentRecords)") + """ + ) + #if SharingGRDBSwiftLog + case .swiftLogger(let logger): + logger.debug( + "nextRecordZoneChangeBatch", + metadata: [ + "databaseScope.label": "\(syncEngine.database.databaseScope.label)", + "syncEngine.reason": "\(reason)", + "missing.tables": state.missingTables.isEmpty ? "⚪️ No missing tables" : "⚠️ Missing tables: \(missingTables)", + "missing.records": state.missingRecords.isEmpty ? "⚪️ No missing records" : "⚠️ Missing records: \(missingRecords)", + "sent.records": state.sentRecords.isEmpty ? "⚪️ No sent records" : "✅ Sent records: \(sentRecords)" + ] + ) + #endif + } } #endif diff --git a/Sources/SharingGRDBCore/Traits/SwiftLog.swift b/Sources/SharingGRDBCore/Traits/SwiftLog.swift new file mode 100644 index 00000000..36f17b4d --- /dev/null +++ b/Sources/SharingGRDBCore/Traits/SwiftLog.swift @@ -0,0 +1,14 @@ +import os +import IssueReporting + +#if SharingGRDBSwiftLog + import Logging +#endif + +@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) +public enum Logger: Sendable { + case osLogger(os.Logger) + #if SharingGRDBSwiftLog + case swiftLogger(Logging.Logger) + #endif +} diff --git a/Tests/SharingGRDBTests/Internal/BaseCloudKitTests.swift b/Tests/SharingGRDBTests/Internal/BaseCloudKitTests.swift index 00b5419b..a5d93a16 100644 --- a/Tests/SharingGRDBTests/Internal/BaseCloudKitTests.swift +++ b/Tests/SharingGRDBTests/Internal/BaseCloudKitTests.swift @@ -156,7 +156,7 @@ extension SyncEngine { ) }, userDatabase: userDatabase, - logger: Logger(.disabled), + logger: .osLogger(os.Logger(.disabled)), tables: tables, privateTables: privateTables )