-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathstats_hourly.go
More file actions
167 lines (146 loc) · 4.26 KB
/
stats_hourly.go
File metadata and controls
167 lines (146 loc) · 4.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/*
File: stats_hourly.go
Version: 1.2.0
Updated: 2026-04-17 11:25 CEST
Description:
Hourly 24h Ring buffer tracking for sdproxy.
Extracted from stats.go to improve modularity and reduce token burn.
Changes:
1.2.0 - [FEAT] Added cascade pruning for statTopNXDomain.
1.1.0 - [FEAT] Added cascade pruning for statTopUpstreamHosts.
*/
package main
import (
"sync"
"time"
)
// ---------------------------------------------------------------------------
// Hourly ring buffer
// ---------------------------------------------------------------------------
// HourlyPoint is one bar in the activity graph returned by /api/stats.
type HourlyPoint struct {
Label string `json:"label"` // "14:00" or "14:00…" for the current hour
Total int64 `json:"total"` // queries during this hour
Blocked int64 `json:"blocked"` // blocks during this hour
Fwd int64 `json:"fwd"` // forwarded queries during this hour
}
// hourSlot is one completed-hour snapshot stored in the ring. Exported for JSON.
type hourSlot struct {
EpochHour int64 `json:"epoch_hour"`
Total int64 `json:"total"`
Blocked int64 `json:"blocked"`
Fwd int64 `json:"fwd"`
}
var (
// hourlyRing dynamically scales its entries to HistoryRetentionHours.
hourlyRing = make(map[int64]hourSlot)
hourlyMu sync.Mutex
// Cumulative counter snapshots at the last hourly tick.
// Protected by hourlyMu; read in getHourlyStats to compute the live delta.
prevTotal int64
prevBlocked int64
prevFwd int64
)
// runHourlyTicker waits until the next clock-hour boundary, then ticks every
// hour to record the hourly delta into the ring.
func runHourlyTicker() {
now := time.Now()
next := now.Truncate(time.Hour).Add(time.Hour)
time.Sleep(next.Sub(now))
recordHourSlot()
t := time.NewTicker(time.Hour)
defer t.Stop()
for range t.C {
recordHourSlot()
}
}
// recordHourSlot reads the current cumulative counters, computes deltas since
// the last tick, and writes them to the ring.
func recordHourSlot() {
total := thr.queriesTotal.Load()
blocked := statParentalBlocks.Load() + statPolicyBlocks.Load()
fwd := thr.upstreamCalls.Load()
hourlyMu.Lock()
// Subtract one minute so we reliably land in the hour that just ended.
ep := time.Now().Add(-time.Minute).Unix() / 3600
hourlyRing[ep] = hourSlot{
EpochHour: ep,
Total: total - prevTotal,
Blocked: blocked - prevBlocked,
Fwd: fwd - prevFwd,
}
prevTotal = total
prevBlocked = blocked
prevFwd = fwd
// Prune old hours from the ring
minHour := ep - int64(retentionHours()) + 1
for k := range hourlyRing {
if k < minHour {
delete(hourlyRing, k)
}
}
hourlyMu.Unlock()
// Cascade prune to all Top-N trackers
rh := retentionHours()
statTopDomains.Prune(rh)
statTopBlocked.Prune(rh)
statTopTalkers.Prune(rh)
statTopFilteredIPs.Prune(rh)
statTopCategories.Prune(rh)
statTopTLDs.Prune(rh)
statTopVendors.Prune(rh)
statTopGroups.Prune(rh)
statTopBlockReasons.Prune(rh)
statTopUpstreams.Prune(rh)
statTopUpstreamHosts.Prune(rh)
statTopReturnCodes.Prune(rh)
statTopNXDomain.Prune(rh)
}
// getHourlyStats assembles the series returned by /api/stats.
func getHourlyStats() []HourlyPoint {
now := time.Now()
curEpoch := now.Unix() / 3600
retHours := retentionHours()
hourlyMu.Lock()
ringCopy := make(map[int64]hourSlot, len(hourlyRing))
for k, v := range hourlyRing {
ringCopy[k] = v
}
pTot := prevTotal
pBlk := prevBlocked
pFwd := prevFwd
hourlyMu.Unlock()
pts := make([]HourlyPoint, retHours)
// Points 0 to retHours-2: completed hours
for i := 0; i < retHours-1; i++ {
ep := curEpoch - int64(retHours-1-i)
t := time.Unix(ep*3600, 0).Local()
pt := HourlyPoint{Label: t.Format("15:04")}
if s, ok := ringCopy[ep]; ok {
pt.Total = s.Total
pt.Blocked = s.Blocked
pt.Fwd = s.Fwd
}
pts[i] = pt
}
// Point retHours-1: live delta for the current (partial) hour.
curTot := thr.queriesTotal.Load() - pTot
curBlk := statParentalBlocks.Load() + statPolicyBlocks.Load() - pBlk
curFwd := thr.upstreamCalls.Load() - pFwd
if curTot < 0 {
curTot = 0
}
if curBlk < 0 {
curBlk = 0
}
if curFwd < 0 {
curFwd = 0
}
pts[retHours-1] = HourlyPoint{
Label: now.Format("15:04") + "…", // trailing … marks it as still accumulating
Total: curTot,
Blocked: curBlk,
Fwd: curFwd,
}
return pts
}