mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-10 18:34:18 +08:00
433 lines
15 KiB
Diff
433 lines
15 KiB
Diff
From aa3e90c717c234a3483716c81a349736b0d1b27c Mon Sep 17 00:00:00 2001
|
|
From: sbwml <admin@cooluc.com>
|
|
Date: Thu, 3 Sep 2026 22:34:10 +0800
|
|
Subject: [PATCH 2/3] feat(stats_api): exclude hosts and arbitrary queries from
|
|
cache hit percentage
|
|
|
|
Exclude queries resolved by hosts and arbitrary plugins from the cache
|
|
hit rate calculation (total - blocked - hosts - arbitrary), as locally
|
|
resolved queries bypass cache and should not skew the cache hit rate.
|
|
|
|
Also track hosts_queries and arbitrary_queries in stats endpoint and
|
|
dump data.
|
|
|
|
Signed-off-by: sbwml <admin@cooluc.com>
|
|
---
|
|
pkg/query_context/context.go | 22 +++
|
|
plugin/executable/arbitrary/arbitrary.go | 1 +
|
|
plugin/executable/cache/cache.go | 2 +
|
|
plugin/executable/forward/forward.go | 2 +
|
|
plugin/executable/hosts/hosts.go | 1 +
|
|
plugin/executable/stats_api/stats_api.go | 81 +++++++---
|
|
plugin/executable/stats_api/stats_api_test.go | 143 ++++++++++++++++++
|
|
7 files changed, 227 insertions(+), 25 deletions(-)
|
|
|
|
--- a/pkg/query_context/context.go
|
|
+++ b/pkg/query_context/context.go
|
|
@@ -75,6 +75,8 @@ type Context struct {
|
|
UpstreamSelected *UpstreamLog
|
|
RuleHits []RuleHit
|
|
CacheState CacheLog
|
|
+ FromHosts bool
|
|
+ FromArbitrary bool
|
|
|
|
// lazy init.
|
|
kv map[uint32]any
|
|
@@ -107,6 +109,16 @@ func (ctx *Context) SetCacheState(hit, l
|
|
}
|
|
}
|
|
|
|
+func (ctx *Context) SetFromHosts() {
|
|
+ ctx.FromHosts = true
|
|
+ ctx.FromArbitrary = false
|
|
+}
|
|
+
|
|
+func (ctx *Context) SetFromArbitrary() {
|
|
+ ctx.FromArbitrary = true
|
|
+ ctx.FromHosts = false
|
|
+}
|
|
+
|
|
var contextUid atomic.Uint32
|
|
|
|
type ServerMeta = server.QueryMeta
|
|
@@ -241,6 +253,9 @@ func (ctx *Context) CopyTo(d *Context) *
|
|
}
|
|
d.upstreamOpt = ctx.upstreamOpt
|
|
|
|
+ d.FromHosts = ctx.FromHosts
|
|
+ d.FromArbitrary = ctx.FromArbitrary
|
|
+
|
|
d.kv = copyMap(ctx.kv)
|
|
d.marks = copyMap(ctx.marks)
|
|
return d
|
|
@@ -350,6 +365,13 @@ func (ctx *Context) MarshalLogObject(enc
|
|
return nil
|
|
}))
|
|
|
|
+ if ctx.FromHosts {
|
|
+ encoder.AddBool("hosts", true)
|
|
+ }
|
|
+ if ctx.FromArbitrary {
|
|
+ encoder.AddBool("arbitrary", true)
|
|
+ }
|
|
+
|
|
if r := ctx.resp; r != nil {
|
|
encoder.AddInt("rcode", r.Rcode)
|
|
encoder.AddInt("resp_size", r.Len())
|
|
--- a/plugin/executable/arbitrary/arbitrary.go
|
|
+++ b/plugin/executable/arbitrary/arbitrary.go
|
|
@@ -99,6 +99,7 @@ func (a *Arbitrary) Exec(_ context.Conte
|
|
if inner := a.m.Load(); inner != nil {
|
|
if r := inner.Reply(qCtx.Q()); r != nil {
|
|
qCtx.SetResponse(r)
|
|
+ qCtx.SetFromArbitrary()
|
|
}
|
|
}
|
|
return nil
|
|
--- a/plugin/executable/cache/cache.go
|
|
+++ b/plugin/executable/cache/cache.go
|
|
@@ -234,6 +234,8 @@ func (c *Cache) Exec(ctx context.Context
|
|
c.hitTotal.Inc()
|
|
cachedResp.Id = q.Id // change msg id
|
|
qCtx.SetResponse(cachedResp)
|
|
+ qCtx.FromHosts = false
|
|
+ qCtx.FromArbitrary = false
|
|
if v, _, ok := c.backend.Get(key(msgKey)); ok && v != nil {
|
|
v.hitCount.Add(1)
|
|
ttl := int(v.expirationTime.Sub(v.storedTime).Seconds())
|
|
--- a/plugin/executable/forward/forward.go
|
|
+++ b/plugin/executable/forward/forward.go
|
|
@@ -201,6 +201,8 @@ func (f *Forward) Exec(ctx context.Conte
|
|
return err
|
|
}
|
|
qCtx.SetResponse(r)
|
|
+ qCtx.FromHosts = false
|
|
+ qCtx.FromArbitrary = false
|
|
return nil
|
|
}
|
|
|
|
--- a/plugin/executable/hosts/hosts.go
|
|
+++ b/plugin/executable/hosts/hosts.go
|
|
@@ -113,6 +113,7 @@ func (h *Hosts) Exec(_ context.Context,
|
|
r := inner.LookupMsg(qCtx.Q())
|
|
if r != nil {
|
|
qCtx.SetResponse(r)
|
|
+ qCtx.SetFromHosts()
|
|
}
|
|
}
|
|
return nil
|
|
--- a/plugin/executable/stats_api/stats_api.go
|
|
+++ b/plugin/executable/stats_api/stats_api.go
|
|
@@ -487,18 +487,20 @@ func (h *HistoryStats) Import(points map
|
|
}
|
|
|
|
type StatsDumpData struct {
|
|
- Version int `json:"version"`
|
|
- Timestamp int64 `json:"timestamp"`
|
|
- TotalQueries uint64 `json:"total_queries"`
|
|
- BlockedQueries uint64 `json:"blocked_queries"`
|
|
- CachedQueries uint64 `json:"cached_queries"`
|
|
- TotalLatencyUs uint64 `json:"total_latency_us"`
|
|
- TopDomains map[string]uint64 `json:"top_domains,omitempty"`
|
|
- TopClients map[string]uint64 `json:"top_clients,omitempty"`
|
|
- TopBlocked map[string]uint64 `json:"top_blocked,omitempty"`
|
|
- History map[int64]HistoryBucketData `json:"history,omitempty"`
|
|
- Logs []LogEntry `json:"logs,omitempty"`
|
|
- SeqID uint64 `json:"seq_id,omitempty"`
|
|
+ Version int `json:"version"`
|
|
+ Timestamp int64 `json:"timestamp"`
|
|
+ TotalQueries uint64 `json:"total_queries"`
|
|
+ BlockedQueries uint64 `json:"blocked_queries"`
|
|
+ CachedQueries uint64 `json:"cached_queries"`
|
|
+ HostsQueries uint64 `json:"hosts_queries,omitempty"`
|
|
+ ArbitraryQueries uint64 `json:"arbitrary_queries,omitempty"`
|
|
+ TotalLatencyUs uint64 `json:"total_latency_us"`
|
|
+ TopDomains map[string]uint64 `json:"top_domains,omitempty"`
|
|
+ TopClients map[string]uint64 `json:"top_clients,omitempty"`
|
|
+ TopBlocked map[string]uint64 `json:"top_blocked,omitempty"`
|
|
+ History map[int64]HistoryBucketData `json:"history,omitempty"`
|
|
+ Logs []LogEntry `json:"logs,omitempty"`
|
|
+ SeqID uint64 `json:"seq_id,omitempty"`
|
|
}
|
|
|
|
type StatsAPI struct {
|
|
@@ -509,11 +511,13 @@ type StatsAPI struct {
|
|
topStats *TopStats
|
|
historyStats *HistoryStats
|
|
|
|
- totalQueries atomic.Uint64
|
|
- blockedQueries atomic.Uint64
|
|
- cachedQueries atomic.Uint64
|
|
- totalLatencyUs atomic.Uint64
|
|
- currentQPS atomic.Uint64
|
|
+ totalQueries atomic.Uint64
|
|
+ blockedQueries atomic.Uint64
|
|
+ cachedQueries atomic.Uint64
|
|
+ hostsQueries atomic.Uint64
|
|
+ arbitraryQueries atomic.Uint64
|
|
+ totalLatencyUs atomic.Uint64
|
|
+ currentQPS atomic.Uint64
|
|
|
|
updatedCount atomic.Uint64
|
|
closeNotify chan struct{}
|
|
@@ -626,6 +630,8 @@ func (s *StatsAPI) handleStats(w http.Re
|
|
total := s.totalQueries.Load()
|
|
blocked := s.blockedQueries.Load()
|
|
cached := s.cachedQueries.Load()
|
|
+ hosts := s.hostsQueries.Load()
|
|
+ arbitrary := s.arbitraryQueries.Load()
|
|
latUs := s.totalLatencyUs.Load()
|
|
|
|
var blockedPct, cachedPct, avgLat float64
|
|
@@ -633,8 +639,9 @@ func (s *StatsAPI) handleStats(w http.Re
|
|
blockedPct = float64(blocked) / float64(total) * 100.0
|
|
avgLat = (float64(latUs) / float64(total)) / 1000.0
|
|
}
|
|
- if total > blocked {
|
|
- cachedPct = float64(cached) / float64(total-blocked) * 100.0
|
|
+ excluded := blocked + hosts + arbitrary
|
|
+ if total > excluded {
|
|
+ cachedPct = float64(cached) / float64(total-excluded) * 100.0
|
|
if cachedPct > 100.0 {
|
|
cachedPct = 100.0
|
|
}
|
|
@@ -649,6 +656,8 @@ func (s *StatsAPI) handleStats(w http.Re
|
|
"total_queries": total,
|
|
"blocked_queries": blocked,
|
|
"cached_queries": cached,
|
|
+ "hosts_queries": hosts,
|
|
+ "arbitrary_queries": arbitrary,
|
|
"blocked_percentage": blockedPct,
|
|
"cached_percentage": cachedPct,
|
|
"avg_latency_ms": avgLat,
|
|
@@ -778,12 +787,14 @@ func (s *StatsAPI) writeDump(w io.Writer
|
|
gw.Name = statsDumpHeader
|
|
|
|
data := StatsDumpData{
|
|
- Version: 1,
|
|
- Timestamp: time.Now().Unix(),
|
|
- TotalQueries: s.totalQueries.Load(),
|
|
- BlockedQueries: s.blockedQueries.Load(),
|
|
- CachedQueries: s.cachedQueries.Load(),
|
|
- TotalLatencyUs: s.totalLatencyUs.Load(),
|
|
+ Version: 1,
|
|
+ Timestamp: time.Now().Unix(),
|
|
+ TotalQueries: s.totalQueries.Load(),
|
|
+ BlockedQueries: s.blockedQueries.Load(),
|
|
+ CachedQueries: s.cachedQueries.Load(),
|
|
+ HostsQueries: s.hostsQueries.Load(),
|
|
+ ArbitraryQueries: s.arbitraryQueries.Load(),
|
|
+ TotalLatencyUs: s.totalLatencyUs.Load(),
|
|
}
|
|
|
|
data.TopDomains, data.TopClients, data.TopBlocked = s.topStats.Export()
|
|
@@ -817,6 +828,8 @@ func (s *StatsAPI) readDump(r io.Reader)
|
|
s.totalQueries.Store(data.TotalQueries)
|
|
s.blockedQueries.Store(data.BlockedQueries)
|
|
s.cachedQueries.Store(data.CachedQueries)
|
|
+ s.hostsQueries.Store(data.HostsQueries)
|
|
+ s.arbitraryQueries.Store(data.ArbitraryQueries)
|
|
s.totalLatencyUs.Store(data.TotalLatencyUs)
|
|
|
|
s.topStats.Import(data.TopDomains, data.TopClients, data.TopBlocked)
|
|
@@ -1022,10 +1035,24 @@ func (s *StatsAPI) Exec(ctx context.Cont
|
|
s.blockedQueries.Add(1)
|
|
}
|
|
|
|
+ isHosts := qCtx.FromHosts && !isBlocked && !isCached && qCtx.UpstreamSelected == nil
|
|
+ if isHosts {
|
|
+ s.hostsQueries.Add(1)
|
|
+ }
|
|
+
|
|
+ isArbitrary := qCtx.FromArbitrary && !isBlocked && !isCached && qCtx.UpstreamSelected == nil
|
|
+ if isArbitrary {
|
|
+ s.arbitraryQueries.Add(1)
|
|
+ }
|
|
+
|
|
// Extract Upstream information
|
|
var upstream string
|
|
if isCached {
|
|
upstream = "cache"
|
|
+ } else if isHosts {
|
|
+ upstream = "hosts"
|
|
+ } else if isArbitrary {
|
|
+ upstream = "arbitrary"
|
|
} else if u := qCtx.UpstreamSelected; u != nil {
|
|
if u.Addr != "" {
|
|
if !strings.Contains(u.Addr, "://") {
|
|
@@ -1083,6 +1110,10 @@ func (s *StatsAPI) Exec(ctx context.Cont
|
|
if rule == "" {
|
|
if isCached {
|
|
rule = "cache"
|
|
+ } else if isHosts {
|
|
+ rule = "hosts"
|
|
+ } else if isArbitrary {
|
|
+ rule = "arbitrary"
|
|
} else {
|
|
rule = "-"
|
|
}
|
|
--- a/plugin/executable/stats_api/stats_api_test.go
|
|
+++ b/plugin/executable/stats_api/stats_api_test.go
|
|
@@ -428,6 +428,12 @@ func TestStatsAPIPersistence(t *testing.
|
|
if i%3 == 0 {
|
|
s1.cachedQueries.Add(1)
|
|
}
|
|
+ if i%4 == 0 {
|
|
+ s1.hostsQueries.Add(1)
|
|
+ }
|
|
+ if i%5 == 0 {
|
|
+ s1.arbitraryQueries.Add(1)
|
|
+ }
|
|
s1.totalLatencyUs.Add(uint64(i * 5000))
|
|
s1.topStats.Record(fmt.Sprintf("test%d.com.", i), "192.168.1.100", i%2 == 0)
|
|
s1.historyStats.Record(time.Now(), i%2 == 0, i%3 == 0)
|
|
@@ -479,6 +485,12 @@ func TestStatsAPIPersistence(t *testing.
|
|
if s2.cachedQueries.Load() != 3 {
|
|
t.Errorf("expected 3 cached queries after load, got %d", s2.cachedQueries.Load())
|
|
}
|
|
+ if s2.hostsQueries.Load() != 2 {
|
|
+ t.Errorf("expected 2 hosts queries after load, got %d", s2.hostsQueries.Load())
|
|
+ }
|
|
+ if s2.arbitraryQueries.Load() != 2 {
|
|
+ t.Errorf("expected 2 arbitrary queries after load, got %d", s2.arbitraryQueries.Load())
|
|
+ }
|
|
|
|
totalLogs, logs := s2.ringBuffer.QueryLogs(10, 0, "", "all")
|
|
if totalLogs != 10 || len(logs) != 10 {
|
|
@@ -569,4 +581,135 @@ func TestStatsAPIQPS(t *testing.T) {
|
|
}
|
|
}
|
|
|
|
+func TestStatsAPICachedPercentageExcludesHostsAndArbitrary(t *testing.T) {
|
|
+ s := NewStatsAPI(&Args{Capacity: 100}, zap.NewNop())
|
|
+ router := s.Router()
|
|
+
|
|
+ // 100 total queries: 20 blocked, 15 hosts, 15 arbitrary, 25 cached, 25 forwarded to upstream
|
|
+ // denominator = 100 - (20 + 15 + 15) = 50
|
|
+ // cached_percentage = 25 / 50 = 50%
|
|
+ s.totalQueries.Store(100)
|
|
+ s.blockedQueries.Store(20)
|
|
+ s.hostsQueries.Store(15)
|
|
+ s.arbitraryQueries.Store(15)
|
|
+ s.cachedQueries.Store(25)
|
|
+ s.totalLatencyUs.Store(50000)
|
|
+
|
|
+ req := httptest.NewRequest(http.MethodGet, "/api/v1/stats", nil)
|
|
+ w := httptest.NewRecorder()
|
|
+ router.ServeHTTP(w, req)
|
|
+
|
|
+ if w.Code != http.StatusOK {
|
|
+ t.Fatalf("expected HTTP 200, got %d", w.Code)
|
|
+ }
|
|
+
|
|
+ var statsResp map[string]any
|
|
+ if err := json.Unmarshal(w.Body.Bytes(), &statsResp); err != nil {
|
|
+ t.Fatalf("failed to unmarshal stats response: %v", err)
|
|
+ }
|
|
+
|
|
+ if hostsQueries := statsResp["hosts_queries"].(float64); hostsQueries != 15 {
|
|
+ t.Errorf("expected hosts_queries 15, got %v", hostsQueries)
|
|
+ }
|
|
+ if arbitraryQueries := statsResp["arbitrary_queries"].(float64); arbitraryQueries != 15 {
|
|
+ t.Errorf("expected arbitrary_queries 15, got %v", arbitraryQueries)
|
|
+ }
|
|
+ if blockedPct := statsResp["blocked_percentage"].(float64); blockedPct != 20.0 {
|
|
+ t.Errorf("expected blocked_percentage 20.0, got %v", blockedPct)
|
|
+ }
|
|
+ if cachedPct := statsResp["cached_percentage"].(float64); cachedPct != 50.0 {
|
|
+ t.Errorf("expected cached_percentage 50.0, got %v", cachedPct)
|
|
+ }
|
|
+}
|
|
+
|
|
+func TestStatsAPIExecWithHostsAndArbitrary(t *testing.T) {
|
|
+ s := NewStatsAPI(&Args{Capacity: 100}, zap.NewNop())
|
|
+
|
|
+ // Test 1: Normal hosts resolution
|
|
+ q1 := new(dns.Msg)
|
|
+ q1.SetQuestion("lan.local.", dns.TypeA)
|
|
+ qCtx1 := query_context.NewContext(q1)
|
|
+
|
|
+ execHosts := sequence.ExecutableFunc(func(ctx context.Context, qCtx *query_context.Context) error {
|
|
+ resp := new(dns.Msg)
|
|
+ resp.SetReply(qCtx.Q())
|
|
+ resp.Answer = append(resp.Answer, &dns.A{
|
|
+ Hdr: dns.RR_Header{Name: "lan.local.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
|
+ A: []byte{192, 168, 1, 10},
|
|
+ })
|
|
+ qCtx.SetResponse(resp)
|
|
+ qCtx.SetFromHosts()
|
|
+ return nil
|
|
+ })
|
|
+
|
|
+ walker1 := sequence.NewChainWalker([]*sequence.ChainNode{{E: execHosts}}, nil)
|
|
+ if err := s.Exec(context.Background(), qCtx1, walker1); err != nil {
|
|
+ t.Fatalf("Exec failed: %v", err)
|
|
+ }
|
|
+ if s.hostsQueries.Load() != 1 {
|
|
+ t.Errorf("expected hostsQueries 1, got %d", s.hostsQueries.Load())
|
|
+ }
|
|
+ _, logs1 := s.ringBuffer.QueryLogs(1, 0, "", "all")
|
|
+ if len(logs1) != 1 || logs1[0].Upstream != "hosts" {
|
|
+ t.Errorf("expected upstream 'hosts', got %+v", logs1)
|
|
+ }
|
|
+
|
|
+ // Test 2: Hosts returning 0.0.0.0 (adblock) -> should be counted in blockedQueries, NOT hostsQueries
|
|
+ q2 := new(dns.Msg)
|
|
+ q2.SetQuestion("ad.example.com.", dns.TypeA)
|
|
+ qCtx2 := query_context.NewContext(q2)
|
|
+
|
|
+ execHostsBlock := sequence.ExecutableFunc(func(ctx context.Context, qCtx *query_context.Context) error {
|
|
+ resp := new(dns.Msg)
|
|
+ resp.SetReply(qCtx.Q())
|
|
+ resp.Answer = append(resp.Answer, &dns.A{
|
|
+ Hdr: dns.RR_Header{Name: "ad.example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
|
+ A: []byte{0, 0, 0, 0},
|
|
+ })
|
|
+ qCtx.SetResponse(resp)
|
|
+ qCtx.SetFromHosts()
|
|
+ return nil
|
|
+ })
|
|
+
|
|
+ walker2 := sequence.NewChainWalker([]*sequence.ChainNode{{E: execHostsBlock}}, nil)
|
|
+ if err := s.Exec(context.Background(), qCtx2, walker2); err != nil {
|
|
+ t.Fatalf("Exec failed: %v", err)
|
|
+ }
|
|
+ if s.blockedQueries.Load() != 1 {
|
|
+ t.Errorf("expected blockedQueries 1, got %d", s.blockedQueries.Load())
|
|
+ }
|
|
+ if s.hostsQueries.Load() != 1 { // Should still be 1 (not incremented)
|
|
+ t.Errorf("expected hostsQueries to remain 1, got %d", s.hostsQueries.Load())
|
|
+ }
|
|
+
|
|
+ // Test 3: Arbitrary resolution
|
|
+ q3 := new(dns.Msg)
|
|
+ q3.SetQuestion("custom.zone.", dns.TypeA)
|
|
+ qCtx3 := query_context.NewContext(q3)
|
|
+
|
|
+ execArbitrary := sequence.ExecutableFunc(func(ctx context.Context, qCtx *query_context.Context) error {
|
|
+ resp := new(dns.Msg)
|
|
+ resp.SetReply(qCtx.Q())
|
|
+ resp.Answer = append(resp.Answer, &dns.A{
|
|
+ Hdr: dns.RR_Header{Name: "custom.zone.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
|
+ A: []byte{10, 0, 0, 1},
|
|
+ })
|
|
+ qCtx.SetResponse(resp)
|
|
+ qCtx.SetFromArbitrary()
|
|
+ return nil
|
|
+ })
|
|
+
|
|
+ walker3 := sequence.NewChainWalker([]*sequence.ChainNode{{E: execArbitrary}}, nil)
|
|
+ if err := s.Exec(context.Background(), qCtx3, walker3); err != nil {
|
|
+ t.Fatalf("Exec failed: %v", err)
|
|
+ }
|
|
+ if s.arbitraryQueries.Load() != 1 {
|
|
+ t.Errorf("expected arbitraryQueries 1, got %d", s.arbitraryQueries.Load())
|
|
+ }
|
|
+ _, logs3 := s.ringBuffer.QueryLogs(1, 0, "", "all")
|
|
+ if len(logs3) != 1 || logs3[0].Upstream != "arbitrary" {
|
|
+ t.Errorf("expected upstream 'arbitrary', got %+v", logs3)
|
|
+ }
|
|
+}
|
|
+
|
|
|