Skip to content

Configurable Logging #34

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 12 commits into from
Apr 14, 2025
Merged
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
# Changelog

# 1.0.0-Beta.10 (unreleased)

* Added the ability to specify a custom logging implementation
```swift
let db = PowerSyncDatabase(
schema: Schema(
tables: [
Table(
name: "users",
columns: [
.text("name"),
.text("email")
]
)
]
),
logger: DefaultLogger(minSeverity: .debug)
)
```
* added `.close()` method on `PowerSyncDatabaseProtocol`

## 1.0.0-Beta.9

* Update PowerSync SQLite core extension to 0.3.12.
Expand Down
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<a href="https://www.powersync.com" target="_blank"><img src="https://github.com/powersync-ja/.github/assets/7372448/d2538c43-c1a0-4c47-9a76-41462dba484f"/></a>
</p>

*[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.*
_[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._

# PowerSync Swift

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

- [Sources](./Sources/)

- This is the Swift SDK implementation.
- This is the Swift SDK implementation.

## Demo Apps / Example Projects

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

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

## Usage

Create a PowerSync client

```swift
import PowerSync

let powersync = PowerSyncDatabase(
schema: Schema(
tables: [
Table(
name: "users",
columns: [
.text("count"),
.integer("is_active"),
.real("weight"),
.text("description")
]
)
]
),
logger: DefaultLogger(minSeverity: .debug)
)
```

## Underlying Kotlin Dependency

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).


## Migration from Alpha to Beta

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.
88 changes: 88 additions & 0 deletions Sources/PowerSync/Kotlin/DatabaseLogger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import PowerSyncKotlin

/// Adapts a Swift `LoggerProtocol` to Kermit's `LogWriter` interface.
///
/// This allows Kotlin logging (via Kermit) to call into the Swift logging implementation.
private class KermitLogWriterAdapter: Kermit_coreLogWriter {
/// The underlying Swift log writer to forward log messages to.
let logger: any LoggerProtocol

/// Initializes a new adapter.
///
/// - Parameter logger: A Swift log writer that will handle log output.
init(logger: any LoggerProtocol) {
self.logger = logger
super.init()
}

/// Called by Kermit to log a message.
///
/// - Parameters:
/// - severity: The severity level of the log.
/// - message: The content of the log message.
/// - tag: A string categorizing the log.
/// - throwable: An optional Kotlin exception (ignored here).
override func log(severity: Kermit_coreSeverity, message: String, tag: String, throwable: KotlinThrowable?) {
switch severity {
case PowerSyncKotlin.Kermit_coreSeverity.verbose:
return logger.debug(message, tag: tag)
case PowerSyncKotlin.Kermit_coreSeverity.debug:
return logger.debug(message, tag: tag)
case PowerSyncKotlin.Kermit_coreSeverity.info:
return logger.info(message, tag: tag)
case PowerSyncKotlin.Kermit_coreSeverity.warn:
return logger.warning(message, tag: tag)
case PowerSyncKotlin.Kermit_coreSeverity.error:
return logger.error(message, tag: tag)
case PowerSyncKotlin.Kermit_coreSeverity.assert:
return logger.fault(message, tag: tag)
}
}
}

/// A logger implementation that integrates with PowerSync's Kotlin core using Kermit.
///
/// This class bridges Swift log writers with the Kotlin logging system and supports
/// runtime configuration of severity levels and writer lists.
internal class DatabaseLogger: LoggerProtocol {
/// The underlying Kermit logger instance provided by the PowerSyncKotlin SDK.
public let kLogger = PowerSyncKotlin.generateLogger(logger: nil)
public let logger: any LoggerProtocol

/// Initializes a new logger with an optional list of writers.
///
/// - Parameter logger: A logger which will be called for each internal log operation
init(_ logger: any LoggerProtocol) {
self.logger = logger
// Set to the lowest severity. The provided logger should filter by severity
kLogger.mutableConfig.setMinSeverity(Kermit_coreSeverity.verbose)
kLogger.mutableConfig.setLogWriterList(
[KermitLogWriterAdapter(logger: logger)]
)
}

/// Logs a debug-level message.
public func debug(_ message: String, tag: String?) {
logger.debug(message, tag: tag)
}

/// Logs an info-level message.
public func info(_ message: String, tag: String?) {
logger.info(message, tag: tag)
}

/// Logs a warning-level message.
public func warning(_ message: String, tag: String?) {
logger.warning(message, tag: tag)
}

/// Logs an error-level message.
public func error(_ message: String, tag: String?) {
logger.error(message, tag: tag)
}

/// Logs a fault (assert-level) message, typically used for critical issues.
public func fault(_ message: String, tag: String?) {
logger.fault(message, tag: tag)
}
}
10 changes: 8 additions & 2 deletions Sources/PowerSync/Kotlin/KotlinPowerSyncDatabaseImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {

init(
schema: Schema,
dbFilename: String
dbFilename: String,
logger: DatabaseLogger? = nil
) {
let factory = PowerSyncKotlin.DatabaseDriverFactory()
kotlinDatabase = PowerSyncDatabase(
factory: factory,
schema: KotlinAdapter.Schema.toKotlin(schema),
dbFilename: dbFilename
dbFilename: dbFilename,
logger: logger?.kLogger
)
}

Expand Down Expand Up @@ -232,4 +234,8 @@ final class KotlinPowerSyncDatabaseImpl: PowerSyncDatabaseProtocol {
func readTransaction<R>(callback: @escaping (any PowerSyncTransaction) throws -> R) async throws -> R {
return try safeCast(await kotlinDatabase.readTransaction(callback: TransactionCallback(callback: callback)), to: R.self)
}

func close() async throws{
try await kotlinDatabase.close()
}
}
112 changes: 112 additions & 0 deletions Sources/PowerSync/Logger.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import OSLog

/// A log writer which prints to the standard output
///
/// This writer uses `os.Logger` on iOS/macOS/tvOS/watchOS 14+ and falls back to `print` for earlier versions.
public class PrintLogWriter: LogWriterProtocol {

private let subsystem: String
private let category: String
private lazy var logger: Any? = {
if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
return Logger(subsystem: subsystem, category: category)
}
return nil
}()

/// Creates a new PrintLogWriter
/// - Parameters:
/// - subsystem: The subsystem identifier (typically reverse DNS notation of your app)
/// - category: The category within your subsystem
public init(subsystem: String = Bundle.main.bundleIdentifier ?? "com.powersync.logger",
category: String = "default") {
self.subsystem = subsystem
self.category = category
}

/// Logs a message with a given severity and optional tag.
/// - Parameters:
/// - severity: The severity level of the message.
/// - message: The content of the log message.
/// - tag: An optional tag used to categorize the message. If empty, no brackets are shown.
public func log(severity: LogSeverity, message: String, tag: String?) {
let tagPrefix = tag.map { !$0.isEmpty ? "[\($0)] " : "" } ?? ""
let formattedMessage = "\(tagPrefix)\(message)"

if #available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) {
guard let logger = logger as? Logger else { return }

switch severity {
case .info:
logger.info("\(formattedMessage, privacy: .public)")
case .error:
logger.error("\(formattedMessage, privacy: .public)")
case .debug:
logger.debug("\(formattedMessage, privacy: .public)")
case .warning:
logger.warning("\(formattedMessage, privacy: .public)")
case .fault:
logger.fault("\(formattedMessage, privacy: .public)")
}
} else {
print("\(severity.stringValue): \(formattedMessage)")
}
}
}



