Skip to content
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## 1.4.0 (unreleased)

* Added the ability to log PowerSync sync network requests.

```swift
try await database.connect(
connector: Connector(),
options: ConnectOptions(
clientConfiguration: SyncClientConfiguration(
requestLogger: SyncRequestLoggerConfiguration(
requestLevel: .headers
) { message in
// Handle Network request logs here
print(message)
}
)
)
)

```
## 1.3.1

* Update SQLite to 3.50.3.
Expand Down
13 changes: 12 additions & 1 deletion Demo/PowerSyncExample/PowerSync/SystemManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,18 @@ class SystemManager {

func connect() async {
do {
try await db.connect(connector: connector)
try await db.connect(
connector: connector,
options: ConnectOptions(
clientConfiguration: SyncClientConfiguration(
requestLogger: SyncRequestLoggerConfiguration(
requestLevel: .headers
) { message in
self.db.logger.debug(message, tag: "SyncRequest")
}
)
)
)
try await attachments?.startSync()
} catch {
print("Unexpected error: \(error.localizedDescription)") // Catches any other error
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ if let kotlinSdkPath = localKotlinSdkOverride {
// Not using a local build, so download from releases
conditionalTargets.append(.binaryTarget(
name: "PowerSyncKotlin",
url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.3.1/PowersyncKotlinRelease.zip",
checksum: "b01b72cbf88a2e7b9b67efce966799493fc48d4523b5989d8c645ed182880975"
url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.4.0/PowersyncKotlinRelease.zip",
checksum: "e800db216fc1c9722e66873deb4f925530267db6dbd5e2114dd845cc62c28cd9"
))
}

Expand Down
29 changes: 29 additions & 0 deletions Sources/PowerSync/Kotlin/KotlinNetworkLogger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import PowerSyncKotlin

extension SyncRequestLogLevel {
func toKotlin() -> SwiftSyncRequestLogLevel {
switch self {
case .all:
return SwiftSyncRequestLogLevel.all
case .headers:
return SwiftSyncRequestLogLevel.headers
case .body:
return SwiftSyncRequestLogLevel.body
case .info:
return SwiftSyncRequestLogLevel.info
case .none:
return SwiftSyncRequestLogLevel.none
}
}
}

extension SyncRequestLoggerConfiguration {
func toKotlinConfig() -> SwiftRequestLoggerConfig {
return SwiftRequestLoggerConfig(
logLevel: self.requestLevel.toKotlin(),
log: { [log] message in
log(message)
}
)
}
}
3 changes: 2 additions & 1 deletion Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
params: resolvedOptions.params.mapValues { $0.toKotlinMap() },
options: createSyncOptions(
newClient: resolvedOptions.newClientImplementation,
userAgent: "PowerSync Swift SDK"
userAgent: "PowerSync Swift SDK",
loggingConfig: resolvedOptions.clientConfiguration?.requestLogger?.toKotlinConfig()
)
)
}
Expand Down
48 changes: 42 additions & 6 deletions Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
import Foundation

/// Configuration for the sync client used to connect to the PowerSync service.
///
/// Provides options to customize network behavior and logging for PowerSync
/// HTTP requests and responses.
public struct SyncClientConfiguration {
/// Optional configuration for logging PowerSync HTTP requests.
///
/// When provided, network requests will be logged according to the
/// specified `SyncRequestLoggerConfiguration`. Set to `nil` to disable request logging entirely.
///
/// - SeeAlso: `SyncRequestLoggerConfiguration` for configuration options
public let requestLogger: SyncRequestLoggerConfiguration?

/// Creates a new sync client configuration.
/// - Parameter requestLogger: Optional network logger configuration
public init(requestLogger: SyncRequestLoggerConfiguration? = nil) {
self.requestLogger = requestLogger
}
}

/// Options for configuring a PowerSync connection.
///
/// Provides optional parameters to customize sync behavior such as throttling and retry policies.
Expand Down Expand Up @@ -42,21 +62,36 @@ public struct ConnectOptions {
@_spi(PowerSyncExperimental)
public var newClientImplementation: Bool

/// Configuration for the sync client used for PowerSync requests.
///
/// Provides options to customize network behavior including logging of HTTP
/// requests and responses. When `nil`, default HTTP client settings are used
/// with no network logging.
///
/// Set this to configure network logging or other HTTP client behaviors
/// specific to PowerSync operations.
///
/// - SeeAlso: `SyncClientConfiguration` for available configuration options
public var clientConfiguration: SyncClientConfiguration?

/// Initializes a `ConnectOptions` instance with optional values.
///
/// - Parameters:
/// - crudThrottle: TimeInterval between CRUD operations in milliseconds. Defaults to `1` second.
/// - retryDelay: Delay TimeInterval between retry attempts in milliseconds. Defaults to `5` seconds.
/// - params: Custom sync parameters to send to the server. Defaults to an empty dictionary.
/// - clientConfiguration: Configuration for the HTTP client used to connect to PowerSync.
public init(
crudThrottle: TimeInterval = 1,
retryDelay: TimeInterval = 5,
params: JsonParam = [:]
params: JsonParam = [:],
clientConfiguration: SyncClientConfiguration? = nil
) {
self.crudThrottle = crudThrottle
self.retryDelay = retryDelay
self.params = params
self.newClientImplementation = false
self.clientConfiguration = clientConfiguration
}

/// Initializes a ``ConnectOptions`` instance with optional values, including experimental options.
Expand All @@ -65,12 +100,14 @@ public struct ConnectOptions {
crudThrottle: TimeInterval = 1,
retryDelay: TimeInterval = 5,
params: JsonParam = [:],
newClientImplementation: Bool = false
newClientImplementation: Bool = false,
clientConfiguration: SyncClientConfiguration? = nil
) {
self.crudThrottle = crudThrottle
self.retryDelay = retryDelay
self.params = params
self.newClientImplementation = newClientImplementation
self.clientConfiguration = clientConfiguration
}
}

Expand All @@ -91,7 +128,6 @@ public protocol PowerSyncDatabaseProtocol: Queries {
/// Wait for the first sync to occur
func waitForFirstSync() async throws


/// Replace the schema with a new version. This is for advanced use cases - typically the schema
/// should just be specified once in the constructor.
///
Expand Down Expand Up @@ -179,7 +215,7 @@ public protocol PowerSyncDatabaseProtocol: Queries {
/// The database can still be queried after this is called, but the tables
/// would be empty.
///
/// - Parameter clearLocal: Set to false to preserve data in local-only tables.
/// - Parameter clearLocal: Set to false to preserve data in local-only tables. Defaults to `true`.
func disconnectAndClear(clearLocal: Bool) async throws

/// Close the database, releasing resources.
Expand Down Expand Up @@ -229,8 +265,8 @@ public extension PowerSyncDatabaseProtocol {
)
}

func disconnectAndClear(clearLocal: Bool = true) async throws {
try await self.disconnectAndClear(clearLocal: clearLocal)
func disconnectAndClear() async throws {
try await disconnectAndClear(clearLocal: true)
}

func getCrudBatch(limit: Int32 = 100) async throws -> CrudBatch? {
Expand Down
83 changes: 83 additions & 0 deletions Sources/PowerSync/Protocol/SyncRequestLogger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/// Level of logs to expose to a `SyncRequestLogger` handler.
///
/// Controls the verbosity of network logging for PowerSync HTTP requests.
/// The log level is configured once during initialization and determines
/// which network events will be logged throughout the session.
public enum SyncRequestLogLevel {
/// Log all network activity including headers, body, and info
case all
/// Log only request/response headers
case headers
/// Log only request/response body content
case body
/// Log basic informational messages about requests
case info
/// Disable all network logging
case none
}

/// Configuration for PowerSync HTTP request logging.
///
/// This configuration is set once during initialization and used throughout
/// the PowerSync session. The `requestLevel` determines which network events
/// are logged.
///
/// - Note: The request level cannot be changed after initialization. A new call to `PowerSyncDatabase.connect` is required to change the level.
public struct SyncRequestLoggerConfiguration {
/// The request logging level that determines which network events are logged.
/// Set once during initialization and used throughout the session.
public let requestLevel: SyncRequestLogLevel

private let logHandler: (_ message: String) -> Void

/// Creates a new network logger configuration.
/// - Parameters:
/// - requestLevel: The `SyncRequestLogLevel` to use for filtering log messages
/// - logHandler: A closure which handles log messages
public init(
requestLevel: SyncRequestLogLevel,
logHandler: @escaping (_ message: String) -> Void)
{
self.requestLevel = requestLevel
self.logHandler = logHandler
}

public func log(_ message: String) {
logHandler(message)
}

/// Creates a new network logger configuration using a `LoggerProtocol` instance.
///
/// This initializer allows integration with an existing logging framework by adapting
/// a `LoggerProtocol` to conform to `SyncRequestLogger`. The specified `logSeverity`
/// controls the severity level at which log messages are recorded. An optional `logTag`
/// may be used to help categorize logs.
///
/// - Parameters:
/// - requestLevel: The `SyncRequestLogLevel` to use for filtering which network events are logged.
/// - logger: An object conforming to `LoggerProtocol` that will receive log messages.
/// - logSeverity: The severity level to use for all log messages (defaults to `.debug`).
/// - logTag: An optional tag to include with each log message, for use by the logging backend.
public init(
requestLevel: SyncRequestLogLevel,
logger: LoggerProtocol,
logSeverity: LogSeverity = .debug,
logTag: String? = nil)
{
self.requestLevel = requestLevel
self.logHandler = { message in
switch logSeverity {
case .debug:
logger.debug(message, tag: logTag)
case .info:
logger.info(message, tag: logTag)
case .warning:
logger.warning(message, tag: logTag)
case .error:
logger.error(message, tag: logTag)
case .fault:
logger.fault(message, tag: logTag)
}
}
}
}
43 changes: 43 additions & 0 deletions Tests/PowerSyncTests/ConnectTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,47 @@ final class ConnectTests: XCTestCase {
await fulfillment(of: [expectation], timeout: 5)
watchTask.cancel()
}

func testSyncHTTPLogs() async throws {
let expectation = XCTestExpectation(
description: "Should log a request to the PowerSync endpoint"
)

let fakeUrl = "https://fakepowersyncinstance.fakepowersync.local"

class TestConnector: PowerSyncBackendConnector {
let url: String

init(url: String) {
self.url = url
}

override func fetchCredentials() async throws -> PowerSyncCredentials? {
PowerSyncCredentials(
endpoint: url,
token: "123"
)
}
}

try await database.connect(
connector: TestConnector(url: fakeUrl),
options: ConnectOptions(
clientConfiguration: SyncClientConfiguration(
requestLogger: SyncRequestLoggerConfiguration(
requestLevel: .all
) { message in
// We want to see a request to the specified instance
if message.contains(fakeUrl) {
expectation.fulfill()
}
}
)
)
)

await fulfillment(of: [expectation], timeout: 5)

try await database.disconnectAndClear()
}
}
Loading