From 42ca887e693278614b359320bc35870a59eeaf2b Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 16 Jan 2024 19:05:41 -0300 Subject: [PATCH] feat: Add SupabaseLogger (#219) * Add logger type * Add SupabaseLogger * Fix tests * Add SupabaseLoggingConfiguration * Simplify logger * Inline log methods * Revert some breaking changes and deprecate some initializers --- Examples/UserManagement/Supabase.swift | 23 +++- Package.resolved | 11 +- Sources/Auth/AuthClient.swift | 27 +++- Sources/Auth/Deprecated.swift | 81 +++++++++++ Sources/Auth/Internal/Dependencies.swift | 2 + Sources/Auth/Storage/AuthLocalStorage.swift | 12 ++ Sources/PostgREST/Deprecated.swift | 80 +++++++++++ Sources/PostgREST/PostgrestBuilder.swift | 4 +- Sources/PostgREST/PostgrestClient.swift | 8 ++ Sources/Realtime/Deprecated.swift | 67 +++++++++ Sources/Realtime/RealtimeClient.swift | 17 ++- Sources/Storage/Deprecated.swift | 32 +++++ Sources/Storage/StorageApi.swift | 2 +- Sources/Storage/SupabaseStorage.swift | 6 +- Sources/Supabase/SupabaseClient.swift | 12 +- Sources/Supabase/Types.swift | 41 +++++- Sources/_Helpers/Logger.swift | 36 ----- Sources/_Helpers/Request.swift | 7 +- Sources/_Helpers/SupabaseLogger.swift | 127 ++++++++++++++++++ Tests/AuthTests/GoTrueClientTests.swift | 11 +- Tests/AuthTests/Mocks/Mocks.swift | 3 +- Tests/AuthTests/RequestsTests.swift | 5 +- .../xcshareddata/swiftpm/Package.resolved | 8 +- 23 files changed, 550 insertions(+), 72 deletions(-) create mode 100644 Sources/PostgREST/Deprecated.swift create mode 100644 Sources/Realtime/Deprecated.swift create mode 100644 Sources/Storage/Deprecated.swift delete mode 100644 Sources/_Helpers/Logger.swift create mode 100644 Sources/_Helpers/SupabaseLogger.swift diff --git a/Examples/UserManagement/Supabase.swift b/Examples/UserManagement/Supabase.swift index fdc04748..14b19d90 100644 --- a/Examples/UserManagement/Supabase.swift +++ b/Examples/UserManagement/Supabase.swift @@ -6,13 +6,28 @@ // import Foundation +import OSLog import Supabase let supabase = SupabaseClient( supabaseURL: URL(string: "https://PROJECT_ID.supabase.co")!, supabaseKey: "YOUR_SUPABASE_ANON_KEY", - options: .init(auth: .init(storage: KeychainLocalStorage( - service: "supabase.gotrue.swift", - accessGroup: nil - ))) + options: .init( + global: .init(logger: AppLogger()) + ) ) + +struct AppLogger: SupabaseLogger { + let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "supabase") + + func log(message: SupabaseLogMessage) { + switch message.level { + case .verbose: + logger.log(level: .info, "\(message.description)") + case .debug: + logger.log(level: .debug, "\(message.description)") + case .warning, .error: + logger.log(level: .error, "\(message.description)") + } + } +} diff --git a/Package.resolved b/Package.resolved index 5c8656bb..4ac5291a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "keychainaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kishikawakatsumi/KeychainAccess", + "state" : { + "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", + "version" : "4.2.2" + } + }, { "identity" : "swift-concurrency-extras", "kind" : "remoteSourceControl", @@ -47,4 +56,4 @@ } ], "version" : 2 -} \ No newline at end of file +} diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 0e351cfc..35521872 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -16,6 +16,7 @@ public actor AuthClient { public var headers: [String: String] public let flowType: AuthFlowType public let localStorage: AuthLocalStorage + public let logger: SupabaseLogger? public let encoder: JSONEncoder public let decoder: JSONDecoder public let fetch: FetchHandler @@ -27,6 +28,7 @@ public actor AuthClient { /// - headers: Custom headers to be included in requests. /// - flowType: The authentication flow type. /// - localStorage: The storage mechanism for local data. + /// - logger: The logger to use. /// - encoder: The JSON encoder to use for encoding requests. /// - decoder: The JSON decoder to use for decoding responses. /// - fetch: The asynchronous fetch handler for network requests. @@ -35,6 +37,7 @@ public actor AuthClient { headers: [String: String] = [:], flowType: AuthFlowType = Configuration.defaultFlowType, localStorage: AuthLocalStorage, + logger: SupabaseLogger? = nil, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) } @@ -45,6 +48,7 @@ public actor AuthClient { self.headers = headers self.flowType = flowType self.localStorage = localStorage + self.logger = logger self.encoder = encoder self.decoder = decoder self.fetch = fetch @@ -75,6 +79,10 @@ public actor AuthClient { Dependencies.current.value!.currentDate } + private var logger: SupabaseLogger? { + Dependencies.current.value!.logger + } + /// Returns the session, refreshing it if necessary. /// /// If no session can be found, a ``AuthError/sessionNotFound`` error is thrown. @@ -94,6 +102,7 @@ public actor AuthClient { /// - headers: Custom headers to be included in requests. /// - flowType: The authentication flow type.. /// - localStorage: The storage mechanism for local data.. + /// - logger: The logger to use. /// - encoder: The JSON encoder to use for encoding requests. /// - decoder: The JSON decoder to use for decoding responses. /// - fetch: The asynchronous fetch handler for network requests. @@ -102,6 +111,7 @@ public actor AuthClient { headers: [String: String] = [:], flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, localStorage: AuthLocalStorage, + logger: SupabaseLogger? = nil, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) } @@ -112,6 +122,7 @@ public actor AuthClient { headers: headers, flowType: flowType, localStorage: localStorage, + logger: logger, encoder: encoder, decoder: decoder, fetch: fetch @@ -124,7 +135,10 @@ public actor AuthClient { /// - Parameters: /// - configuration: The client configuration. public init(configuration: Configuration) { - let api = APIClient.live(http: HTTPClient(fetchHandler: configuration.fetch)) + let api = APIClient.live(http: HTTPClient( + logger: configuration.logger, + fetchHandler: configuration.fetch + )) self.init( configuration: configuration, @@ -132,7 +146,8 @@ public actor AuthClient { codeVerifierStorage: .live, api: api, eventEmitter: .live, - sessionStorage: .live + sessionStorage: .live, + logger: configuration.logger ) } @@ -143,7 +158,8 @@ public actor AuthClient { codeVerifierStorage: CodeVerifierStorage, api: APIClient, eventEmitter: EventEmitter, - sessionStorage: SessionStorage + sessionStorage: SessionStorage, + logger: SupabaseLogger? ) { mfa = AuthMFA() @@ -159,7 +175,8 @@ public actor AuthClient { try await self?.refreshSession(refreshToken: $0) ?? .empty } ), - codeVerifierStorage: codeVerifierStorage + codeVerifierStorage: codeVerifierStorage, + logger: logger ) ) } @@ -172,9 +189,11 @@ public actor AuthClient { session: Session? )> { let (id, stream) = eventEmitter.attachListener() + logger?.debug("auth state change listener with id '\(id.uuidString)' attached.") Task { [id] in await emitInitialSession(forStreamWithID: id) + logger?.debug("initial session for listener with id '\(id.uuidString)' emitted.") } return stream diff --git a/Sources/Auth/Deprecated.swift b/Sources/Auth/Deprecated.swift index 3e1721a2..8f428c24 100644 --- a/Sources/Auth/Deprecated.swift +++ b/Sources/Auth/Deprecated.swift @@ -5,8 +5,13 @@ // Created by Guilherme Souza on 14/12/23. // +import _Helpers import Foundation +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + @available(*, deprecated, renamed: "AuthClient") public typealias GoTrueClient = AuthClient @@ -45,3 +50,79 @@ extension JSONDecoder { AuthClient.Configuration.jsonDecoder } } + +extension AuthClient.Configuration { + /// Initializes a AuthClient Configuration with optional parameters. + /// + /// - Parameters: + /// - url: The base URL of the Auth server. + /// - headers: Custom headers to be included in requests. + /// - flowType: The authentication flow type. + /// - localStorage: The storage mechanism for local data. + /// - encoder: The JSON encoder to use for encoding requests. + /// - decoder: The JSON decoder to use for decoding responses. + /// - fetch: The asynchronous fetch handler for network requests. + @available( + *, + deprecated, + message: "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)" + ) + public init( + url: URL, + headers: [String: String] = [:], + flowType: AuthFlowType = Self.defaultFlowType, + localStorage: AuthLocalStorage, + encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, + decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, + fetch: @escaping AuthClient.FetchHandler = { try await URLSession.shared.data(for: $0) } + ) { + self.init( + url: url, + headers: headers, + flowType: flowType, + localStorage: localStorage, + logger: nil, + encoder: encoder, + decoder: decoder, + fetch: fetch + ) + } +} + +extension AuthClient { + /// Initializes a AuthClient Configuration with optional parameters. + /// + /// - Parameters: + /// - url: The base URL of the Auth server. + /// - headers: Custom headers to be included in requests. + /// - flowType: The authentication flow type. + /// - localStorage: The storage mechanism for local data. + /// - encoder: The JSON encoder to use for encoding requests. + /// - decoder: The JSON decoder to use for decoding responses. + /// - fetch: The asynchronous fetch handler for network requests. + @available( + *, + deprecated, + message: "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)" + ) + public init( + url: URL, + headers: [String: String] = [:], + flowType: AuthFlowType = Configuration.defaultFlowType, + localStorage: AuthLocalStorage, + encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, + decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, + fetch: @escaping AuthClient.FetchHandler = { try await URLSession.shared.data(for: $0) } + ) { + self.init( + url: url, + headers: headers, + flowType: flowType, + localStorage: localStorage, + logger: nil, + encoder: encoder, + decoder: decoder, + fetch: fetch + ) + } +} diff --git a/Sources/Auth/Internal/Dependencies.swift b/Sources/Auth/Internal/Dependencies.swift index 33e042ca..99722b0b 100644 --- a/Sources/Auth/Internal/Dependencies.swift +++ b/Sources/Auth/Internal/Dependencies.swift @@ -1,3 +1,4 @@ +@_spi(Internal) import _Helpers import ConcurrencyExtras import Foundation @@ -12,4 +13,5 @@ struct Dependencies: Sendable { var sessionRefresher: SessionRefresher var codeVerifierStorage: CodeVerifierStorage var currentDate: @Sendable () -> Date = { Date() } + var logger: SupabaseLogger? } diff --git a/Sources/Auth/Storage/AuthLocalStorage.swift b/Sources/Auth/Storage/AuthLocalStorage.swift index 40e4930c..e00234cd 100644 --- a/Sources/Auth/Storage/AuthLocalStorage.swift +++ b/Sources/Auth/Storage/AuthLocalStorage.swift @@ -5,3 +5,15 @@ public protocol AuthLocalStorage: Sendable { func retrieve(key: String) throws -> Data? func remove(key: String) throws } + +extension AuthClient.Configuration { + #if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) + public static let defaultLocalStorage: AuthLocalStorage = KeychainLocalStorage( + service: "supabase.gotrue.swift", + accessGroup: nil + ) + #elseif os(Windows) + public static let defaultLocalStorage: AuthLocalStorage = + WinCredLocalStorage(service: "supabase.gotrue.swift") + #endif +} diff --git a/Sources/PostgREST/Deprecated.swift b/Sources/PostgREST/Deprecated.swift new file mode 100644 index 00000000..1dcd9884 --- /dev/null +++ b/Sources/PostgREST/Deprecated.swift @@ -0,0 +1,80 @@ +// +// Deprecated.swift +// +// +// Created by Guilherme Souza on 16/01/24. +// + +import Foundation + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +extension PostgrestClient.Configuration { + /// Initializes a new configuration for the PostgREST client. + /// - Parameters: + /// - url: The URL of the PostgREST server. + /// - schema: The schema to use. + /// - headers: The headers to include in requests. + /// - fetch: The fetch handler to use for requests. + /// - encoder: The JSONEncoder to use for encoding. + /// - decoder: The JSONDecoder to use for decoding. + @available( + *, + deprecated, + message: "Replace usages of this initializer with new init(url:schema:headers:logger:fetch:encoder:decoder:)" + ) + public init( + url: URL, + schema: String? = nil, + headers: [String: String] = [:], + fetch: @escaping PostgrestClient.FetchHandler = { try await URLSession.shared.data(for: $0) }, + encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, + decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder + ) { + self.init( + url: url, + schema: schema, + headers: headers, + logger: nil, + fetch: fetch, + encoder: encoder, + decoder: decoder + ) + } +} + +extension PostgrestClient { + /// Creates a PostgREST client with the specified parameters. + /// - Parameters: + /// - url: The URL of the PostgREST server. + /// - schema: The schema to use. + /// - headers: The headers to include in requests. + /// - session: The URLSession to use for requests. + /// - encoder: The JSONEncoder to use for encoding. + /// - decoder: The JSONDecoder to use for decoding. + @available( + *, + deprecated, + message: "Replace usages of this initializer with new init(url:schema:headers:logger:fetch:encoder:decoder:)" + ) + public init( + url: URL, + schema: String? = nil, + headers: [String: String] = [:], + fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, + decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder + ) { + self.init( + url: url, + schema: schema, + headers: headers, + logger: nil, + fetch: fetch, + encoder: encoder, + decoder: decoder + ) + } +} diff --git a/Sources/PostgREST/PostgrestBuilder.swift b/Sources/PostgREST/PostgrestBuilder.swift index 539eba35..d2bcea42 100644 --- a/Sources/PostgREST/PostgrestBuilder.swift +++ b/Sources/PostgREST/PostgrestBuilder.swift @@ -26,7 +26,7 @@ public class PostgrestBuilder: @unchecked Sendable { request: Request ) { self.configuration = configuration - http = HTTPClient(fetchHandler: configuration.fetch) + http = HTTPClient(logger: configuration.logger, fetchHandler: configuration.fetch) mutableState = LockIsolated( MutableState( @@ -74,7 +74,7 @@ public class PostgrestBuilder: @unchecked Sendable { do { return try configuration.decoder.decode(T.self, from: data) } catch { - debug("Fail to decode type '\(T.self) with error: \(error)") + configuration.logger?.error("Fail to decode type '\(T.self) with error: \(error)") throw error } } diff --git a/Sources/PostgREST/PostgrestClient.swift b/Sources/PostgREST/PostgrestClient.swift index 2d317130..f171cc0a 100644 --- a/Sources/PostgREST/PostgrestClient.swift +++ b/Sources/PostgREST/PostgrestClient.swift @@ -20,11 +20,14 @@ public actor PostgrestClient { public var encoder: JSONEncoder public var decoder: JSONDecoder + let logger: SupabaseLogger? + /// Initializes a new configuration for the PostgREST client. /// - Parameters: /// - url: The URL of the PostgREST server. /// - schema: The schema to use. /// - headers: The headers to include in requests. + /// - logger: The logger to use. /// - fetch: The fetch handler to use for requests. /// - encoder: The JSONEncoder to use for encoding. /// - decoder: The JSONDecoder to use for decoding. @@ -32,6 +35,7 @@ public actor PostgrestClient { url: URL, schema: String? = nil, headers: [String: String] = [:], + logger: SupabaseLogger? = nil, fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder @@ -39,6 +43,7 @@ public actor PostgrestClient { self.url = url self.schema = schema self.headers = headers + self.logger = logger self.fetch = fetch self.encoder = encoder self.decoder = decoder @@ -60,6 +65,7 @@ public actor PostgrestClient { /// - url: The URL of the PostgREST server. /// - schema: The schema to use. /// - headers: The headers to include in requests. + /// - logger: The logger to use. /// - session: The URLSession to use for requests. /// - encoder: The JSONEncoder to use for encoding. /// - decoder: The JSONDecoder to use for decoding. @@ -67,6 +73,7 @@ public actor PostgrestClient { url: URL, schema: String? = nil, headers: [String: String] = [:], + logger: SupabaseLogger? = nil, fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder @@ -76,6 +83,7 @@ public actor PostgrestClient { url: url, schema: schema, headers: headers, + logger: logger, fetch: fetch, encoder: encoder, decoder: decoder diff --git a/Sources/Realtime/Deprecated.swift b/Sources/Realtime/Deprecated.swift new file mode 100644 index 00000000..99238b03 --- /dev/null +++ b/Sources/Realtime/Deprecated.swift @@ -0,0 +1,67 @@ +// +// Deprecated.swift +// +// +// Created by Guilherme Souza on 16/01/24. +// + +import Foundation + +extension RealtimeClient { + @available( + *, + deprecated, + message: "Replace usages of this initializer with new init(_:headers:params:vsn:logger)" + ) + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public convenience init( + _ endPoint: String, + headers: [String: String] = [:], + params: Payload? = nil, + vsn: String = Defaults.vsn + ) { + self.init(endPoint, headers: headers, params: params, vsn: vsn, logger: nil) + } + + @available( + *, + deprecated, + message: "Replace usages of this initializer with new init(_:headers:paramsClosure:vsn:logger)" + ) + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *) + public convenience init( + _ endPoint: String, + headers: [String: String] = [:], + paramsClosure: PayloadClosure?, + vsn: String = Defaults.vsn + ) { + self.init( + endPoint, + headers: headers, paramsClosure: paramsClosure, + vsn: vsn, + logger: nil + ) + } + + @available( + *, + deprecated, + message: "Replace usages of this initializer with new init(endPoint:headers:transport:paramsClosure:vsn:logger)" + ) + public convenience init( + endPoint: String, + headers: [String: String] = [:], + transport: @escaping ((URL) -> PhoenixTransport), + paramsClosure: PayloadClosure? = nil, + vsn: String = Defaults.vsn + ) { + self.init( + endPoint: endPoint, + headers: headers, + transport: transport, + paramsClosure: paramsClosure, + vsn: vsn, + logger: nil + ) + } +} diff --git a/Sources/Realtime/RealtimeClient.swift b/Sources/Realtime/RealtimeClient.swift index 3f9d1659..9ab9fe31 100644 --- a/Sources/Realtime/RealtimeClient.swift +++ b/Sources/Realtime/RealtimeClient.swift @@ -186,14 +186,16 @@ public class RealtimeClient: PhoenixTransportDelegate { _ endPoint: String, headers: [String: String] = [:], params: Payload? = nil, - vsn: String = Defaults.vsn + vsn: String = Defaults.vsn, + logger: SupabaseLogger? = nil ) { self.init( endPoint: endPoint, headers: headers, transport: { url in URLSessionTransport(url: url) }, paramsClosure: { params }, - vsn: vsn + vsn: vsn, + logger: logger ) } @@ -202,14 +204,16 @@ public class RealtimeClient: PhoenixTransportDelegate { _ endPoint: String, headers: [String: String] = [:], paramsClosure: PayloadClosure?, - vsn: String = Defaults.vsn + vsn: String = Defaults.vsn, + logger: SupabaseLogger? = nil ) { self.init( endPoint: endPoint, headers: headers, transport: { url in URLSessionTransport(url: url) }, paramsClosure: paramsClosure, - vsn: vsn + vsn: vsn, + logger: logger ) } @@ -218,7 +222,8 @@ public class RealtimeClient: PhoenixTransportDelegate { headers: [String: String] = [:], transport: @escaping ((URL) -> PhoenixTransport), paramsClosure: PayloadClosure? = nil, - vsn: String = Defaults.vsn + vsn: String = Defaults.vsn, + logger: SupabaseLogger? = nil ) { self.transport = transport self.paramsClosure = paramsClosure @@ -230,7 +235,7 @@ public class RealtimeClient: PhoenixTransportDelegate { headers["X-Client-Info"] = "realtime-swift/\(version)" } self.headers = headers - http = HTTPClient(fetchHandler: { try await URLSession.shared.data(for: $0) }) + http = HTTPClient(logger: logger, fetchHandler: { try await URLSession.shared.data(for: $0) }) let params = paramsClosure?() if let jwt = (params?["Authorization"] as? String)?.split(separator: " ").last { diff --git a/Sources/Storage/Deprecated.swift b/Sources/Storage/Deprecated.swift new file mode 100644 index 00000000..c4ffccc6 --- /dev/null +++ b/Sources/Storage/Deprecated.swift @@ -0,0 +1,32 @@ +// +// Deprecated.swift +// +// +// Created by Guilherme Souza on 16/01/24. +// + +import Foundation + +extension StorageClientConfiguration { + @available( + *, + deprecated, + message: "Replace usages of this initializer with new init(url:headers:encoder:decoder:session:logger)" + ) + public init( + url: URL, + headers: [String: String], + encoder: JSONEncoder = .defaultStorageEncoder, + decoder: JSONDecoder = .defaultStorageDecoder, + session: StorageHTTPSession = .init() + ) { + self.init( + url: url, + headers: headers, + encoder: encoder, + decoder: decoder, + session: session, + logger: nil + ) + } +} diff --git a/Sources/Storage/StorageApi.swift b/Sources/Storage/StorageApi.swift index 57b115bf..69f3421f 100644 --- a/Sources/Storage/StorageApi.swift +++ b/Sources/Storage/StorageApi.swift @@ -16,7 +16,7 @@ public class StorageApi: @unchecked Sendable { configuration.headers["X-Client-Info"] = "storage-swift/\(version)" } self.configuration = configuration - http = HTTPClient(fetchHandler: configuration.session.fetch) + http = HTTPClient(logger: configuration.logger, fetchHandler: configuration.session.fetch) } @discardableResult diff --git a/Sources/Storage/SupabaseStorage.swift b/Sources/Storage/SupabaseStorage.swift index 1b7c283c..9fc2d5f5 100644 --- a/Sources/Storage/SupabaseStorage.swift +++ b/Sources/Storage/SupabaseStorage.swift @@ -1,3 +1,4 @@ +import _Helpers import Foundation public struct StorageClientConfiguration { @@ -6,19 +7,22 @@ public struct StorageClientConfiguration { public let encoder: JSONEncoder public let decoder: JSONDecoder public let session: StorageHTTPSession + public let logger: SupabaseLogger? public init( url: URL, headers: [String: String], encoder: JSONEncoder = .defaultStorageEncoder, decoder: JSONDecoder = .defaultStorageDecoder, - session: StorageHTTPSession = .init() + session: StorageHTTPSession = .init(), + logger: SupabaseLogger? = nil ) { self.url = url self.headers = headers self.encoder = encoder self.decoder = decoder self.session = session + self.logger = logger } } diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index 03f9b758..087e58d7 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -11,6 +11,10 @@ import Foundation import FoundationNetworking #endif +public typealias SupabaseLogger = _Helpers.SupabaseLogger +public typealias SupabaseLogLevel = _Helpers.SupabaseLogLevel +public typealias SupabaseLogMessage = _Helpers.SupabaseLogMessage + let version = _Helpers.version /// Supabase Client. @@ -31,6 +35,7 @@ public final class SupabaseClient: @unchecked Sendable { url: databaseURL, schema: options.db.schema, headers: defaultHeaders, + logger: options.global.logger, fetch: fetchWithAuth, encoder: options.db.encoder, decoder: options.db.decoder @@ -41,7 +46,8 @@ public final class SupabaseClient: @unchecked Sendable { configuration: StorageClientConfiguration( url: storageURL, headers: defaultHeaders, - session: StorageHTTPSession(fetch: fetchWithAuth, upload: uploadWithAuth) + session: StorageHTTPSession(fetch: fetchWithAuth, upload: uploadWithAuth), + logger: options.global.logger ) ) @@ -93,6 +99,7 @@ public final class SupabaseClient: @unchecked Sendable { headers: defaultHeaders, flowType: options.auth.flowType, localStorage: options.auth.storage, + logger: options.global.logger, encoder: options.auth.encoder, decoder: options.auth.decoder, fetch: { @@ -104,7 +111,8 @@ public final class SupabaseClient: @unchecked Sendable { realtime = RealtimeClient( supabaseURL.appendingPathComponent("/realtime/v1").absoluteString, headers: defaultHeaders, - params: defaultHeaders + params: defaultHeaders, + logger: options.global.logger ) listenForAuthEvents() diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index 3da0cd8f..d659d2b0 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -1,3 +1,4 @@ +import _Helpers import Auth import Foundation import PostgREST @@ -67,9 +68,17 @@ public struct SupabaseClientOptions: Sendable { /// A session to use for making requests, defaults to `URLSession.shared`. public let session: URLSession - public init(headers: [String: String] = [:], session: URLSession = .shared) { + /// The logger to use across all Supabase sub-packages. + public let logger: SupabaseLogger? + + public init( + headers: [String: String] = [:], + session: URLSession = .shared, + logger: SupabaseLogger? = nil + ) { self.headers = headers self.session = session + self.logger = logger } } @@ -83,3 +92,33 @@ public struct SupabaseClientOptions: Sendable { self.global = global } } + +extension SupabaseClientOptions { + #if !os(Linux) + public init( + db: DatabaseOptions = .init(), + global: GlobalOptions = .init() + ) { + self.db = db + auth = .init() + self.global = global + } + #endif +} + +extension SupabaseClientOptions.AuthOptions { + #if !os(Linux) + public init( + flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, + encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, + decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder + ) { + self.init( + storage: AuthClient.Configuration.defaultLocalStorage, + flowType: flowType, + encoder: encoder, + decoder: decoder + ) + } + #endif +} diff --git a/Sources/_Helpers/Logger.swift b/Sources/_Helpers/Logger.swift deleted file mode 100644 index 36a4a186..00000000 --- a/Sources/_Helpers/Logger.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// Logger.swift -// -// -// Created by Guilherme Souza on 11/12/23. -// - -import ConcurrencyExtras -import Foundation - -private let _debugLoggingEnabled = LockIsolated(false) -@_spi(Internal) public var debugLoggingEnabled: Bool { - get { _debugLoggingEnabled.value } - set { _debugLoggingEnabled.setValue(newValue) } -} - -private let standardError = LockIsolated(FileHandle.standardError) -@_spi(Internal) public func debug( - _ message: @autoclosure () -> String, - function: String = #function, - file: String = #file, - line: UInt = #line -) { - assert( - { - if debugLoggingEnabled { - standardError.withValue { - let logLine = "[\(function) \(file.split(separator: "/").last!):\(line)] \(message())\n" - $0.write(Data(logLine.utf8)) - } - } - - return true - }() - ) -} diff --git a/Sources/_Helpers/Request.swift b/Sources/_Helpers/Request.swift index 3fc13be2..ca9c20e5 100644 --- a/Sources/_Helpers/Request.swift +++ b/Sources/_Helpers/Request.swift @@ -8,15 +8,20 @@ import Foundation public struct HTTPClient: Sendable { public typealias FetchHandler = @Sendable (URLRequest) async throws -> (Data, URLResponse) + let logger: SupabaseLogger? let fetchHandler: FetchHandler - public init(fetchHandler: @escaping FetchHandler) { + public init(logger: SupabaseLogger?, fetchHandler: @escaping FetchHandler) { + self.logger = logger self.fetchHandler = fetchHandler } public func fetch(_ request: Request, baseURL: URL) async throws -> Response { + let id = UUID().uuidString let urlRequest = try request.urlRequest(withBaseURL: baseURL) + logger?.verbose("Request [\(id)]: \(urlRequest)") let (data, response) = try await fetchHandler(urlRequest) + logger?.verbose("Response [\(id)]: \(response)") guard let httpResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) diff --git a/Sources/_Helpers/SupabaseLogger.swift b/Sources/_Helpers/SupabaseLogger.swift new file mode 100644 index 00000000..ec9c5ca5 --- /dev/null +++ b/Sources/_Helpers/SupabaseLogger.swift @@ -0,0 +1,127 @@ +import Foundation + +public enum SupabaseLogLevel: Int, Codable, CustomStringConvertible, Sendable { + case verbose + case debug + case warning + case error + + public var description: String { + switch self { + case .verbose: "verbose" + case .debug: "debug" + case .warning: "warning" + case .error: "error" + } + } +} + +public struct SupabaseLogMessage: Codable, CustomStringConvertible { + public let system: String + public let level: SupabaseLogLevel + public let message: String + public let fileID: String + public let function: String + public let line: UInt + public let timestamp: TimeInterval + + @usableFromInline + init( + system: String, + level: SupabaseLogLevel, + message: String, + fileID: String, + function: String, + line: UInt, + timestamp: TimeInterval + ) { + self.system = system + self.level = level + self.message = message + self.fileID = fileID + self.function = function + self.line = line + self.timestamp = timestamp + } + + public var description: String { + let date = iso8601Formatter.string(from: Date(timeIntervalSince1970: timestamp)) + let file = fileID.split(separator: ".", maxSplits: 1).first.map(String.init) ?? fileID + return "\(date) [\(level)] [\(system)] [\(file).\(function):\(line)] \(message)" + } +} + +private let iso8601Formatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime] + return formatter +}() + +public protocol SupabaseLogger: Sendable { + func log(message: SupabaseLogMessage) +} + +extension SupabaseLogger { + @inlinable + public func log( + _ level: SupabaseLogLevel, + message: @autoclosure () -> String, + fileID: StaticString = #fileID, + function: StaticString = #function, + line: UInt = #line + ) { + let system = "\(fileID)".split(separator: "/").first ?? "" + + log( + message: SupabaseLogMessage( + system: "\(system)", + level: level, + message: message(), + fileID: "\(fileID)", + function: "\(function)", + line: line, + timestamp: Date().timeIntervalSince1970 + ) + ) + } + + @inlinable + public func verbose( + _ message: @autoclosure () -> String, + fileID: StaticString = #fileID, + function: StaticString = #function, + line: UInt = #line + ) { + log(.verbose, message: message(), fileID: fileID, function: function, line: line) + } + + @inlinable + public func debug( + _ message: @autoclosure () -> String, + fileID: StaticString = #fileID, + function: StaticString = #function, + line: UInt = #line + ) { + log(.debug, message: message(), fileID: fileID, function: function, line: line) + } + + @inlinable + public func warning( + _ message: @autoclosure () -> String, + fileID: StaticString = #fileID, + function: StaticString = #function, + line: UInt = #line + ) { + log(.warning, message: message(), fileID: fileID, function: function, line: line) + } + + @inlinable + public func error( + _ message: @autoclosure () -> String, + fileID: StaticString = #fileID, + function: StaticString = #function, + line: UInt = #line + ) { + log(.error, message: message(), fileID: fileID, function: function, line: line) + } +} diff --git a/Tests/AuthTests/GoTrueClientTests.swift b/Tests/AuthTests/GoTrueClientTests.swift index ac2e1d63..e0640a45 100644 --- a/Tests/AuthTests/GoTrueClientTests.swift +++ b/Tests/AuthTests/GoTrueClientTests.swift @@ -24,9 +24,7 @@ final class AuthClientTests: XCTestCase { let events = ActorIsolated([AuthChangeEvent]()) - // We use a semaphore here instead of the nicer XCTestExpectation as that isn't fully available - // on Linux. - let semaphore = DispatchSemaphore(value: 0) + let (stream, continuation) = AsyncStream.makeStream() await withDependencies { $0.eventEmitter = .live @@ -40,11 +38,11 @@ final class AuthClientTests: XCTestCase { $0.append(event) } - semaphore.signal() + continuation.yield() } } - _ = semaphore.wait(timeout: .now() + 2.0) + _ = await stream.first { _ in true } let events = await events.value XCTAssertEqual(events, [.initialSession]) @@ -168,7 +166,8 @@ final class AuthClientTests: XCTestCase { codeVerifierStorage: .mock, api: .mock, eventEmitter: .mock, - sessionStorage: .mock + sessionStorage: .mock, + logger: nil ) addTeardownBlock { [weak sut] in diff --git a/Tests/AuthTests/Mocks/Mocks.swift b/Tests/AuthTests/Mocks/Mocks.swift index eefa19b5..d163fbba 100644 --- a/Tests/AuthTests/Mocks/Mocks.swift +++ b/Tests/AuthTests/Mocks/Mocks.swift @@ -114,7 +114,8 @@ extension Dependencies { eventEmitter: .mock, sessionStorage: .mock, sessionRefresher: .mock, - codeVerifierStorage: .mock + codeVerifierStorage: .mock, + logger: nil ) } diff --git a/Tests/AuthTests/RequestsTests.swift b/Tests/AuthTests/RequestsTests.swift index 43414b5e..5564328c 100644 --- a/Tests/AuthTests/RequestsTests.swift +++ b/Tests/AuthTests/RequestsTests.swift @@ -422,7 +422,7 @@ final class RequestsTests: XCTestCase { } ) - let api = APIClient.live(http: HTTPClient(fetchHandler: configuration.fetch)) + let api = APIClient.live(http: HTTPClient(logger: nil, fetchHandler: configuration.fetch)) return AuthClient( configuration: configuration, @@ -430,7 +430,8 @@ final class RequestsTests: XCTestCase { codeVerifierStorage: .mock, api: api, eventEmitter: .mock, - sessionStorage: .mock + sessionStorage: .mock, + logger: nil ) } } diff --git a/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved b/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved index 987897dc..4d19c818 100644 --- a/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "59b663f68e69f27a87b45de48cb63264b8194605", - "version" : "1.15.1" + "revision" : "8e68404f641300bfd0e37d478683bb275926760c", + "version" : "1.15.2" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", - "version" : "509.0.2" + "revision" : "43c802fb7f96e090dde015344a94b5e85779eff1", + "version" : "509.1.0" } }, {