fix: six small fixes — rate limiter leak, DST SLA, probe sort, TUI cleanup #117
@@ -131,14 +131,11 @@ func ComputeDailyBreakdown(changes []models.StateChange, currentStatus models.St
|
|||||||
reports := make([]DayReport, days)
|
reports := make([]DayReport, days)
|
||||||
|
|
||||||
for i := 0; i < days; i++ {
|
for i := 0; i < days; i++ {
|
||||||
dayEnd := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Add(-time.Duration(i) * 24 * time.Hour)
|
dayStart := time.Date(now.Year(), now.Month(), now.Day()-i, 0, 0, 0, 0, now.Location())
|
||||||
|
dayEnd := time.Date(now.Year(), now.Month(), now.Day()-i+1, 0, 0, 0, 0, now.Location())
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
dayEnd = now
|
dayEnd = now
|
||||||
}
|
}
|
||||||
dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Add(-time.Duration(i) * 24 * time.Hour)
|
|
||||||
if i > 0 {
|
|
||||||
dayEnd = dayStart.Add(24 * time.Hour)
|
|
||||||
}
|
|
||||||
|
|
||||||
windowChanges := filterChangesForWindow(changes, dayStart, dayEnd)
|
windowChanges := filterChangesForWindow(changes, dayStart, dayEnd)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type RateLimiter struct {
|
|||||||
rate float64
|
rate float64
|
||||||
burst float64
|
burst float64
|
||||||
trusted []*net.IPNet
|
trusted []*net.IPNet
|
||||||
|
stop chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRateLimiter(requestsPerMinute int, trusted []*net.IPNet) *RateLimiter {
|
func NewRateLimiter(requestsPerMinute int, trusted []*net.IPNet) *RateLimiter {
|
||||||
@@ -33,11 +34,16 @@ func NewRateLimiter(requestsPerMinute int, trusted []*net.IPNet) *RateLimiter {
|
|||||||
rate: float64(requestsPerMinute) / 60.0,
|
rate: float64(requestsPerMinute) / 60.0,
|
||||||
burst: float64(requestsPerMinute),
|
burst: float64(requestsPerMinute),
|
||||||
trusted: trusted,
|
trusted: trusted,
|
||||||
|
stop: make(chan struct{}),
|
||||||
}
|
}
|
||||||
go rl.cleanup()
|
go rl.cleanup()
|
||||||
return rl
|
return rl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) Stop() {
|
||||||
|
close(rl.stop)
|
||||||
|
}
|
||||||
|
|
||||||
func (rl *RateLimiter) Allow(ip string) bool {
|
func (rl *RateLimiter) Allow(ip string) bool {
|
||||||
rl.mu.Lock()
|
rl.mu.Lock()
|
||||||
defer rl.mu.Unlock()
|
defer rl.mu.Unlock()
|
||||||
@@ -84,16 +90,22 @@ func (rl *RateLimiter) evictOldest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rl *RateLimiter) cleanup() {
|
func (rl *RateLimiter) cleanup() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
time.Sleep(5 * time.Minute)
|
select {
|
||||||
rl.mu.Lock()
|
case <-ticker.C:
|
||||||
cutoff := time.Now().Add(-10 * time.Minute)
|
rl.mu.Lock()
|
||||||
for ip, v := range rl.visitors {
|
cutoff := time.Now().Add(-10 * time.Minute)
|
||||||
if v.lastSeen.Before(cutoff) {
|
for ip, v := range rl.visitors {
|
||||||
delete(rl.visitors, ip)
|
if v.lastSeen.Before(cutoff) {
|
||||||
|
delete(rl.visitors, ip)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
rl.mu.Unlock()
|
||||||
|
case <-rl.stop:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
rl.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,10 +75,7 @@ func fmtAlertType(t string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) fmtAlertConfig(alert struct {
|
func (m Model) fmtAlertConfig(alert models.AlertConfig) string {
|
||||||
Type string
|
|
||||||
Settings map[string]string
|
|
||||||
}) string {
|
|
||||||
switch alert.Type {
|
switch alert.Type {
|
||||||
case "email":
|
case "email":
|
||||||
host := alert.Settings["host"]
|
host := alert.Settings["host"]
|
||||||
@@ -201,10 +198,7 @@ func (m Model) viewAlertsTab() string {
|
|||||||
m.fmtAlertHealth(h),
|
m.fmtAlertHealth(h),
|
||||||
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)),
|
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)),
|
||||||
fmtAlertType(a.Type),
|
fmtAlertType(a.Type),
|
||||||
limitStr(m.fmtAlertConfig(struct {
|
limitStr(m.fmtAlertConfig(a), cfgW-2),
|
||||||
Type string
|
|
||||||
Settings map[string]string
|
|
||||||
}{a.Type, a.Settings}), cfgW-2),
|
|
||||||
m.fmtAlertLastSent(h),
|
m.fmtAlertLastSent(h),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,10 +44,7 @@ func TestAlertDetailPanel_MasksSecretsStableOrder(t *testing.T) {
|
|||||||
func TestFmtAlertConfig_MasksSecrets(t *testing.T) {
|
func TestFmtAlertConfig_MasksSecrets(t *testing.T) {
|
||||||
m := newTestModel(&tuiMockStore{})
|
m := newTestModel(&tuiMockStore{})
|
||||||
|
|
||||||
webhook := m.fmtAlertConfig(struct {
|
webhook := m.fmtAlertConfig(models.AlertConfig{Type: "discord", Settings: map[string]string{"url": "https://discord.com/api/webhooks/123456/SeCrEtToKeN"}})
|
||||||
Type string
|
|
||||||
Settings map[string]string
|
|
||||||
}{"discord", map[string]string{"url": "https://discord.com/api/webhooks/123456/SeCrEtToKeN"}})
|
|
||||||
if strings.Contains(webhook, "SeCrEtToKeN") || strings.Contains(webhook, "123456") {
|
if strings.Contains(webhook, "SeCrEtToKeN") || strings.Contains(webhook, "123456") {
|
||||||
t.Errorf("webhook URL path (the credential) rendered in table: %q", webhook)
|
t.Errorf("webhook URL path (the credential) rendered in table: %q", webhook)
|
||||||
}
|
}
|
||||||
@@ -55,10 +52,7 @@ func TestFmtAlertConfig_MasksSecrets(t *testing.T) {
|
|||||||
t.Errorf("webhook host missing from table config: %q", webhook)
|
t.Errorf("webhook host missing from table config: %q", webhook)
|
||||||
}
|
}
|
||||||
|
|
||||||
pd := m.fmtAlertConfig(struct {
|
pd := m.fmtAlertConfig(models.AlertConfig{Type: "pagerduty", Settings: map[string]string{"routing_key": "R0123456789ABCDEFGHIJ"}})
|
||||||
Type string
|
|
||||||
Settings map[string]string
|
|
||||||
}{"pagerduty", map[string]string{"routing_key": "R0123456789ABCDEFGHIJ"}})
|
|
||||||
if strings.Contains(pd, "R0123456789ABCDEFGHIJ") {
|
if strings.Contains(pd, "R0123456789ABCDEFGHIJ") {
|
||||||
t.Errorf("pagerduty routing key rendered raw in table: %q", pd)
|
t.Errorf("pagerduty routing key rendered raw in table: %q", pd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,10 +110,6 @@ func (m *Model) handleConfirmDelete(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if wsm, ok := msg.(tea.WindowSizeMsg); ok {
|
|
||||||
m.termWidth = wsm.Width
|
|
||||||
m.termHeight = wsm.Height
|
|
||||||
}
|
|
||||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||||
if keyMsg.String() == "ctrl+c" {
|
if keyMsg.String() == "ctrl+c" {
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
@@ -609,7 +605,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, writeCmd("Save theme", func() error {
|
return m, writeCmd("Save theme", func() error {
|
||||||
return st.SetPreference(context.Background(), "theme", name)
|
return st.SetPreference(context.Background(), "theme", name)
|
||||||
})
|
})
|
||||||
case "d", "backspace":
|
case "d":
|
||||||
return m.handleDeleteItem()
|
return m.handleDeleteItem()
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -163,8 +164,14 @@ func (m Model) viewDetailPanel() string {
|
|||||||
|
|
||||||
probeResults := m.engine.GetProbeResults(site.ID)
|
probeResults := m.engine.GetProbeResults(site.ID)
|
||||||
if len(probeResults) > 0 {
|
if len(probeResults) > 0 {
|
||||||
|
nodeIDs := make([]string, 0, len(probeResults))
|
||||||
|
for id := range probeResults {
|
||||||
|
nodeIDs = append(nodeIDs, id)
|
||||||
|
}
|
||||||
|
sort.Strings(nodeIDs)
|
||||||
b.WriteString("\n" + m.st.subtleStyle.Render(" PROBE RESULTS") + "\n")
|
b.WriteString("\n" + m.st.subtleStyle.Render(" PROBE RESULTS") + "\n")
|
||||||
for nodeID, result := range probeResults {
|
for _, nodeID := range nodeIDs {
|
||||||
|
result := probeResults[nodeID]
|
||||||
status := m.st.specialStyle.Render("UP")
|
status := m.st.specialStyle.Render("UP")
|
||||||
if !result.IsUp {
|
if !result.IsUp {
|
||||||
status = m.st.dangerStyle.Render("DN")
|
status = m.st.dangerStyle.Render("DN")
|
||||||
|
|||||||
Reference in New Issue
Block a user