Skip to content

Add network logging support #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 7, 2025
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