b14d5e19db
Logs tab visual overhaul:
- Severity-classified entries: DOWN (red), UP (green), WARN (amber),
SYS (cyan), info (gray) — rendered as inline tags, not whole-line color
- Column-aligned format: [timestamp] [severity tag] [message]
- Filter toggle (f key): All vs Important only (hides retry noise)
- Header shows entry count, filter state, hidden count
Engine log improvements:
- Recovery messages include downtime duration ("was down 14m")
- LATE transition logged ("heartbeat overdue")
- Push monitor recovery includes downtime duration
123 lines
2.9 KiB
Go
123 lines
2.9 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
type logSeverity int
|
|
|
|
const (
|
|
severityInfo logSeverity = iota
|
|
severityWarn
|
|
severityDown
|
|
severityUp
|
|
severitySystem
|
|
)
|
|
|
|
func classifyLog(line string) logSeverity {
|
|
lower := strings.ToLower(line)
|
|
switch {
|
|
case strings.Contains(lower, "confirmed down"),
|
|
strings.Contains(lower, "is down"),
|
|
strings.Contains(lower, "missed heartbeat"),
|
|
strings.Contains(lower, "alert send failed"):
|
|
return severityDown
|
|
case strings.Contains(lower, "recovered"),
|
|
strings.Contains(lower, "is up"),
|
|
strings.Contains(lower, "recovery"),
|
|
strings.Contains(lower, "first heartbeat"):
|
|
return severityUp
|
|
case strings.Contains(lower, "failed check"),
|
|
strings.Contains(lower, "ssl warning"),
|
|
strings.Contains(lower, "overdue"),
|
|
strings.Contains(lower, "was late"):
|
|
return severityWarn
|
|
case strings.Contains(lower, "engine"),
|
|
strings.Contains(lower, "cluster"),
|
|
strings.Contains(lower, "loaded"),
|
|
strings.Contains(lower, "paused"),
|
|
strings.Contains(lower, "resumed"):
|
|
return severitySystem
|
|
default:
|
|
return severityInfo
|
|
}
|
|
}
|
|
|
|
func isImportantLog(sev logSeverity) bool {
|
|
return sev == severityDown || sev == severityUp || sev == severitySystem
|
|
}
|
|
|
|
func renderLogTag(sev logSeverity) string {
|
|
switch sev {
|
|
case severityDown:
|
|
return dangerStyle.Render(" DOWN ")
|
|
case severityUp:
|
|
return specialStyle.Render(" UP ")
|
|
case severityWarn:
|
|
return warnStyle.Render(" WARN ")
|
|
case severitySystem:
|
|
return titleStyle.Render(" SYS ")
|
|
default:
|
|
return subtleStyle.Render(" info ")
|
|
}
|
|
}
|
|
|
|
func renderLogLine(line string) string {
|
|
sev := classifyLog(line)
|
|
tag := renderLogTag(sev)
|
|
|
|
ts := ""
|
|
msg := line
|
|
if len(line) > 10 && line[0] == '[' {
|
|
if idx := strings.Index(line, "]"); idx > 0 && idx < 12 {
|
|
ts = subtleStyle.Render(line[1:idx])
|
|
msg = strings.TrimSpace(line[idx+1:])
|
|
}
|
|
}
|
|
|
|
if ts != "" {
|
|
return fmt.Sprintf(" %s %s %s", ts, tag, msg)
|
|
}
|
|
return fmt.Sprintf(" %s %s", tag, msg)
|
|
}
|
|
|
|
func (m Model) viewLogsTab() string {
|
|
content := m.logViewport.View()
|
|
if strings.TrimSpace(content) == "" || content == "Waiting for logs..." {
|
|
return "\n No log entries yet. Logs appear as monitors run checks."
|
|
}
|
|
|
|
lines := strings.Split(content, "\n")
|
|
var rendered []string
|
|
total := 0
|
|
shown := 0
|
|
|
|
for _, line := range lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
total++
|
|
sev := classifyLog(line)
|
|
if m.logFilterImportant && !isImportantLog(sev) {
|
|
continue
|
|
}
|
|
shown++
|
|
rendered = append(rendered, renderLogLine(line))
|
|
}
|
|
|
|
filterLabel := "All"
|
|
if m.logFilterImportant {
|
|
filterLabel = "Important"
|
|
}
|
|
|
|
header := subtleStyle.Render(fmt.Sprintf(
|
|
" %d entries [↑/↓] Scroll [PgUp/PgDn] Page [f] Filter: %s", shown, filterLabel))
|
|
|
|
if m.logFilterImportant && shown < total {
|
|
header += subtleStyle.Render(fmt.Sprintf(" (%d hidden)", total-shown))
|
|
}
|
|
|
|
return "\n" + header + "\n\n" + strings.Join(rendered, "\n")
|
|
}
|