Skip to content

Commit 1d166f1

Browse files
Configurable Logging (#34)
1 parent 3209c98 commit 1d166f1

10 files changed

+414
-8
lines changed

CHANGELOG.md

+21
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
# Changelog
22

3+
# 1.0.0-Beta.10 (unreleased)
4+
5+
* Added the ability to specify a custom logging implementation
6+
```swift
7+
let db = PowerSyncDatabase(
8+
schema: Schema(
9+
tables: [
10+
Table(
11+
name: "users",
12+
columns: [
13+
.text("name"),
14+
.text("email")
15+
]
16+
)
17+
]
18+
),
19+
logger: DefaultLogger(minSeverity: .debug)
20+
)
21+
```
22+
* added `.close()` method on `PowerSyncDatabaseProtocol`
23+
324
## 1.0.0-Beta.9
425

526
* Update PowerSync SQLite core extension to 0.3.12.

README.md

+27-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<a href="https://www.powersync.com" target="_blank"><img src="https://github.com/powersync-ja/.github/assets/7372448/d2538c43-c1a0-4c47-9a76-41462dba484f"/></a>
33
</p>
44

5-
*[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side.*
5+
_[PowerSync](https://www.powersync.com) is a sync engine for building local-first apps with instantly-responsive UI/UX and simplified state transfer. Syncs between SQLite on the client-side and Postgres, MongoDB or MySQL on the server-side._
66

77
# PowerSync Swift
88

@@ -16,7 +16,7 @@ This SDK is currently in a beta release it is suitable for production use, given
1616

1717
- [Sources](./Sources/)
1818

19-
- This is the Swift SDK implementation.
19+
- This is the Swift SDK implementation.
2020

2121
## Demo Apps / Example Projects
2222

@@ -51,11 +51,35 @@ to your `Package.swift` file and pin the dependency to a specific version. The v
5151

5252
to your `Package.swift` file and pin the dependency to a specific version. This is required because the package is in beta.
5353

54+
## Usage
55+
56+
Create a PowerSync client
57+
58+
```swift
59+
import PowerSync
60+
61+
let powersync = PowerSyncDatabase(
62+
schema: Schema(
63+
tables: [
64+
Table(
65+
name: "users",
66+
columns: [
67+
.text("count"),
68+
.integer("is_active"),
69+
.real("weight"),
70+
.text("description")
71+
]
72+
)
73+
]
74+
),
75+
logger: DefaultLogger(minSeverity: .debug)
76+
)
77+
```
78+
5479
## Underlying Kotlin Dependency
5580

5681
The PowerSync Swift SDK currently makes use of the [PowerSync Kotlin Multiplatform SDK](https://github.com/powersync-ja/powersync-kotlin) with the API tool [SKIE](https://skie.touchlab.co/) and KMMBridge under the hood to help generate and publish a native Swift package. We will move to an entirely Swift native API in v1 and do not expect there to be any breaking changes. For more details, see the [Swift SDK reference](https://docs.powersync.com/client-sdk-references/swift).
5782

58-
5983
## Migration from Alpha to Beta
6084

6185
See these [developer notes](https://docs.powersync.com/client-sdk-references/swift#migrating-from-the-alpha-to-the-beta-sdk) if you are migrating from the alpha to the beta version of the Swift SDK.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import PowerSyncKotlin
2+
3+
/// Adapts a Swift `LoggerProtocol` to Kermit's `LogWriter` interface.
4+
///
5+
/// This allows Kotlin logging (via Kermit) to call into the Swift logging implementation.
6+
private class KermitLogWriterAdapter: Kermit_coreLogWriter {
7+
/// The underlying Swift log writer to forward log messages to.
8+
let logger: any LoggerProtocol
9+
10+
/// Initializes a new adapter.
11+
///
12+
/// - Parameter logger: A Swift log writer that will handle log output.
13+
init(logger: any LoggerProtocol) {
14+
self.logger = logger
15+
super.init()
16+
}
17+
18+
/// Called by Kermit to log a message.
19+
///
20+
/// - Parameters:
21+
/// - severity: The severity level of the log.
22+
/// - message: The content of the log message.
23+
/// - tag: A string categorizing the log.
24+
/// - throwable: An optional Kotlin exception (ignored here).
25+
override func log(severity: Kermit_coreSeverity, message: String, tag: String, throwable: KotlinThrowable?) {
26+
switch severity {
27+
case PowerSyncKotlin.Kermit_coreSeverity.verbose:
28+
return logger.debug(message, tag: tag)
29+
case PowerSyncKotlin.Kermit_coreSeverity.debug:
30+
return logger.debug(message, tag: tag)
31+
case PowerSyncKotlin.Kermit_coreSeverity.info:
32+
return logger.info(message, tag: tag)
33+
case PowerSyncKotlin.Kermit_coreSeverity.warn:
34+
return logger.warning(message, tag: tag)
35+
case PowerSyncKotlin.Kermit_coreSeverity.error:
36+
return logger.error(message, tag: tag)
37+
case PowerSyncKotlin.Kermit_coreSeverity.assert:
38+
return logger.fault(message, tag: tag)
39+
}
40+
}
41+
}
42+
43+
/// A logger implementation that integrates with PowerSync's Kotlin core using Kermit.
44+
///
45+
/// This class bridges Swift log writers with the Kotlin logging system and supports
46+
/// runtime configuration of severity levels and writer lists.
47+
internal class DatabaseLogger: LoggerProtocol {
48+
/// The underlying Kermit logger instance provided by the PowerSyncKotlin SDK.
49+
public let kLogger = PowerSyncKotlin.generateLogger(logger: nil)
50+
public let logger: any LoggerProtocol
51+
52+
/// Initializes a new logger with an optional list of writers.
53+
///
54+
/// - Parameter logger: A logger which will be called for each internal log operation
55+
init(_ logger: any LoggerProtocol) {
56+
self.logger = logger
57+
// Set to the lowest severity. The provided logger should filter by severity
58+
kLogger.mutableConfig.setMinSeverity(Kermit_coreSeverity.verbose)
59+
kLogger.mutableConfig.setLogWriterList(
60+
[KermitLogWriterAdapter(logger: logger)]
61+
)
62+
}
63+
64+
/// Logs a debug-level message.
65+
public func debug(_ message: String, tag: String?) {
66+
logger.debug(message, tag: tag)
67+
}
68+
69+
/// Logs an info-level message.
70+
public func info(_ message: String, tag: String?) {
71+
logger.info(message, tag: tag)
72+
}
73+
74+
/// Logs a warning-level message.
75+
public func warning(_ message: String, tag: String?) {
76+
logger.warning(message, tag: tag)
77+
}
78+
79+
/// Logs an error-level message.
80+
public func error(_ message: String, tag: String?) {
81+
logger.error(message, tag: tag)
82+
}
83+
84+
/// Logs a fault (assert-level) message, typically used for critical issues.
85+
public func fault(_ message: String, tag: String?) {
86+
logger.fault(message, tag: tag)
87+
}
88+
}

Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
88

99
init(
1010
schema: Schema,
11-
dbFilename: String
11+
dbFilename: String,
12+
logger: DatabaseLogger? = nil
1213
) {
1314
let factory = PowerSyncKotlin.DatabaseDriverFactory()
1415
kotlinDatabase = PowerSyncDatabase(
1516
factory: factory,
1617
schema: KotlinAdapter.Schema.toKotlin(schema),
17-
dbFilename: dbFilename
18+
dbFilename: dbFilename,
19+
logger: logger?.kLogger
1820
)
1921
}
2022

@@ -232,4 +234,8 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
232234
func readTransaction<R>(callback: @escaping (any PowerSyncTransaction) throws -> R) async throws -> R {
233235
return try safeCast(await kotlinDatabase.readTransaction(callback: TransactionCallback(callback: callback)), to: R.self)
234236
}
237+
238+
func close() async throws{
239+
try await kotlinDatabase.close()
240+
}
235241
}

Sources/PowerSync/Logger.swift

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import OSLog
2+
3+
/// A log writer which prints to the standard output
4+
///
5+
/// This writer uses `os.Logger` on iOS/macOS/tvOS/watchOS 14+ and falls back to `print` for earlier versions.
6+
public class PrintLogWriter: LogWriterProtocol {
7+
8+
private let subsystem: String
9+
private let category: String
10+
private lazy var logger: Any? = {
11+
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
12+
return Logger(subsystem: subsystem, category: category)
13+
}
14+
return nil
15+
}()
16+
17+
/// Creates a new PrintLogWriter
18+
/// - Parameters:
19+
/// - subsystem: The subsystem identifier (typically reverse DNS notation of your app)
20+
/// - category: The category within your subsystem
21+
public init(subsystem: String = Bundle.main.bundleIdentifier ?? "com.powersync.logger",
22+
category: String = "default") {
23+
self.subsystem = subsystem
24+
self.category = category
25+
}
26+
27+
/// Logs a message with a given severity and optional tag.
28+
/// - Parameters:
29+
/// - severity: The severity level of the message.
30+
/// - message: The content of the log message.
31+
/// - tag: An optional tag used to categorize the message. If empty, no brackets are shown.
32+
public func log(severity: LogSeverity, message: String, tag: String?) {
33+
let tagPrefix = tag.map { !$0.isEmpty ? "[\($0)] " : "" } ?? ""
34+
let formattedMessage = "\(tagPrefix)\(message)"
35+
36+
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
37+
guard let logger = logger as? Logger else { return }
38+
39+
switch severity {
40+
case .info:
41+
logger.info("\(formattedMessage, privacy: .public)")
42+
case .error:
43+
logger.error("\(formattedMessage, privacy: .public)")
44+
case .debug:
45+
logger.debug("\(formattedMessage, privacy: .public)")
46+
case .warning:
47+
logger.warning("\(formattedMessage, privacy: .public)")
48+
case .fault:
49+
logger.fault("\(formattedMessage, privacy: .public)")
50+
}
51+
} else {
52+
print("\(severity.stringValue): \(formattedMessage)")
53+
}
54+
}
55+
}
56+
57+
58+
59+
/// A default logger configuration that uses `PrintLogWritter` and filters messages by minimum severity.
60+
public class DefaultLogger: LoggerProtocol {
61+
public var minSeverity: LogSeverity
62+
public var writers: [any LogWriterProtocol]
63+
64+
/// Initializes the default logger with an optional minimum severity level.
65+
///
66+
/// - Parameters
67+
/// - minSeverity: The minimum severity level to log. Defaults to `.debug`.
68+
/// - writers: Optional writers which logs should be written to. Defaults to a `PrintLogWriter`.
69+
public init(minSeverity: LogSeverity = .debug, writers: [any LogWriterProtocol]? = nil ) {
70+
self.writers = writers ?? [ PrintLogWriter() ]
71+
self.minSeverity = minSeverity
72+
}
73+
74+
public func setWriters(_ writters: [any LogWriterProtocol]) {
75+
self.writers = writters
76+
}
77+
78+
public func setMinSeverity(_ severity: LogSeverity) {
79+
self.minSeverity = severity
80+
}
81+
82+
83+
public func debug(_ message: String, tag: String? = nil) {
84+
self.writeLog(message, severity: LogSeverity.debug, tag: tag)
85+
}
86+
87+
public func error(_ message: String, tag: String? = nil) {
88+
self.writeLog(message, severity: LogSeverity.error, tag: tag)
89+
}
90+
91+
public func info(_ message: String, tag: String? = nil) {
92+
self.writeLog(message, severity: LogSeverity.info, tag: tag)
93+
}
94+
95+
public func warning(_ message: String, tag: String? = nil) {
96+
self.writeLog(message, severity: LogSeverity.warning, tag: tag)
97+
}
98+
99+
public func fault(_ message: String, tag: String? = nil) {
100+
self.writeLog(message, severity: LogSeverity.fault, tag: tag)
101+
}
102+
103+
private func writeLog(_ message: String, severity: LogSeverity, tag: String?) {
104+
if (severity.rawValue < self.minSeverity.rawValue) {
105+
return
106+
}
107+
108+
for writer in self.writers {
109+
writer.log(severity: severity, message: message, tag: tag)
110+
}
111+
}
112+
}
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
public enum LogSeverity: Int, CaseIterable {
2+
/// Detailed information typically used for debugging.
3+
case debug = 0
4+
5+
/// Informational messages that highlight the progress of the application.
6+
case info = 1
7+
8+
/// Potentially harmful situations that are not necessarily errors.
9+
case warning = 2
10+
11+
/// Error events that might still allow the application to continue running.
12+
case error = 3
13+
14+
/// Serious errors indicating critical failures, often unrecoverable.
15+
case fault = 4
16+
17+
/// Map severity to its string representation
18+
public var stringValue: String {
19+
switch self {
20+
case .debug: return "DEBUG"
21+
case .info: return "INFO"
22+
case .warning: return "WARNING"
23+
case .error: return "ERROR"
24+
case .fault: return "FAULT"
25+
}
26+
}
27+
28+
/// Convert Int to String representation
29+
public static func string(from intValue: Int) -> String? {
30+
return LogSeverity(rawValue: intValue)?.stringValue
31+
}
32+
}
33+
34+
/// A protocol for writing log messages to a specific backend or output.
35+
///
36+
/// Conformers handle the actual writing or forwarding of log messages.
37+
public protocol LogWriterProtocol {
38+
/// Logs a message with the given severity and optional tag.
39+
///
40+
/// - Parameters:
41+
/// - severity: The severity level of the log message.
42+
/// - message: The content of the log message.
43+
/// - tag: An optional tag to categorize or group the log message.
44+
func log(severity: LogSeverity, message: String, tag: String?)
45+
}
46+
47+
/// A protocol defining the interface for a logger that supports severity filtering and multiple writers.
48+
///
49+
/// Conformers provide logging APIs and manage attached log writers.
50+
public protocol LoggerProtocol {
51+
/// Logs an informational message.
52+
///
53+
/// - Parameters:
54+
/// - message: The content of the log message.
55+
/// - tag: An optional tag to categorize the message.
56+
func info(_ message: String, tag: String?)
57+
58+
/// Logs an error message.
59+
///
60+
/// - Parameters:
61+
/// - message: The content of the log message.
62+
/// - tag: An optional tag to categorize the message.
63+
func error(_ message: String, tag: String?)
64+
65+
/// Logs a debug message.
66+
///
67+
/// - Parameters:
68+
/// - message: The content of the log message.
69+
/// - tag: An optional tag to categorize the message.
70+
func debug(_ message: String, tag: String?)
71+
72+
/// Logs a warning message.
73+
///
74+
/// - Parameters:
75+
/// - message: The content of the log message.
76+
/// - tag: An optional tag to categorize the message.
77+
func warning(_ message: String, tag: String?)
78+
79+
/// Logs a fault message, typically used for critical system-level failures.
80+
///
81+
/// - Parameters:
82+
/// - message: The content of the log message.
83+
/// - tag: An optional tag to categorize the message.
84+
func fault(_ message: String, tag: String?)
85+
}

0 commit comments

Comments
 (0)