Files
godns/internal/stats/stats.go
2026-01-06 02:25:24 +08:00

650 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}