/// A default logger configuration that uses `PrintLogWritter` and filters messages by minimum severity.
public class DefaultLogger: LoggerProtocol {
public var minSeverity: LogSeverity
public var writers: [any LogWriterProtocol]

/// Initializes the default logger with an optional minimum severity level.
///
/// - Parameters
/// - minSeverity: The minimum severity level to log. Defaults to `.debug`.
/// - writers: Optional writers which logs should be written to. Defaults to a `PrintLogWriter`.
public init(minSeverity: LogSeverity = .debug, writers: [any LogWriterProtocol]? = nil ) {
self.writers = writers ?? [ PrintLogWriter() ]
self.minSeverity = minSeverity
}

public func setWriters(_ writters: [any LogWriterProtocol]) {
self.writers = writters
}

public func setMinSeverity(_ severity: LogSeverity) {
self.minSeverity = severity
}


public func debug(_ message: String, tag: String? = nil) {
self.writeLog(message, severity: LogSeverity.debug, tag: tag)
}

public func error(_ message: String, tag: String? = nil) {
self.writeLog(message, severity: LogSeverity.error, tag: tag)
}

public func info(_ message: String, tag: String? = nil) {
self.writeLog(message, severity: LogSeverity.info, tag: tag)
}

public func warning(_ message: String, tag: String? = nil) {
self.writeLog(message, severity: LogSeverity.warning, tag: tag)
}

public func fault(_ message: String, tag: String? = nil) {
self.writeLog(message, severity: LogSeverity.fault, tag: tag)
}

private func writeLog(_ message: String, severity: LogSeverity, tag: String?) {
if (severity.rawValue < self.minSeverity.rawValue) {
return
}

for writer in self.writers {
writer.log(severity: severity, message: message, tag: tag)
}
}
}
85 changes: 85 additions & 0 deletions Sources/PowerSync/LoggerProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
public enum LogSeverity: Int, CaseIterable {
/// Detailed information typically used for debugging.
case debug = 0

/// Informational messages that highlight the progress of the application.
case info = 1

/// Potentially harmful situations that are not necessarily errors.
case warning = 2

/// Error events that might still allow the application to continue running.
case error = 3

/// Serious errors indicating critical failures, often unrecoverable.
case fault = 4

/// Map severity to its string representation
public var stringValue: String {
switch self {
case .debug: return "DEBUG"
case .info: return "INFO"
case .warning: return "WARNING"
case .error: return "ERROR"
case .fault: return "FAULT"
}
}

/// Convert Int to String representation
public static func string(from intValue: Int) -> String? {
return LogSeverity(rawValue: intValue)?.stringValue
}
}

