update once

This commit is contained in:
XOF
2026-01-06 02:25:24 +08:00
commit 7bf4f27be3
25 changed files with 4587 additions and 0 deletions

649
internal/stats/stats.go Normal file
View File

@@ -0,0 +1,649 @@
package stats
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"sort"
"sync"
"sync/atomic"
"time"
)
// StatsRecorder 定义统计接口
type StatsRecorder interface {
RecordQuery()
RecordDoHQuery()
RecordCacheHit()
RecordCacheMiss()
RecordFailed()
RecordUpstreamQuery(address string, isError bool)
RecordClientQuery(clientIP, domain string)
GetSnapshot() StatsSnapshot
Reset()
Save(dataPath string) error
Load(dataPath string) error
}
// Stats DNS服务器统计信息
type Stats struct {
StartTime time.Time // 应用启动时间(不持久化)
StatsStartTime time.Time // 统计数据开始时间(可持久化)
// 查询统计
TotalQueries atomic.Uint64
DoHQueries atomic.Uint64
CacheHits atomic.Uint64
CacheMisses atomic.Uint64
FailedQueries atomic.Uint64
// 上游服务器统计
upstreamStats map[string]*UpstreamStats
mu sync.RWMutex
// Top N 统计
topClients *TopNTracker // 客户端 IP Top N
topDomains *TopNTracker // 查询域名 Top N
}
// UpstreamStats 上游服务器统计
type UpstreamStats struct {
Address string
TotalQueries atomic.Uint64
Errors atomic.Uint64
LastUsed time.Time
mu sync.RWMutex
}
// NewStats 创建统计实例
func NewStats() *Stats {
now := time.Now()
return &Stats{
StartTime: now,
StatsStartTime: now,
upstreamStats: make(map[string]*UpstreamStats),
topClients: NewTopNTracker(100), // 最多保留 100 个客户端 IP
topDomains: NewTopNTracker(200), // 最多保留 200 个域名
}
}
// RecordQuery 记录DNS查询
func (s *Stats) RecordQuery() {
s.TotalQueries.Add(1)
}
// RecordDoHQuery 记录DoH查询
func (s *Stats) RecordDoHQuery() {
s.DoHQueries.Add(1)
}
// RecordCacheHit 记录缓存命中
func (s *Stats) RecordCacheHit() {
s.CacheHits.Add(1)
}
// RecordCacheMiss 记录缓存未命中
func (s *Stats) RecordCacheMiss() {
s.CacheMisses.Add(1)
}
// RecordFailed 记录查询失败
func (s *Stats) RecordFailed() {
s.FailedQueries.Add(1)
}
// RecordUpstreamQuery 记录上游服务器查询
func (s *Stats) RecordUpstreamQuery(address string, isError bool) {
// 先尝试读锁快速查找
s.mu.RLock()
us, ok := s.upstreamStats[address]
s.mu.RUnlock()
// 如果不存在才使用写锁创建
if !ok {
s.mu.Lock()
// 双重检查,防止并发创建
us, ok = s.upstreamStats[address]
if !ok {
us = &UpstreamStats{
Address: address,
}
s.upstreamStats[address] = us
}
s.mu.Unlock()
}
us.TotalQueries.Add(1)
if isError {
us.Errors.Add(1)
}
us.mu.Lock()
us.LastUsed = time.Now()
us.mu.Unlock()
}
// RecordClientQuery 记录客户端查询IP 和域名)
func (s *Stats) RecordClientQuery(clientIP, domain string) {
if clientIP != "" {
s.topClients.Record(clientIP, "")
}
if domain != "" {
s.topDomains.Record(domain, clientIP)
}
}
// Reset 重置统计数据
func (s *Stats) Reset() {
s.mu.Lock()
defer s.mu.Unlock()
// 重置统计开始时间
s.StatsStartTime = time.Now()
// 重置查询统计
s.TotalQueries.Store(0)
s.DoHQueries.Store(0)
s.CacheHits.Store(0)
s.CacheMisses.Store(0)
s.FailedQueries.Store(0)
// 重置上游服务器统计
s.upstreamStats = make(map[string]*UpstreamStats)
// 重置 Top N 统计
s.topClients = NewTopNTracker(100)
s.topDomains = NewTopNTracker(200)
}
// RuntimeStats 运行时统计信息
type RuntimeStats struct {
Uptime int64 `json:"uptime"` // 运行时间(秒)
UptimeStr string `json:"uptime_str"` // 运行时间(可读格式)
StatsDuration int64 `json:"stats_duration"` // 统计时长(秒)
StatsDurationStr string `json:"stats_duration_str"` // 统计时长(可读格式)
Goroutines int `json:"goroutines"` // Goroutine数量
MemAllocMB uint64 `json:"mem_alloc_mb"` // 已分配内存MB
MemTotalMB uint64 `json:"mem_total_mb"` // 总分配内存MB
MemSysMB uint64 `json:"mem_sys_mb"` // 系统内存MB
NumGC uint32 `json:"num_gc"` // GC次数
}
// QueryStats 查询统计信息
type QueryStats struct {
Total uint64 `json:"total"` // 总查询数
DoH uint64 `json:"doh"` // DoH查询数
CacheHits uint64 `json:"cache_hits"` // 缓存命中数
CacheMisses uint64 `json:"cache_misses"` // 缓存未命中数
Failed uint64 `json:"failed"` // 失败查询数
HitRate float64 `json:"hit_rate"` // 缓存命中率
}
// UpstreamStatsJSON 上游服务器统计JSON格式
type UpstreamStatsJSON struct {
Address string `json:"address"` // 服务器地址
TotalQueries uint64 `json:"total_queries"` // 总查询数
Errors uint64 `json:"errors"` // 错误数
ErrorRate float64 `json:"error_rate"` // 错误率
LastUsed string `json:"last_used"` // 最后使用时间
}
// TopNItemJSON Top N 项目JSON格式
type TopNItemJSON struct {
Key string `json:"key"` // IP 地址或域名
Count uint64 `json:"count"` // 查询次数
TopClient string `json:"top_client,omitempty"` // 查询最多的客户端 IP仅域名统计有
}
// StatsSnapshot 完整统计快照
type StatsSnapshot struct {
Runtime RuntimeStats `json:"runtime"` // 运行时信息
Queries QueryStats `json:"queries"` // 查询统计
Upstreams []UpstreamStatsJSON `json:"upstreams"` // 上游服务器统计
TopClients []TopNItemJSON `json:"top_clients"` // Top 客户端 IP
TopDomains []TopNItemJSON `json:"top_domains"` // Top 查询域名
}
// GetSnapshot 获取统计快照
func (s *Stats) GetSnapshot() StatsSnapshot {
// 运行时信息
var m runtime.MemStats
runtime.ReadMemStats(&m)
uptime := time.Since(s.StartTime)
uptimeStr := formatDuration(uptime)
statsDuration := time.Since(s.StatsStartTime)
statsDurationStr := formatDuration(statsDuration)
runtimeStats := RuntimeStats{
Uptime: int64(uptime.Seconds()),
UptimeStr: uptimeStr,
StatsDuration: int64(statsDuration.Seconds()),
StatsDurationStr: statsDurationStr,
Goroutines: runtime.NumGoroutine(),
MemAllocMB: m.Alloc / 1024 / 1024,
MemTotalMB: m.TotalAlloc / 1024 / 1024,
MemSysMB: m.Sys / 1024 / 1024,
NumGC: m.NumGC,
}
// 查询统计
total := s.TotalQueries.Load()
hits := s.CacheHits.Load()
misses := s.CacheMisses.Load()
failed := s.FailedQueries.Load()
var hitRate float64
if total > 0 {
hitRate = float64(hits) / float64(total) * 100
}
queryStats := QueryStats{
Total: total,
DoH: s.DoHQueries.Load(),
CacheHits: hits,
CacheMisses: misses,
Failed: failed,
HitRate: hitRate,
}
// 上游服务器统计
s.mu.RLock()
upstreams := make([]UpstreamStatsJSON, 0, len(s.upstreamStats))
for _, us := range s.upstreamStats {
queries := us.TotalQueries.Load()
errors := us.Errors.Load()
var errorRate float64
if queries > 0 {
errorRate = float64(errors) / float64(queries) * 100
}
us.mu.RLock()
lastUsed := us.LastUsed.Format("2006-01-02 15:04:05")
if us.LastUsed.IsZero() {
lastUsed = "Never"
}
us.mu.RUnlock()
upstreams = append(upstreams, UpstreamStatsJSON{
Address: us.Address,
TotalQueries: queries,
Errors: errors,
ErrorRate: errorRate,
LastUsed: lastUsed,
})
}
s.mu.RUnlock()
// 按服务器地址字符串排序
sort.Slice(upstreams, func(i, j int) bool {
return upstreams[i].Address < upstreams[j].Address
})
// Top N 客户端 IP
topClients := make([]TopNItemJSON, 0)
for _, item := range s.topClients.GetTopN(20) { // 返回 Top 20
topClients = append(topClients, TopNItemJSON{
Key: item.Key,
Count: item.Count,
})
}
// Top N 查询域名
topDomains := make([]TopNItemJSON, 0)
for _, item := range s.topDomains.GetTopN(20) { // 返回 Top 20
topDomains = append(topDomains, TopNItemJSON{
Key: item.Key,
Count: item.Count,
TopClient: item.TopClient,
})
}
return StatsSnapshot{
Runtime: runtimeStats,
Queries: queryStats,
Upstreams: upstreams,
TopClients: topClients,
TopDomains: topDomains,
}
}
// formatDuration 格式化时长为可读格式
func formatDuration(d time.Duration) string {
days := int(d.Hours()) / 24
hours := int(d.Hours()) % 24
minutes := int(d.Minutes()) % 60
seconds := int(d.Seconds()) % 60
if days > 0 {
return formatString("%d天%d小时%d分钟", days, hours, minutes)
} else if hours > 0 {
return formatString("%d小时%d分钟%d秒", hours, minutes, seconds)
} else if minutes > 0 {
return formatString("%d分钟%d秒", minutes, seconds)
}
return formatString("%d秒", seconds)
}
// formatString 简单的字符串格式化
func formatString(format string, args ...interface{}) string {
result := format
for _, arg := range args {
switch v := arg.(type) {
case int:
result = replaceFirst(result, "%d", itoa(v))
}
}
return result
}
// replaceFirst 替换第一个匹配的字符串
func replaceFirst(s, old, new string) string {
for i := 0; i <= len(s)-len(old); i++ {
if s[i:i+len(old)] == old {
return s[:i] + new + s[i+len(old):]
}
}
return s
}
// itoa 整数转字符串
func itoa(i int) string {
if i == 0 {
return "0"
}
negative := i < 0
if negative {
i = -i
}
var buf [32]byte
pos := len(buf)
for i > 0 {
pos--
buf[pos] = byte('0' + i%10)
i /= 10
}
if negative {
pos--
buf[pos] = '-'
}
return string(buf[pos:])
}
// TopNTracker 追踪 Top N 项目,内存可控
type TopNTracker struct {
mu sync.RWMutex
items map[string]*TopNItem
maxItems int // 最大保留项目数
}
// TopNItem Top N 项目统计
type TopNItem struct {
Key string
Count uint64
TopClient string // 对于域名统计,记录查询最多的客户端 IP
clients map[string]uint64 // 临时记录客户端分布(仅用于找 Top1
}
// PersistentStats 持久化统计数据结构
type PersistentStats struct {
StatsStartTime time.Time `json:"stats_start_time"` // 统计开始时间(可持久化)
TotalQueries uint64 `json:"total_queries"`
DoHQueries uint64 `json:"doh_queries"`
CacheHits uint64 `json:"cache_hits"`
CacheMisses uint64 `json:"cache_misses"`
FailedQueries uint64 `json:"failed_queries"`
Upstreams map[string]*PersistentUpstream `json:"upstreams"`
TopClients []PersistentTopNItem `json:"top_clients"`
TopDomains []PersistentTopNItem `json:"top_domains"`
}
// PersistentUpstream 持久化上游服务器统计
type PersistentUpstream struct {
Address string `json:"address"`
TotalQueries uint64 `json:"total_queries"`
Errors uint64 `json:"errors"`
LastUsed time.Time `json:"last_used"`
}
// PersistentTopNItem 持久化 Top N 项目
type PersistentTopNItem struct {
Key string `json:"key"`
Count uint64 `json:"count"`
TopClient string `json:"top_client,omitempty"`
Clients map[string]uint64 `json:"clients,omitempty"`
}
// NewTopNTracker 创建 Top N 追踪器
func NewTopNTracker(maxItems int) *TopNTracker {
return &TopNTracker{
items: make(map[string]*TopNItem),
maxItems: maxItems,
}
}
// Record 记录一次访问(可选关联的客户端 IP
func (t *TopNTracker) Record(key, associatedClient string) {
t.mu.Lock()
defer t.mu.Unlock()
item, exists := t.items[key]
if !exists {
// 如果超过最大数量,删除计数最少的项
if len(t.items) >= t.maxItems {
t.evictLowest()
}
item = &TopNItem{
Key: key,
clients: make(map[string]uint64),
}
t.items[key] = item
}
item.Count++
// 如果有关联客户端,记录客户端分布
if associatedClient != "" {
item.clients[associatedClient]++
// 更新 Top1 客户端
if item.clients[associatedClient] > item.clients[item.TopClient] {
item.TopClient = associatedClient
}
}
}
// evictLowest 删除计数最少的项(不加锁,由调用者加锁)
func (t *TopNTracker) evictLowest() {
var minKey string
var minCount uint64 = ^uint64(0) // 最大值
for key, item := range t.items {
if item.Count < minCount {
minCount = item.Count
minKey = key
}
}
if minKey != "" {
delete(t.items, minKey)
}
}
// GetTopN 获取 Top N 列表
func (t *TopNTracker) GetTopN(n int) []TopNItem {
t.mu.RLock()
defer t.mu.RUnlock()
// 复制所有项
items := make([]TopNItem, 0, len(t.items))
for _, item := range t.items {
items = append(items, TopNItem{
Key: item.Key,
Count: item.Count,
TopClient: item.TopClient,
})
}
// 按查询次数降序排序
sort.Slice(items, func(i, j int) bool {
return items[i].Count > items[j].Count
})
// 返回前 N 项
if n > len(items) {
n = len(items)
}
return items[:n]
}
// Save 保存统计数据到 JSON 文件
func (s *Stats) Save(dataPath string) error {
s.mu.RLock()
defer s.mu.RUnlock()
// 准备持久化数据
persistent := PersistentStats{
StatsStartTime: s.StatsStartTime,
TotalQueries: s.TotalQueries.Load(),
DoHQueries: s.DoHQueries.Load(),
CacheHits: s.CacheHits.Load(),
CacheMisses: s.CacheMisses.Load(),
FailedQueries: s.FailedQueries.Load(),
Upstreams: make(map[string]*PersistentUpstream),
TopClients: make([]PersistentTopNItem, 0),
TopDomains: make([]PersistentTopNItem, 0),
}
// 保存上游服务器统计
for addr, us := range s.upstreamStats {
us.mu.RLock()
persistent.Upstreams[addr] = &PersistentUpstream{
Address: us.Address,
TotalQueries: us.TotalQueries.Load(),
Errors: us.Errors.Load(),
LastUsed: us.LastUsed,
}
us.mu.RUnlock()
}
// 保存 Top 客户端
s.topClients.mu.RLock()
for _, item := range s.topClients.items {
persistent.TopClients = append(persistent.TopClients, PersistentTopNItem{
Key: item.Key,
Count: item.Count,
TopClient: item.TopClient,
Clients: item.clients,
})
}
s.topClients.mu.RUnlock()
// 保存 Top 域名
s.topDomains.mu.RLock()
for _, item := range s.topDomains.items {
persistent.TopDomains = append(persistent.TopDomains, PersistentTopNItem{
Key: item.Key,
Count: item.Count,
TopClient: item.TopClient,
Clients: item.clients,
})
}
s.topDomains.mu.RUnlock()
// 序列化为 JSON
data, err := json.MarshalIndent(persistent, "", " ")
if err != nil {
return err
}
// 确保目录存在
statsPath := filepath.Join(dataPath, "cache")
if err := os.MkdirAll(statsPath, 0755); err != nil {
return err
}
// 写入文件
statsFile := filepath.Join(statsPath, "stats.json")
return os.WriteFile(statsFile, data, 0644)
}
// Load 从 JSON 文件加载统计数据
func (s *Stats) Load(dataPath string) error {
statsFile := filepath.Join(dataPath, "cache", "stats.json")
// 检查文件是否存在
if _, err := os.Stat(statsFile); os.IsNotExist(err) {
return nil // 文件不存在不是错误,返回 nil
}
// 读取文件
data, err := os.ReadFile(statsFile)
if err != nil {
return err
}
// 解析 JSON
var persistent PersistentStats
if err := json.Unmarshal(data, &persistent); err != nil {
return err
}
// 恢复统计数据
s.mu.Lock()
defer s.mu.Unlock()
// StartTime 保持为应用启动时间,不从磁盘恢复
// 只恢复 StatsStartTime统计数据开始时间
s.StatsStartTime = persistent.StatsStartTime
s.TotalQueries.Store(persistent.TotalQueries)
s.DoHQueries.Store(persistent.DoHQueries)
s.CacheHits.Store(persistent.CacheHits)
s.CacheMisses.Store(persistent.CacheMisses)
s.FailedQueries.Store(persistent.FailedQueries)
// 恢复上游服务器统计
for addr, pus := range persistent.Upstreams {
us := &UpstreamStats{
Address: pus.Address,
LastUsed: pus.LastUsed,
}
us.TotalQueries.Store(pus.TotalQueries)
us.Errors.Store(pus.Errors)
s.upstreamStats[addr] = us
}
// 恢复 Top 客户端
s.topClients.mu.Lock()
for _, pitem := range persistent.TopClients {
item := &TopNItem{
Key: pitem.Key,
Count: pitem.Count,
TopClient: pitem.TopClient,
clients: pitem.Clients,
}
if item.clients == nil {
item.clients = make(map[string]uint64)
}
s.topClients.items[pitem.Key] = item
}
s.topClients.mu.Unlock()
// 恢复 Top 域名
s.topDomains.mu.Lock()
for _, pitem := range persistent.TopDomains {
item := &TopNItem{
Key: pitem.Key,
Count: pitem.Count,
TopClient: pitem.TopClient,
clients: pitem.Clients,
}
if item.clients == nil {
item.clients = make(map[string]uint64)
}
s.topDomains.items[pitem.Key] = item
}
s.topDomains.mu.Unlock()
return nil
}