diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 8e7e2984db8564b066fbe84cec6094b37838f6e3..30d0173a710e8da3e70ba9d62809ce8f3da4d60e 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -56,6 +56,10 @@ let package = Package( url: "https://github.com/pointfreeco/swift-custom-dump.git", .upToNextMajor(from: "0.5.2") ), + .package( + url: "https://github.com/apple/swift-log.git", + .upToNextMajor(from: "1.4.4") + ), .package( url: "https://github.com/kean/Pulse.git", .upToNextMajor(from: "2.1.2") @@ -65,7 +69,7 @@ let package = Package( .target( name: "AppCore", dependencies: [ - .product(name: "Pulse", package: "Pulse"), + .product(name: "Logging", package: "swift-log"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXDatabase", package: "client-ios-db"), @@ -104,6 +108,8 @@ let package = Package( .target(name: "WelcomeFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ComposablePresentation", package: "swift-composable-presentation"), + .product(name: "Logging", package: "swift-log"), + .product(name: "PulseLogHandler", package: "Pulse"), .product(name: "PulseUI", package: "Pulse"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXModels", package: "client-ios-db"), diff --git a/Examples/xx-messenger/Sources/AppCore/Logger/Logger.swift b/Examples/xx-messenger/Sources/AppCore/Logger/Logger.swift index 6766d98d4f6ef506112e5cee8d6efa060ec9b214..128d31124ce1e51ab89c023c3cac258c2d6a9182 100644 --- a/Examples/xx-messenger/Sources/AppCore/Logger/Logger.swift +++ b/Examples/xx-messenger/Sources/AppCore/Logger/Logger.swift @@ -1,5 +1,5 @@ import Foundation -import Pulse +import Logging import XCTestDynamicOverlay public struct Logger { @@ -21,14 +21,12 @@ public struct Logger { extension Logger { public static func live() -> Logger { - Logger { msg, file, function, line in + let logger = Logging.Logger(label: "xx.network.XXMessengerExample") + return Logger { msg, file, function, line in switch msg { case .error(let error): - LoggerStore.shared.storeMessage( - label: "xx-messenger", - level: .error, - message: error.localizedDescription, - metadata: [:], + logger.error( + .init(stringLiteral: error.localizedDescription), file: file, function: function, line: line diff --git a/Examples/xx-messenger/Sources/AppFeature/App.swift b/Examples/xx-messenger/Sources/AppFeature/App.swift index d6b0b722c4d610d823b5313aae797e3f72dc10a3..7c3f0e82b5b00ee3aa4ff601b50556dbc8a148d2 100644 --- a/Examples/xx-messenger/Sources/AppFeature/App.swift +++ b/Examples/xx-messenger/Sources/AppFeature/App.swift @@ -1,8 +1,14 @@ import ComposableArchitecture +import Logging +import PulseLogHandler import SwiftUI @main struct App: SwiftUI.App { + init() { + LoggingSystem.bootstrap(PersistentLogHandler.init) + } + var body: some Scene { WindowGroup { AppView(store: Store( diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index fef073d1e4844f71b974de12c5c37ee7c988ba00..c4ff9b27f027a0eef0d02eaeab6e8e7db519025c 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -34,6 +34,11 @@ extension AppEnvironment { let mainQueue = DispatchQueue.main.eraseToAnyScheduler() let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler() + defer { + _ = try! messenger.setLogLevel(.debug) + messenger.startLogging() + } + let contactEnvironment = ContactEnvironment( messenger: messenger, db: dbManager.getDB, diff --git a/Package.resolved b/Package.resolved index 225eb0fdb5e6eb944de29fa3bc030fed309735c0..0e663ab05e42e13ad7bd42275e604f72a8041347 100644 --- a/Package.resolved +++ b/Package.resolved @@ -18,6 +18,15 @@ "version" : "0.5.2" } }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c", + "version" : "1.4.4" + } + }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 9136971583d840fd35a2993ec5be731e8ac774c4..cd25ca17a71b46a3f2dfe1d01374d552fb429475 100644 --- a/Package.swift +++ b/Package.swift @@ -31,6 +31,10 @@ let package = Package( url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMajor(from: "4.2.2") ), + .package( + url: "https://github.com/apple/swift-log.git", + .upToNextMajor(from: "1.4.4") + ), ], targets: [ .target( @@ -55,6 +59,7 @@ let package = Package( dependencies: [ .target(name: "XXClient"), .product(name: "KeychainAccess", package: "KeychainAccess"), + .product(name: "Logging", package: "swift-log"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ], swiftSettings: swiftSettings diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerSetLogLevel.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerSetLogLevel.swift new file mode 100644 index 0000000000000000000000000000000000000000..21e39bd78035438955587c816602956313389fa4 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerSetLogLevel.swift @@ -0,0 +1,22 @@ +import XCTestDynamicOverlay +import XXClient + +public struct MessengerSetLogLevel { + public var run: (LogLevel) throws -> Bool + + public func callAsFunction(_ logLevel: LogLevel) throws -> Bool { + try run(logLevel) + } +} + +extension MessengerSetLogLevel { + public static func live(_ env: MessengerEnvironment) -> MessengerSetLogLevel { + MessengerSetLogLevel(run: env.setLogLevel.run) + } +} + +extension MessengerSetLogLevel { + public static let unimplemented = MessengerSetLogLevel( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerStartLogging.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartLogging.swift new file mode 100644 index 0000000000000000000000000000000000000000..d073ee41cd1c954b41395d2c0de4760d94ed4b5a --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartLogging.swift @@ -0,0 +1,29 @@ +import Foundation +import Logging +import XCTestDynamicOverlay +import XXClient + +public struct MessengerStartLogging { + public var run: () -> Void + + public func callAsFunction() -> Void { + run() + } +} + +extension MessengerStartLogging { + public static func live(_ env: MessengerEnvironment) -> MessengerStartLogging { + return MessengerStartLogging { + env.registerLogWriter(.init { messageString in + let message = LogMessage.parse(messageString) + env.log(message) + }) + } + } +} + +extension MessengerStartLogging { + public static let unimplemented = MessengerStartLogging( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift index 692d81fd9bdb7ee8cb540b9a4f1a844e7541966d..22f110303674337006b77b2f1de436a09d0392b1 100644 --- a/Sources/XXMessengerClient/Messenger/Messenger.swift +++ b/Sources/XXMessengerClient/Messenger/Messenger.swift @@ -38,6 +38,8 @@ public struct Messenger { public var resumeBackup: MessengerResumeBackup public var backupParams: MessengerBackupParams public var stopBackup: MessengerStopBackup + public var setLogLevel: MessengerSetLogLevel + public var startLogging: MessengerStartLogging } extension Messenger { @@ -79,7 +81,9 @@ extension Messenger { startBackup: .live(env), resumeBackup: .live(env), backupParams: .live(env), - stopBackup: .live(env) + stopBackup: .live(env), + setLogLevel: .live(env), + startLogging: .live(env) ) } } @@ -122,6 +126,8 @@ extension Messenger { startBackup: .unimplemented, resumeBackup: .unimplemented, backupParams: .unimplemented, - stopBackup: .unimplemented + stopBackup: .unimplemented, + setLogLevel: .unimplemented, + startLogging: .unimplemented ) } diff --git a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift index f019df1618b0985f70936fdeb97e4558060a46eb..a98e76f042b5e53a6e8556d3bee32d7ebb946162 100644 --- a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift +++ b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift @@ -1,4 +1,5 @@ import Foundation +import Logging import XXClient import XCTestDynamicOverlay @@ -18,6 +19,7 @@ public struct MessengerEnvironment { public var isListeningForMessages: Stored<Bool> public var isRegisteredWithUD: IsRegisteredWithUD public var loadCMix: LoadCMix + public var log: (LogMessage) -> Void public var login: Login public var lookupUD: LookupUD public var messageListeners: ListenersRegistry @@ -29,8 +31,10 @@ public struct MessengerEnvironment { public var newUdManagerFromBackup: NewUdManagerFromBackup public var passwordStorage: PasswordStorage public var registerForNotifications: RegisterForNotifications + public var registerLogWriter: RegisterLogWriter public var resumeBackup: ResumeBackup public var searchUD: SearchUD + public var setLogLevel: SetLogLevel public var sleep: (TimeInterval) -> Void public var storageDir: String public var ud: Stored<UserDiscovery?> @@ -47,7 +51,9 @@ extension MessengerEnvironment { .path public static func live() -> MessengerEnvironment { - MessengerEnvironment( + let logger = Logger(label: "xx.network.client") + + return MessengerEnvironment( authCallbacks: .live(), backup: .inMemory(), backupCallbacks: .live(), @@ -63,6 +69,7 @@ extension MessengerEnvironment { isListeningForMessages: .inMemory(false), isRegisteredWithUD: .live, loadCMix: .live, + log: { logger.log(level: $0.level, .init(stringLiteral: $0.text)) }, login: .live, lookupUD: .live, messageListeners: .live(), @@ -74,8 +81,10 @@ extension MessengerEnvironment { newUdManagerFromBackup: .live, passwordStorage: .keychain, registerForNotifications: .live, + registerLogWriter: .live, resumeBackup: .live, searchUD: .live, + setLogLevel: .live, sleep: { Thread.sleep(forTimeInterval: $0) }, storageDir: MessengerEnvironment.defaultStorageDir, ud: .inMemory(), @@ -103,6 +112,7 @@ extension MessengerEnvironment { isListeningForMessages: .unimplemented(placeholder: false), isRegisteredWithUD: .unimplemented, loadCMix: .unimplemented, + log: XCTUnimplemented("\(Self.self).log"), login: .unimplemented, lookupUD: .unimplemented, messageListeners: .unimplemented, @@ -114,8 +124,10 @@ extension MessengerEnvironment { newUdManagerFromBackup: .unimplemented, passwordStorage: .unimplemented, registerForNotifications: .unimplemented, + registerLogWriter: .unimplemented, resumeBackup: .unimplemented, searchUD: .unimplemented, + setLogLevel: .unimplemented, sleep: XCTUnimplemented("\(Self.self).sleep"), storageDir: "unimplemented", ud: .unimplemented(), diff --git a/Sources/XXMessengerClient/Utils/LogMessage.swift b/Sources/XXMessengerClient/Utils/LogMessage.swift new file mode 100644 index 0000000000000000000000000000000000000000..7169d953a60109e654c6d7063cbbe3b1857394cd --- /dev/null +++ b/Sources/XXMessengerClient/Utils/LogMessage.swift @@ -0,0 +1,56 @@ +import Foundation +import Logging + +public struct LogMessage: Equatable { + public init(level: Logger.Level, text: String) { + self.level = level + self.text = text + } + + public var level: Logger.Level + public var text: String +} + +extension LogMessage { + public static func parse(_ string: String) -> LogMessage { + let level: Logger.Level + let text: String + let pattern = #"^([A-Z]+)( \d{4}/\d{2}/\d{2})?( \d{1,2}:\d{2}:\d{2}\.\d+)? (.*)"# + let regex = try! NSRegularExpression( + pattern: pattern, + options: .dotMatchesLineSeparators + ) + let stringRange = NSRange(location: 0, length: string.utf16.count) + if let match = regex.firstMatch(in: string, range: stringRange) { + var groups: [Int: String] = [:] + for rangeIndex in 1..<match.numberOfRanges { + let nsRange = match.range(at: rangeIndex) + if !NSEqualRanges(nsRange, NSMakeRange(NSNotFound, 0)) { + let group = (string as NSString).substring(with: nsRange) + groups[rangeIndex] = group + } + } + level = .fromString(groups[1]) + text = groups[4] ?? string + } else { + level = .notice + text = string + } + return LogMessage(level: level, text: text) + } +} + +private extension Logger.Level { + static func fromString(_ string: String?) -> Logger.Level { + switch string { + case "TRACE": return .trace + case "DEBUG": return .debug + case "INFO": return .info + case "WARN": return .warning + case "ERROR": return .error + case "CRITICAL": return .critical + case "FATAL": return .critical + default: return .notice + } + } +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSetLogLevelTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSetLogLevelTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..d6d44d05465f52cc6c9cb7b0a949397d22560b42 --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSetLogLevelTests.swift @@ -0,0 +1,44 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerSetLogLevelTests: XCTestCase { + func testSetLogLevel() throws { + var didSetLogLevel: [LogLevel] = [] + var env: MessengerEnvironment = .unimplemented + env.setLogLevel.run = { level in + didSetLogLevel.append(level) + return true + } + let setLogLevel: MessengerSetLogLevel = .live(env) + + let result = try setLogLevel(.debug) + + XCTAssertNoDifference(didSetLogLevel, [.debug]) + XCTAssertNoDifference(result, true) + } + + func testSetLogLevelReturnsFalse() throws { + var env: MessengerEnvironment = .unimplemented + env.setLogLevel.run = { _ in return false } + let setLogLevel: MessengerSetLogLevel = .live(env) + + let result = try setLogLevel(.debug) + + XCTAssertNoDifference(result, false) + } + + func testSetLogLevelFailure() { + struct Failure: Error, Equatable {} + let failure = Failure() + + var env: MessengerEnvironment = .unimplemented + env.setLogLevel.run = { _ in throw failure } + let setLogLevel: MessengerSetLogLevel = .live(env) + + XCTAssertThrowsError(try setLogLevel(.debug)) { error in + XCTAssertNoDifference(error as NSError, failure as NSError) + } + } +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartLoggingTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartLoggingTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..b0d0a7c35453348c2f56b4e5e13c973c6fbc639a --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartLoggingTests.swift @@ -0,0 +1,30 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerStartLoggingTests: XCTestCase { + func testStartLogging() { + var registeredLogWriters: [LogWriter] = [] + var logMessages: [LogMessage] = [] + + var env: MessengerEnvironment = .unimplemented + env.registerLogWriter.run = { writer in + registeredLogWriters.append(writer) + } + env.log = { message in + logMessages.append(message) + } + let start: MessengerStartLogging = .live(env) + + start() + + XCTAssertNoDifference(registeredLogWriters.count, 1) + + registeredLogWriters.first?.handle("DEBUG Hello, World!") + + XCTAssertNoDifference(logMessages, [ + .init(level: .debug, text: "Hello, World!"), + ]) + } +} diff --git a/Tests/XXMessengerClientTests/Utils/LogMessageTests.swift b/Tests/XXMessengerClientTests/Utils/LogMessageTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..bbea8995439aec672f0ebddf778484cf7889f0ee --- /dev/null +++ b/Tests/XXMessengerClientTests/Utils/LogMessageTests.swift @@ -0,0 +1,71 @@ +import CustomDump +import XCTest +@testable import XXMessengerClient + +final class LogMessageTests: XCTestCase { + func testParsing() { + XCTAssertNoDifference( + LogMessage.parse("TRACE Tracing..."), + LogMessage(level: .trace, text: "Tracing...") + ) + XCTAssertNoDifference( + LogMessage.parse("DEBUG Debugging..."), + LogMessage(level: .debug, text: "Debugging...") + ) + XCTAssertNoDifference( + LogMessage.parse("INFO Informing..."), + LogMessage(level: .info, text: "Informing...") + ) + XCTAssertNoDifference( + LogMessage.parse("WARN Warning!"), + LogMessage(level: .warning, text: "Warning!") + ) + XCTAssertNoDifference( + LogMessage.parse("ERROR Failure!"), + LogMessage(level: .error, text: "Failure!") + ) + XCTAssertNoDifference( + LogMessage.parse("CRITICAL Critical failure!"), + LogMessage(level: .critical, text: "Critical failure!") + ) + XCTAssertNoDifference( + LogMessage.parse("FATAL Fatal failure!"), + LogMessage(level: .critical, text: "Fatal failure!") + ) + } + + func testParsingFallbacks() { + XCTAssertNoDifference( + LogMessage.parse("1234 Wrongly formatted"), + LogMessage(level: .notice, text: "1234 Wrongly formatted") + ) + } + + func testParsingStripsDateTime() { + XCTAssertNoDifference( + LogMessage.parse("INFO 2022/10/04 Informing..."), + LogMessage(level: .info, text: "Informing...") + ) + XCTAssertNoDifference( + LogMessage.parse("INFO 23:36:55.755390 Informing..."), + LogMessage(level: .info, text: "Informing...") + ) + XCTAssertNoDifference( + LogMessage.parse("INFO 2022/10/04 23:36:55.755390 Informing..."), + LogMessage(level: .info, text: "Informing...") + ) + } + + func testParsingMultilineMessage() { + XCTAssertNoDifference( + LogMessage.parse(""" + ERROR 2022/10/04 23:51:15.021658 First line + Second line + """), + LogMessage(level: .error, text: """ + First line + Second line + """) + ) + } +}