package accesslogs import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs" "github.com/TeaOSLab/EdgeDNS/internal/remotelogs" "gopkg.in/natefinch/lumberjack.v2" ) const ( defaultDNSLogDir = "/var/log/edge/edge-dns" envDNSLogDir = "EDGE_DNS_LOG_DIR" ) var ( sharedDNSFileWriter *DNSFileWriter sharedDNSFileWriterOnce sync.Once ) // SharedDNSFileWriter 返回 DNS 本地日志写入器(单例). func SharedDNSFileWriter() *DNSFileWriter { sharedDNSFileWriterOnce.Do(func() { sharedDNSFileWriter = NewDNSFileWriter() }) return sharedDNSFileWriter } // DNSFileWriter 将 DNS 访问日志以 JSON Lines 写入本地文件,供 Fluent Bit 采集。 // 文件轮转由 lumberjack 内建完成。 type DNSFileWriter struct { dir string mu sync.Mutex file *lumberjack.Logger rotateConfig *serverconfigs.AccessLogRotateConfig inited bool } // NewDNSFileWriter 创建 DNS 本地日志写入器. func NewDNSFileWriter() *DNSFileWriter { logDir := resolveDefaultDNSLogDir() return &DNSFileWriter{ dir: logDir, rotateConfig: serverconfigs.NewDefaultAccessLogRotateConfig(), } } func resolveDefaultDNSLogDir() string { logDir := strings.TrimSpace(os.Getenv(envDNSLogDir)) if logDir == "" { return defaultDNSLogDir } return logDir } func resolveDNSDirFromPolicyPath(policyPath string) string { policyPath = strings.TrimSpace(policyPath) if policyPath == "" { return "" } if strings.HasSuffix(policyPath, "/") || strings.HasSuffix(policyPath, "\\") { return filepath.Clean(policyPath) } baseName := filepath.Base(policyPath) if strings.Contains(baseName, ".") || strings.Contains(baseName, "${") { return filepath.Clean(filepath.Dir(policyPath)) } return filepath.Clean(policyPath) } // Dir 返回当前日志目录. func (w *DNSFileWriter) Dir() string { return w.dir } // SetDirByPolicyPath 使用公用日志策略 path 更新目录,空值时回退到 EDGE_DNS_LOG_DIR/default。 func (w *DNSFileWriter) SetDirByPolicyPath(policyPath string) { dir := resolveDNSDirFromPolicyPath(policyPath) w.SetDir(dir) } // SetDir 更新目录并重置文件句柄。 func (w *DNSFileWriter) SetDir(dir string) { if strings.TrimSpace(dir) == "" { dir = resolveDefaultDNSLogDir() } w.mu.Lock() defer w.mu.Unlock() if dir == w.dir { return } if w.file != nil { _ = w.file.Close() w.file = nil } w.inited = false w.dir = dir } // SetRotateConfig 更新日志轮转配置并重建 writer。 func (w *DNSFileWriter) SetRotateConfig(config *serverconfigs.AccessLogRotateConfig) { normalized := config.Normalize() w.mu.Lock() defer w.mu.Unlock() if equalDNSRotateConfig(w.rotateConfig, normalized) { return } if w.file != nil { _ = w.file.Close() w.file = nil } w.inited = false w.rotateConfig = normalized } // EnsureInit 在启动时预创建目录与 access.log. func (w *DNSFileWriter) EnsureInit() error { if w.dir == "" { return nil } return w.init() } func (w *DNSFileWriter) init() error { w.mu.Lock() defer w.mu.Unlock() if w.inited && w.file != nil { return nil } if w.dir == "" { return nil } if err := os.MkdirAll(w.dir, 0755); err != nil { remotelogs.Error("DNS_ACCESS_LOG_FILE", "mkdir log dir failed: "+err.Error()) return err } rotateConfig := w.rotateConfig.Normalize() w.file = &lumberjack.Logger{ Filename: filepath.Join(w.dir, "access.log"), MaxSize: rotateConfig.MaxSizeMB, MaxBackups: rotateConfig.MaxBackups, MaxAge: rotateConfig.MaxAgeDays, Compress: *rotateConfig.Compress, LocalTime: *rotateConfig.LocalTime, } w.inited = true return nil } // WriteBatch 批量写入 DNS 访问日志. func (w *DNSFileWriter) WriteBatch(logs []*pb.NSAccessLog, clusterId int64) { if len(logs) == 0 || w.dir == "" { return } if err := w.init(); err != nil { return } w.mu.Lock() file := w.file w.mu.Unlock() if file == nil { return } for _, logItem := range logs { ingestLog := FromNSAccessLog(logItem, clusterId) if ingestLog == nil { continue } line, err := json.Marshal(ingestLog) if err != nil { continue } _, _ = file.Write(append(line, '\n')) } } // Reopen 关闭并重建日志 writer(供兼容调用). func (w *DNSFileWriter) Reopen() error { w.mu.Lock() if w.file != nil { _ = w.file.Close() w.file = nil } w.inited = false w.mu.Unlock() return w.init() } // Close 关闭日志文件. func (w *DNSFileWriter) Close() error { w.mu.Lock() defer w.mu.Unlock() if w.file == nil { return nil } err := w.file.Close() w.file = nil w.inited = false if err != nil { remotelogs.Error("DNS_ACCESS_LOG_FILE", fmt.Sprintf("close access.log failed: %v", err)) return err } return nil } func equalDNSRotateConfig(left *serverconfigs.AccessLogRotateConfig, right *serverconfigs.AccessLogRotateConfig) bool { if left == nil || right == nil { return left == right } return left.MaxSizeMB == right.MaxSizeMB && left.MaxBackups == right.MaxBackups && left.MaxAgeDays == right.MaxAgeDays && *left.Compress == *right.Compress && *left.LocalTime == *right.LocalTime }