update once
This commit is contained in:
649
internal/stats/stats.go
Normal file
649
internal/stats/stats.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user