refactor: extract magic numbers to named constants
Replace inline numeric literals with named constants across 14 files: server timeouts/rate limits, cluster thresholds/intervals, DB pool sizes, alert/dial timeouts, TUI uptime thresholds, node status thresholds, and state history limits.
This commit was merged in pull request #153.
This commit is contained in:
@@ -18,7 +18,9 @@ import (
|
|||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var alertClient = &http.Client{Timeout: 10 * time.Second}
|
const alertHTTPTimeout = 10 * time.Second
|
||||||
|
|
||||||
|
var alertClient = &http.Client{Timeout: alertHTTPTimeout}
|
||||||
|
|
||||||
// sanitizeError strips the request URL from transport errors before they are
|
// sanitizeError strips the request URL from transport errors before they are
|
||||||
// stored or displayed. *url.Error embeds the full URL, which for several
|
// stored or displayed. *url.Error embeds the full URL, which for several
|
||||||
|
|||||||
@@ -38,14 +38,20 @@ func Start(ctx context.Context, cfg Config, eng *monitor.Engine) {
|
|||||||
// "probe" mode is handled directly in main.go before cluster.Start is called
|
// "probe" mode is handled directly in main.go before cluster.Start is called
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
followerTimeout = 2 * time.Second
|
||||||
|
leaderFailureThreshold = 3
|
||||||
|
followerRetryInterval = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
func runFollowerLoop(ctx context.Context, cfg Config, eng *monitor.Engine) {
|
func runFollowerLoop(ctx context.Context, cfg Config, eng *monitor.Engine) {
|
||||||
client := http.Client{Timeout: 2 * time.Second}
|
client := http.Client{Timeout: followerTimeout}
|
||||||
failures := 0
|
failures := 0
|
||||||
threshold := 3
|
threshold := leaderFailureThreshold
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(followerRetryInterval):
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -59,7 +65,7 @@ func runFollowerLoop(ctx context.Context, cfg Config, eng *monitor.Engine) {
|
|||||||
isLeaderHealthy := false
|
isLeaderHealthy := false
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
isLeaderHealthy = resp.StatusCode == 200
|
isLeaderHealthy = resp.StatusCode == http.StatusOK
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,18 @@ type ProbeConfig struct {
|
|||||||
AllowPrivateTargets bool
|
AllowPrivateTargets bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
probeMinInterval = 10
|
||||||
|
probeDefaultInterval = 30
|
||||||
|
probeAPITimeout = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
func RunProbe(ctx context.Context, cfg ProbeConfig) error {
|
func RunProbe(ctx context.Context, cfg ProbeConfig) error {
|
||||||
if cfg.Interval < 10 {
|
if cfg.Interval < probeMinInterval {
|
||||||
cfg.Interval = 30
|
cfg.Interval = probeDefaultInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
apiClient := &http.Client{Timeout: 10 * time.Second}
|
apiClient := &http.Client{Timeout: probeAPITimeout}
|
||||||
dial := monitor.SafeDialContext(cfg.AllowPrivateTargets)
|
dial := monitor.SafeDialContext(cfg.AllowPrivateTargets)
|
||||||
strictClient := &http.Client{
|
strictClient := &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const (
|
|||||||
maintPruneInterval = 15 * time.Minute
|
maintPruneInterval = 15 * time.Minute
|
||||||
defaultMaintRetention = 7 * 24 * time.Hour
|
defaultMaintRetention = 7 * 24 * time.Hour
|
||||||
dbWriteBuffer = 4096
|
dbWriteBuffer = 4096
|
||||||
|
alertSendTimeout = 30 * time.Second
|
||||||
dbPruneInterval = 10 * time.Minute
|
dbPruneInterval = 10 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -900,7 +901,7 @@ func (e *Engine) triggerAlert(alertID int, title, message string) {
|
|||||||
provider := alert.GetProvider(cfg)
|
provider := alert.GetProvider(cfg)
|
||||||
if provider != nil {
|
if provider != nil {
|
||||||
go func() {
|
go func() {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), alertSendTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := provider.Send(ctx, title, message); err != nil {
|
if err := provider.Send(ctx, title, message); err != nil {
|
||||||
e.AddLog(fmt.Sprintf("Alert send failed (%s): %v", cfg.Name, err))
|
e.AddLog(fmt.Sprintf("Alert send failed (%s): %v", cfg.Name, err))
|
||||||
@@ -953,7 +954,7 @@ func (e *Engine) TestAlert(alertID int) error {
|
|||||||
if provider == nil {
|
if provider == nil {
|
||||||
return fmt.Errorf("no provider for type %q", cfg.Type)
|
return fmt.Errorf("no provider for type %q", cfg.Type)
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), alertSendTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
err = provider.Send(ctx, "🧪 Test Alert", fmt.Sprintf("Test notification from uptop for channel '%s'.", cfg.Name))
|
err = provider.Send(ctx, "🧪 Test Alert", fmt.Sprintf("Test notification from uptop for channel '%s'.", cfg.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const dialTimeout = 10 * time.Second
|
||||||
|
|
||||||
var privateRanges []*net.IPNet
|
var privateRanges []*net.IPNet
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -60,7 +62,7 @@ func SafeDialContext(allowPrivate bool) func(ctx context.Context, network, addr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dialer := &net.Dialer{Timeout: 10 * time.Second}
|
dialer := &net.Dialer{Timeout: dialTimeout}
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
target := net.JoinHostPort(ip.IP.String(), port)
|
target := net.JoinHostPort(ip.IP.String(), port)
|
||||||
conn, err := dialer.DialContext(ctx, network, target)
|
conn, err := dialer.DialContext(ctx, network, target)
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ import (
|
|||||||
// guard.
|
// guard.
|
||||||
const maxVisitors = 10000
|
const maxVisitors = 10000
|
||||||
|
|
||||||
|
const (
|
||||||
|
visitorCleanupInterval = 5 * time.Minute
|
||||||
|
visitorIdleCutoff = 10 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
type visitor struct {
|
type visitor struct {
|
||||||
tokens float64
|
tokens float64
|
||||||
lastSeen time.Time
|
lastSeen time.Time
|
||||||
@@ -90,13 +95,13 @@ func (rl *RateLimiter) evictOldest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rl *RateLimiter) cleanup() {
|
func (rl *RateLimiter) cleanup() {
|
||||||
ticker := time.NewTicker(5 * time.Minute)
|
ticker := time.NewTicker(visitorCleanupInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
rl.mu.Lock()
|
rl.mu.Lock()
|
||||||
cutoff := time.Now().Add(-10 * time.Minute)
|
cutoff := time.Now().Add(-visitorIdleCutoff)
|
||||||
for ip, v := range rl.visitors {
|
for ip, v := range rl.visitors {
|
||||||
if v.lastSeen.Before(cutoff) {
|
if v.lastSeen.Before(cutoff) {
|
||||||
delete(rl.visitors, ip)
|
delete(rl.visitors, ip)
|
||||||
|
|||||||
@@ -45,15 +45,27 @@ type Server struct {
|
|||||||
statusRL *RateLimiter
|
statusRL *RateLimiter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
pushRateLimit = 60
|
||||||
|
probeRateLimit = 30
|
||||||
|
backupRateLimit = 10
|
||||||
|
statusRateLimit = 120
|
||||||
|
|
||||||
|
httpReadHeaderTimeout = 10 * time.Second
|
||||||
|
httpReadTimeout = 30 * time.Second
|
||||||
|
httpWriteTimeout = 60 * time.Second
|
||||||
|
httpIdleTimeout = 120 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
func NewServer(cfg ServerConfig, s store.Store, eng *monitor.Engine) *Server {
|
func NewServer(cfg ServerConfig, s store.Store, eng *monitor.Engine) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
store: s,
|
store: s,
|
||||||
eng: eng,
|
eng: eng,
|
||||||
pushRL: NewRateLimiter(60, cfg.TrustedProxies),
|
pushRL: NewRateLimiter(pushRateLimit, cfg.TrustedProxies),
|
||||||
probeRL: NewRateLimiter(30, cfg.TrustedProxies),
|
probeRL: NewRateLimiter(probeRateLimit, cfg.TrustedProxies),
|
||||||
backupRL: NewRateLimiter(10, cfg.TrustedProxies),
|
backupRL: NewRateLimiter(backupRateLimit, cfg.TrustedProxies),
|
||||||
statusRL: NewRateLimiter(120, cfg.TrustedProxies),
|
statusRL: NewRateLimiter(statusRateLimit, cfg.TrustedProxies),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,10 +89,10 @@ func (s *Server) Start() *http.Server {
|
|||||||
httpSrv := &http.Server{
|
httpSrv := &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: handler,
|
Handler: handler,
|
||||||
ReadHeaderTimeout: 10 * time.Second,
|
ReadHeaderTimeout: httpReadHeaderTimeout,
|
||||||
ReadTimeout: 30 * time.Second,
|
ReadTimeout: httpReadTimeout,
|
||||||
WriteTimeout: 60 * time.Second,
|
WriteTimeout: httpWriteTimeout,
|
||||||
IdleTimeout: 120 * time.Second,
|
IdleTimeout: httpIdleTimeout,
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
if s.cfg.TLSCert != "" && s.cfg.TLSKey != "" {
|
if s.cfg.TLSCert != "" && s.cfg.TLSKey != "" {
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ const (
|
|||||||
maxLogRows = 200
|
maxLogRows = 200
|
||||||
maxStateChangesPerSite = 5000
|
maxStateChangesPerSite = 5000
|
||||||
maxMaintenanceExport = 1000
|
maxMaintenanceExport = 1000
|
||||||
|
maxOpenConns = 25
|
||||||
|
maxIdleConns = 5
|
||||||
|
connMaxLifetime = 5 * time.Minute
|
||||||
|
tokenByteLen = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
type SQLStore struct {
|
type SQLStore struct {
|
||||||
@@ -32,9 +36,9 @@ func NewSQLStore(driverName, dsn string, dialect Dialect) (*SQLStore, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
db.SetMaxOpenConns(25)
|
db.SetMaxOpenConns(maxOpenConns)
|
||||||
db.SetMaxIdleConns(5)
|
db.SetMaxIdleConns(maxIdleConns)
|
||||||
db.SetConnMaxLifetime(5 * time.Minute)
|
db.SetConnMaxLifetime(connMaxLifetime)
|
||||||
_, isDollar := dialect.(*PostgresDialect)
|
_, isDollar := dialect.(*PostgresDialect)
|
||||||
return &SQLStore{db: db, dialect: dialect, dollar: isDollar}, nil
|
return &SQLStore{db: db, dialect: dialect, dollar: isDollar}, nil
|
||||||
}
|
}
|
||||||
@@ -62,7 +66,7 @@ func (s *SQLStore) q(query string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateToken() (string, error) {
|
func generateToken() (string, error) {
|
||||||
b := make([]byte, 16)
|
b := make([]byte, tokenByteLen)
|
||||||
if _, err := rand.Read(b); err != nil {
|
if _, err := rand.Read(b); err != nil {
|
||||||
return "", fmt.Errorf("crypto/rand failed: %w", err)
|
return "", fmt.Errorf("crypto/rand failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const (
|
||||||
|
nodeOnlineThreshold = 60 * time.Second
|
||||||
|
nodeStaleThreshold = 5 * time.Minute
|
||||||
|
|
||||||
|
uptimeGoodPct = 99.0
|
||||||
|
uptimeExcellentPct = 99.9
|
||||||
|
uptimeWarnPct = 95.0
|
||||||
|
uptimePrecisionPct = 99.99
|
||||||
|
|
||||||
|
stateHistoryLookback = 30 * 24 * time.Hour
|
||||||
|
stateHistoryDays = 30
|
||||||
|
stateHistoryLimit = 100
|
||||||
|
|
||||||
|
httpErrorThreshold = 400
|
||||||
|
errorDetailMaxLen = 30
|
||||||
|
)
|
||||||
@@ -202,8 +202,8 @@ func (m *Model) loadDetailCmd(siteID int) tea.Cmd {
|
|||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
changes := eng.GetStateChanges(siteID, 5)
|
changes := eng.GetStateChanges(siteID, 5)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
allChanges := eng.GetStateChangesSince(siteID, now.Add(-30*24*time.Hour))
|
allChanges := eng.GetStateChangesSince(siteID, now.Add(-stateHistoryLookback))
|
||||||
daily := monitor.ComputeDailyBreakdown(allChanges, currentStatus, 30, now)
|
daily := monitor.ComputeDailyBreakdown(allChanges, currentStatus, stateHistoryDays, now)
|
||||||
return detailDataMsg{siteID: siteID, changes: changes, dailyDays: daily}
|
return detailDataMsg{siteID: siteID, changes: changes, dailyDays: daily}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +213,7 @@ func (m *Model) loadDetailCmd(siteID int) tea.Cmd {
|
|||||||
func (m *Model) loadHistoryCmd(siteID int) tea.Cmd {
|
func (m *Model) loadHistoryCmd(siteID int) tea.Cmd {
|
||||||
eng := m.engine
|
eng := m.engine
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
return historyDataMsg{siteID: siteID, changes: eng.GetStateChanges(siteID, 100)}
|
return historyDataMsg{siteID: siteID, changes: eng.GetStateChanges(siteID, stateHistoryLimit)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ func classifyError(errorReason string, siteType string, statusCode int) ErrorCat
|
|||||||
if strings.HasPrefix(lower, "http ") || strings.Contains(lower, "keyword not found") {
|
if strings.HasPrefix(lower, "http ") || strings.Contains(lower, "keyword not found") {
|
||||||
return ErrCatHTTP
|
return ErrCatHTTP
|
||||||
}
|
}
|
||||||
if statusCode >= 400 {
|
if statusCode >= httpErrorThreshold {
|
||||||
return ErrCatHTTP
|
return ErrCatHTTP
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ func extractDetail(errorReason string, cat ErrorCategory, statusCode int) string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if detail == "" {
|
if detail == "" {
|
||||||
detail = limitStr(errorReason, 30)
|
detail = limitStr(errorReason, errorDetailMaxLen)
|
||||||
}
|
}
|
||||||
case ErrCatTCP:
|
case ErrCatTCP:
|
||||||
for _, keyword := range []string{"connection refused", "connection reset", "no route to host", "network unreachable"} {
|
for _, keyword := range []string{"connection refused", "connection reset", "no route to host", "network unreachable"} {
|
||||||
@@ -155,7 +155,7 @@ func extractDetail(errorReason string, cat ErrorCategory, statusCode int) string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if detail == "" {
|
if detail == "" {
|
||||||
detail = limitStr(errorReason, 30)
|
detail = limitStr(errorReason, errorDetailMaxLen)
|
||||||
}
|
}
|
||||||
case ErrCatTLS:
|
case ErrCatTLS:
|
||||||
for _, keyword := range []string{"certificate expired", "certificate has expired", "handshake failure", "unknown authority"} {
|
for _, keyword := range []string{"certificate expired", "certificate has expired", "handshake failure", "unknown authority"} {
|
||||||
@@ -166,16 +166,16 @@ func extractDetail(errorReason string, cat ErrorCategory, statusCode int) string
|
|||||||
}
|
}
|
||||||
if detail == "" && strings.Contains(lower, "x509:") {
|
if detail == "" && strings.Contains(lower, "x509:") {
|
||||||
idx := strings.Index(lower, "x509:")
|
idx := strings.Index(lower, "x509:")
|
||||||
detail = limitStr(errorReason[idx:], 30)
|
detail = limitStr(errorReason[idx:], errorDetailMaxLen)
|
||||||
}
|
}
|
||||||
if detail == "" {
|
if detail == "" {
|
||||||
detail = limitStr(errorReason, 30)
|
detail = limitStr(errorReason, errorDetailMaxLen)
|
||||||
}
|
}
|
||||||
case ErrCatHTTP:
|
case ErrCatHTTP:
|
||||||
if statusCode > 0 {
|
if statusCode > 0 {
|
||||||
detail = fmt.Sprintf("HTTP %d", statusCode)
|
detail = fmt.Sprintf("HTTP %d", statusCode)
|
||||||
} else {
|
} else {
|
||||||
detail = limitStr(errorReason, 30)
|
detail = limitStr(errorReason, errorDetailMaxLen)
|
||||||
}
|
}
|
||||||
case ErrCatTimeout:
|
case ErrCatTimeout:
|
||||||
if strings.Contains(lower, "i/o timeout") {
|
if strings.Contains(lower, "i/o timeout") {
|
||||||
@@ -187,12 +187,12 @@ func extractDetail(errorReason string, cat ErrorCategory, statusCode int) string
|
|||||||
if strings.Contains(lower, "no icmp response") {
|
if strings.Contains(lower, "no icmp response") {
|
||||||
detail = "no response"
|
detail = "no response"
|
||||||
} else {
|
} else {
|
||||||
detail = limitStr(errorReason, 30)
|
detail = limitStr(errorReason, errorDetailMaxLen)
|
||||||
}
|
}
|
||||||
case ErrCatPrivate:
|
case ErrCatPrivate:
|
||||||
detail = "private IP blocked"
|
detail = "private IP blocked"
|
||||||
default:
|
default:
|
||||||
detail = limitStr(errorReason, 30)
|
detail = limitStr(errorReason, errorDetailMaxLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
return detail
|
return detail
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func (m Model) viewNodesTab() string {
|
|||||||
regions := make(map[string]bool)
|
regions := make(map[string]bool)
|
||||||
var leader string
|
var leader string
|
||||||
for _, n := range m.nodes {
|
for _, n := range m.nodes {
|
||||||
if time.Since(n.LastSeen) < 60*time.Second {
|
if time.Since(n.LastSeen) < nodeOnlineThreshold {
|
||||||
online++
|
online++
|
||||||
}
|
}
|
||||||
if n.Region != "" {
|
if n.Region != "" {
|
||||||
@@ -81,10 +81,10 @@ func (m Model) fmtNodeStatus(lastSeen time.Time) string {
|
|||||||
return m.st.subtleStyle.Render("UNKNOWN")
|
return m.st.subtleStyle.Render("UNKNOWN")
|
||||||
}
|
}
|
||||||
ago := time.Since(lastSeen)
|
ago := time.Since(lastSeen)
|
||||||
if ago < 60*time.Second {
|
if ago < nodeOnlineThreshold {
|
||||||
return m.st.specialStyle.Render("ONLINE")
|
return m.st.specialStyle.Render("ONLINE")
|
||||||
}
|
}
|
||||||
if ago < 5*time.Minute {
|
if ago < nodeStaleThreshold {
|
||||||
return m.st.warnStyle.Render("STALE")
|
return m.st.warnStyle.Render("STALE")
|
||||||
}
|
}
|
||||||
return m.st.dangerStyle.Render("OFFLINE")
|
return m.st.dangerStyle.Render("OFFLINE")
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ func (m Model) uptimeTimeline(days []monitor.DayReport, width int) string {
|
|||||||
for _, d := range display {
|
for _, d := range display {
|
||||||
ch := "█"
|
ch := "█"
|
||||||
switch {
|
switch {
|
||||||
case d.UptimePct >= 99.0:
|
case d.UptimePct >= uptimeGoodPct:
|
||||||
sb.WriteString(m.st.specialStyle.Render(ch))
|
sb.WriteString(m.st.specialStyle.Render(ch))
|
||||||
case d.UptimePct >= 95.0:
|
case d.UptimePct >= uptimeWarnPct:
|
||||||
sb.WriteString(m.st.warnStyle.Render(ch))
|
sb.WriteString(m.st.warnStyle.Render(ch))
|
||||||
case d.UptimePct > 0:
|
case d.UptimePct > 0:
|
||||||
sb.WriteString(m.st.dangerStyle.Render(ch))
|
sb.WriteString(m.st.dangerStyle.Render(ch))
|
||||||
@@ -39,9 +39,9 @@ func (m Model) uptimeTimeline(days []monitor.DayReport, width int) string {
|
|||||||
|
|
||||||
pct := days[len(days)-1].UptimePct
|
pct := days[len(days)-1].UptimePct
|
||||||
pctStyle := m.st.specialStyle
|
pctStyle := m.st.specialStyle
|
||||||
if pct < 99.0 {
|
if pct < uptimeGoodPct {
|
||||||
pctStyle = m.st.dangerStyle
|
pctStyle = m.st.dangerStyle
|
||||||
} else if pct < 99.9 {
|
} else if pct < uptimeExcellentPct {
|
||||||
pctStyle = m.st.warnStyle
|
pctStyle = m.st.warnStyle
|
||||||
}
|
}
|
||||||
sb.WriteString(" " + pctStyle.Render(fmt.Sprintf("%.2f%%", pct)))
|
sb.WriteString(" " + pctStyle.Render(fmt.Sprintf("%.2f%%", pct)))
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ func (m Model) computeStats() dashboardStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, n := range m.nodes {
|
for _, n := range m.nodes {
|
||||||
if !n.LastSeen.IsZero() && time.Since(n.LastSeen) > 5*time.Minute {
|
if !n.LastSeen.IsZero() && time.Since(n.LastSeen) > nodeStaleThreshold {
|
||||||
s.offlineNodes++
|
s.offlineNodes++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -294,7 +294,7 @@ func (m Model) renderFooter(stats dashboardStats) string {
|
|||||||
if len(m.nodes) > 0 {
|
if len(m.nodes) > 0 {
|
||||||
online := 0
|
online := 0
|
||||||
for _, n := range m.nodes {
|
for _, n := range m.nodes {
|
||||||
if !n.LastSeen.IsZero() && time.Since(n.LastSeen) < 60*time.Second {
|
if !n.LastSeen.IsZero() && time.Since(n.LastSeen) < nodeOnlineThreshold {
|
||||||
online++
|
online++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ func (m Model) viewSLAPanel() string {
|
|||||||
}
|
}
|
||||||
bar := m.uptimeBar(r.UptimePct, barWidth)
|
bar := m.uptimeBar(r.UptimePct, barWidth)
|
||||||
uptimeColor := m.st.specialStyle
|
uptimeColor := m.st.specialStyle
|
||||||
if r.UptimePct < 99.9 {
|
if r.UptimePct < uptimeExcellentPct {
|
||||||
uptimeColor = m.st.warnStyle
|
uptimeColor = m.st.warnStyle
|
||||||
}
|
}
|
||||||
if r.UptimePct < 99.0 {
|
if r.UptimePct < uptimeGoodPct {
|
||||||
uptimeColor = m.st.dangerStyle
|
uptimeColor = m.st.dangerStyle
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&b, " %-16s %s %s\n", m.st.subtleStyle.Render("Uptime"), uptimeColor.Render(fmt.Sprintf("%s%%", fmtPct(r.UptimePct))), bar)
|
fmt.Fprintf(&b, " %-16s %s %s\n", m.st.subtleStyle.Render("Uptime"), uptimeColor.Render(fmt.Sprintf("%s%%", fmtPct(r.UptimePct))), bar)
|
||||||
@@ -94,10 +94,10 @@ func (m Model) buildSLADailyContent() string {
|
|||||||
pctStr := fmtPct(day.UptimePct) + "%"
|
pctStr := fmtPct(day.UptimePct) + "%"
|
||||||
|
|
||||||
color := m.st.specialStyle
|
color := m.st.specialStyle
|
||||||
if day.UptimePct < 99.9 {
|
if day.UptimePct < uptimeExcellentPct {
|
||||||
color = m.st.warnStyle
|
color = m.st.warnStyle
|
||||||
}
|
}
|
||||||
if day.UptimePct < 99.0 {
|
if day.UptimePct < uptimeGoodPct {
|
||||||
color = m.st.dangerStyle
|
color = m.st.dangerStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ func fmtPct(pct float64) string {
|
|||||||
if pct == 100 {
|
if pct == 100 {
|
||||||
return "100.00"
|
return "100.00"
|
||||||
}
|
}
|
||||||
if pct >= 99.99 {
|
if pct >= uptimePrecisionPct {
|
||||||
return fmt.Sprintf("%.3f", pct)
|
return fmt.Sprintf("%.3f", pct)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%.2f", pct)
|
return fmt.Sprintf("%.2f", pct)
|
||||||
|
|||||||
Reference in New Issue
Block a user