Merge pull request 'feat: logs tab overhaul — severity tags, filtering, recovery durations' (#35) from feat/logs-overhaul into main
Reviewed-on: lerko/uptop#35
This commit was merged in pull request #35.
This commit is contained in:
@@ -96,6 +96,19 @@ func sanitizeLog(s string) string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fmtDurationShort(d time.Duration) string {
|
||||||
|
if d < time.Minute {
|
||||||
|
return fmt.Sprintf("%ds", int(d.Seconds()))
|
||||||
|
}
|
||||||
|
if d < time.Hour {
|
||||||
|
return fmt.Sprintf("%dm", int(d.Minutes()))
|
||||||
|
}
|
||||||
|
if d < 24*time.Hour {
|
||||||
|
return fmt.Sprintf("%dh %dm", int(d.Hours()), int(d.Minutes())%60)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%dd %dh", int(d.Hours())/24, int(d.Hours())%24)
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Engine) AddLog(msg string) {
|
func (e *Engine) AddLog(msg string) {
|
||||||
e.logMu.Lock()
|
e.logMu.Lock()
|
||||||
defer e.logMu.Unlock()
|
defer e.logMu.Unlock()
|
||||||
@@ -206,8 +219,12 @@ func (e *Engine) RecordHeartbeat(token string) bool {
|
|||||||
case "LATE":
|
case "LATE":
|
||||||
e.AddLog(fmt.Sprintf("Push Monitor '%s' heartbeat arrived (was late)", site.Name))
|
e.AddLog(fmt.Sprintf("Push Monitor '%s' heartbeat arrived (was late)", site.Name))
|
||||||
case "DOWN":
|
case "DOWN":
|
||||||
e.AddLog(fmt.Sprintf("Push Monitor '%s' recovered", site.Name))
|
downDur := ""
|
||||||
go e.triggerAlert(site.AlertID, "✅ RECOVERY", fmt.Sprintf("Push Monitor '%s' is receiving heartbeats.", site.Name))
|
if !site.StatusChangedAt.IsZero() {
|
||||||
|
downDur = fmt.Sprintf(" (was down %s)", fmtDurationShort(time.Since(site.StatusChangedAt)))
|
||||||
|
}
|
||||||
|
e.AddLog(fmt.Sprintf("Push Monitor '%s' recovered%s", site.Name, downDur))
|
||||||
|
go e.triggerAlert(site.AlertID, "✅ RECOVERY", fmt.Sprintf("Push Monitor '%s' is receiving heartbeats.%s", site.Name, downDur))
|
||||||
}
|
}
|
||||||
|
|
||||||
if prevStatus != "UP" && prevStatus != "PENDING" {
|
if prevStatus != "UP" && prevStatus != "PENDING" {
|
||||||
@@ -514,6 +531,11 @@ func (e *Engine) handleStatusChange(site models.Site, rawStatus string, code int
|
|||||||
}
|
}
|
||||||
|
|
||||||
isBroken := func(s string) bool { return s == "DOWN" || s == "SSL EXP" }
|
isBroken := func(s string) bool { return s == "DOWN" || s == "SSL EXP" }
|
||||||
|
|
||||||
|
if site.Status == "UP" && newState.Status == "LATE" {
|
||||||
|
e.AddLog(fmt.Sprintf("Monitor '%s' heartbeat overdue", site.Name))
|
||||||
|
}
|
||||||
|
|
||||||
if !isBroken(site.Status) && isBroken(newState.Status) && newState.Status != "PENDING" {
|
if !isBroken(site.Status) && isBroken(newState.Status) && newState.Status != "PENDING" {
|
||||||
if inMaint {
|
if inMaint {
|
||||||
e.AddLog(fmt.Sprintf("Monitor '%s' is DOWN (alerts suppressed — maintenance)", site.Name))
|
e.AddLog(fmt.Sprintf("Monitor '%s' is DOWN (alerts suppressed — maintenance)", site.Name))
|
||||||
@@ -529,11 +551,17 @@ func (e *Engine) handleStatusChange(site models.Site, rawStatus string, code int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if isBroken(site.Status) && newState.Status == "UP" {
|
if isBroken(site.Status) && newState.Status == "UP" {
|
||||||
if !inMaint {
|
downDur := ""
|
||||||
e.triggerAlert(site.AlertID, "✅ RECOVERY", fmt.Sprintf("Monitor '%s' is UP", site.Name))
|
if !site.StatusChangedAt.IsZero() {
|
||||||
} else {
|
downDur = fmt.Sprintf(" (was down %s)", fmtDurationShort(time.Since(site.StatusChangedAt)))
|
||||||
e.AddLog(fmt.Sprintf("Monitor '%s' recovered (maintenance active, alert suppressed)", site.Name))
|
|
||||||
}
|
}
|
||||||
|
e.AddLog(fmt.Sprintf("Monitor '%s' recovered%s", site.Name, downDur))
|
||||||
|
if !inMaint {
|
||||||
|
e.triggerAlert(site.AlertID, "✅ RECOVERY", fmt.Sprintf("Monitor '%s' is UP%s", site.Name, downDur))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if site.Status == "LATE" && newState.Status == "UP" && !isBroken(site.Status) {
|
||||||
|
e.AddLog(fmt.Sprintf("Monitor '%s' heartbeat arrived (was late)", site.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+89
-21
@@ -5,27 +5,83 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func colorizeLog(line string) string {
|
type logSeverity int
|
||||||
|
|
||||||
|
const (
|
||||||
|
severityInfo logSeverity = iota
|
||||||
|
severityWarn
|
||||||
|
severityDown
|
||||||
|
severityUp
|
||||||
|
severitySystem
|
||||||
|
)
|
||||||
|
|
||||||
|
func classifyLog(line string) logSeverity {
|
||||||
lower := strings.ToLower(line)
|
lower := strings.ToLower(line)
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(lower, "confirmed down"),
|
case strings.Contains(lower, "confirmed down"),
|
||||||
strings.Contains(lower, "is down"),
|
strings.Contains(lower, "is down"),
|
||||||
strings.Contains(lower, "missed heartbeat"),
|
strings.Contains(lower, "missed heartbeat"),
|
||||||
strings.Contains(lower, "failed check"),
|
strings.Contains(lower, "alert send failed"):
|
||||||
strings.Contains(lower, "ssl warning"):
|
return severityDown
|
||||||
return dangerStyle.Render(line)
|
|
||||||
case strings.Contains(lower, "recovered"),
|
case strings.Contains(lower, "recovered"),
|
||||||
strings.Contains(lower, "is up"),
|
strings.Contains(lower, "is up"),
|
||||||
strings.Contains(lower, "recovery"):
|
strings.Contains(lower, "recovery"),
|
||||||
return specialStyle.Render(line)
|
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"),
|
case strings.Contains(lower, "engine"),
|
||||||
strings.Contains(lower, "cluster"):
|
strings.Contains(lower, "cluster"),
|
||||||
return titleStyle.Render(line)
|
strings.Contains(lower, "loaded"),
|
||||||
|
strings.Contains(lower, "paused"),
|
||||||
|
strings.Contains(lower, "resumed"):
|
||||||
|
return severitySystem
|
||||||
default:
|
default:
|
||||||
return line
|
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 {
|
func (m Model) viewLogsTab() string {
|
||||||
content := m.logViewport.View()
|
content := m.logViewport.View()
|
||||||
if strings.TrimSpace(content) == "" || content == "Waiting for logs..." {
|
if strings.TrimSpace(content) == "" || content == "Waiting for logs..." {
|
||||||
@@ -33,22 +89,34 @@ func (m Model) viewLogsTab() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lines := strings.Split(content, "\n")
|
lines := strings.Split(content, "\n")
|
||||||
var colored []string
|
var rendered []string
|
||||||
|
total := 0
|
||||||
|
shown := 0
|
||||||
|
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if line == "" {
|
if strings.TrimSpace(line) == "" {
|
||||||
colored = append(colored, line)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
colored = append(colored, colorizeLog(line))
|
total++
|
||||||
}
|
sev := classifyLog(line)
|
||||||
|
if m.logFilterImportant && !isImportantLog(sev) {
|
||||||
count := 0
|
continue
|
||||||
for _, l := range lines {
|
|
||||||
if strings.TrimSpace(l) != "" {
|
|
||||||
count++
|
|
||||||
}
|
}
|
||||||
|
shown++
|
||||||
|
rendered = append(rendered, renderLogLine(line))
|
||||||
}
|
}
|
||||||
|
|
||||||
header := subtleStyle.Render(fmt.Sprintf(" %d entries [↑/↓] Scroll [PgUp/PgDn] Page", count))
|
filterLabel := "All"
|
||||||
return "\n" + header + "\n\n" + strings.Join(colored, "\n")
|
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")
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-3
@@ -92,9 +92,10 @@ type Model struct {
|
|||||||
userFormData *userFormData
|
userFormData *userFormData
|
||||||
maintFormData *maintFormData
|
maintFormData *maintFormData
|
||||||
|
|
||||||
logViewport viewport.Model
|
logViewport viewport.Model
|
||||||
isAdmin bool
|
logFilterImportant bool
|
||||||
zones *zone.Manager
|
isAdmin bool
|
||||||
|
zones *zone.Manager
|
||||||
|
|
||||||
deleteID int
|
deleteID int
|
||||||
deleteName string
|
deleteName string
|
||||||
@@ -392,6 +393,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.filterMode = true
|
m.filterMode = true
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
case "f":
|
||||||
|
if m.state == stateLogs {
|
||||||
|
m.logFilterImportant = !m.logFilterImportant
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
case "tab":
|
case "tab":
|
||||||
m.switchTab(m.currentTab + 1)
|
m.switchTab(m.currentTab + 1)
|
||||||
case "pgup", "pgdown":
|
case "pgup", "pgdown":
|
||||||
@@ -937,6 +943,8 @@ func (m Model) viewDashboard() string {
|
|||||||
switch m.currentTab {
|
switch m.currentTab {
|
||||||
case 0:
|
case 0:
|
||||||
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [T]Theme [Tab]Switch [q]Quit"
|
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [T]Theme [Tab]Switch [q]Quit"
|
||||||
|
case 2:
|
||||||
|
keys = "[f]Filter [T]Theme [Tab]Switch [q]Quit"
|
||||||
case 4:
|
case 4:
|
||||||
keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit"
|
keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit"
|
||||||
case 5:
|
case 5:
|
||||||
|
|||||||
Reference in New Issue
Block a user