updates
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
+105
-17
@@ -7,7 +7,7 @@
|
||||
"key": "ALL SERVERS: %lld OK · %lld WARN · %lld FAIL",
|
||||
"location": {
|
||||
"startingColumn": 18,
|
||||
"startingLine": 139
|
||||
"startingLine": 289
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -15,7 +15,7 @@
|
||||
"key": "%lld offline",
|
||||
"location": {
|
||||
"startingColumn": 26,
|
||||
"startingLine": 17
|
||||
"startingLine": 38
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -23,7 +23,7 @@
|
||||
"key": "PEGASUS MONITOR",
|
||||
"location": {
|
||||
"startingColumn": 27,
|
||||
"startingLine": 11
|
||||
"startingLine": 32
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -31,7 +31,7 @@
|
||||
"key": "SERVERS",
|
||||
"location": {
|
||||
"startingColumn": 22,
|
||||
"startingLine": 22
|
||||
"startingLine": 43
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -39,7 +39,7 @@
|
||||
"key": "Switch Server",
|
||||
"location": {
|
||||
"startingColumn": 27,
|
||||
"startingLine": 41
|
||||
"startingLine": 62
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -47,7 +47,7 @@
|
||||
"key": "%lld OK",
|
||||
"location": {
|
||||
"startingColumn": 46,
|
||||
"startingLine": 64
|
||||
"startingLine": 85
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -55,7 +55,31 @@
|
||||
"key": "Up %@",
|
||||
"location": {
|
||||
"startingColumn": 50,
|
||||
"startingLine": 66
|
||||
"startingLine": 87
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Show Details",
|
||||
"location": {
|
||||
"startingColumn": 36,
|
||||
"startingLine": 97
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Copy Error Details",
|
||||
"location": {
|
||||
"startingColumn": 36,
|
||||
"startingLine": 135
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Refresh Server",
|
||||
"location": {
|
||||
"startingColumn": 36,
|
||||
"startingLine": 154
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -63,7 +87,7 @@
|
||||
"key": "SERVICES",
|
||||
"location": {
|
||||
"startingColumn": 38,
|
||||
"startingLine": 101
|
||||
"startingLine": 187
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -71,7 +95,7 @@
|
||||
"key": "HEALTH GROUPS",
|
||||
"location": {
|
||||
"startingColumn": 38,
|
||||
"startingLine": 117
|
||||
"startingLine": 217
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -79,7 +103,7 @@
|
||||
"key": "Uptime",
|
||||
"location": {
|
||||
"startingColumn": 38,
|
||||
"startingLine": 92
|
||||
"startingLine": 178
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -87,7 +111,39 @@
|
||||
"key": "REQUIRED",
|
||||
"location": {
|
||||
"startingColumn": 50,
|
||||
"startingLine": 110
|
||||
"startingLine": 196
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Show Details",
|
||||
"location": {
|
||||
"startingColumn": 48,
|
||||
"startingLine": 203
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Copy Details",
|
||||
"location": {
|
||||
"startingColumn": 48,
|
||||
"startingLine": 211
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Show Details",
|
||||
"location": {
|
||||
"startingColumn": 44,
|
||||
"startingLine": 232
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Copy Details",
|
||||
"location": {
|
||||
"startingColumn": 44,
|
||||
"startingLine": 270
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -95,7 +151,7 @@
|
||||
"key": "Refresh All",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 147
|
||||
"startingLine": 297
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -103,7 +159,7 @@
|
||||
"key": "Diagnostics",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 149
|
||||
"startingLine": 299
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -111,7 +167,7 @@
|
||||
"key": "Settings",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 150
|
||||
"startingLine": 300
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -119,7 +175,7 @@
|
||||
"key": "Quit",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 151
|
||||
"startingLine": 301
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -127,7 +183,7 @@
|
||||
"key": "%@ — Diagnostics",
|
||||
"location": {
|
||||
"startingColumn": 18,
|
||||
"startingLine": 161
|
||||
"startingLine": 317
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -135,7 +191,39 @@
|
||||
"key": "Select a server in the menu bar to change this report.",
|
||||
"location": {
|
||||
"startingColumn": 18,
|
||||
"startingLine": 162
|
||||
"startingLine": 318
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Show Details",
|
||||
"location": {
|
||||
"startingColumn": 36,
|
||||
"startingLine": 332
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Copy Details",
|
||||
"location": {
|
||||
"startingColumn": 36,
|
||||
"startingLine": 344
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Copy Details",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 418
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": "",
|
||||
"key": "Close",
|
||||
"location": {
|
||||
"startingColumn": 24,
|
||||
"startingLine": 428
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
+1
@@ -8,3 +8,4 @@ ae0397526b5e3c6e8848cdfd6a483288
|
||||
7e38a5d8e664e612ef350f00745dc212
|
||||
0339bfa5899b58e708b1bdc579f1e095
|
||||
fc9e0d46560e0e3f6c4e552bd51875d2
|
||||
aa90f81d02489c13e5c788a9f2cc7c59
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -6,6 +6,48 @@
|
||||
<integer>13</integer>
|
||||
<key>logs</key>
|
||||
<dict>
|
||||
<key>17BB5E1E-C7B0-4A22-A2E4-7B3343B7C0A0</key>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
<string>IDECommandLineBuildLog</string>
|
||||
<key>documentTypeString</key>
|
||||
<string><nil></string>
|
||||
<key>domainType</key>
|
||||
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
|
||||
<key>fileName</key>
|
||||
<string>17BB5E1E-C7B0-4A22-A2E4-7B3343B7C0A0.xcactivitylog</string>
|
||||
<key>hasPrimaryLog</key>
|
||||
<true/>
|
||||
<key>primaryObservable</key>
|
||||
<dict>
|
||||
<key>highLevelStatus</key>
|
||||
<string>S</string>
|
||||
<key>totalNumberOfAnalyzerIssues</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfErrors</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfTestFailures</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfWarnings</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>schemeIdentifier-containerName</key>
|
||||
<string>PegasusMonitor project</string>
|
||||
<key>schemeIdentifier-schemeName</key>
|
||||
<string>PegasusMonitor</string>
|
||||
<key>schemeIdentifier-sharedScheme</key>
|
||||
<integer>1</integer>
|
||||
<key>signature</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>timeStartedRecording</key>
|
||||
<real>814350397.61442196</real>
|
||||
<key>timeStoppedRecording</key>
|
||||
<real>814350402.64668095</real>
|
||||
<key>title</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>uniqueIdentifier</key>
|
||||
<string>17BB5E1E-C7B0-4A22-A2E4-7B3343B7C0A0</string>
|
||||
</dict>
|
||||
<key>B61B7A1E-4BB1-405D-9BB7-4318215BFC0D</key>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
@@ -48,48 +90,6 @@
|
||||
<key>uniqueIdentifier</key>
|
||||
<string>B61B7A1E-4BB1-405D-9BB7-4318215BFC0D</string>
|
||||
</dict>
|
||||
<key>FEA45B81-12F7-48C7-BB4F-0CA06A4FC9A8</key>
|
||||
<dict>
|
||||
<key>className</key>
|
||||
<string>IDECommandLineBuildLog</string>
|
||||
<key>documentTypeString</key>
|
||||
<string><nil></string>
|
||||
<key>domainType</key>
|
||||
<string>Xcode.IDEActivityLogDomainType.BuildLog</string>
|
||||
<key>fileName</key>
|
||||
<string>FEA45B81-12F7-48C7-BB4F-0CA06A4FC9A8.xcactivitylog</string>
|
||||
<key>hasPrimaryLog</key>
|
||||
<true/>
|
||||
<key>primaryObservable</key>
|
||||
<dict>
|
||||
<key>highLevelStatus</key>
|
||||
<string>W</string>
|
||||
<key>totalNumberOfAnalyzerIssues</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfErrors</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfTestFailures</key>
|
||||
<integer>0</integer>
|
||||
<key>totalNumberOfWarnings</key>
|
||||
<integer>2</integer>
|
||||
</dict>
|
||||
<key>schemeIdentifier-containerName</key>
|
||||
<string>PegasusMonitor project</string>
|
||||
<key>schemeIdentifier-schemeName</key>
|
||||
<string>PegasusMonitor</string>
|
||||
<key>schemeIdentifier-sharedScheme</key>
|
||||
<integer>1</integer>
|
||||
<key>signature</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>timeStartedRecording</key>
|
||||
<real>814283652.52319503</real>
|
||||
<key>timeStoppedRecording</key>
|
||||
<real>814283659.16746604</real>
|
||||
<key>title</key>
|
||||
<string>Building project PegasusMonitor with scheme PegasusMonitor and configuration Release</string>
|
||||
<key>uniqueIdentifier</key>
|
||||
<string>FEA45B81-12F7-48C7-BB4F-0CA06A4FC9A8</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1 +1 @@
|
||||
1792592179.2212691: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/ModuleCache.noindex")
|
||||
1792657599.394216: Module build session file for module cache at Path(str: "/Volumes/Developer/Applications/PegasusMonitor/.build/ReleaseDerivedData/ModuleCache.noindex")
|
||||
|
||||
BIN
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LastAccessedDate</key>
|
||||
<date>2026-10-21T14:16:17Z</date>
|
||||
<date>2026-10-22T08:26:37Z</date>
|
||||
<key>WorkspacePath</key>
|
||||
<string>/Volumes/Developer/Applications/PegasusMonitor/PegasusMonitor.xcodeproj</string>
|
||||
</dict>
|
||||
|
||||
@@ -3,6 +3,27 @@ import SwiftUI
|
||||
struct MonitorMenuView: View {
|
||||
@ObservedObject var monitor: ServerMonitor
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
@State private var detailItem: MonitorDetailItem?
|
||||
|
||||
private func showDetails(
|
||||
title: String,
|
||||
status: String,
|
||||
details: String
|
||||
) {
|
||||
detailItem = MonitorDetailItem(
|
||||
title: title,
|
||||
status: status,
|
||||
details: details
|
||||
)
|
||||
}
|
||||
|
||||
private func copyDetails(_ details: String) {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(
|
||||
details,
|
||||
forType: .string
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
@@ -70,7 +91,72 @@ struct MonitorMenuView: View {
|
||||
if server.id == monitor.selectedID { Image(systemName: "checkmark") }
|
||||
}.padding(.horizontal, 14).padding(.vertical, 8)
|
||||
.contentShape(Rectangle())
|
||||
}.buttonStyle(.plain)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contextMenu {
|
||||
Button("Show Details") {
|
||||
let result = monitor.results[server.id]
|
||||
let report = result?.report
|
||||
|
||||
let failures = report?.checks.filter {
|
||||
$0.status != .ok
|
||||
} ?? []
|
||||
|
||||
let checkDetails = failures.map {
|
||||
"\($0.section) / \($0.name) [\($0.status.title)]\\n\($0.detail)"
|
||||
}.joined(separator: "\\n\\n")
|
||||
|
||||
let missingGroups = report?.groups
|
||||
.filter { $0.status != .ok }
|
||||
.map {
|
||||
"\($0.name): \($0.missing.joined(separator: ", "))"
|
||||
}
|
||||
.joined(separator: "\\n") ?? ""
|
||||
|
||||
let details = [
|
||||
"Server: \(server.name)",
|
||||
"Host: \(server.host)",
|
||||
"Status: \(result?.status.title ?? "Unknown")",
|
||||
result?.error ?? "",
|
||||
missingGroups,
|
||||
checkDetails,
|
||||
report?.diagnostics ?? ""
|
||||
]
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: "\\n\\n")
|
||||
|
||||
showDetails(
|
||||
title: server.name,
|
||||
status: result?.status.title ?? "Unknown",
|
||||
details: details
|
||||
)
|
||||
}
|
||||
|
||||
Button("Copy Error Details") {
|
||||
let result = monitor.results[server.id]
|
||||
let failures = result?.report?.checks
|
||||
.filter { $0.status != .ok }
|
||||
.map {
|
||||
"\($0.name): \($0.detail)"
|
||||
}
|
||||
.joined(separator: "\\n") ?? ""
|
||||
|
||||
copyDetails(
|
||||
result?.error
|
||||
?? (failures.isEmpty
|
||||
? "No errors reported."
|
||||
: failures)
|
||||
)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Refresh Server") {
|
||||
Task {
|
||||
await monitor.refresh(id: server.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,6 +198,20 @@ struct MonitorMenuView: View {
|
||||
Text(service.title).font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 14).padding(.vertical, 5)
|
||||
.contentShape(Rectangle())
|
||||
.contextMenu {
|
||||
Button("Show Details") {
|
||||
showDetails(
|
||||
title: service.name,
|
||||
status: service.title,
|
||||
details: service.detail
|
||||
)
|
||||
}
|
||||
|
||||
Button("Copy Details") {
|
||||
copyDetails(service.detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
Text("HEALTH GROUPS")
|
||||
@@ -124,7 +224,57 @@ struct MonitorMenuView: View {
|
||||
Spacer()
|
||||
Label(group.status.title, systemImage: group.status.symbol)
|
||||
.foregroundStyle(group.status.color).font(.caption)
|
||||
}.padding(.horizontal, 14).padding(.vertical, 8)
|
||||
}
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 8)
|
||||
.contentShape(Rectangle())
|
||||
.contextMenu {
|
||||
Button("Show Details") {
|
||||
let related = report.checks.filter {
|
||||
group.missing.contains($0.name)
|
||||
|| (
|
||||
$0.status != .ok
|
||||
&& $0.section == group.name
|
||||
)
|
||||
}
|
||||
|
||||
let messages = related.map {
|
||||
"\($0.name): \($0.detail)"
|
||||
}.joined(separator: "\\n\\n")
|
||||
|
||||
let missing = group.missing.isEmpty
|
||||
? ""
|
||||
: "Missing: "
|
||||
+ group.missing.joined(separator: ", ")
|
||||
|
||||
let details = [
|
||||
"Group: \(group.name)",
|
||||
"Status: \(group.status.title)",
|
||||
missing,
|
||||
messages.isEmpty
|
||||
? (group.status == .ok
|
||||
? "All required checks passed."
|
||||
: "No matching individual check details were returned.")
|
||||
: messages
|
||||
]
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: "\\n\\n")
|
||||
|
||||
showDetails(
|
||||
title: group.name,
|
||||
status: group.status.title,
|
||||
details: details
|
||||
)
|
||||
}
|
||||
|
||||
Button("Copy Details") {
|
||||
copyDetails(
|
||||
"\(group.name): \(group.status.title)\\n"
|
||||
+ group.missing.joined(separator: "\\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider().padding(.leading, 14)
|
||||
}
|
||||
} else {
|
||||
@@ -150,12 +300,18 @@ struct MonitorMenuView: View {
|
||||
Button("Settings") { openWindow(id: "monitor-settings") }
|
||||
Button("Quit") { NSApp.terminate(nil) }
|
||||
}.buttonStyle(.borderless).padding(14)
|
||||
}.frame(width: 410)
|
||||
}
|
||||
.frame(width: 410)
|
||||
.sheet(item: $detailItem) { item in
|
||||
MonitorDetailSheet(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DiagnosticsView: View {
|
||||
@ObservedObject var monitor: ServerMonitor
|
||||
@State private var detailItem: MonitorDetailItem?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("\(monitor.selected?.name ?? "Server") — Diagnostics").font(.title2.bold())
|
||||
@@ -171,9 +327,133 @@ struct DiagnosticsView: View {
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.contextMenu {
|
||||
Button("Show Details") {
|
||||
detailItem = MonitorDetailItem(
|
||||
title: check.name,
|
||||
status: check.status.title,
|
||||
details: """
|
||||
Section: \(check.section)
|
||||
|
||||
\(check.detail)
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
Button("Copy Details") {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(
|
||||
check.detail,
|
||||
forType: .string
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.textSelection(.enabled)
|
||||
}.padding(20).frame(minWidth: 650, minHeight: 450)
|
||||
}
|
||||
.padding(20)
|
||||
.frame(minWidth: 650, minHeight: 450)
|
||||
.sheet(item: $detailItem) { item in
|
||||
MonitorDetailSheet(item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Monitoring Details
|
||||
|
||||
struct MonitorDetailItem: Identifiable {
|
||||
let id = UUID()
|
||||
let title: String
|
||||
let status: String
|
||||
let details: String
|
||||
}
|
||||
|
||||
struct MonitorDetailSheet: View {
|
||||
let item: MonitorDetailItem
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
HStack {
|
||||
Image(systemName: icon)
|
||||
.font(.title2)
|
||||
.foregroundStyle(statusColor)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(item.title)
|
||||
.font(.title2.bold())
|
||||
|
||||
Text(item.status)
|
||||
.foregroundStyle(statusColor)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
ScrollView {
|
||||
Text(item.details.isEmpty
|
||||
? "No additional diagnostic information available."
|
||||
: item.details)
|
||||
.font(.system(
|
||||
size: 12,
|
||||
design: .monospaced
|
||||
))
|
||||
.frame(
|
||||
maxWidth: .infinity,
|
||||
alignment: .leading
|
||||
)
|
||||
.textSelection(.enabled)
|
||||
.padding(12)
|
||||
}
|
||||
.background(.quaternary.opacity(0.3))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
HStack {
|
||||
Button("Copy Details") {
|
||||
NSPasteboard.general.clearContents()
|
||||
NSPasteboard.general.setString(
|
||||
item.details,
|
||||
forType: .string
|
||||
)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Close") {
|
||||
dismiss()
|
||||
}
|
||||
.keyboardShortcut(.defaultAction)
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.frame(minWidth: 540, minHeight: 350)
|
||||
}
|
||||
|
||||
private var icon: String {
|
||||
switch item.status.lowercased() {
|
||||
case "critical", "failed":
|
||||
return "xmark.circle.fill"
|
||||
case "warning", "stopped":
|
||||
return "exclamationmark.triangle.fill"
|
||||
default:
|
||||
return "checkmark.circle.fill"
|
||||
}
|
||||
}
|
||||
|
||||
private var statusColor: Color {
|
||||
switch item.status.lowercased() {
|
||||
case "critical", "failed":
|
||||
return .red
|
||||
case "warning", "stopped":
|
||||
return .orange
|
||||
default:
|
||||
return .green
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,25 @@ def output():
|
||||
match = re.match(r'^\[(OK\s*|WARN|FAIL)\]\s+(.+?)(?:\s+—\s+(.*))?$', line)
|
||||
if match:
|
||||
checks.append({'name': match[2].strip(), 'status': match[1].strip().lower(), 'detail': (match[3] or '').strip(), 'section': section})
|
||||
# Noncritical firmware metadata failures are warnings.
|
||||
# Other failed systemd units remain critical.
|
||||
for item in checks:
|
||||
if item['name'] != 'Failed systemd units':
|
||||
continue
|
||||
if item['status'] != 'fail':
|
||||
continue
|
||||
|
||||
failed_units = re.findall(
|
||||
r'([A-Za-z0-9@_.:-]+\\.service)\\s+loaded\\s+failed\\s+failed',
|
||||
item['detail']
|
||||
)
|
||||
|
||||
if failed_units and all(
|
||||
unit == 'fwupd-refresh.service'
|
||||
for unit in failed_units
|
||||
):
|
||||
item['status'] = 'warn'
|
||||
|
||||
lookup = {c['name']: c['status'] for c in checks}
|
||||
groups = []
|
||||
for title, names in GROUPS:
|
||||
@@ -62,7 +81,12 @@ def output():
|
||||
groups.append({'name':label, 'status':state, 'missing':[n for n in relevant if n not in lookup]})
|
||||
counts_match = re.search(r'SUMMARY:\s*(\d+) OK\s*\|\s*(\d+) WARN\s*\|\s*(\d+) FAIL', plain)
|
||||
counts = dict(zip(('ok','warn','fail'), (int(x) for x in counts_match.groups()))) if counts_match else {'ok':0,'warn':0,'fail':1}
|
||||
if not checks:
|
||||
if checks:
|
||||
counts = {
|
||||
state: sum(c['status'] == state for c in checks)
|
||||
for state in ('ok', 'warn', 'fail')
|
||||
}
|
||||
else:
|
||||
counts['fail'] = max(1, counts['fail'])
|
||||
overall = 'fail' if counts['fail'] or any(g['status']=='fail' for g in groups) else 'warn' if counts['warn'] or any(g['status']=='warn' for g in groups) else 'ok'
|
||||
print(json.dumps({'schema':1, 'host':'pegasus', 'uptimeSeconds':system_uptime_seconds(), 'generatedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(), 'overall':overall, 'counts':counts,'groups':groups,'checks':checks, 'diagnostics':plain[-24000:], 'commandExitCode':p.returncode}, ensure_ascii=False))
|
||||
|
||||
@@ -62,7 +62,26 @@ for directory in ('/', '/backup'):
|
||||
systemctl = run(['systemctl', '--failed', '--no-legend', '--plain'])
|
||||
if systemctl is not None:
|
||||
failed = [line for line in systemctl.stdout.splitlines() if line.strip()]
|
||||
check('Services', 'Failed systemd units', 'fail' if failed else 'ok',
|
||||
# Firmware metadata refresh is noncritical.
|
||||
# Any other failed systemd unit remains critical.
|
||||
optional_units = {'fwupd-refresh.service'}
|
||||
|
||||
failed_names = [
|
||||
line.split()[0].lstrip('●')
|
||||
for line in failed
|
||||
if line.split()
|
||||
]
|
||||
|
||||
if not failed:
|
||||
severity = 'ok'
|
||||
elif failed_names and all(
|
||||
unit in optional_units for unit in failed_names
|
||||
):
|
||||
severity = 'warn'
|
||||
else:
|
||||
severity = 'fail'
|
||||
|
||||
check('Services', 'Failed systemd units', severity,
|
||||
', '.join(failed[:5]) if failed else 'None')
|
||||
|
||||
if shutil.which('docker'):
|
||||
|
||||
Executable
+311
@@ -0,0 +1,311 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import commands
|
||||
|
||||
def command(args):
|
||||
try:
|
||||
quoted = []
|
||||
for arg in args:
|
||||
value = str(arg)
|
||||
quoted.append("'" + value.replace("'", "'\\''") + "'")
|
||||
|
||||
cmd = " ".join(quoted)
|
||||
return commands.getoutput(cmd).strip()
|
||||
except:
|
||||
return ""
|
||||
|
||||
def escape(value):
|
||||
value = str(value)
|
||||
return value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r")
|
||||
|
||||
def string(value):
|
||||
return '"' + escape(value) + '"'
|
||||
|
||||
def status(name, state, detail):
|
||||
return '{"name":%s,"state":%s,"detail":%s}' % (
|
||||
string(name), string(state), string(detail)
|
||||
)
|
||||
|
||||
def check(section, name, state, detail):
|
||||
return '{"section":%s,"name":%s,"status":%s,"detail":%s}' % (
|
||||
string(section), string(name), string(state), string(detail)
|
||||
)
|
||||
|
||||
def group(name, state):
|
||||
return '{"name":%s,"status":%s,"missing":[]}' % (
|
||||
string(name), string(state)
|
||||
)
|
||||
|
||||
def read_file(path):
|
||||
try:
|
||||
f = open(path)
|
||||
data = f.read()
|
||||
f.close()
|
||||
return data
|
||||
except:
|
||||
return ""
|
||||
|
||||
def process_running(name):
|
||||
output = command(["ps", "ax"])
|
||||
return name in output
|
||||
|
||||
hostname = socket.gethostname()
|
||||
|
||||
# CPU
|
||||
try:
|
||||
load = float(read_file("/proc/loadavg").split()[0])
|
||||
except:
|
||||
load = 0.0
|
||||
|
||||
try:
|
||||
cpu_count = len([
|
||||
x for x in read_file("/proc/cpuinfo").splitlines()
|
||||
if x.startswith("processor")
|
||||
])
|
||||
if cpu_count < 1:
|
||||
cpu_count = 1
|
||||
except:
|
||||
cpu_count = 1
|
||||
|
||||
cpu_state = "ok"
|
||||
if load > cpu_count * 2:
|
||||
cpu_state = "fail"
|
||||
elif load > cpu_count:
|
||||
cpu_state = "warn"
|
||||
|
||||
# Memory
|
||||
mem = {}
|
||||
for line in read_file("/proc/meminfo").splitlines():
|
||||
parts = line.replace(":", "").split()
|
||||
if len(parts) >= 2:
|
||||
try:
|
||||
mem[parts[0]] = int(parts[1])
|
||||
except:
|
||||
pass
|
||||
|
||||
total = mem.get("MemTotal", 1)
|
||||
free = (
|
||||
mem.get("MemFree", 0) +
|
||||
mem.get("Buffers", 0) +
|
||||
mem.get("Cached", 0)
|
||||
)
|
||||
|
||||
memory_percent = int(100 * (total - free) / total)
|
||||
|
||||
memory_state = "ok"
|
||||
if memory_percent >= 95:
|
||||
memory_state = "fail"
|
||||
elif memory_percent >= 85:
|
||||
memory_state = "warn"
|
||||
|
||||
# Disk
|
||||
disk_percent = 0
|
||||
disk_state = "ok"
|
||||
|
||||
try:
|
||||
st = os.statvfs("/")
|
||||
disk_percent = int(
|
||||
100 * (st.f_blocks - st.f_bavail) / st.f_blocks
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
if disk_percent >= 95:
|
||||
disk_state = "fail"
|
||||
elif disk_percent >= 85:
|
||||
disk_state = "warn"
|
||||
|
||||
|
||||
# Notes data partition
|
||||
notes_disk_state = "ok"
|
||||
notes_disk_percent = 0
|
||||
notes_disk_detail = "Not checked"
|
||||
|
||||
try:
|
||||
mounts = open("/proc/mounts").read()
|
||||
mounted = 0
|
||||
|
||||
for line in mounts.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[1] == "/local/notesdata":
|
||||
mounted = 1
|
||||
break
|
||||
|
||||
if not mounted:
|
||||
notes_disk_state = "fail"
|
||||
notes_disk_detail = "/local/notesdata is not mounted"
|
||||
else:
|
||||
st = os.statvfs("/local/notesdata")
|
||||
|
||||
if st.f_blocks > 0:
|
||||
notes_disk_percent = int(
|
||||
100 * (st.f_blocks - st.f_bavail) / st.f_blocks
|
||||
)
|
||||
|
||||
notes_disk_detail = "%d%% used" % notes_disk_percent
|
||||
|
||||
if notes_disk_percent >= 95:
|
||||
notes_disk_state = "fail"
|
||||
elif notes_disk_percent >= 85:
|
||||
notes_disk_state = "warn"
|
||||
else:
|
||||
notes_disk_state = "fail"
|
||||
notes_disk_detail = "Filesystem size unavailable"
|
||||
|
||||
except:
|
||||
notes_disk_state = "fail"
|
||||
notes_disk_detail = "Cannot read Notes data filesystem"
|
||||
|
||||
# Uptime
|
||||
try:
|
||||
uptime = int(float(read_file("/proc/uptime").split()[0]))
|
||||
except:
|
||||
uptime = 0
|
||||
|
||||
checks = [
|
||||
check(
|
||||
"System",
|
||||
"CPU load",
|
||||
cpu_state,
|
||||
"1m load %.2f; %d CPUs" % (load, cpu_count)
|
||||
),
|
||||
check(
|
||||
"System",
|
||||
"Memory",
|
||||
memory_state,
|
||||
"%d%% used" % memory_percent
|
||||
),
|
||||
check(
|
||||
"Storage",
|
||||
"Disk /",
|
||||
disk_state,
|
||||
"%d%% used" % disk_percent
|
||||
),
|
||||
check(
|
||||
"Storage",
|
||||
"Notes data disk",
|
||||
notes_disk_state,
|
||||
notes_disk_detail
|
||||
)
|
||||
]
|
||||
|
||||
# Legacy SysV services
|
||||
services = []
|
||||
|
||||
service_names = [
|
||||
("ssh", "sshd"),
|
||||
("apache2", "apache2"),
|
||||
("nginx", "nginx"),
|
||||
("mysql", "mysqld"),
|
||||
("postgresql", "postgres"),
|
||||
("redis-server", "redis-server"),
|
||||
("domino", "server")
|
||||
]
|
||||
|
||||
for service_name, process_name in service_names:
|
||||
init_script = "/etc/init.d/" + service_name
|
||||
|
||||
if not os.path.exists(init_script):
|
||||
services.append(
|
||||
status(service_name, "not_installed", "SysV service not found")
|
||||
)
|
||||
continue
|
||||
|
||||
running = process_running(process_name)
|
||||
|
||||
if running:
|
||||
service_state = "running"
|
||||
else:
|
||||
service_state = "stopped"
|
||||
|
||||
services.append(
|
||||
status(
|
||||
service_name,
|
||||
service_state,
|
||||
"SysV process check"
|
||||
)
|
||||
)
|
||||
|
||||
# Domino is a required service on Europa.
|
||||
domino_state = "ok"
|
||||
domino_detail = "Domino process running"
|
||||
|
||||
if not process_running("server"):
|
||||
domino_state = "fail"
|
||||
domino_detail = "Domino server process not running"
|
||||
|
||||
checks.append(
|
||||
check(
|
||||
"Services",
|
||||
"Domino",
|
||||
domino_state,
|
||||
domino_detail
|
||||
)
|
||||
)
|
||||
|
||||
states = [
|
||||
cpu_state,
|
||||
memory_state,
|
||||
disk_state,
|
||||
notes_disk_state,
|
||||
domino_state
|
||||
]
|
||||
|
||||
overall = "ok"
|
||||
if "fail" in states:
|
||||
overall = "fail"
|
||||
elif "warn" in states:
|
||||
overall = "warn"
|
||||
|
||||
ok_count = states.count("ok")
|
||||
warn_count = states.count("warn")
|
||||
fail_count = states.count("fail")
|
||||
|
||||
if cpu_state == "fail" or memory_state == "fail":
|
||||
system_state = "fail"
|
||||
elif cpu_state == "warn" or memory_state == "warn":
|
||||
system_state = "warn"
|
||||
else:
|
||||
system_state = "ok"
|
||||
|
||||
if disk_state == "fail" or notes_disk_state == "fail":
|
||||
storage_state = "fail"
|
||||
elif disk_state == "warn" or notes_disk_state == "warn":
|
||||
storage_state = "warn"
|
||||
else:
|
||||
storage_state = "ok"
|
||||
|
||||
groups = [
|
||||
group("System", system_state),
|
||||
group("Storage", storage_state),
|
||||
group("Services", domino_state)
|
||||
]
|
||||
|
||||
output = """{
|
||||
"schema":1,
|
||||
"host":%s,
|
||||
"generatedAt":%s,
|
||||
"overall":%s,
|
||||
"counts":{"ok":%d,"warn":%d,"fail":%d},
|
||||
"groups":[%s],
|
||||
"checks":[%s],
|
||||
"diagnostics":null,
|
||||
"services":[%s],
|
||||
"uptimeSeconds":%d
|
||||
}""" % (
|
||||
string(hostname),
|
||||
string(time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())),
|
||||
string(overall),
|
||||
ok_count,
|
||||
warn_count,
|
||||
fail_count,
|
||||
",".join(groups),
|
||||
",".join(checks),
|
||||
",".join(services),
|
||||
uptime
|
||||
)
|
||||
|
||||
print output
|
||||
Reference in New Issue
Block a user