Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,33 @@ jobs:

Swift-Package:
strategy:
fail-fast: false
matrix:
os: [macos-15, ubuntu-latest]
os: [macos-26, ubuntu-latest]

runs-on: ${{ matrix.os }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Resolve Packages
uses: actions/checkout@v6

- name: Cache
uses: actions/cache@v5
with:
path: |
.build
~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex
~/Library/Caches/org.swift.swiftpm
~/Library/org.swift.swiftpm
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-

- name: Package Resolution
run: swift package resolve

- name: Compile Source
- name: Build
run: swift build

- name: Run Tests
- name: Test
run: swift test
29 changes: 24 additions & 5 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 17 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:6.2
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand All @@ -22,9 +22,11 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/apple/swift-log.git", .upToNextMajor(from: "1.6.4")),
.package(url: "https://github.com/richardpiazza/Statement.git", .upToNextMajor(from: "0.8.1")),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", .upToNextMajor(from: "0.15.3")),
.package(url: "https://github.com/swiftlang/swift-toolchain-sqlite.git", from: "1.0.7"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.9.1"),
.package(url: "https://github.com/richardpiazza/Statement.git", from: "0.8.1"),
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.15.5", traits: ["SwiftToolchainCSQLite"]),
.package(url: "https://github.com/swhitty/swift-mutex.git", from: "0.0.6"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand All @@ -36,10 +38,7 @@ let package = Package(
.product(name: "Statement", package: "Statement"),
.product(name: "StatementSQLite", package: "Statement"),
.product(name: "SQLite", package: "SQLite.swift"),
],
swiftSettings: [
.enableExperimentalFeature("ExistentialAny"),
.enableExperimentalFeature("StrictConcurrency=complete"),
.product(name: "Mutex", package: "swift-mutex"),
]
),
.testTarget(
Expand All @@ -51,3 +50,13 @@ let package = Package(
),
]
)

for target in package.targets {
var settings = target.swiftSettings ?? []
settings.append(contentsOf: [
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("MemberImportVisibility"),
.enableUpcomingFeature("StrictConcurrency=complete"),
])
target.swiftSettings = settings
}
62 changes: 35 additions & 27 deletions Sources/Occurrence/CoreData/CoreDataLogProvider.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import Foundation
import Logging
import Mutex
#if canImport(CoreData)
@preconcurrency import CoreData

class CoreDataLogProvider: LogProvider {
final class CoreDataLogProvider: LogProvider {

let storeUrl: URL
private var persistentContainer: NSPersistentContainer

private lazy var context: NSManagedObjectContext = {
let context = persistentContainer.newBackgroundContext()
context.automaticallyMergesChangesFromParent = true
return context
}()
private let persistentContainer: Mutex<NSPersistentContainer>
private let context: Mutex<NSManagedObjectContext>

init(url: URL? = nil) throws {
storeUrl = try url ?? FileManager.default.defaultDatabaseUrl()
Expand All @@ -38,11 +34,16 @@ class CoreDataLogProvider: LogProvider {
}

container.viewContext.automaticallyMergesChangesFromParent = true
persistentContainer = container
persistentContainer = Mutex(container)

let backgroundContext = container.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
context = Mutex(backgroundContext)
}

deinit {
let context = persistentContainer.viewContext
let container = persistentContainer.withLock { $0 }
let context = container.viewContext
context.performAndWait {
if context.hasChanges {
do {
Expand All @@ -53,7 +54,7 @@ class CoreDataLogProvider: LogProvider {
}
}

let coordinator = persistentContainer.persistentStoreCoordinator
let coordinator = container.persistentStoreCoordinator
let stores = coordinator.persistentStores
do {
for store in stores {
Expand All @@ -74,27 +75,30 @@ class CoreDataLogProvider: LogProvider {
}
}

public func log(_ entry: Logger.Entry) {
context.performAndWait {
func log(_ entry: Logger.Entry) {
let backgroundContext = context.withLock { $0 }
backgroundContext.performAndWait {
do {
try ManagedEntry(context: context, entry: entry)
try context.save()
try ManagedEntry(context: backgroundContext, entry: entry)
try backgroundContext.save()
} catch {
print(error)
}
}
}

public func subsystems() -> [Logger.Subsystem] {
func subsystems() -> [Logger.Subsystem] {
let request = NSFetchRequest<any NSFetchRequestResult>(entityName: ManagedEntry.fetchRequest().entityName ?? "ManagedEntry")
request.resultType = .dictionaryResultType
request.propertiesToFetch = [#keyPath(ManagedEntry.subsystem)]
request.returnsDistinctResults = true

let subsystems = context.performAndWait {
let backgroundContext = context.withLock { $0 }

let subsystems = backgroundContext.performAndWait {
var subsystems: Set<Logger.Subsystem> = [.occurrence]
do {
let results = try context.fetch(request) as? [[String: String]]
let results = try backgroundContext.fetch(request) as? [[String: String]]
results?
.compactMap { $0[#keyPath(ManagedEntry.subsystem)] }
.map { Logger.Subsystem($0) }
Expand All @@ -110,7 +114,7 @@ class CoreDataLogProvider: LogProvider {
return Array(subsystems)
}

public func entries(matching filter: Logger.Filter?, ascending: Bool, limit: UInt) -> [Logger.Entry] {
func entries(matching filter: Logger.Filter?, ascending: Bool, limit: UInt) -> [Logger.Entry] {
let request = ManagedEntry.fetchRequest()
request.predicate = filter?.predicate
request.sortDescriptors = [
Expand All @@ -120,10 +124,12 @@ class CoreDataLogProvider: LogProvider {
request.fetchLimit = Int(limit)
}

let results = context.performAndWait {
let backgroundContext = context.withLock { $0 }

let results = backgroundContext.performAndWait {
var results: [Logger.Entry] = []
do {
let entries = try context.fetch(request)
let entries = try backgroundContext.fetch(request)
results = entries.map(\.entry)
} catch {
print(error)
Expand All @@ -134,18 +140,20 @@ class CoreDataLogProvider: LogProvider {
return results
}

public func purge(matching filter: Logger.Filter?) {
func purge(matching filter: Logger.Filter?) {
let request = ManagedEntry.fetchRequest()
request.predicate = filter?.predicate

context.performAndWait {
let backgroundContext = context.withLock { $0 }

backgroundContext.performAndWait {
do {
let entities = try context.fetch(request)
let entities = try backgroundContext.fetch(request)
for entity in entities {
context.delete(entity)
backgroundContext.delete(entity)
}
if context.hasChanges {
try context.save()
if backgroundContext.hasChanges {
try backgroundContext.save()
}
} catch {
print(error)
Expand Down
8 changes: 6 additions & 2 deletions Sources/Occurrence/CoreData/LogModel.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import Foundation
import Mutex
#if canImport(CoreData)
import CoreData

enum LogModel {
case version_1_0_0

static var `default`: LogModel = .version_1_0_0
static var `default`: LogModel { .version_1_0_0 }

var managedObjectModel: NSManagedObjectModel {
switch self {
Expand All @@ -15,9 +16,12 @@ enum LogModel {
}

class Version_1_0_0: NSManagedObjectModel, NSSecureCoding {
private static let protectedState = Mutex(Version_1_0_0())
/// Provide a singular instance of the model to be referenced. There is a known issue where when referencing
/// a model in an app target, as well as unit tests, a model - and therefore its entities - can be loaded twice.
static let instance: Version_1_0_0 = Version_1_0_0()
static var instance: Version_1_0_0 {
protectedState.withLock { $0 }
}

static var supportsSecureCoding: Bool { true }

Expand Down
2 changes: 1 addition & 1 deletion Sources/Occurrence/Extensions/Logger+Entry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ extension Logger.Entry: CustomStringConvertible {
public var description: String {
let _date = Self.gmtDateFormatter.string(from: date)
let sourceFile = [source, fileName].filter { !$0.isEmpty }.joined(separator: " ")
let output = "[\(_date) \(level) | \(subsystem) | \(sourceFile) | \(function) \(line)] \(message)"
let output = "[\(_date) \(level.fancyDescription) | \(subsystem) | \(sourceFile) | \(function) \(line)] \(message)"
if let metadata {
let sortedMetadata = metadata.sorted(by: { $0.key < $1.key })
let values = sortedMetadata.map { "\($0.key): \($0.value)" }.joined(separator: ", ")
Expand Down
12 changes: 1 addition & 11 deletions Sources/Occurrence/Extensions/Logger.Level+Occurrence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,8 @@ public extension Logger.Level {
let max = Logger.Level.allCases.map(\.rawValue.count).max() ?? rawValue.count
return rawValue.padding(toLength: max, withPad: " ", startingAt: 0)
}
}

#if hasFeature(RetroactiveAttribute)
extension Logger.Level: @retroactive CustomStringConvertible {
public var description: String {
"\(gem) \(fixedWidthDescription.uppercased())"
}
}
#else
extension Logger.Level: CustomStringConvertible {
public var description: String {
var fancyDescription: String {
"\(gem) \(fixedWidthDescription.uppercased())"
}
}
#endif
2 changes: 1 addition & 1 deletion Sources/Occurrence/LogProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation
import Logging

/// A source of logging data.
public protocol LogProvider {
public protocol LogProvider: Sendable {
/// Consume a log entry.
func log(_ entry: Logger.Entry)

Expand Down
14 changes: 1 addition & 13 deletions Sources/Occurrence/LogStreamer.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,10 @@
import Foundation
import Logging
#if canImport(Combine)
import Combine
#endif

public protocol LogStreamer {
public protocol LogStreamer: Sendable {
/// `AsyncStream` which emits log entries.
///
/// Due to limitations with the underlying `AsyncSequence` implementation, only a single receiver can await elements.
/// Each access will _finish_ an existing stream and return a fresh stream.
var stream: AsyncStream<Logger.Entry> { get }

#if canImport(Combine)
/// Publisher which emits log entries.
@available(*, deprecated, message: "Use `AsyncStream` variant.")
var publisher: AnyPublisher<Logger.Entry, Never> { get }
#endif

/// Consume a log entry.
func log(_ entry: Logger.Entry)
}
Loading