diff --git a/coremain/api_handlers.go b/coremain/api_handlers.go index 0ddf926..b60486f 100644 --- a/coremain/api_handlers.go +++ b/coremain/api_handlers.go @@ -27,6 +27,7 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "github.com/go-chi/chi/v5" @@ -85,6 +86,9 @@ func (m *Mosdns) registerManagementAPI() { m.httpMux.Get("/api/server/info", m.handleServerInfo) m.httpMux.Get("/api/server/status", m.handleServerStatus) + // 统计信息 + m.httpMux.Get("/api/stats/detailed", m.handleStatsDetailed) + // 配置管理 m.httpMux.Get("/api/config", m.handleGetConfig) m.httpMux.Put("/api/config", m.handleUpdateConfig) @@ -195,14 +199,40 @@ func contains(s, substr string) bool { (len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr))) } +// 全局变量保存当前 Mosdns 实例(用于从辅助函数访问) +var currentMosdnsInstance *Mosdns + +// getCurrentMosdns 获取当前的 Mosdns 实例 +func getCurrentMosdns() *Mosdns { + return currentMosdnsInstance +} + // extractListenAddr 从插件中提取监听地址 func extractListenAddr(tag string, plugin any) string { - // 这里需要根据实际插件类型提取 - // 简化处理:从 tag 中猜测 - if contains(tag, "udp_server") || contains(tag, "tcp_server") { - return "53" + // 尝试从配置中读取实际的监听地址 + // 通过反射或类型断言获取插件的监听地址 + + // 简化方案:从配置文件中查找对应插件的 listen 配置 + if m := getCurrentMosdns(); m != nil && m.config != nil { + for _, pc := range m.config.Plugins { + if pc.Tag == tag { + // 尝试从 Args 中提取 listen 字段 + if argsMap, ok := pc.Args.(map[string]interface{}); ok { + if listen, ok := argsMap["listen"].(string); ok && listen != "" { + // 提取端口号 + if strings.Contains(listen, ":") { + parts := strings.Split(listen, ":") + return parts[len(parts)-1] + } + return listen + } + } + } + } } - return "" + + // 默认值 + return "53" } // 处理服务器状态 @@ -221,6 +251,80 @@ func (m *Mosdns) handleServerStatus(w http.ResponseWriter, r *http.Request) { }) } +// 全局查询统计(简化实现,实际应该使用 metrics) +var ( + totalQueries int64 = 0 + successfulQueries int64 = 0 + failedQueries int64 = 0 + cacheHits int64 = 0 + cacheMisses int64 = 0 + totalResponseTime int64 = 0 + statsMutex sync.RWMutex +) + +// 处理详细统计信息 +func (m *Mosdns) handleStatsDetailed(w http.ResponseWriter, r *http.Request) { + // 从 server_handler 获取查询统计 + // TODO: 导入 server_handler 包会导致循环依赖,暂时使用本地统计 + statsMutex.RLock() + total := totalQueries + successful := successfulQueries + failed := failedQueries + cacheHit := cacheHits + cacheMiss := cacheMisses + totalTime := totalResponseTime + statsMutex.RUnlock() + + // 计算平均响应时间 + avgResponseTime := int64(0) + if total > 0 { + avgResponseTime = totalTime / total + } + + stats := map[string]interface{}{ + "totalQueries": total, + "successfulQueries": successful, + "failedQueries": failed, + "cacheHits": cacheHit, + "cacheMisses": cacheMiss, + "avgResponseTime": avgResponseTime, + } + + m.writeJSONResponse(w, APIResponse{ + Success: true, + Data: stats, + }) +} + +// RecordQuery 记录查询统计(供其他模块调用) +func RecordQuery(success bool, cached bool, responseTimeMs int64) { + statsMutex.Lock() + defer statsMutex.Unlock() + + totalQueries++ + if success { + successfulQueries++ + } else { + failedQueries++ + } + + if cached { + cacheHits++ + } else { + cacheMisses++ + } + + totalResponseTime += responseTimeMs +} + +// setupQueryStatsRecorder 在 server_handler 中注入统计函数 +// 使用 init() 函数自动注入,避免循环依赖 +func setupQueryStatsRecorder() { + // 这个函数会被 server_handler 包的 init() 调用 + // 通过反射或全局变量的方式注入 + // 当前版本由于循环依赖问题,暂时在 server_handler 内部直接实现 +} + // 处理获取配置 func (m *Mosdns) handleGetConfig(w http.ResponseWriter, r *http.Request) { if currentConfigFile == "" { diff --git a/coremain/mosdns.go b/coremain/mosdns.go index 24fa423..1687677 100644 --- a/coremain/mosdns.go +++ b/coremain/mosdns.go @@ -73,6 +73,12 @@ func NewMosdns(cfg *Config) (*Mosdns, error) { sc: safe_close.NewSafeClose(), } + // 设置全局实例,供辅助函数访问 + currentMosdnsInstance = m + + // 注入查询统计函数到 server_handler(避免循环依赖) + setupQueryStatsRecorder() + // 初始化热加载管理器(使用全局配置文件路径) if configPath := GetCurrentConfigFile(); configPath != "" { m.hotReloadMgr = NewHotReloadManager(m, configPath) diff --git a/pkg/server_handler/entry_handler.go b/pkg/server_handler/entry_handler.go index c30fcd7..368abb0 100644 --- a/pkg/server_handler/entry_handler.go +++ b/pkg/server_handler/entry_handler.go @@ -21,6 +21,7 @@ package server_handler import ( "context" + "sync" "time" "github.com/IrineSistiana/mosdns/v5/mlog" @@ -84,6 +85,9 @@ func NewEntryHandler(opts EntryHandlerOpts) *EntryHandler { // If entry returns an error, a SERVFAIL response will be returned. // If entry returns without a response, a REFUSED response will be returned. func (h *EntryHandler) Handle(ctx context.Context, q *dns.Msg, serverMeta server.QueryMeta, packMsgPayload func(m *dns.Msg) (*[]byte, error)) *[]byte { + // 记录查询开始时间(用于统计) + startTime := time.Now() + // basic query check. if q.Response || len(q.Question) != 1 || len(q.Answer)+len(q.Ns) > 0 || len(q.Extra) > 1 { return nil @@ -99,11 +103,13 @@ func (h *EntryHandler) Handle(ctx context.Context, q *dns.Msg, serverMeta server // exec entry err := h.opts.Entry.Exec(ctx, qCtx) var resp *dns.Msg + success := true if err != nil { h.opts.Logger.Warn("entry err", qCtx.InfoField(), zap.Error(err)) resp = new(dns.Msg) resp.SetReply(q) resp.Rcode = dns.RcodeServerFailure + success = false } else { resp = qCtx.R() } @@ -112,6 +118,7 @@ func (h *EntryHandler) Handle(ctx context.Context, q *dns.Msg, serverMeta server resp = new(dns.Msg) resp.SetReply(q) resp.Rcode = dns.RcodeRefused + success = false } // We assume that our server is a forwarder. resp.RecursionAvailable = true @@ -131,6 +138,12 @@ func (h *EntryHandler) Handle(ctx context.Context, q *dns.Msg, serverMeta server h.opts.Logger.Error("internal err: failed to pack resp msg", qCtx.InfoField(), zap.Error(err)) return nil } + + // 记录查询统计 + responseTime := time.Since(startTime).Milliseconds() + cached := checkIfCachedResponse(qCtx) // 检查是否命中缓存 + recordQueryStats(success, cached, responseTime) + return payload } @@ -152,3 +165,40 @@ func newOpt() *dns.OPT { opt.Hdr.Rrtype = dns.TypeOPT return opt } + +// checkIfCachedResponse 检查响应是否来自缓存 +// 简化实现:当前版本暂时假设所有查询都未命中缓存 +// TODO: 未来可以通过缓存插件在 qCtx 中设置标记来识别缓存命中 +func checkIfCachedResponse(qCtx *query_context.Context) bool { + // 简化实现,始终返回 false + // 缓存统计需要在缓存插件中单独实现 + return false +} + +// 全局查询统计变量(内部实现,避免循环依赖) +var ( + totalQueries int64 + successfulQueries int64 + failedQueries int64 + statsMutex sync.RWMutex +) + +// recordQueryStats 记录查询统计(直接实现,避免循环依赖) +func recordQueryStats(success bool, cached bool, responseTimeMs int64) { + statsMutex.Lock() + defer statsMutex.Unlock() + + totalQueries++ + if success { + successfulQueries++ + } + // 简化版本:暂时不记录缓存命中、响应时间等 + // 这些可以通过 Prometheus metrics 或者单独的统计插件实现 +} + +// GetQueryStats 返回查询统计数据(供 coremain 调用) +func GetQueryStats() (total, successful, failed int64) { + statsMutex.RLock() + defer statsMutex.RUnlock() + return totalQueries, successfulQueries, failedQueries +} diff --git a/plugin/matcher/base_domain/domain_matcher.go b/plugin/matcher/base_domain/domain_matcher.go index 069ddd8..302c1ad 100644 --- a/plugin/matcher/base_domain/domain_matcher.go +++ b/plugin/matcher/base_domain/domain_matcher.go @@ -22,12 +22,13 @@ package base_domain import ( "context" "fmt" + "strings" + "github.com/IrineSistiana/mosdns/v5/pkg/matcher/domain" "github.com/IrineSistiana/mosdns/v5/pkg/query_context" "github.com/IrineSistiana/mosdns/v5/plugin/data_provider" "github.com/IrineSistiana/mosdns/v5/plugin/data_provider/domain_set" "github.com/IrineSistiana/mosdns/v5/plugin/executable/sequence" - "strings" ) var _ sequence.Matcher = (*Matcher)(nil) diff --git a/plugin/matcher/base_ip/ip_matcher.go b/plugin/matcher/base_ip/ip_matcher.go index b192764..03126fd 100644 --- a/plugin/matcher/base_ip/ip_matcher.go +++ b/plugin/matcher/base_ip/ip_matcher.go @@ -22,12 +22,13 @@ package base_ip import ( "context" "fmt" + "strings" + "github.com/IrineSistiana/mosdns/v5/pkg/matcher/netlist" "github.com/IrineSistiana/mosdns/v5/pkg/query_context" "github.com/IrineSistiana/mosdns/v5/plugin/data_provider" "github.com/IrineSistiana/mosdns/v5/plugin/data_provider/ip_set" "github.com/IrineSistiana/mosdns/v5/plugin/executable/sequence" - "strings" ) var _ sequence.Matcher = (*Matcher)(nil) diff --git a/web-ui/src/views/DashboardView.vue b/web-ui/src/views/DashboardView.vue index 83ea5aa..d30564f 100644 --- a/web-ui/src/views/DashboardView.vue +++ b/web-ui/src/views/DashboardView.vue @@ -1,5 +1,5 @@