feat(ios): add cross-instance log center
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user