initial commit

This commit is contained in:
developer committed 2026-09-27 11:20:34 +00:00
1 parent 5b935301e5
commit 85453852d5
105 files changed
+24846

No files matched your search

+148
View File
@@ -0,0 +1,148 @@
package qlog
import (
"math/rand/v2"
"sort"
"sync"
"time"
)
// randFloat is a 0..1 sample draw, isolated so tests can reason about
// sampling without seeding the global source.
func randFloat() float64 { return rand.Float64() }
// domainCount is a rolling per-domain counter.
type domainCount struct {
name string
qtype uint16
qtypeS string
total uint64
blocked uint64
last time.Time
}
// domainCounts is a bounded, TTL-evicting map of domain counters. It replaces
// the hot half of "what is everyone asking for" without the cost of a sorted
// set on every query.
type domainCounts struct {
mu sync.Mutex
counts map[string]*domainCount
limit int
ttl time.Duration
now func() time.Time
}
func newDomainCounts(limit int, ttl time.Duration) *domainCounts {
return &domainCounts{
counts: make(map[string]*domainCount, 1024),
limit: limit,
ttl: ttl,
now: time.Now,
}
}
func countKey(name string, qtype uint16) string {
return name + "|" + itoa(int(qtype))
}
func itoa(n int) string {
if n == 0 {
return "0"
}
var buf [8]byte
i := len(buf)
for n > 0 {
i--
buf[i] = byte('0' + n%10)
n /= 10
}
return string(buf[i:])
}
// observe records a query against its domain counter.
func (d *domainCounts) observe(e Entry) {
if e.Name == "" {
return
}
key := countKey(e.Name, e.QType)
now := d.now()
d.mu.Lock()
c, ok := d.counts[key]
if !ok {
if len(d.counts) >= d.limit {
d.evictLocked(now)
}
c = &domainCount{name: trimDot(e.Name), qtype: e.QType, qtypeS: e.QTypeStr}
d.counts[key] = c
}
c.total++
if e.Blocked {
c.blocked++
}
c.last = now
d.mu.Unlock()
}
// evictLocked removes expired counters, and if that is not enough, the oldest
// quarter. Callers hold d.mu.
func (d *domainCounts) evictLocked(now time.Time) {
cutoff := now.Add(-d.ttl)
removed := 0
for k, c := range d.counts {
if c.last.Before(cutoff) {
delete(d.counts, k)
removed++
}
}
if removed == 0 && len(d.counts) > 0 {
// Nothing has expired (a very busy window): drop the oldest quarter.
type kv struct {
k string
t time.Time
}
all := make([]kv, 0, len(d.counts))
for k, c := range d.counts {
all = append(all, kv{k, c.last})
}
sort.Slice(all, func(i, j int) bool { return all[i].t.Before(all[j].t) })
n := len(all) / 4
if n < 1 {
n = 1
}
for i := 0; i < n && i < len(all); i++ {
delete(d.counts, all[i].k)
}
}
}
// top returns the n hottest domains.
func (d *domainCounts) top(n int, blockedOnly bool) []DomainCount {
if n <= 0 {
n = 25
}
d.mu.Lock()
out := make([]DomainCount, 0, n)
for _, c := range d.counts {
if blockedOnly && c.blocked == 0 {
continue
}
dc := DomainCount{Name: c.name, Count: c.total, Blocked: c.blocked, QType: c.qtype, QTypeS: c.qtypeS}
if blockedOnly {
dc.Count = c.blocked
}
out = append(out, dc)
}
d.mu.Unlock()
sort.Slice(out, func(i, j int) bool { return out[i].Count > out[j].Count })
if len(out) > n {
out = out[:n]
}
return out
}
func trimDot(s string) string {
if n := len(s); n > 1 && s[n-1] == '.' {
return s[:n-1]
}
return s
}