diff --git a/Sources/ATProtoKit/Models/Common/ATEventStreamConfiguration/ATEventStreamConfiguration.swift b/Sources/ATProtoKit/Models/Common/ATEventStreamConfiguration/ATEventStreamConfiguration.swift index 68acf5d41b..241b9b9b4d 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 8baecde3fc..f859a72a2c 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 4191689fa9..50b2f2334d 100644 --- a/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATFirehoseStream/ATFirehoseStream.swift +++ b/Sources/ATProtoKit/Networking/ATEventStreamConfiguration/ATFirehoseStream/ATFirehoseStream.swift @@ -6,13 +6,13 @@ // import Foundation +import Logging /// The base class for the AT Protocol's Firehose event stream. class ATFirehoseStream: ATEventStreamConfiguration { - + 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`. public var relayURL: String = "wss://bsky.network" @@ -49,6 +49,8 @@ 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 self.cursor = cursor @@ -56,8 +58,15 @@ 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("Creating the websocket task") self.webSocketTask = urlSession.webSocketTask(with: webSocketURL) + logger.trace("Exiting init()") } } diff --git a/Sources/ATProtoKit/Networking/SessionManager/ATProtocolConfiguration.swift b/Sources/ATProtoKit/Networking/SessionManager/ATProtocolConfiguration.swift index 240e581ea6..ddcf44df36 100644 --- a/Sources/ATProtoKit/Networking/SessionManager/ATProtocolConfiguration.swift +++ b/Sources/ATProtoKit/Networking/SessionManager/ATProtocolConfiguration.swift @@ -62,14 +62,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 @@ -94,10 +91,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 = ComAtprotoLexicon.Server.CreateSessionRequestBody( identifier: handle, password: appPassword, @@ -107,6 +108,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) @@ -115,11 +118,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. @@ -165,11 +173,14 @@ public class ATProtocolConfiguration: ProtocolConfiguration { 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) } let requestBody = ComAtprotoLexicon.Server.CreateAccountRequestBody( + develop email: email, handle: handle, existingDID: existingDID, @@ -187,6 +198,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) @@ -195,9 +208,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) } } @@ -220,8 +237,10 @@ public class ATProtocolConfiguration: ProtocolConfiguration { 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) } @@ -229,11 +248,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) } } @@ -256,8 +280,10 @@ public class ATProtocolConfiguration: ProtocolConfiguration { 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) } @@ -265,6 +291,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 @@ -273,8 +300,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) } } @@ -295,8 +326,10 @@ public class ATProtocolConfiguration: ProtocolConfiguration { 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 } @@ -304,10 +337,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 } } diff --git a/Sources/ATProtoKit/Utilities/APIClientService.swift b/Sources/ATProtoKit/Utilities/APIClientService.swift index c84b8641d2..2c9a8903b6 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 -> ComAtprotoLexicon.Repository.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: ComAtprotoLexicon.Repository.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) } diff --git a/Sources/ATProtoKit/Utilities/ATCBORManager.swift b/Sources/ATProtoKit/Utilities/ATCBORManager.swift index cc8e26b174..89e45796aa 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 } } diff --git a/Sources/ATProtoKit/Utilities/ATFacetParser.swift b/Sources/ATProtoKit/Utilities/ATFacetParser.swift index b06c464c46..577c5774e1 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 ``AppBskyLexicon/RichText/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 = "(? [AppBskyLexicon.RichText.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(AppBskyLexicon.RichText.Facet.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 } } diff --git a/Sources/ATProtoKit/Utilities/ATProtoTools.swift b/Sources/ATProtoKit/Utilities/ATProtoTools.swift index 3cc8572e3e..a3ec9f13fb 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,12 +21,14 @@ 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. public 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 ``AppBskyLexicon/Feed/PostRecord/ReplyReference``. public func resolveReplyReferences(parentURI: String) async throws -> AppBskyLexicon.Feed.PostRecord.ReplyReference { + logger.trace("In resolveReplyReferences()") let threadRecords = try await fetchRecordForURI(parentURI) guard let parentRecord = threadRecords.value else { @@ -69,6 +72,7 @@ public class ATProtoTools { return AppBskyLexicon.Feed.PostRecord.ReplyReference(root: replyReference.root, parent: replyReference.parent) } } + logger.trace("Exiting resolveReplyReferences()") return AppBskyLexicon.Feed.PostRecord.ReplyReference(root: replyReference.root, parent: replyReference.parent) } @@ -77,14 +81,19 @@ public class ATProtoTools { /// - Parameter uri: The URI of the record. /// - Returns: A ``ComAtprotoLexicon/Repository/GetRecordOutput`` public func fetchRecordForURI(_ uri: String) async throws -> ComAtprotoLexicon.Repository.GetRecordOutput { + logger.trace("In fetchRecordForURI()") let query = try parseURI(uri) let record = try await ATProtoKit().getRepositoryRecord(from: query.repository, collection: query.collection, recordKey: query.recordKey, 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 } } @@ -94,6 +103,7 @@ public class ATProtoTools { /// - Parameter record: The record to convert. /// - Returns: A ``ReplyReference``. private func createReplyReference(from record: ComAtprotoLexicon.Repository.GetRecordOutput) -> AppBskyLexicon.Feed.PostRecord.ReplyReference { + logger.trace("In createReplyReference()") let reference = ComAtprotoLexicon.Repository.StrongReference(recordURI: record.recordURI, cidHash: record.recordCID) return AppBskyLexicon.Feed.PostRecord.ReplyReference(root: reference, parent: reference) @@ -112,12 +122,14 @@ public 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 } - return ATProtoTools.RecordQuery(repository: 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 @@ -135,11 +147,12 @@ public 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 } - return RecordQuery(repository: record, collection: collection, recordKey: recordKey) } else { + logger.error("Failed to parse the URI", metadata: ["error": "\(ATRequestPrepareError.invalidFormat)"]) throw ATRequestPrepareError.invalidFormat } } diff --git a/Sources/ATProtoKit/Utilities/Logging/Logging.swift b/Sources/ATProtoKit/Utilities/Logging/Logging.swift index 41e13848f1..d8785484c0 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) +#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 +import Logging +/// 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 { + // Component public let subsystem: String + // Category Metadata Field public let category: String + // 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 = [:] - private var appleLogger: os.Logger +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) + private var logger: os.Logger +#else + private var logger: Logging.StreamLogHandler +#endif + /// 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" - self.appleLogger = Logger(subsystem: subsystem, category: category ?? "ATProtoKit") +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) + // Using the apple logger built-into the Apple OSs + self.logger = Logger(subsystem: subsystem, category: category ?? "ATProtoKit") +#else + // Otherwise, use the cross-platform logging lib + self.logger = Logging.StreamLogHandler(label: "\(subsystem) \(category ?? "ATProtoKit")") +#endif } + /// 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?, @@ -30,29 +57,48 @@ 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)") + // 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: + logger.trace("\(logMsgPrefix) \(message, privacy: .auto)") + case .debug: + logger.debug("\(logMsgPrefix) \(message, privacy: .auto)") case .info: - appleLogger.log(level: .info, "\(message, privacy: .auto)") + logger.info("\(logMsgPrefix) \(message, privacy: .auto)") case .notice: - appleLogger.log(level: .default, "\(message, privacy: .auto)") + logger.notice("\(logMsgPrefix) \(message, privacy: .auto)") case .warning: - appleLogger.log(level: .error, "\(message, privacy: .auto)") + logger.warning("\(logMsgPrefix) \(message, privacy: .auto)") case .error: - appleLogger.log(level: .error, "\(message, privacy: .auto)") + logger.error("\(logMsgPrefix) \(message, privacy: .auto)") case .critical: - appleLogger.log(level: .fault, "\(message, privacy: .auto)") + logger.critical("\(logMsgPrefix) \(message, privacy: .auto)") } +#else + // if logging on other platforms, pass down the log details to the standard logger + logger.log(level: level, message: "\(message, privacy: .auto)", metadata: allMetadata, source: source, file: file, function: function, line: line) +#endif } + /// 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 diff --git a/Sources/ATProtoKit/Utilities/Logging/LoggingBootStrapping.swift b/Sources/ATProtoKit/Utilities/Logging/LoggingBootStrapping.swift index 02c85e08ee..addf911152 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" + } +}