feat(ios): add cross-instance log center
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
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
|
||||
}
|
||||
|
||||
var degradedCount: Int {
|
||||
snapshot?.diagnostics.filter { $0.capabilities.degraded > 0 }.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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user