650 lines
17 KiB
Go
650 lines
17 KiB
Go
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
|
||
}
|