From 863e3fc8faeec9458fec732ee3b8735c7517938a Mon Sep 17 00:00:00 2001 From: stoicswe <31823043+stoicswe@users.noreply.github.com> Date: Thu, 23 May 2024 22:18:41 -0400 Subject: [PATCH 1/9] added a logging impl --- .../Utilities/Logging/Logging.swift | 71 +++++++++++++++---- 1 file changed, 56 insertions(+), 15 deletions(-) diff --git a/Sources/ATProtoKit/Utilities/Logging/Logging.swift b/Sources/ATProtoKit/Utilities/Logging/Logging.swift index 41e13848f17..93a7157b7ef 100644 --- a/Sources/ATProtoKit/Utilities/Logging/Logging.swift +++ b/Sources/ATProtoKit/Utilities/Logging/Logging.swift @@ -5,24 +5,51 @@ // Created by Christopher Jr Riley on 2024-04-04. // -import Logging - -#if canImport(os) +// Choose the logging library based on the +// platform we are working with +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) +// If the platform is based on an Apple-product +// then we will use the default-provided Apple logger import os +#endif +// This library will be used regardless for the log levels +import Logging +// Define the ATLogHandler +// The log handler will automatically choose +// the correct logging library based on the framework struct ATLogHandler: LogHandler { + // Subsystem is the component within the ATProto + // Library that will be logged public let subsystem: String + // The category is a meta-data field for the log to + // help provide more context in-line public let category: String + // The default log level, if not provided public var logLevel: Logging.Logger.Level = .info public var metadata: Logging.Logger.Metadata = [:] +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) + // if on an apple platform, we will use the default lib private var appleLogger: os.Logger +#else + // Otherwise, use the cross-platform logging lib + private var appleLogger: Logging.StreamLogHandler +#endif + // Init the Logger Library init(subsystem: String, category: String? = nil) { self.subsystem = subsystem self.category = category ?? "ATProtoKit" +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) + // Using the apple logger built-into the Apple OSs self.appleLogger = Logger(subsystem: subsystem, category: category ?? "ATProtoKit") +#else + // Otherwise, use the cross-platform logging lib + self.appleLogger = Logging.StreamLogHandler(label: "\(subsystem) \(category ?? "ATProtoKit")") +#endif } + // The logger override function that will actually do the mapping public func log(level: Logging.Logger.Level, message: Logging.Logger.Message, metadata explicitMetadata: Logging.Logger.Metadata?, @@ -30,29 +57,43 @@ struct ATLogHandler: LogHandler { file: String, function: String, line: UInt) { -// let allMetadata = self.metadata.merging(metadata ?? [:]) { _, new in new } -// var messageMetadata = [String: Any]() -// var privacySettings = [String: OSLogPrivacy]() - + + // Obtain all the metadata between the standard and the incoming + let allMetadata = self.metadata.merging(explicitMetadata ?? [:]) { (current, new) in + return new + } + +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) + // Set a log msg prefix made to resemble the alt platform logger + let logMsgPrefix = "\(allMetadata) [\(source)]" + // Map the loglevels to match what the apple logger would expect switch level { - case .trace, .debug: - appleLogger.log(level: .debug, "\(message, privacy: .auto)") + case .trace: + appleLogger.trace("\(logMsgPrefix) \(message, privacy: .auto)") + case .debug: + appleLogger.debug("\(logMsgPrefix) \(message, privacy: .auto)") case .info: - appleLogger.log(level: .info, "\(message, privacy: .auto)") + appleLogger.info("\(logMsgPrefix) \(message, privacy: .auto)") case .notice: - appleLogger.log(level: .default, "\(message, privacy: .auto)") + appleLogger.notice("\(logMsgPrefix) \(message, privacy: .auto)") case .warning: - appleLogger.log(level: .error, "\(message, privacy: .auto)") + appleLogger.warning("\(logMsgPrefix) \(message, privacy: .auto)") case .error: - appleLogger.log(level: .error, "\(message, privacy: .auto)") + appleLogger.error("\(logMsgPrefix) \(message, privacy: .auto)") case .critical: - appleLogger.log(level: .fault, "\(message, privacy: .auto)") + appleLogger.critical("\(logMsgPrefix) \(message, privacy: .auto)") } +#else + // if logging on other platforms, pass down the log details to the standard logger + appleLogger.log(level: level, message: "\(message, privacy: .auto)", metadata: allMetadata, source: source, file: file, function: function, line: line) +#endif } + // Required to extend the protocol subscript(metadataKey key: String) -> Logging.Logger.Metadata.Value? { get { metadata[key] } set { metadata[key] = newValue } } } -#endif + +//#endif From 30de412b492d17296a8c7cc4f42ddc525493ad9a Mon Sep 17 00:00:00 2001 From: stoicswe <31823043+stoicswe@users.noreply.github.com> Date: Sun, 26 May 2024 15:52:48 -0400 Subject: [PATCH 2/9] added improvements to the ATLogHandler --- .../Utilities/Logging/Logging.swift | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/Sources/ATProtoKit/Utilities/Logging/Logging.swift b/Sources/ATProtoKit/Utilities/Logging/Logging.swift index 93a7157b7ef..d8785484c0a 100644 --- a/Sources/ATProtoKit/Utilities/Logging/Logging.swift +++ b/Sources/ATProtoKit/Utilities/Logging/Logging.swift @@ -5,51 +5,51 @@ // Created by Christopher Jr Riley on 2024-04-04. // -// Choose the logging library based on the -// platform we are working with #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) // If the platform is based on an Apple-product // then we will use the default-provided Apple logger +// for processing the SDK logs import os #endif -// This library will be used regardless for the log levels import Logging -// Define the ATLogHandler -// The log handler will automatically choose -// the correct logging library based on the framework +/// The ATLogHandler is a handler class that dyanmically switches between the +/// cross-platform compatible SwiftLogger framework for multiplatform use and +/// for use on Apple-based OSs. struct ATLogHandler: LogHandler { - // Subsystem is the component within the ATProto - // Library that will be logged + // Component public let subsystem: String - // The category is a meta-data field for the log to - // help provide more context in-line + // Category Metadata Field public let category: String - // The default log level, if not provided + // INFO will be the default log level public var logLevel: Logging.Logger.Level = .info + // Metadata for the specific log msg, consisting of keys and values public var metadata: Logging.Logger.Metadata = [:] #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) - // if on an apple platform, we will use the default lib - private var appleLogger: os.Logger + private var logger: os.Logger #else - // Otherwise, use the cross-platform logging lib - private var appleLogger: Logging.StreamLogHandler + private var logger: Logging.StreamLogHandler #endif - // Init the Logger Library + /// Initialization of the ATLogHandler + /// - Parameters: + /// - subSystem: The subsystem component in which the logs are applicable. + /// - category: The categorty that the log applies to. Most of the time this can be nil and will default to ATProtoKit. init(subsystem: String, category: String? = nil) { self.subsystem = subsystem self.category = category ?? "ATProtoKit" #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) // Using the apple logger built-into the Apple OSs - self.appleLogger = Logger(subsystem: subsystem, category: category ?? "ATProtoKit") + self.logger = Logger(subsystem: subsystem, category: category ?? "ATProtoKit") #else // Otherwise, use the cross-platform logging lib - self.appleLogger = Logging.StreamLogHandler(label: "\(subsystem) \(category ?? "ATProtoKit")") + self.logger = Logging.StreamLogHandler(label: "\(subsystem) \(category ?? "ATProtoKit")") #endif } - // The logger override function that will actually do the mapping + /// The Log function will perform the mapping betweent the StreamLogHandler from the SwiftLogger + /// library for cross-platform logging and the Apple logger from Apple's OS frameworks for when an Apple + /// OS is chosen as the target. public func log(level: Logging.Logger.Level, message: Logging.Logger.Message, metadata explicitMetadata: Logging.Logger.Metadata?, @@ -68,32 +68,37 @@ struct ATLogHandler: LogHandler { let logMsgPrefix = "\(allMetadata) [\(source)]" // Map the loglevels to match what the apple logger would expect switch level { + // Given the log status, pass in the formatted string that should + // closely resemble what the StreamLogHandler format so that logs + // will hopefully resemble each other despite being on different + // platforms case .trace: - appleLogger.trace("\(logMsgPrefix) \(message, privacy: .auto)") + logger.trace("\(logMsgPrefix) \(message, privacy: .auto)") case .debug: - appleLogger.debug("\(logMsgPrefix) \(message, privacy: .auto)") + logger.debug("\(logMsgPrefix) \(message, privacy: .auto)") case .info: - appleLogger.info("\(logMsgPrefix) \(message, privacy: .auto)") + logger.info("\(logMsgPrefix) \(message, privacy: .auto)") case .notice: - appleLogger.notice("\(logMsgPrefix) \(message, privacy: .auto)") + logger.notice("\(logMsgPrefix) \(message, privacy: .auto)") case .warning: - appleLogger.warning("\(logMsgPrefix) \(message, privacy: .auto)") + logger.warning("\(logMsgPrefix) \(message, privacy: .auto)") case .error: - appleLogger.error("\(logMsgPrefix) \(message, privacy: .auto)") + logger.error("\(logMsgPrefix) \(message, privacy: .auto)") case .critical: - appleLogger.critical("\(logMsgPrefix) \(message, privacy: .auto)") + logger.critical("\(logMsgPrefix) \(message, privacy: .auto)") } #else // if logging on other platforms, pass down the log details to the standard logger - appleLogger.log(level: level, message: "\(message, privacy: .auto)", metadata: allMetadata, source: source, file: file, function: function, line: line) + logger.log(level: level, message: "\(message, privacy: .auto)", metadata: allMetadata, source: source, file: file, function: function, line: line) #endif } - // Required to extend the protocol + /// Obtain or set a particular metadata key for the standard metadata for the handler. + /// - Parameters: + /// - key: The metadata key whos value is to be obtained or inserted + /// - Returns: A `Logging.Logger.Metadata.Value` that contains the configured value for a given metadata key. subscript(metadataKey key: String) -> Logging.Logger.Metadata.Value? { get { metadata[key] } set { metadata[key] = newValue } } } - -//#endif From 1e1a5bf62eb5772cc80552e41abae871b176b636 Mon Sep 17 00:00:00 2001 From: stoicswe <31823043+stoicswe@users.noreply.github.com> Date: Mon, 27 May 2024 15:11:56 -0400 Subject: [PATCH 3/9] added improved logging strut, added bootstrapping, added logging to ATFirehoseStream --- .../ATFirehoseStream/ATFirehoseStream.swift | 16 ++++- .../Logging/LoggingBootStrapping.swift | 61 +++++++++---------- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATFirehoseStream/ATFirehoseStream.swift b/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATFirehoseStream/ATFirehoseStream.swift index fb399f55182..9ee1999f3f3 100644 --- a/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATFirehoseStream/ATFirehoseStream.swift +++ b/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATFirehoseStream/ATFirehoseStream.swift @@ -7,8 +7,11 @@ import Foundation +import Logging + /// The base class for the AT Protocol's Firehose event stream. class ATFirehoseStream: ATEventStreamConfiguration { + private var logger = Logger(label: "ATFirehoseStream") /// The URL of the relay. Defaults to `wss://bsky.network`. public var relayURL: String = "wss://bsky.network" /// The URL of the endpoint. Defaults to `com.atproto.sync.subscribeRepos`. @@ -39,6 +42,7 @@ class ATFirehoseStream: ATEventStreamConfiguration { /// to `URLSessionConfiguration.default`. required init(relayURL: String, namespacedIdentifiertURL: String, cursor: Int64?, sequencePosition: Int64?, urlSessionConfiguration: URLSessionConfiguration = .default, webSocketTask: URLSessionWebSocketTask) async throws { + logger.trace("Initializing the ATEventStreamConfiguration") self.relayURL = relayURL self.namespacedIdentifiertURL = namespacedIdentifiertURL self.cursor = cursor @@ -46,11 +50,17 @@ class ATFirehoseStream: ATEventStreamConfiguration { self.urlSessionConfiguration = urlSessionConfiguration self.urlSession = URLSession(configuration: urlSessionConfiguration) self.webSocketTask = webSocketTask - - guard let webSocketURL = URL(string: "\(relayURL)/xrpc/\(namespacedIdentifiertURL)") else { throw ATRequestPrepareError.invalidFormat } + + logger.debug("Opening a websocket", metadata: ["relayUrl": "\(relayURL)", "namespacedIdentifiertURL": "\(namespacedIdentifiertURL)"]) + guard let webSocketURL = URL(string: "\(relayURL)/xrpc/\(namespacedIdentifiertURL)") else { + logger.error("Unable to create the websocket URL due to an invalid format.") + throw ATRequestPrepareError.invalidFormat + } + + logger.debug("Running the websocket task") self.webSocketTask = urlSession.webSocketTask(with: webSocketURL) webSocketTask.resume() - + await self.connect() } } diff --git a/Sources/ATProtoKit/Utilities/Logging/LoggingBootStrapping.swift b/Sources/ATProtoKit/Utilities/Logging/LoggingBootStrapping.swift index 02c85e08ee1..addf911152b 100644 --- a/Sources/ATProtoKit/Utilities/Logging/LoggingBootStrapping.swift +++ b/Sources/ATProtoKit/Utilities/Logging/LoggingBootStrapping.swift @@ -5,35 +5,32 @@ // Created by Christopher Jr Riley on 2024-04-04. // -//import Foundation -//import Logging -// -//struct ATLogging { -// public func bootstrap() { -// func bootstrapWithOSLog(subsystem: String?) { -// LoggingSystem.bootstrap { label in -// #if canImport(os) -// OSLogHandler(subsystem: subsystem ?? defaultIdentifier(), category: label) -// #else -// StreamLogHandler.standardOutput(label: label) -// #endif -// } -// } -// } -// -// #if canImport(os) -// private func defaultIdentifier() -> String { -// return Bundle.main.bundleIdentifier ?? "com.cjrriley.ATProtoKit" -// } -// #endif -// -// public func handleBehavior(_ behavior: HandleBehavior = .default) { -// -// } -// -// public enum HandleBehavior { -// case `default` -// case osLog -// case swiftLog -// } -//} +import Foundation +import Logging + +/// The ATLogging Struct houses the basis for booststrapping the logging handler that +/// will be used by the rest of the ATProtoKit. This is an optional component, and can be +/// replaced wiith a seperate Log Handler if there is one already globally created for a +/// project that ustilizes the ATProto framework. +struct ATLogging { + /// Bootstrap the Logging framework, using the built-in implementation + /// that ATProtoKit provides. The Logger Handler provided by this framework + /// will dynamically choose between the Apple OS and cross-platform logging + /// implemnentations based on the target OS. + public func bootstrap() { + // Bootstrap the ATProtoKit logger for the libary + // for a consistent logging format and auto switch + // between the cross-platform and the Apple OS based + // on target. + LoggingSystem.bootstrap { + label in ATLogHandler(subsystem: "\(defaultIdentifier()).\(label)") + } + } + + // Return the bundle for the prefix of the label for the logger + // this prefix will show up in the logs for finding the source of + // the logs + private func defaultIdentifier() -> String { + return Bundle.main.bundleIdentifier ?? "com.cjrriley.ATProtoKit" + } +} From 6f1ce8408a88edb251b0336e508aec7403d58bff Mon Sep 17 00:00:00 2001 From: stoicswe <31823043+stoicswe@users.noreply.github.com> Date: Mon, 27 May 2024 15:39:52 -0400 Subject: [PATCH 4/9] added logging to ATEventStreamConfiguration --- .../ATEventStreamConfiguration.swift | 2 ++ .../ATEventStreamConfigurationExtension.swift | 25 ++++++++++++++----- .../ATFirehoseStream/ATFirehoseStream.swift | 10 +++----- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Sources/ATProtoKit/Models/Common/ATEventStreamConfiguration/ATEventStreamConfiguration.swift b/Sources/ATProtoKit/Models/Common/ATEventStreamConfiguration/ATEventStreamConfiguration.swift index 68acf5d41bc..241b9b9b4dd 100644 --- a/Sources/ATProtoKit/Models/Common/ATEventStreamConfiguration/ATEventStreamConfiguration.swift +++ b/Sources/ATProtoKit/Models/Common/ATEventStreamConfiguration/ATEventStreamConfiguration.swift @@ -6,6 +6,7 @@ // import Foundation +import Logging /// The base protocol which all data stream-related classes conform to. /// @@ -14,6 +15,7 @@ import Foundation /// managing the connection (opening, closing, and reconnecting), creating parameters for allowing /// and disallowing content, and handling sequences. public protocol ATEventStreamConfiguration: AnyObject { + var logger: Logger { get } /// The URL of the relay. /// diff --git a/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATEventStreamConfigurationExtension.swift b/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATEventStreamConfigurationExtension.swift index 8baecde3fc5..f859a72a2cc 100644 --- a/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATEventStreamConfigurationExtension.swift +++ b/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATEventStreamConfigurationExtension.swift @@ -7,9 +7,9 @@ import Foundation import SwiftCBOR +import Logging extension ATEventStreamConfiguration { - /// Connects the client to the event stream. /// /// Normally, when connecting to the event stream, it will start from the first message the event stream gets. The client will always look at the last successful @@ -22,10 +22,13 @@ extension ATEventStreamConfiguration { /// /// - Parameter cursor: The mark used to indicate the starting point for the next set of results. Optional. public func connect(cursor: Int64? = nil) async { + logger.trace("In connect()") self.isConnected = true self.webSocketTask.resume() + logger.debug("WebSocketTask resumed.", metadata: ["isConnected": "\(self.isConnected)"]) await self.receiveMessages() + logger.trace("Exiting connect()") } /// Disconnects the client from the event stream. @@ -34,7 +37,10 @@ extension ATEventStreamConfiguration { /// - closeCode: A code that indicates why the event stream connection closed. /// - reason: The reason why the client disconnected from the server. public func disconnect(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data) { + logger.trace("In disconnect()") + logger.debug("Closing websocket", metadata: ["closeCode": "\(closeCode)", "reason": "\(reason)"]) webSocketTask.cancel(with: closeCode, reason: reason) + logger.trace("Exiting disconnect()") } /// Attempts to reconnect the client to the event stream after a disconnect. @@ -45,18 +51,19 @@ extension ATEventStreamConfiguration { /// - cursor: The mark used to indicate the starting point for the next set of results. Optional. /// - retry: The number of times the connection attempts can be retried. func reconnect(cursor: Int64?, retry: Int) async { + logger.trace("In reconnect()") guard isConnected == false else { - print("Already connected. No need to reconnect.") + logger.debug("Already connected. No need to reconnect.") return } let lastCursor: Int64 = sequencePosition ?? 0 if lastCursor > 0 { + logger.debug("Fetching missed messages", metadata: ["lastCursor": "\(lastCursor)"]) await fetchMissedMessages(fromSequence: lastCursor) } - - + logger.trace("Exiting reconnect()") } /// Receives decoded messages and manages the sequence number. @@ -65,26 +72,32 @@ extension ATEventStreamConfiguration { /// /// [DAG_CBOR]: https://ipld.io/docs/codecs/known/dag-cbor/ public func receiveMessages() async { + logger.trace("In receiveMessages()") while isConnected { do { let message = try await webSocketTask.receive() switch message { case .string(let base64String): + logger.debug("Received a string message", metadata: ["length": "\(base64String.count)"]) ATCBORManager().decodeCBOR(from: base64String) case .data(let data): + logger.debug("Received a data message", metadata: ["length": "\(data.count)"]) let base64String = data.base64EncodedString() ATCBORManager().decodeCBOR(from: base64String) @unknown default: - print("Received an unknown type of message.") + logger.warning("Received an unknown type of message.") } } catch { - print("Error receiving message: \(error)") + logger.error("Error while receiving message.", metadata: ["error": "\(error)"]) } } + logger.trace("Exiting receiveMessages()") } public func fetchMissedMessages(fromSequence lastCursor: Int64) async { + logger.trace("In fetchMissedMessages()") + logger.trace("Exiting fetchMissedMessages()") } } diff --git a/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATFirehoseStream/ATFirehoseStream.swift b/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATFirehoseStream/ATFirehoseStream.swift index e8b871f5f17..50b2f2334db 100644 --- a/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATFirehoseStream/ATFirehoseStream.swift +++ b/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATFirehoseStream/ATFirehoseStream.swift @@ -6,12 +6,11 @@ // import Foundation - import Logging /// The base class for the AT Protocol's Firehose event stream. class ATFirehoseStream: ATEventStreamConfiguration { - private var logger = Logger(label: "ATFirehoseStream") + internal var logger = Logger(label: "ATFirehoseStream") /// Indicates whether the event stream is connected. Defaults to `false`. internal var isConnected: Bool = false /// The URL of the relay. Defaults to `wss://bsky.network`. @@ -50,6 +49,7 @@ class ATFirehoseStream: ATEventStreamConfiguration { /// to `URLSessionConfiguration.default`. required init(relayURL: String, namespacedIdentifiertURL: String, cursor: Int64?, sequencePosition: Int64?, urlSessionConfiguration: URLSessionConfiguration = .default, webSocketTask: URLSessionWebSocketTask) async throws { + logger.trace("In init()") logger.trace("Initializing the ATEventStreamConfiguration") self.relayURL = relayURL self.namespacedIdentifiertURL = namespacedIdentifiertURL @@ -65,10 +65,8 @@ class ATFirehoseStream: ATEventStreamConfiguration { throw ATRequestPrepareError.invalidFormat } - logger.debug("Running the websocket task") + logger.debug("Creating the websocket task") self.webSocketTask = urlSession.webSocketTask(with: webSocketURL) - webSocketTask.resume() - - await self.connect() + logger.trace("Exiting init()") } } From ab8a59629d86ea8409c99c80f1475b030ab57798 Mon Sep 17 00:00:00 2001 From: stoicswe <31823043+stoicswe@users.noreply.github.com> Date: Mon, 27 May 2024 16:16:48 -0400 Subject: [PATCH 5/9] added logging to ATProtocolConfiguration --- .../ATProtocolConfiguration.swift | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/Sources/ATProtoKit/Networking/SessionManager/ATProtocolConfiguration.swift b/Sources/ATProtoKit/Networking/SessionManager/ATProtocolConfiguration.swift index 04806f168b8..5356dafa608 100644 --- a/Sources/ATProtoKit/Networking/SessionManager/ATProtocolConfiguration.swift +++ b/Sources/ATProtoKit/Networking/SessionManager/ATProtocolConfiguration.swift @@ -57,14 +57,11 @@ public class ATProtocolConfiguration: ProtocolConfiguration { self.logIdentifier = logIdentifier ?? Bundle.main.bundleIdentifier ?? "com.cjrriley.ATProtoKit" self.logCategory = logCategory ?? "ATProtoKit" self.logLevel = logLevel - - #if canImport(os) + + // Create the logger and bootstrap it for use in the library LoggingSystem.bootstrap { label in ATLogHandler(subsystem: label, category: logCategory ?? "ATProtoKit") } - #else - LoggingSystem.bootstrap(StreamLogHandler.standardOutput) - #endif logger = Logger(label: logIdentifier ?? "com.cjrriley.ATProtoKit") logger?.logLevel = logLevel ?? .info @@ -88,10 +85,14 @@ public class ATProtocolConfiguration: ProtocolConfiguration { /// - Throws: An ``ATProtoError``-conforming error type, depending on the issye. Go to /// ``ATAPIError`` and ``ATRequestPrepareError`` for more details. public func authenticate(authenticationFactorToken: String? = nil) async throws -> Result { + logger?.trace("In authenticate()") + guard let requestURL = URL(string: "\(self.pdsURL)/xrpc/com.atproto.server.createSession") else { + logger?.error("Error while authenticating with the server", metadata: ["error": "\(ATRequestPrepareError.invalidRequestURL)"]) return .failure(ATRequestPrepareError.invalidRequestURL) } + logger?.debug("Setting the session credentials") let credentials = SessionCredentials( identifier: handle, password: appPassword, @@ -101,6 +102,8 @@ public class ATProtocolConfiguration: ProtocolConfiguration { do { let request = APIClientService.createRequest(forRequest: requestURL, andMethod: .post) + + logger?.debug("Authenticating with the server.", metadata: ["requestURL": "\(requestURL)"]) var response = try await APIClientService.sendRequest(request, withEncodingBody: credentials, decodeTo: UserSession.self) @@ -109,11 +112,16 @@ public class ATProtocolConfiguration: ProtocolConfiguration { if self.logger != nil { response.logger = self.logger } - + + logger?.debug("Authentication successful") + logger?.trace("Exiting authenticate()") return .success(response) } catch { + logger?.error("Authentication request failed with error.", metadata: ["error": "\(error)"]) + logger?.trace("Exiting authenticate()") return .failure(error) } + } /// Creates an a new account for the user. @@ -151,10 +159,13 @@ public class ATProtocolConfiguration: ProtocolConfiguration { public func createAccount(email: String? = nil, handle: String, existingDID: String? = nil, inviteCode: String? = nil, verificationCode: String? = nil, verificationPhone: String? = nil, password: String? = nil, recoveryKey: String? = nil, plcOp: UnknownType? = nil) async throws -> Result { + logger?.trace("In createAccount()") guard let requestURL = URL(string: "\(self.pdsURL)/xrpc/com.atproto.server.createAccount") else { + logger?.error("Error while creating account", metadata: ["error": "\(ATRequestPrepareError.invalidRequestURL)"]) return .failure(ATRequestPrepareError.invalidRequestURL) } - + + logger?.debug("Setting the account creation request body") let requestBody = ServerCreateAccount( email: email, handle: handle, @@ -173,6 +184,8 @@ public class ATProtocolConfiguration: ProtocolConfiguration { acceptValue: nil, contentTypeValue: nil, authorizationValue: nil) + + logger?.debug("Crreating user account", metadata: ["handle": "\(handle)"]) var response = try await APIClientService.sendRequest(request, withEncodingBody: requestBody, decodeTo: UserSession.self) @@ -181,9 +194,13 @@ public class ATProtocolConfiguration: ProtocolConfiguration { if self.logger != nil { response.logger = self.logger } - + + logger?.debug("User account creation successful", metadata: ["handle": "\(handle)"]) + logger?.trace("Exiting createAccount()") return .success(response) } catch { + logger?.error("Account creation failed with error.", metadata: ["error": "\(error)"]) + logger?.trace("Exiting createAccount()") return .failure(error) } } @@ -204,8 +221,10 @@ public class ATProtocolConfiguration: ProtocolConfiguration { /// if successful, or an `Error` if not. public func getSession(by accessToken: String, pdsURL: String? = nil) async throws -> Result { + logger?.trace("In getSession()") guard let sessionURL = pdsURL != nil ? pdsURL : self.pdsURL, let requestURL = URL(string: "\(sessionURL)/xrpc/com.atproto.server.getSession") else { + logger?.error("Error while obtaining session", metadata: ["error": "\(ATRequestPrepareError.invalidRequestURL)"]) return .failure(ATRequestPrepareError.invalidRequestURL) } @@ -213,11 +232,16 @@ public class ATProtocolConfiguration: ProtocolConfiguration { let request = APIClientService.createRequest(forRequest: requestURL, andMethod: .get, authorizationValue: "Bearer \(accessToken)") + logger?.debug("Obtaining the session") let response = try await APIClientService.sendRequest(request, decodeTo: SessionResponse.self) + logger?.debug("Session obtained successfully") + logger?.trace("Exiting getSession()") return .success(response) } catch { + logger?.error("Error while obtaining session", metadata: ["error": "\(error)"]) + logger?.trace("Exiting getSession()") return .failure(error) } } @@ -238,8 +262,10 @@ public class ATProtocolConfiguration: ProtocolConfiguration { /// if successful, or an `Error` if not. public func refreshSession(using refreshToken: String, pdsURL: String? = nil) async throws -> Result { + logger?.info("In refreshSession()") guard let sessionURL = pdsURL != nil ? pdsURL : self.pdsURL, let requestURL = URL(string: "\(sessionURL)/xrpc/com.atproto.server.refreshSession") else { + logger?.error("Error while refreshing the session", metadata: ["error": "\(ATRequestPrepareError.invalidRequestURL)"]) return .failure(ATRequestPrepareError.invalidRequestURL) } @@ -247,6 +273,7 @@ public class ATProtocolConfiguration: ProtocolConfiguration { let request = APIClientService.createRequest(forRequest: requestURL, andMethod: .post, authorizationValue: "Bearer \(refreshToken)") + logger?.debug("Refreshing the session") var response = try await APIClientService.sendRequest(request, decodeTo: UserSession.self) response.pdsURL = self.pdsURL @@ -255,8 +282,12 @@ public class ATProtocolConfiguration: ProtocolConfiguration { response.logger = self.logger } + logger?.debug("Session refreshed successfully") + logger?.trace("Exiting refreshSession()") return .success(response) } catch { + logger?.error("Error while refreshing the session", metadata: ["error": "\(error)"]) + logger?.trace("Exiting refreshSession()") return .failure(error) } } @@ -275,8 +306,10 @@ public class ATProtocolConfiguration: ProtocolConfiguration { /// - pdsURL: The URL of the Personal Data Server (PDS). Defaults to `nil`. public func deleteSession(using accessToken: String, pdsURL: String? = nil) async throws { + logger?.trace("In deleteSession()") guard let sessionURL = pdsURL != nil ? pdsURL : self.pdsURL, let requestURL = URL(string: "\(sessionURL)/xrpc/com.atproto.server.deleteSession") else { + logger?.error("Error while deleting the session", metadata: ["error": "\(ATRequestPrepareError.invalidRequestURL)"]) throw ATRequestPrepareError.invalidRequestURL } @@ -284,10 +317,11 @@ public class ATProtocolConfiguration: ProtocolConfiguration { let request = APIClientService.createRequest(forRequest: requestURL, andMethod: .post, authorizationValue: "Bearer \(accessToken)") - + logger?.debug("Deleting the session") _ = try await APIClientService.sendRequest(request, withEncodingBody: nil) } catch { + logger?.error("Error while deleting the session", metadata: ["error": "\(error)"]) throw error } } From 83350f28fe832ca965fe48eeccd387cf467cbb88 Mon Sep 17 00:00:00 2001 From: stoicswe <31823043+stoicswe@users.noreply.github.com> Date: Mon, 27 May 2024 18:28:15 -0400 Subject: [PATCH 6/9] added logging to APIClientService --- .../Utilities/APIClientService.swift | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/Sources/ATProtoKit/Utilities/APIClientService.swift b/Sources/ATProtoKit/Utilities/APIClientService.swift index 560ffe49572..00176896148 100644 --- a/Sources/ATProtoKit/Utilities/APIClientService.swift +++ b/Sources/ATProtoKit/Utilities/APIClientService.swift @@ -6,9 +6,12 @@ // import Foundation +import Logging /// A helper class to handle the most common HTTP Requests for the AT Protocol. public class APIClientService { + private static var logger = Logger(label: "APIClientService") + private init() {} // MARK: Creating requests - @@ -25,39 +28,51 @@ public class APIClientService { public static func createRequest(forRequest requestURL: URL, andMethod httpMethod: HTTPMethod, acceptValue: String? = "application/json", contentTypeValue: String? = "application/json", authorizationValue: String? = nil, labelersValue: String? = nil, proxyValue: String? = nil) -> URLRequest { + logger.trace("In createRequest()") var request = URLRequest(url: requestURL) request.httpMethod = httpMethod.rawValue if let acceptValue { + logger.trace("Adding header", metadata: ["Accept": "\(acceptValue)"]) request.addValue(acceptValue, forHTTPHeaderField: "Accept") } if let authorizationValue { + logger.trace("Adding header", metadata: ["Authorization": "\(authorizationValue)"]) request.addValue(authorizationValue, forHTTPHeaderField: "Authorization") } // Send the data if it matches a POST or PUT request. if httpMethod == .post || httpMethod == .put { if let contentTypeValue { + logger.trace("Adding header", metadata: ["Content-Type": "\(contentTypeValue)"]) request.addValue(contentTypeValue, forHTTPHeaderField: "Content-Type") } } // Send the data specifically for proxy-related data. if let proxyValue { + logger.trace("Adding header", metadata: ["atproto-proxy": "\(proxyValue)"]) request.addValue(proxyValue, forHTTPHeaderField: "atproto-proxy") } // Send the data specifically for label-related calls. if let labelersValue { + logger.trace("Adding header", metadata: ["atproto-accept-labelers": "\(labelersValue)"]) request.addValue(labelersValue, forHTTPHeaderField: "atproto-accept-labelers") } + logger.debug("Created request successfully") + logger.trace("Exiting createRequest()") return request } static func encode(_ jsonData: T) async throws -> Data { + logger.trace("In encode()") guard let httpBody = try? JSONSerialization.data(withJSONObject: jsonData) else { + logger.error("Data encoding failed with error", metadata: ["error": "\(ATHTTPRequestError.unableToEncodeRequestBody)"]) throw ATHTTPRequestError.unableToEncodeRequestBody } + logger.debug("Body contents have been encoded successfully", metadata: ["size": "\(httpBody.count)"]) + logger.trace("Exiting encode()") return httpBody } @@ -68,14 +83,20 @@ public class APIClientService { /// - queryItems: An array of key-value pairs to be set as query items. /// - Returns: A new URL with the query items appended. public static func setQueryItems(for requestURL: URL, with queryItems: [(String, String)]) throws -> URL { + logger.trace("In setQueryItems()") var components = URLComponents(url: requestURL, resolvingAgainstBaseURL: true) + logger.debug("Setting query items", metadata: ["size": "\(queryItems.count)"]) // Map out each URLQueryItem with the key ($0.0) and value ($0.1) of the item. components?.queryItems = queryItems.map { URLQueryItem(name: $0.0, value: $0.1) } guard let finalURL = components?.url else { + logger.error("Error while setting query items", metadata: ["error": "\(ATHTTPRequestError.failedToConstructURLWithParameters)"]) throw ATHTTPRequestError.failedToConstructURLWithParameters } + + logger.debug("Query items have been set successfully") + logger.trace("Exiting setQueryItems()") return finalURL } @@ -87,9 +108,17 @@ public class APIClientService { /// - decodeTo: The type to decode the response into. /// - Returns: An instance of the specified `Decodable` type. public static func sendRequest(_ request: URLRequest, withEncodingBody body: Encodable? = nil, decodeTo: T.Type) async throws -> T { + logger.trace("In sendRequest()") + + logger.debug("Sending request", metadata: ["url": "\(String(describing: request.url))", "method": "\(String(describing: request.httpMethod))"]) let (data, _) = try await performRequest(request, withEncodingBody: body) - + logger.debug("Request has been sent successfully") + + logger.debug("Decoding the response data", metadata: ["size": "\(data.count)"]) let decodedData = try JSONDecoder().decode(T.self, from: data) + logger.debug("Data decoded successfully") + + logger.trace("Exiting sendRequest()") return decodedData } @@ -98,7 +127,9 @@ public class APIClientService { /// - request: The `URLRequest` to send. /// - body: An optional `Encodable` body to be encoded and attached to the request. public static func sendRequest(_ request: URLRequest, withEncodingBody body: Encodable? = nil) async throws { + logger.trace("In sendRequest()") _ = try await performRequest(request, withEncodingBody: body) + logger.trace("Exiting sendRequest()") } /// Sends a `URLRequest` in order to receive a data object. @@ -112,7 +143,10 @@ public class APIClientService { /// - Parameter request: The `URLRequest` to send. /// - Returns: A `Data` object that contains the blob. public static func sendRequest(_ request: URLRequest) async throws -> Data { + logger.trace("In sendRequest()") let (data, _) = try await performRequest(request) + logger.debug("Data received from request", metadata: ["size": "\(data.count)"]) + logger.trace("Exiting sendRequest()") return data } @@ -135,7 +169,9 @@ public class APIClientService { /// - Returns: A `BlobContainer` instance with the upload result. public static func uploadBlob(pdsURL: String = "https://bsky.social", accessToken: String, filename: String, imageData: Data) async throws -> BlobContainer { + logger.trace("In uploadBlob()") guard let requestURL = URL(string: "\(pdsURL)/xrpc/com.atproto.repo.uploadBlob") else { + logger.error("Error while uploading blob", metadata: ["error": "\(ATRequestPrepareError.invalidRequestURL)"]) throw ATRequestPrepareError.invalidRequestURL } @@ -149,11 +185,15 @@ public class APIClientService { authorizationValue: "Bearer \(accessToken)") request.httpBody = imageData + logger.debug("Uploading blob", metadata: ["url": "\(requestURL)", "mime-type": "\(mimeType)", "size": "\(imageData.count)"]) let response = try await sendRequest(request, decodeTo: BlobContainer.self) + logger.debug("Blob upload successful") + logger.trace("Exiting uploadBlob()") return response } catch { + logger.error("Error while uploading blob", metadata: ["error": "\(error)"]) throw ATHTTPRequestError.invalidResponse } } @@ -165,31 +205,41 @@ public class APIClientService { /// - body: An optional `Encodable` body to be encoded and attached to the request. /// - Returns: A `Dictionary` representation of the JSON response. public static func sendRequestWithRawJSONOutput(_ request: URLRequest, withEncodingBody body: Encodable? = nil) async throws -> [String: Any] { + logger.trace("In sendRequestWithRawJSONOutput()") var urlRequest = request // Encode the body to JSON data if it's not nil + logger.debug("Building the request to send", metadata: ["url": "\(String(describing: request.url))", "method": "\(String(describing: request.httpMethod))"]) if let body = body { do { + logger.debug("Encoding request body to JSON") urlRequest.httpBody = try body.toJsonData() + logger.debug("Encoded request body has been set", metadata: ["size": "\(String(describing: urlRequest.httpBody?.count))"]) } catch { + logger.error("Error while setting the encoded request body", metadata: ["error": "\(error)"]) throw ATHTTPRequestError.unableToEncodeRequestBody } } + logger.debug("Sending the request") let (data, response) = try await URLSession.shared.data(for: urlRequest) guard let httpResponse = response as? HTTPURLResponse else { + logger.error("Error while sending the request", metadata: ["error": "\(ATHTTPRequestError.errorGettingResponse)"]) throw ATHTTPRequestError.errorGettingResponse } guard httpResponse.statusCode == 200 else { let responseBody = String(data: data, encoding: .utf8) ?? "No response body" - print("HTTP Status Code: \(httpResponse.statusCode) - Response Body: \(responseBody)") + logger.error("Error while sending the request", metadata: ["status": "\(httpResponse.statusCode)", "responseBody": "\(responseBody)"]) throw URLError(.badServerResponse) } guard let response = try JSONSerialization.jsonObject( with: data, options: .mutableLeaves) as? [String: Any] else { return ["Response": "No response"] } + + logger.debug("Request sent successfully") + logger.trace("Exiting sendRequestWithRawJSONOutput()") return response } @@ -199,22 +249,30 @@ public class APIClientService { /// - request: The `URLRequest` to send. /// - Returns: A `String` representation of the HTML response. public static func sendRequestWithRawHTMLOutput(_ request: URLRequest) async throws -> String { + logger.trace("In sendRequestWithRawHTMLOutput()") + + logger.debug("Sending request", metadata: ["url": "\(String(describing: request.url))", "method": "\(String(describing: request.httpMethod))"]) let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { + logger.error("Error while sending request", metadata: ["error": "\(ATHTTPRequestError.errorGettingResponse)"]) throw ATHTTPRequestError.errorGettingResponse } guard httpResponse.statusCode == 200 else { let responseBody = String(data: data, encoding: .utf8) ?? "No response body" print("HTTP Status Code: \(httpResponse.statusCode) - Response Body: \(responseBody)") + logger.error("Error while sending the request", metadata: ["status": "\(httpResponse.statusCode)", "responseBody": "\(responseBody)"]) throw URLError(.badServerResponse) } guard let htmlString = String(data: data, encoding: .utf8) else { + logger.error("Error while decoding the response", metadata: ["error": "\(ATHTTPRequestError.failedToDecodeHTML)"]) throw ATHTTPRequestError.failedToDecodeHTML } + logger.debug("Request sent successfully") + logger.trace("Exiting sendRequestWithRawHTMLOutput()") return htmlString } @@ -225,19 +283,25 @@ public class APIClientService { /// - body: An optional `Encodable` body to be encoded and attached to the request. /// - Returns: A tuple containing the data and the HTTPURLResponse. private static func performRequest(_ request: URLRequest, withEncodingBody body: Encodable? = nil) async throws -> (Data, HTTPURLResponse) { + logger.trace("In performRequest()") var urlRequest = request + logger.debug("Building the request", metadata: ["url": "\(String(describing: request.url))", "method": "\(String(describing: request.httpMethod))"]) if let body = body { do { urlRequest.httpBody = try body.toJsonData() + logger.debug("Request body has been set", metadata: ["size": "\(String(describing: urlRequest.httpBody?.count))"]) } catch { + logger.error("Error while setting the request body", metadata: ["error": "\(error)"]) throw ATHTTPRequestError.unableToEncodeRequestBody } } + logger.debug("Sending the request") let (data, response) = try await URLSession.shared.data(for: urlRequest) guard let httpResponse = response as? HTTPURLResponse else { + logger.error("Error while sending the request", metadata: ["error": "\(ATHTTPRequestError.errorGettingResponse)"]) throw ATHTTPRequestError.errorGettingResponse } @@ -247,9 +311,12 @@ public class APIClientService { guard httpResponse.statusCode == 200 else { let responseBody = String(data: data, encoding: .utf8) ?? "No response body" print("HTTP Status Code: \(httpResponse.statusCode) - Response Body: \(responseBody)") + logger.error("Error while sending the request", metadata: ["status": "\(httpResponse.statusCode)", "responseBody": "\(responseBody)"]) throw URLError(.badServerResponse) } - + + logger.debug("Request sent successfully") + logger.trace("Exiting performRequest()") return (data, httpResponse) } From b0d4d3cc3fce529110a3ca9e63105c84cc3efec4 Mon Sep 17 00:00:00 2001 From: stoicswe <31823043+stoicswe@users.noreply.github.com> Date: Mon, 27 May 2024 19:04:48 -0400 Subject: [PATCH 7/9] added logging to ATCBORManager --- .../ATProtoKit/Utilities/ATCBORManager.swift | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/Sources/ATProtoKit/Utilities/ATCBORManager.swift b/Sources/ATProtoKit/Utilities/ATCBORManager.swift index cc8e26b174e..89e45796aa4 100644 --- a/Sources/ATProtoKit/Utilities/ATCBORManager.swift +++ b/Sources/ATProtoKit/Utilities/ATCBORManager.swift @@ -7,9 +7,11 @@ import Foundation import SwiftCBOR +import Logging /// A class that handles CBOR-related objects. public class ATCBORManager { + private var logger = Logger(label: "ATCBORManager") /// The length of bytes for a CID according to CAR v1. private let cidByteLength: Int = 36 @@ -36,8 +38,9 @@ public class ATCBORManager { /// /// - Parameter base64String: The CBOR string to be decoded. func decodeCBOR(from base64String: String) { + logger.trace("In decodeCBOR()") guard let data = Data(base64Encoded: base64String) else { - print("Invalid Base64 string") + logger.error("Invalid Base64 string") return } @@ -46,10 +49,11 @@ public class ATCBORManager { // if let cborBlocks = extractCborBlocks(from: items) { // print("Decoded CBOR:", cborBlocks) // } - print("Decoded CBOR: \(items)") + logger.debug("Decoded CBOR", metadata: ["size": "\(items.count)", "items": "\(items)"]) } catch { - print("Failed to decode CBOR: \(error)") + logger.error("Failed to decode CBOR", metadata: ["error": "\(error)"]) } + logger.trace("Exiting decodeCBOR()") } /// Decodes individual items from the CBOR string. @@ -60,10 +64,13 @@ public class ATCBORManager { /// - Parameter data: The CBOR string to be decoded. /// - Returns: An array of `CBOR` objects. private func decodeItems(from data: Data) throws -> [CBOR] { + logger.trace("In decodeItems()") guard let decoded = try CBOR.decodeMultipleItems(data.bytes, options: CBOROptions(useStringKeys: false, forbidNonStringMapKeys: true)) else { + logger.error("Failed to decode CBOR items", metadata: ["size": "\(data.count)"]) throw CBORProcessingError.cannotDecode } - + logger.debug("Decoded CBOR items", metadata: ["size": "\(decoded.count)"]) + logger.trace("Exiting decodeItems()") return decoded } @@ -120,9 +127,12 @@ public class ATCBORManager { /// - Returns: A subset of the data if the length is valid. /// - Throws: An error if the data length is not sufficient. func scanData(data: Data, length: Int) throws -> Data { + logger.trace("In scanData()") guard data.count >= length else { + logger.error("Error while scanning data", metadata: ["error": "\(ATEventStreamError.insufficientDataLength)"]) throw ATEventStreamError.insufficientDataLength } + logger.trace("Exiting scanData()") return data.subdata(in: 0.. CBORDecodedBlock? { + logger.trace("In decodeWebSocketData()") var index = 0 var result = [UInt8]() + logger.debug("Decoding web socket data", metadata: ["size": "\(data.count)"]) while index < data.count { let byte = data[index] result.append(byte) @@ -146,9 +158,11 @@ public class ATCBORManager { if result.isEmpty { // TODO: Add error handling. + logger.error("Error while decoding web socket data", metadata: ["error": "result is empty"]) return nil } + logger.trace("Exiting decodeWebSocketData()") return CBORDecodedBlock(value: decode(result), length: result.count) } @@ -158,9 +172,11 @@ public class ATCBORManager { /// - Returns: A ``CBORDecodedBlock`` containing the decoded value and the length of /// the processed data. public func decodeReader(from bytes: [UInt8]) -> CBORDecodedBlock { + logger.trace("In decodeReader()") var index = 0 var result = [UInt8]() + logger.debug("Decoding data block", metadata: ["size": "\(bytes.count)"]) while index < bytes.count { let byte = bytes[index] result.append(byte) @@ -170,6 +186,7 @@ public class ATCBORManager { } } + logger.trace("Exiting decodeReader()") return CBORDecodedBlock(value: decode(result), length: result.count) } @@ -178,11 +195,14 @@ public class ATCBORManager { /// - Parameter bytes: The bytes to decode. /// - Returns: The decoded integer. public func decode(_ bytes: [UInt8]) -> Int { + logger.trace("In decode()") var result = 0 + logger.debug("Decoding LEB128", metadata: ["size": "\(bytes.count)"]) for (i, byte) in bytes.enumerated() { let element = Int(byte & 0x7F) result += element << (i * 7) } + logger.trace("Exiting decode()") return result } } From 575f47864900875858494f7aa03fd0c2fdea57b3 Mon Sep 17 00:00:00 2001 From: stoicswe <31823043+stoicswe@users.noreply.github.com> Date: Mon, 27 May 2024 19:37:49 -0400 Subject: [PATCH 8/9] added logging to ATFacetParser --- .../ATProtoKit/Utilities/ATFacetParser.swift | 48 +++++++++++++++---- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/Sources/ATProtoKit/Utilities/ATFacetParser.swift b/Sources/ATProtoKit/Utilities/ATFacetParser.swift index 1b1cecf1d86..db1f0551445 100644 --- a/Sources/ATProtoKit/Utilities/ATFacetParser.swift +++ b/Sources/ATProtoKit/Utilities/ATFacetParser.swift @@ -6,9 +6,11 @@ // import Foundation +import Logging /// A utility class designed for parsing various elements like mentions, URLs, and hashtags from text. public class ATFacetParser { + private static var logger = Logger(label: "ATFacetParser") /// Manages a collection of ``Facet`` objects, providing thread-safe append operations. actor FacetsActor { @@ -28,6 +30,7 @@ public class ATFacetParser { /// - Returns: An array of `Dictionary`s containing the start and end positions of each mention /// and the mention text. public static func parseMentions(from text: String) -> [[String: Any]] { + logger.trace("In parseMentions()") var spans = [[String: Any]]() // Regex for grabbing @mentions. @@ -35,21 +38,26 @@ public class ATFacetParser { let mentionRegex = "[\\s|^](@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)" do { + logger.trace("Building regex") let regex = try NSRegularExpression(pattern: mentionRegex) let nsRange = NSRange(text.startIndex.. [[String: Any]] { + logger.trace("In parseURLs()") var spans = [[String: Any]]() // Regex for grabbing links. @@ -77,16 +88,20 @@ public class ATFacetParser { let linkRegex = "[\\s|^](https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&//=]*[-a-zA-Z0-9@%_\\+~#//=])?)" do { + logger.trace("Building regex") let regex = try NSRegularExpression(pattern: linkRegex) let nsRange = NSRange(text.startIndex.. [[String: Any]] { + logger.trace("In parseHashtags()") var spans = [[String: Any]]() // Regex for grabbing #hashtags. let hashtagRegex = "(? [Facet] { + logger.trace("In parseFacets()") let facets = FacetsActor() await withTaskGroup(of: Void.self) { group in + logger.trace("Parsing mentions") for mention in self.parseMentions(from: text) { group.addTask { do { // Unless something is wrong with `parseMentions()`, this is unlikely to fail. guard let handle = mention["mention"] as? String else { return } - print("Mention text: \(handle)") + logger.debug("Mention text received", metadata: ["handle": "\(handle)"]) // Remove the `@` from the handle. let notATHandle = String(handle.dropFirst()) @@ -169,6 +194,7 @@ public class ATFacetParser { // } let mentionResult = try await ATProtoKit().resolveHandle(from: notATHandle, pdsURL: pdsURL) + logger.debug("Mention result", metadata: ["result": "\(mentionResult)"]) switch mentionResult { case .success(let resolveHandleOutput): @@ -179,21 +205,23 @@ public class ATFacetParser { features: [.mention(Mention(did: resolveHandleOutput.handleDID))]) await facets.append(mentionFacet) + logger.debug("New mention facet added") case .failure(let error): - print("Error: \(error)") + logger.error("Error while processing mentions", metadata: ["error": "\(error)"]) } } catch { - + logger.error("Error while processing mentions", metadata: ["error": "\(error)"]) } } } // Grab all of the URLs and add them to the facet. + logger.trace("Parsing urls") for link in self.parseURLs(from: text) { group.addTask { // Unless something is wrong with `parseURLs()`, this is unlikely to fail. guard let url = link["link"] as? String else { return } - print("URL: \(link)") + logger.debug("URL text received", metadata: ["url": "\(url)"]) if let start = link["start"] as? Int, let end = link["end"] as? Int { @@ -203,16 +231,18 @@ public class ATFacetParser { ) await facets.append(linkFacet) + logger.debug("New url facet added") } } } // Grab all of the hashtags and add them to the facet. + logger.trace("Parsing hashtags") for hashtag in self.parseHashtags(from: text) { group.addTask { // Unless something is wrong with `parseHashtags()`, this is unlikely to fail. guard let tag = hashtag["tag"] as? String else { return } - print("Hashtag: \(tag)") + logger.debug("New hashtag text recieved", metadata: ["hashtag": "\(tag)"]) if let start = hashtag["start"] as? Int, let end = hashtag["end"] as? Int { @@ -222,11 +252,13 @@ public class ATFacetParser { ) await facets.append(hashTagFacet) + logger.debug("New hashtag facet added") } } } } + logger.trace("Exiting parseFacets()") return await facets.facets } } From ea0ccf231abce626924c06d7dae6336101a2b249 Mon Sep 17 00:00:00 2001 From: stoicswe <31823043+stoicswe@users.noreply.github.com> Date: Tue, 28 May 2024 13:48:47 -0400 Subject: [PATCH 9/9] added logging to ATProtoTools --- .../ATProtoKit/Utilities/ATProtoTools.swift | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/Sources/ATProtoKit/Utilities/ATProtoTools.swift b/Sources/ATProtoKit/Utilities/ATProtoTools.swift index 0d01bb0d927..8d0d86841f8 100644 --- a/Sources/ATProtoKit/Utilities/ATProtoTools.swift +++ b/Sources/ATProtoKit/Utilities/ATProtoTools.swift @@ -6,6 +6,7 @@ // import Foundation +import Logging /// A group of methods for miscellaneous aspects of ATProtoKit. /// @@ -20,22 +21,27 @@ import Foundation /// when version 1.0 is launched or `ATProtoTools` is stabilized, whichever comes first. /// Until then, if a method is better suited elsewhere, it will be immediately moved. class ATProtoTools { + private var logger = Logger(label: "ATProtoTools") /// Resolves the reply references to prepare them for a later post record request. /// /// - Parameter parentURI: The URI of the post record the current one is directly replying to. /// - Returns: A ``ReplyReference``. public func resolveReplyReferences(parentURI: String) async throws -> ReplyReference { + logger.trace("In resolveReplyReferences()") let parentRecord = try await fetchRecordForURI(parentURI) guard let replyReference = parentRecord.value?.reply else { + logger.debug("Creating a reply referrence from current parent", metadata: ["uri": "\(parentRecord.recordURI)", "parentCID": "\(parentRecord.recordCID)"]) // The parent record is a top-level post, so it is also the root return createReplyReference(from: parentRecord) } let rootRecord = try await fetchRecordForURI(replyReference.root.recordURI) let rootReference = rootRecord.value?.reply?.root ?? replyReference.root + logger.debug("Obtaining the reply reference from the parent reply root record", metadata: ["parentCID": "\(parentRecord.recordCID)", "rootCID": "\(rootRecord.recordCID)", "rootURI": "\(rootRecord.recordURI)"]) + logger.trace("Exiting resolveReplyReferences()") return ReplyReference(root: rootReference, parent: replyReference.parent) } @@ -44,14 +50,19 @@ class ATProtoTools { /// - Parameter uri: The URI of the record. /// - Returns: A ``RecordOutput`` public func fetchRecordForURI(_ uri: String) async throws -> RecordOutput { + logger.trace("In fetchRecordForURI()") let query = try parseURI(uri) - + logger.debug("Obtaining the repository record", metadata: ["uri": "\(query)"]) let record = try await ATProtoKit().getRepositoryRecord(from: query, pdsURL: nil) switch record { case .success(let result): + logger.debug("Reporitory record has been aquired", metadata: ["cid": "\(result.recordCID)"]) + logger.trace("In fetchRecordForURI()") return result case .failure(let failure): + logger.debug("Repository record has not been aquired") + logger.trace("In fetchRecordForURI()") throw failure } } @@ -61,7 +72,10 @@ class ATProtoTools { /// - Parameter record: The record to convert. /// - Returns: A ``ReplyReference``. private func createReplyReference(from record: RecordOutput) -> ReplyReference { + logger.trace("In createReplyReference()") let reference = StrongReference(recordURI: record.recordURI, cidHash: record.recordCID) + logger.debug("Creating the reply reference from record", metadata: ["recordURI": "\(record.recordURI)", "recordCID": "\(record.recordCID)"]) + logger.trace("Exiting createReplyReference()") return ReplyReference(root: reference, parent: reference) } @@ -78,15 +92,25 @@ class ATProtoTools { /// - Returns: A ``RecordQuery``. internal func parseURI(_ uri: String, pdsURL: String = "https://bsky.app") throws -> RecordQuery { + logger.trace("In parseURI()") if uri.hasPrefix("at://") { + logger.debug("Parsing URI with 'at://' prefix") let components = uri.split(separator: "/").map(String.init) - guard components.count >= 4 else { throw ATRequestPrepareError.invalidFormat } - + guard components.count >= 4 else { + logger.error("Failed to parse the URI: too many components", metadata: ["error": "\(ATRequestPrepareError.invalidFormat)"]) + throw ATRequestPrepareError.invalidFormat + } + + logger.debug("RecordQuery constructed.", metadata: ["repo": "\(components[1])", "collection": "\(components[2])", "recordKey": "\(components[3])"]) + logger.trace("Exiting parseURI()") return RecordQuery(repo: components[1], collection: components[2], recordKey: components[3]) } else if uri.hasPrefix("\(pdsURL)/") { + logger.debug("Parsing URI with pds url '\(pdsURL)' prefix") let components = uri.split(separator: "/").map(String.init) guard components.count >= 6 else { - throw ATRequestPrepareError.invalidFormat } + logger.error("Failed to parse the URI: too many components", metadata: ["error": "\(ATRequestPrepareError.invalidFormat)"]) + throw ATRequestPrepareError.invalidFormat + } let record = components[3] let recordKey = components[5] @@ -100,11 +124,15 @@ class ATProtoTools { case "feed": collection = "app.bsky.feed.generator" default: + logger.error("Failed to parse the URI: invalid collection format", metadata: ["error": "\(ATRequestPrepareError.invalidFormat)"]) throw ATRequestPrepareError.invalidFormat } + logger.debug("RecordQuery constructed.", metadata: ["repo": "\(record)", "collection": "\(collection)", "recordKey": "\(recordKey)"]) + logger.trace("Exiting parseURI()") return RecordQuery(repo: record, collection: collection, recordKey: recordKey) } else { + logger.error("Failed to parse the URI", metadata: ["error": "\(ATRequestPrepareError.invalidFormat)"]) throw ATRequestPrepareError.invalidFormat } }