package accesslogs import ( "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb" "github.com/TeaOSLab/EdgeNode/internal/remotelogs" ) var ( sharedFileWriter *FileWriter sharedOnce sync.Once ) // SharedFileWriter 返回全局本地日志文件写入器(单例) func SharedFileWriter() *FileWriter { sharedOnce.Do(func() { sharedFileWriter = NewFileWriter() }) return sharedFileWriter } const ( defaultLogDir = "/var/log/edge/edge-node" envLogDir = "EDGE_LOG_DIR" ) // FileWriter 将访问/WAF/错误日志以 JSON Lines 写入本地文件,便于 logrotate 与 Fluent Bit 采集 type FileWriter struct { dir string mu sync.Mutex files map[string]*os.File // access.log, waf.log, error.log inited bool } // NewFileWriter 创建本地日志文件写入器 func NewFileWriter() *FileWriter { dir := resolveDefaultLogDir() return &FileWriter{ dir: dir, files: make(map[string]*os.File), } } func resolveDefaultLogDir() string { dir := strings.TrimSpace(os.Getenv(envLogDir)) if dir == "" { return defaultLogDir } return dir } func resolveDirFromPolicyPath(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 *FileWriter) Dir() string { return w.dir } // SetDirByPolicyPath 使用公用日志策略 path 更新目录,空值时回退到 EDGE_LOG_DIR/default。 func (w *FileWriter) SetDirByPolicyPath(policyPath string) { dir := resolveDirFromPolicyPath(policyPath) w.SetDir(dir) } // SetDir 更新日志目录并重置文件句柄。 func (w *FileWriter) SetDir(dir string) { if strings.TrimSpace(dir) == "" { dir = resolveDefaultLogDir() } w.mu.Lock() defer w.mu.Unlock() if dir == w.dir { return } for name, f := range w.files { if f != nil { _ = f.Close() } w.files[name] = nil } w.inited = false w.dir = dir } // IsEnabled 是否启用落盘(目录非空即视为启用) func (w *FileWriter) IsEnabled() bool { return w.dir != "" } // EnsureInit 在启动时预创建日志目录和空文件,便于 Fluent Bit 立即 tail,无需等首条访问日志 func (w *FileWriter) EnsureInit() error { if w.dir == "" { return nil } return w.init() } // init 确保目录存在并打开三个日志文件(仅首次或 Reopen 时) func (w *FileWriter) init() error { w.mu.Lock() defer w.mu.Unlock() if w.inited && len(w.files) > 0 { return nil } if w.dir == "" { return nil } if err := os.MkdirAll(w.dir, 0755); err != nil { remotelogs.Error("ACCESS_LOG_FILE", "mkdir log dir failed: "+err.Error()) return err } for _, name := range []string{"access.log", "waf.log", "error.log"} { if w.files[name] != nil { continue } fp, err := os.OpenFile(filepath.Join(w.dir, name), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { remotelogs.Error("ACCESS_LOG_FILE", "open "+name+" failed: "+err.Error()) continue } w.files[name] = fp } w.inited = true return nil } // Write 将一条访问日志按 log_type 写入对应文件(access.log / waf.log / error.log) func (w *FileWriter) Write(l *pb.HTTPAccessLog, clusterId int64) { if w.dir == "" { return } if err := w.init(); err != nil || len(w.files) == 0 { return } ingest, logType := FromHTTPAccessLog(l, clusterId) line, err := json.Marshal(ingest) if err != nil { remotelogs.Error("ACCESS_LOG_FILE", "marshal ingest log: "+err.Error()) return } var fileName string switch logType { case LogTypeWAF: fileName = "waf.log" case LogTypeError: fileName = "error.log" default: fileName = "access.log" } w.mu.Lock() fp := w.files[fileName] w.mu.Unlock() if fp == nil { return } // 单行写入,末尾换行,便于 Fluent Bit / JSON 解析 _, err = fp.Write(append(line, '\n')) if err != nil { remotelogs.Error("ACCESS_LOG_FILE", "write "+fileName+" failed: "+err.Error()) } } // WriteBatch 批量写入,减少锁竞争 func (w *FileWriter) WriteBatch(logs []*pb.HTTPAccessLog, clusterId int64) { if w.dir == "" || len(logs) == 0 { return } if err := w.init(); err != nil || len(w.files) == 0 { return } w.mu.Lock() accessFp := w.files["access.log"] wafFp := w.files["waf.log"] errorFp := w.files["error.log"] w.mu.Unlock() if accessFp == nil && wafFp == nil && errorFp == nil { return } for _, l := range logs { ingest, logType := FromHTTPAccessLog(l, clusterId) line, err := json.Marshal(ingest) if err != nil { continue } line = append(line, '\n') var fp *os.File switch logType { case LogTypeWAF: fp = wafFp case LogTypeError: fp = errorFp default: fp = accessFp } if fp != nil { _, _ = fp.Write(line) } } } // Reopen 关闭并重新打开所有日志文件(供 logrotate copytruncate 或 SIGHUP 后重开句柄) func (w *FileWriter) Reopen() error { if w.dir == "" { return nil } w.mu.Lock() defer w.mu.Unlock() for name, f := range w.files { if f != nil { _ = f.Close() w.files[name] = nil } } w.inited = false return w.init() } // Close 关闭所有已打开的文件 func (w *FileWriter) Close() error { w.mu.Lock() defer w.mu.Unlock() var lastErr error for name, f := range w.files { if f != nil { if err := f.Close(); err != nil { lastErr = err remotelogs.Error("ACCESS_LOG_FILE", fmt.Sprintf("close %s: %v", name, err)) } w.files[name] = nil } } w.inited = false return lastErr }