129 lines
4.9 KiB
Swift
129 lines
4.9 KiB
Swift
import Foundation
|
|
|
|
@MainActor
|
|
final class LogsViewModel: ObservableObject {
|
|
@Published private(set) var snapshot: LogSnapshot?
|
|
@Published private(set) var instances: [Instance] = []
|
|
@Published private(set) var isLoading = false
|
|
@Published var errorMessage: String?
|
|
|
|
@Published var selectedSource: RuntimeLogSource?
|
|
@Published var selectedLevel: RuntimeLogLevel?
|
|
@Published var selectedOutcome: ConnectionOutcome?
|
|
@Published var selectedInstanceID: UUID?
|
|
@Published var searchText = ""
|
|
|
|
private let repository: any InstanceRepository
|
|
private let service: LogCenterService
|
|
private let store: any LogStore
|
|
|
|
init(
|
|
repository: any InstanceRepository,
|
|
service: LogCenterService,
|
|
store: any LogStore
|
|
) {
|
|
self.repository = repository
|
|
self.service = service
|
|
self.store = store
|
|
}
|
|
|
|
var filteredRuntime: [RuntimeLogEntry] {
|
|
guard let snapshot else { return [] }
|
|
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
return snapshot.runtime.filter { entry in
|
|
(selectedSource == nil || entry.source == selectedSource)
|
|
&& (selectedLevel == nil || entry.level == selectedLevel)
|
|
&& (selectedInstanceID == nil || entry.instanceID == selectedInstanceID)
|
|
&& (query.isEmpty || matches(entry.message, query)
|
|
|| matches(entry.code, query)
|
|
|| matches(entry.eventType, query)
|
|
|| matches(entry.instanceName, query))
|
|
}
|
|
}
|
|
|
|
var filteredConnections: [ConnectionLogEntry] {
|
|
guard let snapshot else { return [] }
|
|
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
return snapshot.connections.filter { entry in
|
|
(selectedOutcome == nil || entry.outcome == selectedOutcome)
|
|
&& (selectedInstanceID == nil || entry.instanceID == selectedInstanceID)
|
|
&& (query.isEmpty || matches(entry.errorCode, query)
|
|
|| matches(entry.instanceName, query))
|
|
}
|
|
}
|
|
|
|
var filteredDiagnostics: [InstanceDiagnostic] {
|
|
guard let snapshot else { return [] }
|
|
let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
guard !query.isEmpty else { return snapshot.diagnostics }
|
|
return snapshot.diagnostics.filter { diagnostic in
|
|
matches(diagnostic.name, query)
|
|
|| matches(diagnostic.origin, query)
|
|
|| diagnostic.tags.contains { matches($0, query) }
|
|
}
|
|
}
|
|
|
|
var offlineCount: Int {
|
|
snapshot?.diagnostics.filter { $0.state == .offline }.count ?? 0
|
|
}
|
|
|
|
func refresh() async {
|
|
isLoading = true
|
|
errorMessage = nil
|
|
do {
|
|
let loaded = try await repository.list(InstanceQuery())
|
|
instances = loaded
|
|
let fetched = try await service.refresh(loaded)
|
|
let storedRuntime = try await store.runtimeLogs()
|
|
let storedConnections = try await store.connectionLogs()
|
|
var runtime = fetched.runtime
|
|
for entry in storedRuntime where !runtime.contains(where: { $0.id == entry.id }) {
|
|
runtime.append(entry)
|
|
}
|
|
runtime.sort { $0.occurredAt > $1.occurredAt }
|
|
|
|
var connections = fetched.connections
|
|
for entry in storedConnections where !connections.contains(where: { $0.id == entry.id }) {
|
|
connections.append(entry)
|
|
}
|
|
connections.sort { $0.observedAt > $1.observedAt }
|
|
snapshot = LogSnapshot(
|
|
observedAt: fetched.observedAt,
|
|
runtime: Array(runtime.prefix(500)),
|
|
counts: Self.recount(runtime),
|
|
connections: Array(connections.prefix(200)),
|
|
diagnostics: fetched.diagnostics
|
|
)
|
|
} catch {
|
|
errorMessage = "无法加载日志中心。"
|
|
}
|
|
isLoading = false
|
|
}
|
|
|
|
func pruneConnections(olderThanDays days: Int) async -> Int {
|
|
let cutoff = Date().addingTimeInterval(TimeInterval(-days * 24 * 60 * 60))
|
|
do {
|
|
let removed = try await store.pruneConnections(before: cutoff)
|
|
if removed > 0 {
|
|
await refresh()
|
|
}
|
|
return removed
|
|
} catch {
|
|
errorMessage = "清理连接日志失败。"
|
|
return 0
|
|
}
|
|
}
|
|
|
|
private func matches(_ value: String?, _ query: String) -> Bool {
|
|
value?.localizedLowercase.contains(query) == true
|
|
}
|
|
|
|
private static func recount(_ runtime: [RuntimeLogEntry]) -> [RuntimeLogSource: Int] {
|
|
var counts: [RuntimeLogSource: Int] = [:]
|
|
for source in RuntimeLogSource.allCases {
|
|
counts[source] = runtime.filter { $0.source == source }.count
|
|
}
|
|
return counts
|
|
}
|
|
}
|