/// A protocol for writing log messages to a specific backend or output.
///
/// Conformers handle the actual writing or forwarding of log messages.
public protocol LogWriterProtocol {
/// Logs a message with the given severity and optional tag.
///
/// - Parameters:
/// - severity: The severity level of the log message.
/// - message: The content of the log message.
/// - tag: An optional tag to categorize or group the log message.
func log(severity: LogSeverity, message: String, tag: String?)
}

/// A protocol defining the interface for a logger that supports severity filtering and multiple writers.
///
/// Conformers provide logging APIs and manage attached log writers.
public protocol LoggerProtocol {
/// Logs an informational message.
///
/// - Parameters:
/// - message: The content of the log message.
/// - tag: An optional tag to categorize the message.
func info(_ message: String, tag: String?)

/// Logs an error message.
///
/// - Parameters:
/// - message: The content of the log message.
/// - tag: An optional tag to categorize the message.
func error(_ message: String, tag: String?)

/// Logs a debug message.
///
/// - Parameters:
/// - message: The content of the log message.
/// - tag: An optional tag to categorize the message.
func debug(_ message: String, tag: String?)

/// Logs a warning message.
///
/// - Parameters:
/// - message: The content of the log message.
/// - tag: An optional tag to categorize the message.
func warning(_ message: String, tag: String?)

/// Logs a fault message, typically used for critical system-level failures.
///
/// - Parameters:
/// - message: The content of the log message.
/// - tag: An optional tag to categorize the message.
func fault(_ message: String, tag: String?)
}
Loading