Skip to content

Commit 3c186a2

Browse files
Add network logging support (#63)
1 parent e20efb2 commit 3c186a2

File tree

8 files changed

+233
-10
lines changed

8 files changed

+233
-10
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
## 1.4.0 (unreleased)
4+
5+
* Added the ability to log PowerSync sync network requests.
6+
7+
```swift
8+
try await database.connect(
9+
connector: Connector(),
10+
options: ConnectOptions(
11+
clientConfiguration: SyncClientConfiguration(
12+
requestLogger: SyncRequestLoggerConfiguration(
13+
requestLevel: .headers
14+
) { message in
15+
// Handle Network request logs here
16+
print(message)
17+
}
18+
)
19+
)
20+
)
21+
22+
```
323
## 1.3.1
424

525
* Update SQLite to 3.50.3.

Demo/PowerSyncExample/PowerSync/SystemManager.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,18 @@ class SystemManager {
7070

7171
func connect() async {
7272
do {
73-
try await db.connect(connector: connector)
73+
try await db.connect(
74+
connector: connector,
75+
options: ConnectOptions(
76+
clientConfiguration: SyncClientConfiguration(
77+
requestLogger: SyncRequestLoggerConfiguration(
78+
requestLevel: .headers
79+
) { message in
80+
self.db.logger.debug(message, tag: "SyncRequest")
81+
}
82+
)
83+
)
84+
)
7485
try await attachments?.startSync()
7586
} catch {
7687
print("Unexpected error: \(error.localizedDescription)") // Catches any other error

Package.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ if let kotlinSdkPath = localKotlinSdkOverride {
3131
// Not using a local build, so download from releases
3232
conditionalTargets.append(.binaryTarget(
3333
name: "PowerSyncKotlin",
34-
url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.3.1/PowersyncKotlinRelease.zip",
35-
checksum: "b01b72cbf88a2e7b9b67efce966799493fc48d4523b5989d8c645ed182880975"
34+
url: "https://github.com/powersync-ja/powersync-kotlin/releases/download/v1.4.0/PowersyncKotlinRelease.zip",
35+
checksum: "e800db216fc1c9722e66873deb4f925530267db6dbd5e2114dd845cc62c28cd9"
3636
))
3737
}
3838

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import PowerSyncKotlin
2+
3+
extension SyncRequestLogLevel {
4+
func toKotlin() -> SwiftSyncRequestLogLevel {
5+
switch self {
6+
case .all:
7+
return SwiftSyncRequestLogLevel.all
8+
case .headers:
9+
return SwiftSyncRequestLogLevel.headers
10+
case .body:
11+
return SwiftSyncRequestLogLevel.body
12+
case .info:
13+
return SwiftSyncRequestLogLevel.info
14+
case .none:
15+
return SwiftSyncRequestLogLevel.none
16+
}
17+
}
18+
}
19+
20+
extension SyncRequestLoggerConfiguration {
21+
func toKotlinConfig() -> SwiftRequestLoggerConfig {
22+
return SwiftRequestLoggerConfig(
23+
logLevel: self.requestLevel.toKotlin(),
24+
log: { [log] message in
25+
log(message)
26+
}
27+
)
28+
}
29+
}

Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
5959
params: resolvedOptions.params.mapValues { $0.toKotlinMap() },
6060
options: createSyncOptions(
6161
newClient: resolvedOptions.newClientImplementation,
62-
userAgent: "PowerSync Swift SDK"
62+
userAgent: "PowerSync Swift SDK",
63+
loggingConfig: resolvedOptions.clientConfiguration?.requestLogger?.toKotlinConfig()
6364
)
6465
)
6566
}

Sources/PowerSync/Protocol/PowerSyncDatabaseProtocol.swift

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
import Foundation
22

3+
/// Configuration for the sync client used to connect to the PowerSync service.
4+
///
5+
/// Provides options to customize network behavior and logging for PowerSync
6+
/// HTTP requests and responses.
7+
public struct SyncClientConfiguration {
8+
/// Optional configuration for logging PowerSync HTTP requests.
9+
///
10+
/// When provided, network requests will be logged according to the
11+
/// specified `SyncRequestLoggerConfiguration`. Set to `nil` to disable request logging entirely.
12+
///
13+
/// - SeeAlso: `SyncRequestLoggerConfiguration` for configuration options
14+
public let requestLogger: SyncRequestLoggerConfiguration?
15+
16+
/// Creates a new sync client configuration.
17+
/// - Parameter requestLogger: Optional network logger configuration
18+
public init(requestLogger: SyncRequestLoggerConfiguration? = nil) {
19+
self.requestLogger = requestLogger
20+
}
21+
}
22+
323
/// Options for configuring a PowerSync connection.
424
///
525
/// Provides optional parameters to customize sync behavior such as throttling and retry policies.
@@ -42,21 +62,36 @@ public struct ConnectOptions {
4262
@_spi(PowerSyncExperimental)
4363
public var newClientImplementation: Bool
4464

65+
/// Configuration for the sync client used for PowerSync requests.
66+
///
67+
/// Provides options to customize network behavior including logging of HTTP
68+
/// requests and responses. When `nil`, default HTTP client settings are used
69+
/// with no network logging.
70+
///
71+
/// Set this to configure network logging or other HTTP client behaviors
72+
/// specific to PowerSync operations.
73+
///
74+
/// - SeeAlso: `SyncClientConfiguration` for available configuration options
75+
public var clientConfiguration: SyncClientConfiguration?
76+
4577
/// Initializes a `ConnectOptions` instance with optional values.
4678
///
4779
/// - Parameters:
4880
/// - crudThrottle: TimeInterval between CRUD operations in milliseconds. Defaults to `1` second.
4981
/// - retryDelay: Delay TimeInterval between retry attempts in milliseconds. Defaults to `5` seconds.
5082
/// - params: Custom sync parameters to send to the server. Defaults to an empty dictionary.
83+
/// - clientConfiguration: Configuration for the HTTP client used to connect to PowerSync.
5184
public init(
5285
crudThrottle: TimeInterval = 1,
5386
retryDelay: TimeInterval = 5,
54-
params: JsonParam = [:]
87+
params: JsonParam = [:],
88+
clientConfiguration: SyncClientConfiguration? = nil
5589
) {
5690
self.crudThrottle = crudThrottle
5791
self.retryDelay = retryDelay
5892
self.params = params
5993
self.newClientImplementation = false
94+
self.clientConfiguration = clientConfiguration
6095
}
6196

6297
/// Initializes a ``ConnectOptions`` instance with optional values, including experimental options.
@@ -65,12 +100,14 @@ public struct ConnectOptions {
65100
crudThrottle: TimeInterval = 1,
66101
retryDelay: TimeInterval = 5,
67102
params: JsonParam = [:],
68-
newClientImplementation: Bool = false
103+
newClientImplementation: Bool = false,
104+
clientConfiguration: SyncClientConfiguration? = nil
69105
) {
70106
self.crudThrottle = crudThrottle
71107
self.retryDelay = retryDelay
72108
self.params = params
73109
self.newClientImplementation = newClientImplementation
110+
self.clientConfiguration = clientConfiguration
74111
}
75112
}
76113

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

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

185221
/// Close the database, releasing resources.
@@ -229,8 +265,8 @@ public extension PowerSyncDatabaseProtocol {
229265
)
230266
}
231267

232-
func disconnectAndClear(clearLocal: Bool = true) async throws {
233-
try await self.disconnectAndClear(clearLocal: clearLocal)
268+
func disconnectAndClear() async throws {
269+
try await disconnectAndClear(clearLocal: true)
234270
}
235271

236272
func getCrudBatch(limit: Int32 = 100) async throws -> CrudBatch? {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/// Level of logs to expose to a `SyncRequestLogger` handler.
2+
///
3+
/// Controls the verbosity of network logging for PowerSync HTTP requests.
4+
/// The log level is configured once during initialization and determines
5+
/// which network events will be logged throughout the session.
6+
public enum SyncRequestLogLevel {
7+
/// Log all network activity including headers, body, and info
8+
case all
9+
/// Log only request/response headers
10+
case headers
11+
/// Log only request/response body content
12+
case body
13+
/// Log basic informational messages about requests
14+
case info
15+
/// Disable all network logging
16+
case none
17+
}
18+
19+
/// Configuration for PowerSync HTTP request logging.
20+
///
21+
/// This configuration is set once during initialization and used throughout
22+
/// the PowerSync session. The `requestLevel` determines which network events
23+
/// are logged.
24+
///
25+
/// - Note: The request level cannot be changed after initialization. A new call to `PowerSyncDatabase.connect` is required to change the level.
26+
public struct SyncRequestLoggerConfiguration {
27+
/// The request logging level that determines which network events are logged.
28+
/// Set once during initialization and used throughout the session.
29+
public let requestLevel: SyncRequestLogLevel
30+
31+
private let logHandler: (_ message: String) -> Void
32+
33+
/// Creates a new network logger configuration.
34+
/// - Parameters:
35+
/// - requestLevel: The `SyncRequestLogLevel` to use for filtering log messages
36+
/// - logHandler: A closure which handles log messages
37+
public init(
38+
requestLevel: SyncRequestLogLevel,
39+
logHandler: @escaping (_ message: String) -> Void)
40+
{
41+
self.requestLevel = requestLevel
42+
self.logHandler = logHandler
43+
}
44+
45+
public func log(_ message: String) {
46+
logHandler(message)
47+
}
48+
49+
/// Creates a new network logger configuration using a `LoggerProtocol` instance.
50+
///
51+
/// This initializer allows integration with an existing logging framework by adapting
52+
/// a `LoggerProtocol` to conform to `SyncRequestLogger`. The specified `logSeverity`
53+
/// controls the severity level at which log messages are recorded. An optional `logTag`
54+
/// may be used to help categorize logs.
55+
///
56+
/// - Parameters:
57+
/// - requestLevel: The `SyncRequestLogLevel` to use for filtering which network events are logged.
58+
/// - logger: An object conforming to `LoggerProtocol` that will receive log messages.
59+
/// - logSeverity: The severity level to use for all log messages (defaults to `.debug`).
60+
/// - logTag: An optional tag to include with each log message, for use by the logging backend.
61+
public init(
62+
requestLevel: SyncRequestLogLevel,
63+
logger: LoggerProtocol,
64+
logSeverity: LogSeverity = .debug,
65+
logTag: String? = nil)
66+
{
67+
self.requestLevel = requestLevel
68+
self.logHandler = { message in
69+
switch logSeverity {
70+
case .debug:
71+
logger.debug(message, tag: logTag)
72+
case .info:
73+
logger.info(message, tag: logTag)
74+
case .warning:
75+
logger.warning(message, tag: logTag)
76+
case .error:
77+
logger.error(message, tag: logTag)
78+
case .fault:
79+
logger.fault(message, tag: logTag)
80+
}
81+
}
82+
}
83+
}

Tests/PowerSyncTests/ConnectTests.swift

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,47 @@ final class ConnectTests: XCTestCase {
8888
await fulfillment(of: [expectation], timeout: 5)
8989
watchTask.cancel()
9090
}
91+
92+
func testSyncHTTPLogs() async throws {
93+
let expectation = XCTestExpectation(
94+
description: "Should log a request to the PowerSync endpoint"
95+
)
96+
97+
let fakeUrl = "https://fakepowersyncinstance.fakepowersync.local"
98+
99+
class TestConnector: PowerSyncBackendConnector {
100+
let url: String
101+
102+
init(url: String) {
103+
self.url = url
104+
}
105+
106+
override func fetchCredentials() async throws -> PowerSyncCredentials? {
107+
PowerSyncCredentials(
108+
endpoint: url,
109+
token: "123"
110+
)
111+
}
112+
}
113+
114+
try await database.connect(
115+
connector: TestConnector(url: fakeUrl),
116+
options: ConnectOptions(
117+
clientConfiguration: SyncClientConfiguration(
118+
requestLogger: SyncRequestLoggerConfiguration(
119+
requestLevel: .all
120+
) { message in
121+
// We want to see a request to the specified instance
122+
if message.contains(fakeUrl) {
123+
expectation.fulfill()
124+
}
125+
}
126+
)
127+
)
128+
)
129+
130+
await fulfillment(of: [expectation], timeout: 5)
131+
132+
try await database.disconnectAndClear()
133+
}
91134
}

0 commit comments

Comments
 (0)