169 lines
3.5 KiB
Go
169 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type requestCoordinator struct {
|
|
mu sync.Mutex
|
|
id uint64
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
func (r *requestCoordinator) start(timeout time.Duration) (context.Context, context.CancelFunc, uint64) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.cancel != nil {
|
|
r.cancel()
|
|
}
|
|
r.id++
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
r.cancel = cancel
|
|
return ctx, cancel, r.id
|
|
}
|
|
|
|
func (r *requestCoordinator) current(id uint64) bool {
|
|
if id == 0 {
|
|
return true
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
return r.id == id
|
|
}
|
|
|
|
func (r *requestCoordinator) supersede() uint64 {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if r.cancel != nil {
|
|
r.cancel()
|
|
r.cancel = nil
|
|
}
|
|
r.id++
|
|
return r.id
|
|
}
|
|
|
|
type HealthLevel string
|
|
|
|
const (
|
|
healthOK HealthLevel = "ok"
|
|
healthInfo HealthLevel = "info"
|
|
healthWarning HealthLevel = "warning"
|
|
healthError HealthLevel = "error"
|
|
healthUnknown HealthLevel = "unknown"
|
|
)
|
|
|
|
type HealthComponent struct {
|
|
Name string
|
|
Level HealthLevel
|
|
Summary string
|
|
Detail string
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type HealthEvent struct {
|
|
Component string
|
|
Level HealthLevel
|
|
Message string
|
|
At time.Time
|
|
}
|
|
|
|
type RateLimitSnapshot struct {
|
|
Limit int
|
|
Remaining int
|
|
Used int
|
|
ResetAt time.Time
|
|
RetryAfter time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type healthProvider interface {
|
|
HealthReport() []HealthComponent
|
|
RateLimit() RateLimitSnapshot
|
|
}
|
|
|
|
type healthTracker struct {
|
|
mu sync.Mutex
|
|
components map[string]HealthComponent
|
|
rate RateLimitSnapshot
|
|
}
|
|
|
|
func (h *healthTracker) set(component HealthComponent) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
if h.components == nil {
|
|
h.components = make(map[string]HealthComponent)
|
|
}
|
|
if component.UpdatedAt.IsZero() {
|
|
component.UpdatedAt = time.Now()
|
|
}
|
|
h.components[component.Name] = component
|
|
}
|
|
|
|
func (h *healthTracker) setRate(rate RateLimitSnapshot) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.rate = rate
|
|
}
|
|
|
|
func (h *healthTracker) report() []HealthComponent {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
components := make([]HealthComponent, 0, len(h.components))
|
|
for _, component := range h.components {
|
|
components = append(components, component)
|
|
}
|
|
sort.Slice(components, func(i, j int) bool {
|
|
return strings.ToLower(components[i].Name) < strings.ToLower(components[j].Name)
|
|
})
|
|
return components
|
|
}
|
|
|
|
func (h *healthTracker) rateLimit() RateLimitSnapshot {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
return h.rate
|
|
}
|
|
|
|
func (m *App) recordHealth(component string, level HealthLevel, message string) {
|
|
if strings.TrimSpace(message) == "" {
|
|
return
|
|
}
|
|
event := HealthEvent{Component: component, Level: level, Message: message, At: time.Now()}
|
|
const maximumHealthEvents = 100
|
|
m.healthEvents = append(m.healthEvents, event)
|
|
if len(m.healthEvents) > maximumHealthEvents {
|
|
m.healthEvents = append([]HealthEvent(nil), m.healthEvents[len(m.healthEvents)-maximumHealthEvents:]...)
|
|
}
|
|
}
|
|
|
|
func healthLevelLabel(level HealthLevel) string {
|
|
switch level {
|
|
case healthOK:
|
|
return "OK"
|
|
case healthInfo:
|
|
return "INFO"
|
|
case healthWarning:
|
|
return "WARN"
|
|
case healthError:
|
|
return "ERROR"
|
|
default:
|
|
return "UNKNOWN"
|
|
}
|
|
}
|
|
|
|
func healthComponentText(component HealthComponent) string {
|
|
text := component.Summary
|
|
if component.Detail != "" {
|
|
text += " — " + component.Detail
|
|
}
|
|
if !component.UpdatedAt.IsZero() {
|
|
text += " (" + component.UpdatedAt.Local().Format("15:04:05") + ")"
|
|
}
|
|
return fmt.Sprintf("%-7s %-20s %s", healthLevelLabel(component.Level), component.Name, text)
|
|
}
|