mirror of
https://gitee.com/samwaf/SamWaf.git
synced 2026-11-03 11:41:55 +08:00
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// 日志文件写入
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-7
@@ -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) {
|
||||
|
||||
@@ -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" // 日志文件路径
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user