113 lines
3.7 KiB
Swift
113 lines
3.7 KiB
Swift
import Foundation
|
|
|
|
public struct FileLogStore: LogStore {
|
|
private struct Store: Codable {
|
|
var runtime: [RuntimeLogEntry] = []
|
|
var connections: [ConnectionLogEntry] = []
|
|
}
|
|
|
|
private let location: URL
|
|
private let encoder: JSONEncoder
|
|
private let decoder: JSONDecoder
|
|
private let queue = DispatchQueue(label: "fun.chick.multi-simadmin.log-store")
|
|
|
|
public init(location: URL) {
|
|
self.location = location
|
|
let encoder = JSONEncoder()
|
|
encoder.dateEncodingStrategy = .iso8601
|
|
encoder.outputFormatting = [.sortedKeys]
|
|
self.encoder = encoder
|
|
|
|
let decoder = JSONDecoder()
|
|
decoder.dateDecodingStrategy = .iso8601
|
|
self.decoder = decoder
|
|
}
|
|
|
|
public func runtimeLogs() async throws -> [RuntimeLogEntry] {
|
|
try await run { $0.runtime.sorted { $0.occurredAt > $1.occurredAt } }
|
|
}
|
|
|
|
public func connectionLogs() async throws -> [ConnectionLogEntry] {
|
|
try await run { $0.connections.sorted { $0.observedAt > $1.observedAt } }
|
|
}
|
|
|
|
public func append(
|
|
runtime: [RuntimeLogEntry],
|
|
connections: [ConnectionLogEntry]
|
|
) async throws {
|
|
try await run { store in
|
|
var mergedRuntime = store.runtime
|
|
for entry in runtime where !mergedRuntime.contains(where: { $0.id == entry.id }) {
|
|
mergedRuntime.append(entry)
|
|
}
|
|
mergedRuntime.sort { $0.occurredAt > $1.occurredAt }
|
|
store.runtime = Array(mergedRuntime.prefix(2_000))
|
|
|
|
var mergedConnections = store.connections
|
|
for entry in connections
|
|
where !mergedConnections.contains(where: { $0.id == entry.id }) {
|
|
mergedConnections.append(entry)
|
|
}
|
|
mergedConnections.sort { $0.observedAt > $1.observedAt }
|
|
store.connections = Array(mergedConnections.prefix(20_000))
|
|
}
|
|
}
|
|
|
|
public func pruneConnections(before cutoff: Date) async throws -> Int {
|
|
try await run { store in
|
|
let retained = store.connections.filter { $0.observedAt >= cutoff }
|
|
let removed = store.connections.count - retained.count
|
|
store.connections = retained
|
|
return removed
|
|
}
|
|
}
|
|
|
|
public func clearRuntimeLogs() async throws -> Int {
|
|
try await run { store in
|
|
let removed = store.runtime.count
|
|
store.runtime = []
|
|
return removed
|
|
}
|
|
}
|
|
|
|
private func run<T: Sendable>(
|
|
_ transform: @escaping @Sendable (inout Store) throws -> T
|
|
) async throws -> T {
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
queue.async {
|
|
do {
|
|
var store = try self.readStore()
|
|
let result = try transform(&store)
|
|
try self.writeStore(store)
|
|
continuation.resume(returning: result)
|
|
} catch {
|
|
continuation.resume(throwing: error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func readStore() throws -> Store {
|
|
guard FileManager.default.fileExists(atPath: location.path) else {
|
|
return Store()
|
|
}
|
|
do {
|
|
return try decoder.decode(Store.self, from: Data(contentsOf: location))
|
|
} catch {
|
|
return Store()
|
|
}
|
|
}
|
|
|
|
private func writeStore(_ store: Store) throws {
|
|
do {
|
|
try FileManager.default.createDirectory(
|
|
at: location.deletingLastPathComponent(),
|
|
withIntermediateDirectories: true
|
|
)
|
|
try encoder.encode(store).write(to: location, options: [.atomic])
|
|
} catch {
|
|
throw RepositoryError.databaseFailed
|
|
}
|
|
}
|
|
}
|