diff --git a/api/waf_task_api.go b/api/waf_task_api.go index 65b55f8..1390c1e 100644 --- a/api/waf_task_api.go +++ b/api/waf_task_api.go @@ -1,10 +1,12 @@ package api import ( + "SamWaf/common/tasklog" "SamWaf/global" "SamWaf/model/common/response" "SamWaf/model/request" "errors" + "strconv" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -177,3 +179,82 @@ func (w *WafTaskApi) ManualExecuteApi(c *gin.Context) { response.FailWithMessage("解析失败", c) } } + +// GetTaskLogApi 获取任务日志 +// @Summary 获取定时任务日志 +// @Description 读取指定任务方法的日志内容,支持增量加载(通过 offset 参数) +// @Tags 定时任务管理 +// @Accept json +// @Produce json +// @Param task_method query string true "任务方法名" +// @Param lines query int false "最大行数(offset=0时生效,默认200,最大1000)" +// @Param offset query int64 false "文件偏移量(0=从头读取,>0=增量读取)" +// @Success 200 {object} response.Response "获取成功" +// @Security ApiKeyAuth +// @Router /wafhost/task/log [get] +func (w *WafTaskApi) GetTaskLogApi(c *gin.Context) { + taskMethod := c.Query("task_method") + if taskMethod == "" { + response.FailWithMessage("task_method 不能为空", c) + return + } + + linesStr := c.DefaultQuery("lines", "200") + lines, err := strconv.Atoi(linesStr) + if err != nil || lines <= 0 { + lines = 200 + } + if lines > 1000 { + lines = 1000 + } + + offsetStr := c.DefaultQuery("offset", "0") + offset, err := strconv.ParseInt(offsetStr, 10, 64) + if err != nil || offset < 0 { + offset = 0 + } + + if tasklog.GlobalTaskLogManager == nil { + response.FailWithMessage("任务日志管理器未初始化", c) + return + } + + result, err := tasklog.GlobalTaskLogManager.ReadLog(taskMethod, lines, offset) + if err != nil { + response.FailWithMessage("读取任务日志失败: "+err.Error(), c) + return + } + + response.OkWithDetailed(result, "获取成功", c) +} + +// ClearTaskLogApi 清空任务日志 +// @Summary 清空定时任务日志 +// @Description 清空指定任务方法的日志文件内容 +// @Tags 定时任务管理 +// @Accept json +// @Produce json +// @Param task_method query string true "任务方法名" +// @Success 200 {object} response.Response "清空成功" +// @Security ApiKeyAuth +// @Router /wafhost/task/log/clear [get] +func (w *WafTaskApi) ClearTaskLogApi(c *gin.Context) { + taskMethod := c.Query("task_method") + if taskMethod == "" { + response.FailWithMessage("task_method 不能为空", c) + return + } + + if tasklog.GlobalTaskLogManager == nil { + response.FailWithMessage("任务日志管理器未初始化", c) + return + } + + err := tasklog.GlobalTaskLogManager.ClearLog(taskMethod) + if err != nil { + response.FailWithMessage("清空任务日志失败: "+err.Error(), c) + return + } + + response.OkWithMessage("清空成功", c) +} diff --git a/cmd/samwaf/main.go b/cmd/samwaf/main.go index f7a15db..e53cd20 100644 --- a/cmd/samwaf/main.go +++ b/cmd/samwaf/main.go @@ -3,6 +3,7 @@ package main import ( "SamWaf/cache" "SamWaf/common/gwebsocket" + "SamWaf/common/tasklog" "SamWaf/common/zlog" "SamWaf/enums" "SamWaf/global" @@ -51,6 +52,7 @@ import ( dlp "github.com/bytedance/godlp" "github.com/kardianos/service" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "gorm.io/gorm" ) @@ -323,6 +325,11 @@ func (m *wafSystenService) run() { //初始化一次系统参数信息 waftask.TaskLoadSetting(true) + // 初始化任务日志管理器(需在 TaskLoadSetting 后执行,以获取正确的 retainDays 配置) + taskLogDir := filepath.Join(utils.GetCurrentDir(), "logs", "task") + tasklog.InitGlobalTaskLogManager(taskLogDir, int(global.GCONFIG_TASK_LOG_RETAIN_DAYS)) + // 将 TaskZapCore 挂到全局 zlog,使任务执行期间的 zlog 输出同步写入任务日志文件 + zlog.AddCore(tasklog.NewTaskZapCore(zapcore.DebugLevel)) //启动通知相关程序 global.GNOTIFY_KAKFA_SERVICE = wafnotify.InitNotifyKafkaEngine(global.GCONFIG_RECORD_KAFKA_ENABLE, global.GCONFIG_RECORD_KAFKA_URL, global.GCONFIG_RECORD_KAFKA_TOPIC) //kafka // 日志文件写入 diff --git a/common/tasklog/tasklog.go b/common/tasklog/tasklog.go new file mode 100644 index 0000000..2989f25 --- /dev/null +++ b/common/tasklog/tasklog.go @@ -0,0 +1,489 @@ +package tasklog + +import ( + "crypto/md5" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "sync" + "time" + + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +// ---- goroutine 上下文:goroutine ID -> task method ---- + +var goroutineTaskMap sync.Map // key: int64(goroutine ID), value: string(task method) + +// goroutineID 解析当前 goroutine 的 ID(通过 runtime.Stack) +func goroutineID() int64 { + var buf [64]byte + n := runtime.Stack(buf[:], false) + s := strings.TrimPrefix(string(buf[:n]), "goroutine ") + idx := strings.IndexByte(s, ' ') + if idx < 0 { + return -1 + } + id, _ := strconv.ParseInt(s[:idx], 10, 64) + return id +} + +// SetCurrentTask 在当前 goroutine 上注册正在执行的任务方法名 +func SetCurrentTask(taskMethod string) { + goroutineTaskMap.Store(goroutineID(), taskMethod) +} + +// ClearCurrentTask 清除当前 goroutine 的任务上下文 +func ClearCurrentTask() { + goroutineTaskMap.Delete(goroutineID()) +} + +// GetCurrentTask 获取当前 goroutine 关联的任务方法名(若无则返回 "", false) +func GetCurrentTask() (string, bool) { + v, ok := goroutineTaskMap.Load(goroutineID()) + if !ok { + return "", false + } + return v.(string), true +} + +// ---- 自定义 zap Core:将 zlog 输出路由到任务日志文件 ---- + +// TaskZapCore 是一个 zapcore.Core 实现 +// 它在当前 goroutine 有任务上下文时,把日志同步写入对应的任务日志文件 +type TaskZapCore struct { + minLevel zapcore.Level +} + +// NewTaskZapCore 创建 TaskZapCore,minLevel 决定哪些级别的日志写入任务文件 +func NewTaskZapCore(minLevel zapcore.Level) zapcore.Core { + return &TaskZapCore{minLevel: minLevel} +} + +func (c *TaskZapCore) Enabled(level zapcore.Level) bool { + return level >= c.minLevel +} + +func (c *TaskZapCore) With(_ []zapcore.Field) zapcore.Core { + return c +} + +func (c *TaskZapCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if !c.Enabled(entry.Level) { + return ce + } + if GlobalTaskLogManager == nil { + return ce + } + if _, ok := GetCurrentTask(); !ok { + return ce + } + return ce.AddCore(entry, c) +} + +func (c *TaskZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { + if GlobalTaskLogManager == nil { + return nil + } + taskMethod, ok := GetCurrentTask() + if !ok { + return nil + } + level := strings.ToUpper(entry.Level.String()) + + msg := entry.Message + if len(fields) > 0 { + enc := zapcore.NewMapObjectEncoder() + for _, f := range fields { + f.AddTo(enc) + } + var extras []string + for k, v := range enc.Fields { + extras = append(extras, fmt.Sprintf("%s=%v", k, v)) + } + if len(extras) > 0 { + msg = msg + " " + strings.Join(extras, " ") + } + } + + GlobalTaskLogManager.Log(taskMethod, level, msg) + return nil +} + +func (c *TaskZapCore) Sync() error { + return nil +} + +// ---- TaskLogManager ---- + +// rateLimitState 记录某个任务的速率限制状态 +type rateLimitState struct { + lastWriteTime time.Time + suppressedCount int64 +} + +// TaskLogManager 任务日志管理器,为每个任务方法维护独立的日志文件 +type TaskLogManager struct { + logDir string + retainDays int // 全局默认保留天数 + + // writer 相关(受 mu 保护) + mu sync.RWMutex + writers map[string]*lumberjack.Logger + taskRetainDays map[string]int // md5(taskMethod) -> 覆盖保留天数(0 = 使用全局 retainDays) + + // 速率限制相关(受各自 mutex 保护) + policyMu sync.RWMutex + policies map[string]time.Duration // taskMethod -> 最小写入间隔(0 = 不限制) + rateMu sync.Mutex + rateStates map[string]*rateLimitState // taskMethod -> 速率限制状态 +} + +// GlobalTaskLogManager 全局任务日志管理器单例 +var GlobalTaskLogManager *TaskLogManager + +// InitGlobalTaskLogManager 初始化全局任务日志管理器 +func InitGlobalTaskLogManager(logDir string, retainDays int) { + if retainDays <= 0 { + retainDays = 30 + } + GlobalTaskLogManager = &TaskLogManager{ + logDir: logDir, + retainDays: retainDays, + writers: make(map[string]*lumberjack.Logger), + taskRetainDays: make(map[string]int), + policies: make(map[string]time.Duration), + rateStates: make(map[string]*rateLimitState), + } + _ = os.MkdirAll(logDir, 0755) +} + +// methodMD5 计算任务方法名的 MD5,用作文件名 +func methodMD5(taskMethod string) string { + h := md5.New() + _, _ = io.WriteString(h, taskMethod) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +// GetLogFilePath 根据任务方法名返回其日志文件的完整路径 +func (m *TaskLogManager) GetLogFilePath(taskMethod string) string { + return filepath.Join(m.logDir, methodMD5(taskMethod)+".log") +} + +// getEffectiveRetainDays 获取任务实际使用的保留天数(优先使用任务级别覆盖值) +func (m *TaskLogManager) getEffectiveRetainDays(key string) int { + if days, ok := m.taskRetainDays[key]; ok && days > 0 { + return days + } + return m.retainDays +} + +// getWriter 获取或创建对应任务方法的 lumberjack 写入器(内部加锁) +func (m *TaskLogManager) getWriter(taskMethod string) *lumberjack.Logger { + key := methodMD5(taskMethod) + + m.mu.RLock() + w, ok := m.writers[key] + m.mu.RUnlock() + if ok { + return w + } + + m.mu.Lock() + defer m.mu.Unlock() + if w, ok = m.writers[key]; ok { + return w + } + + logFilePath := filepath.Join(m.logDir, key+".log") + w = &lumberjack.Logger{ + Filename: logFilePath, + MaxSize: 50, + MaxBackups: 10, + MaxAge: m.getEffectiveRetainDays(key), + Compress: false, + } + m.writers[key] = w + return w +} + +// SetTaskRetainDays 为指定任务设置独立的日志保留天数(覆盖全局配置) +// days <= 0 表示使用全局默认值 +func (m *TaskLogManager) SetTaskRetainDays(taskMethod string, days int) { + if m == nil { + return + } + key := methodMD5(taskMethod) + m.mu.Lock() + defer m.mu.Unlock() + if days > 0 { + m.taskRetainDays[key] = days + } else { + delete(m.taskRetainDays, key) + } + // 如果 writer 已存在,同步更新其 MaxAge + if w, ok := m.writers[key]; ok { + w.MaxAge = m.getEffectiveRetainDays(key) + } +} + +// SetTaskLogPolicy 为指定任务方法设置最小写入间隔(速率限制) +// minInterval == 0 表示不限制;> 0 时 INFO/DEBUG 按间隔压缩,ERROR/WARN 仍直写 +func (m *TaskLogManager) SetTaskLogPolicy(taskMethod string, minInterval time.Duration) { + if m == nil { + return + } + m.policyMu.Lock() + m.policies[taskMethod] = minInterval + m.policyMu.Unlock() +} + +// getPolicy 获取任务的最小写入间隔(0 = 不限) +func (m *TaskLogManager) getPolicy(taskMethod string) time.Duration { + m.policyMu.RLock() + d := m.policies[taskMethod] + m.policyMu.RUnlock() + return d +} + +// Log 写入一条任务日志 +// ERROR / WARN 始终直接写入;INFO / DEBUG 受速率限制策略约束 +func (m *TaskLogManager) Log(taskMethod, level, message string) { + if m == nil { + return + } + + now := time.Now() + w := m.getWriter(taskMethod) + + isHighPriority := level == "ERROR" || level == "WARN" + minInterval := m.getPolicy(taskMethod) + + if minInterval <= 0 || isHighPriority { + // 高优先级日志:先把积压的压缩摘要冲出来,再写本条 + if isHighPriority { + m.flushSuppressed(taskMethod, w, now) + } + _, _ = w.Write([]byte(formatLogLine(now, level, taskMethod, message))) + return + } + + // INFO / DEBUG 受速率限制 + m.rateMu.Lock() + state, ok := m.rateStates[taskMethod] + if !ok { + state = &rateLimitState{} + m.rateStates[taskMethod] = state + } + + if state.lastWriteTime.IsZero() || now.Sub(state.lastWriteTime) >= minInterval { + suppressed := state.suppressedCount + state.lastWriteTime = now + state.suppressedCount = 0 + m.rateMu.Unlock() + + if suppressed > 0 { + summary := formatLogLine(now, "INFO", taskMethod, + fmt.Sprintf("(已压缩 %d 条日志)", suppressed)) + _, _ = w.Write([]byte(summary)) + } + _, _ = w.Write([]byte(formatLogLine(now, level, taskMethod, message))) + } else { + state.suppressedCount++ + m.rateMu.Unlock() + } +} + +// flushSuppressed 在写高优先级日志前,把积压的压缩摘要冲出来 +func (m *TaskLogManager) flushSuppressed(taskMethod string, w *lumberjack.Logger, now time.Time) { + m.rateMu.Lock() + state, ok := m.rateStates[taskMethod] + if !ok || state.suppressedCount == 0 { + m.rateMu.Unlock() + return + } + suppressed := state.suppressedCount + state.suppressedCount = 0 + state.lastWriteTime = now + m.rateMu.Unlock() + + _, _ = w.Write([]byte(formatLogLine(now, "INFO", taskMethod, + fmt.Sprintf("(已压缩 %d 条日志)", suppressed)))) +} + +// formatLogLine 格式化一行日志 +func formatLogLine(t time.Time, level, taskMethod, message string) string { + return fmt.Sprintf("[%s] [%s] [%s] %s\n", + t.Format("2006-01-02 15:04:05"), + level, + taskMethod, + message, + ) +} + +// LogResult 是 ReadLog 的返回值 +type LogResult struct { + Content string `json:"content"` + NewOffset int64 `json:"new_offset"` + LogFile string `json:"log_file"` + FileSize int64 `json:"file_size"` +} + +// ReadLog 读取任务日志 +// offset=0 从头读取;offset>0 增量读取新增内容 +func (m *TaskLogManager) ReadLog(taskMethod string, lines int, offset int64) (*LogResult, error) { + if m == nil { + return &LogResult{}, nil + } + logFilePath := m.GetLogFilePath(taskMethod) + + f, err := os.Open(logFilePath) + if err != nil { + if os.IsNotExist(err) { + return &LogResult{LogFile: logFilePath}, nil + } + return nil, fmt.Errorf("打开日志文件失败: %w", err) + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return nil, fmt.Errorf("获取日志文件信息失败: %w", err) + } + fileSize := fi.Size() + + if offset > fileSize { + offset = 0 + } + + if offset > 0 { + if offset == fileSize { + return &LogResult{NewOffset: fileSize, LogFile: logFilePath, FileSize: fileSize}, nil + } + _, err = f.Seek(offset, io.SeekStart) + if err != nil { + return nil, fmt.Errorf("定位日志文件失败: %w", err) + } + buf := make([]byte, fileSize-offset) + n, err := f.Read(buf) + if err != nil && err != io.EOF { + return nil, fmt.Errorf("读取日志文件失败: %w", err) + } + return &LogResult{ + Content: string(buf[:n]), + NewOffset: offset + int64(n), + LogFile: logFilePath, + FileSize: fileSize, + }, nil + } + + data, err := io.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("读取日志文件失败: %w", err) + } + + content := string(data) + if lines > 0 { + content = tailLines(content, lines) + } + + return &LogResult{ + Content: content, + NewOffset: fileSize, + LogFile: logFilePath, + FileSize: fileSize, + }, nil +} + +// tailLines 返回文本最后 n 行 +func tailLines(text string, n int) string { + if n <= 0 { + return text + } + lines := strings.Split(text, "\n") + if len(lines) > 0 && lines[len(lines)-1] == "" { + lines = lines[:len(lines)-1] + } + if len(lines) <= n { + return strings.Join(lines, "\n") + "\n" + } + return strings.Join(lines[len(lines)-n:], "\n") + "\n" +} + +// ClearLog 清空指定任务的日志文件 +func (m *TaskLogManager) ClearLog(taskMethod string) error { + if m == nil { + return nil + } + logFilePath := m.GetLogFilePath(taskMethod) + + key := methodMD5(taskMethod) + m.mu.Lock() + if w, ok := m.writers[key]; ok { + _ = w.Close() + delete(m.writers, key) + } + m.mu.Unlock() + + f, err := os.OpenFile(logFilePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return fmt.Errorf("清空日志文件失败: %w", err) + } + return f.Close() +} + +// CleanExpiredLogs 扫描日志目录,删除超过各任务保留策略的过期备份文件 +func (m *TaskLogManager) CleanExpiredLogs() { + if m == nil { + return + } + // 使用全局 retainDays 作为兜底清理基准(单个任务的短保留由 lumberjack MaxAge 自行处理) + cutoff := time.Now().AddDate(0, 0, -m.retainDays) + + entries, err := os.ReadDir(m.logDir) + if err != nil { + return + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + if !strings.HasSuffix(name, ".log") && + !strings.HasSuffix(name, ".gz") && + !strings.Contains(name, ".log-") { + continue + } + info, err := entry.Info() + if err != nil { + continue + } + if info.ModTime().Before(cutoff) { + _ = os.Remove(filepath.Join(m.logDir, name)) + } + } +} + +// UpdateRetainDays 动态更新全局默认保留天数,并刷新未被任务级别覆盖的 writer +func (m *TaskLogManager) UpdateRetainDays(days int) { + if m == nil || days <= 0 { + return + } + m.mu.Lock() + defer m.mu.Unlock() + m.retainDays = days + for key, w := range m.writers { + // 只更新没有任务级别覆盖的 writer + if _, hasOverride := m.taskRetainDays[key]; !hasOverride { + w.MaxAge = days + } + } +} diff --git a/common/zlog/zlog.go b/common/zlog/zlog.go index 1e34dcc..062246e 100644 --- a/common/zlog/zlog.go +++ b/common/zlog/zlog.go @@ -19,6 +19,9 @@ import ( // zlog.Warn("hello", zap.String("name", "Kevin"), zap.Any("arbitraryObj", dummyObject)) var logger *zap.Logger +// baseCores 保存 InitZLog 创建的基础 cores,供 AddCore 重建时使用 +var baseCores []zapcore.Core + // InitZLog 初始化zlog func InitZLog(debugEnable bool, outputFormat string) { encoderConfig := zap.NewProductionEncoderConfig() @@ -35,22 +38,32 @@ func InitZLog(debugEnable bool, outputFormat string) { fileWriteSyncer := getFileLogWriter() if debugEnable == true { - core := zapcore.NewTee( - // 调试默认 + baseCores = []zapcore.Core{ zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel), zapcore.NewCore(encoder, fileWriteSyncer, zapcore.DebugLevel), - ) - logger = zap.New(core) + } } else { - core := zapcore.NewTee( + baseCores = []zapcore.Core{ zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zapcore.InfoLevel), zapcore.NewCore(encoder, fileWriteSyncer, zapcore.InfoLevel), zapcore.NewCore(encoder, fileWriteSyncer, zapcore.ErrorLevel), zapcore.NewCore(encoder, fileWriteSyncer, zapcore.FatalLevel), - ) - logger = zap.New(core) + } } + logger = zap.New(zapcore.NewTee(baseCores...)) +} + +// AddCore 在现有 logger 上追加一个额外的 zapcore.Core(通常在 zlog 初始化之后调用) +// 追加后原有的所有 baseCores 依然保留,新 core 并联写入 +func AddCore(extra zapcore.Core) { + if logger == nil || extra == nil { + return + } + all := make([]zapcore.Core, 0, len(baseCores)+1) + all = append(all, baseCores...) + all = append(all, extra) + logger = zap.New(zapcore.NewTee(all...)) } func getFileLogWriter() (writeSyncer zapcore.WriteSyncer) { diff --git a/global/config.go b/global/config.go index 179bb91..c6ba965 100644 --- a/global/config.go +++ b/global/config.go @@ -73,6 +73,9 @@ var ( // 开放平台配置 GCONFIG_OPEN_PLATFORM_ENABLED int64 = 0 // 开放平台开关,默认关闭 有需要的开启 1启用 0禁用 + // 任务日志配置 + GCONFIG_TASK_LOG_RETAIN_DAYS int64 = 30 // 任务日志保留天数(默认 30 天) + // 日志文件写入配置 (额外输出,不影响SQLite存储) GCONFIG_LOG_FILE_WRITE_ENABLE int64 = 0 // 日志文件写入开关 (0关闭 1开启) GCONFIG_LOG_FILE_WRITE_PATH string = "logs/access.log" // 日志文件路径 diff --git a/router/waf_task_router.go b/router/waf_task_router.go index deda723..f58d709 100644 --- a/router/waf_task_router.go +++ b/router/waf_task_router.go @@ -17,4 +17,6 @@ func (receiver *WafTaskRouter) InitWafTaskRouter(group *gin.RouterGroup) { router.POST("/api/v1/wafhost/task/edit", api.ModifyApi) router.GET("/api/v1/wafhost/task/del", api.DelApi) router.GET("/api/v1/wafhost/task/manual_exec", api.ManualExecuteApi) + router.GET("/api/v1/wafhost/task/log", api.GetTaskLogApi) + router.GET("/api/v1/wafhost/task/log/clear", api.ClearTaskLogApi) } diff --git a/waftask/core_registry.go b/waftask/core_registry.go index c3f3b8f..d97f244 100644 --- a/waftask/core_registry.go +++ b/waftask/core_registry.go @@ -1,11 +1,13 @@ package waftask import ( + "SamWaf/common/tasklog" "SamWaf/common/zlog" "SamWaf/utils" "fmt" "runtime/debug" "sync" + "time" ) // TaskFunc 定义任务执行的函数类型 @@ -53,19 +55,35 @@ func (tr *TaskRegistry) ExecuteTask(taskName string) { // 如果任务锁正在占用,输出提示 if !taskMutex.TryLock() { // 如果锁已经被占用 - zlog.Error(fmt.Sprintf("%s 任务方法 '%s' 正在执行 跳过当前执行", innerName, taskName)) + msg := fmt.Sprintf("%s 任务方法 '%s' 正在执行 跳过当前执行", innerName, taskName) + zlog.Error(msg) + tasklog.GlobalTaskLogManager.Log(taskName, "WARN", "任务正在执行中,跳过本次触发") return } defer taskMutex.Unlock() // 确保任务完成后释放锁 + + // 注册当前 goroutine 的任务上下文,使 zlog 输出同步路由到任务日志文件 + tasklog.SetCurrentTask(taskName) + defer tasklog.ClearCurrentTask() + // 捕获任务执行中的异常并打印详细信息 defer func() { if err := recover(); err != nil { - // 处理异常,打印错误详情 + errMsg := fmt.Sprintf("任务执行出错: %v", err) zlog.Error(fmt.Sprintf("任务 '%s' 执行出错: %v 调试信息:%s", taskName, err, debug.Stack())) debug.PrintStack() + tasklog.GlobalTaskLogManager.Log(taskName, "ERROR", errMsg) } }() + + startTime := time.Now() + tasklog.GlobalTaskLogManager.Log(taskName, "INFO", "任务开始执行") + taskFunc() + + elapsed := time.Since(startTime) + tasklog.GlobalTaskLogManager.Log(taskName, "INFO", + fmt.Sprintf("任务执行完成,耗时 %v", elapsed.Round(time.Millisecond))) }() } else { zlog.Error(fmt.Sprintf("%s 任务方法 '%s' 未找到", innerName, taskName)) diff --git a/waftask/core_scheduler.go b/waftask/core_scheduler.go index e875e8d..6fe4dd6 100644 --- a/waftask/core_scheduler.go +++ b/waftask/core_scheduler.go @@ -1,14 +1,16 @@ package waftask import ( + "SamWaf/common/tasklog" "SamWaf/common/zlog" "SamWaf/enums" "fmt" - "github.com/go-co-op/gocron" "strconv" "strings" "sync" "time" + + "github.com/go-co-op/gocron" ) // TaskScheduler 用于封装任务调度器 @@ -81,9 +83,31 @@ func (ts *TaskScheduler) ScheduleTask(unit string, interval int, at string, task ts.taskTags[taskMethod] = jobTag ts.mu.Unlock() + // 按任务执行频率设置日志策略(速率限制 + 保留天数) + applyTaskLogPolicy(taskMethod, unit, interval) + zlog.Debug(fmt.Sprintf("Task scheduled: %v every %d %s\n", job, interval, unit)) return nil } + +// applyTaskLogPolicy 根据任务执行频率,为其设置日志速率限制和保留策略: +// - 秒级任务:INFO/DEBUG 每 60s 最多一次;日志仅保留 1 天 +// - 分钟级(间隔 ≤ 2 分钟):INFO/DEBUG 每 30s 最多一次;保留天数使用全局默认 +// - 其他任务:不限制,保留天数使用全局默认 +func applyTaskLogPolicy(taskMethod string, unit string, interval int) { + if tasklog.GlobalTaskLogManager == nil { + return + } + switch unit { + case enums.TASK_SECOND: + tasklog.GlobalTaskLogManager.SetTaskLogPolicy(taskMethod, 60*time.Second) + tasklog.GlobalTaskLogManager.SetTaskRetainDays(taskMethod, 1) + case enums.TASK_MIN: + if interval <= 2 { + tasklog.GlobalTaskLogManager.SetTaskLogPolicy(taskMethod, 30*time.Second) + } + } +} func (ts *TaskScheduler) Start() { ts.Scheduler.StartAsync() } diff --git a/waftask/task_config.go b/waftask/task_config.go index fc92a50..551f047 100644 --- a/waftask/task_config.go +++ b/waftask/task_config.go @@ -1,6 +1,7 @@ package waftask import ( + "SamWaf/common/tasklog" "SamWaf/common/zlog" "SamWaf/global" "SamWaf/iplocation" @@ -173,6 +174,12 @@ func setConfigIntValue(name string, value int64, change int) { syncLogFileWriterConfig() case "open_platform_enabled": global.GCONFIG_OPEN_PLATFORM_ENABLED = value + case "task_log_retain_days": + global.GCONFIG_TASK_LOG_RETAIN_DAYS = value + // 动态更新任务日志管理器的保留天数 + if tasklog.GlobalTaskLogManager != nil { + tasklog.GlobalTaskLogManager.UpdateRetainDays(int(value)) + } default: zlog.Warn("Unknown config item:", name) } @@ -437,4 +444,7 @@ func TaskLoadSetting(initLoad bool) { // 开放平台配置 updateConfigIntItem(initLoad, "openplatform", "open_platform_enabled", global.GCONFIG_OPEN_PLATFORM_ENABLED, "开放平台开关(1启用 0禁用)", "options", "0|禁用,1|启用", configMap) + + // 任务日志配置 + updateConfigIntItem(initLoad, "task", "task_log_retain_days", global.GCONFIG_TASK_LOG_RETAIN_DAYS, "任务日志保留天数(默认30天)", "int", "", configMap) }