diff --git a/internal/constant/constant.go b/internal/constant/constant.go index 5751c9c..113469f 100644 --- a/internal/constant/constant.go +++ b/internal/constant/constant.go @@ -60,6 +60,8 @@ const ( KeyPushLogMaxCount = "push_log_max_count" KeyLoginLogDays = "login_log_days" KeyLoginLogMaxCount = "login_log_max_count" + KeySchedulerLogDays = "scheduler_log_days" + KeySchedulerLogMaxCount = "scheduler_log_max_count" // Scheduler Settings Key 常量 KeyWorkerCount = "worker_count" @@ -104,6 +106,7 @@ const ( // 其他事件类型 EventSystemNotice = "system_notice" + EventSchedulerLog = "scheduler_log" EventNotifySent = "notify_sent" EventAppLogAdded = "app_log_added" @@ -153,6 +156,7 @@ const ( LogCategorySystemNotice = "system_notice" LogCategoryPushLog = "push_log" LogCategoryLoginLog = "login_log" + LogCategorySchedulerLog = "scheduler_log" // AppLog 级别 LogLevelInfo = "info" diff --git a/internal/controllers/settings_controller.go b/internal/controllers/settings_controller.go index 72aee83..ad41ab4 100644 --- a/internal/controllers/settings_controller.go +++ b/internal/controllers/settings_controller.go @@ -155,6 +155,8 @@ func (sc *SettingsController) GetSiteSettings(c *gin.Context) { settings["push_log_max_count"] = sc.settingsService.Get(constant.SectionSystem, constant.KeyPushLogMaxCount) settings["login_log_days"] = sc.settingsService.Get(constant.SectionSystem, constant.KeyLoginLogDays) settings["login_log_max_count"] = sc.settingsService.Get(constant.SectionSystem, constant.KeyLoginLogMaxCount) + settings["scheduler_log_days"] = sc.settingsService.Get(constant.SectionSystem, constant.KeySchedulerLogDays) + settings["scheduler_log_max_count"] = sc.settingsService.Get(constant.SectionSystem, constant.KeySchedulerLogMaxCount) utils.Success(c, settings) } @@ -188,6 +190,8 @@ func (sc *SettingsController) UpdateSiteSettings(c *gin.Context) { PushLogMaxCount string `json:"push_log_max_count"` LoginLogDays string `json:"login_log_days"` LoginLogMaxCount string `json:"login_log_max_count"` + SchedulerLogDays string `json:"scheduler_log_days"` + SchedulerLogMaxCount string `json:"scheduler_log_max_count"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -228,6 +232,8 @@ func (sc *SettingsController) UpdateSiteSettings(c *gin.Context) { sc.settingsService.Set(constant.SectionSystem, constant.KeyPushLogMaxCount, req.PushLogMaxCount) sc.settingsService.Set(constant.SectionSystem, constant.KeyLoginLogDays, req.LoginLogDays) sc.settingsService.Set(constant.SectionSystem, constant.KeyLoginLogMaxCount, req.LoginLogMaxCount) + sc.settingsService.Set(constant.SectionSystem, constant.KeySchedulerLogDays, req.SchedulerLogDays) + sc.settingsService.Set(constant.SectionSystem, constant.KeySchedulerLogMaxCount, req.SchedulerLogMaxCount) utils.SuccessMsg(c, "保存成功") } diff --git a/internal/router/events.go b/internal/router/events.go index 4f20481..df0ee99 100644 --- a/internal/router/events.go +++ b/internal/router/events.go @@ -25,8 +25,8 @@ func startAppLogCleanup(appLogSvc *services.AppLogService) { // 初始化时执行一次清理 appLogSvc.CleanUp() - // 每天凌晨或者定期清理 - ticker := time.NewTicker(24 * time.Hour) + // 定期清理(每隔1小时执行一次巡检) + ticker := time.NewTicker(1 * time.Hour) for range ticker.C { appLogSvc.CleanUp() } diff --git a/internal/services/app_log_service.go b/internal/services/app_log_service.go index 9295a24..14c8713 100644 --- a/internal/services/app_log_service.go +++ b/internal/services/app_log_service.go @@ -4,6 +4,7 @@ import ( "time" "fmt" + "strings" "github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/database" @@ -133,6 +134,10 @@ func (s *AppLogService) GetRetentionConfigs() map[string]LogRetentionConfig { Days: utils.ToInt(s.settingsService.Get(constant.SectionSystem, constant.KeyLoginLogDays), 30), MaxCount: utils.ToInt(s.settingsService.Get(constant.SectionSystem, constant.KeyLoginLogMaxCount), 1000), }, + constant.LogCategorySchedulerLog: { + Days: utils.ToInt(s.settingsService.Get(constant.SectionSystem, constant.KeySchedulerLogDays), 30), + MaxCount: utils.ToInt(s.settingsService.Get(constant.SectionSystem, constant.KeySchedulerLogMaxCount), 10000), + }, constant.LogCategoryDefault: { Days: 30, MaxCount: 10000, @@ -141,9 +146,25 @@ func (s *AppLogService) GetRetentionConfigs() map[string]LogRetentionConfig { return configs } +func (s *AppLogService) AddSchedulerLog(title, content, level string) error { + if level == "" { + level = constant.LogLevelInfo + } + return s.Add(&models.AppLog{ + Category: constant.LogCategorySchedulerLog, + Title: title, + Content: models.BigText(content), + Level: level, + Status: constant.LogStatusRead, + }) +} + func (s *AppLogService) CleanUp() { configs := s.GetRetentionConfigs() - categories := []string{constant.LogCategorySystemNotice, constant.LogCategoryPushLog, constant.LogCategoryLoginLog} + categories := []string{constant.LogCategorySystemNotice, constant.LogCategoryPushLog, constant.LogCategoryLoginLog, constant.LogCategorySchedulerLog} + + var totalDeleted int64 + var summaryBuilder strings.Builder for _, cat := range categories { cfg, ok := configs[cat] @@ -151,11 +172,16 @@ func (s *AppLogService) CleanUp() { cfg = configs[constant.LogCategoryDefault] } + var daysDeleted int64 if cfg.Days > 0 { deadline := time.Now().AddDate(0, 0, -cfg.Days) - database.DB.Where("category = ? AND created_at < ?", cat, deadline).Delete(&models.AppLog{}) + res := database.DB.Where("category = ? AND created_at < ?", cat, deadline).Delete(&models.AppLog{}) + if res.Error == nil { + daysDeleted = res.RowsAffected + } } + var countDeleted int64 if cfg.MaxCount > 0 { var total int64 database.DB.Model(&models.AppLog{}).Where("category = ?", cat).Count(&total) @@ -164,12 +190,34 @@ func (s *AppLogService) CleanUp() { var ids []string database.DB.Model(&models.AppLog{}).Where("category = ?", cat).Order("created_at asc").Limit(int(deleteCount)).Pluck("id", &ids) if len(ids) > 0 { - database.DB.Where("id IN ?", ids).Delete(&models.AppLog{}) + res := database.DB.Where("id IN ?", ids).Delete(&models.AppLog{}) + if res.Error == nil { + countDeleted = res.RowsAffected + } } } } + + catTotal := daysDeleted + countDeleted + if catTotal > 0 { + totalDeleted += catTotal + summaryBuilder.WriteString(fmt.Sprintf("分类 [%s]: 过期清理 %d 条,溢出限制清理 %d 条;\n", cat, daysDeleted, countDeleted)) + } + } + + if totalDeleted > 0 { + logger.Infof("[AppLog] 周期巡检完成清理,共删除 %d 条陈旧日志", totalDeleted) + eventbus.DefaultBus.Publish(eventbus.Event{ + Type: constant.EventSystemNotice, + Payload: map[string]interface{}{ + "title": "系统日志定时容量收敛", + "content": fmt.Sprintf("后台巡检已自动执行日志容量清理,共计清除陈旧或溢出记录 %d 条。\n详情明细:\n%s", totalDeleted, summaryBuilder.String()), + "level": constant.LogLevelInfo, + }, + }) + } else { + logger.Debugf("[AppLog] 完成应用日志清理策略,未检测到需要剔除的陈旧记录") } - logger.Debugf("[AppLog] 完成应用日志清理策略") } func (s *AppLogService) SubscribeEvents(bus *eventbus.EventBus) { @@ -281,4 +329,16 @@ func (s *AppLogService) SubscribeEvents(bus *eventbus.EventBus) { }, }) }) + + // 4. [订阅] 调度日志写入 + bus.Subscribe(constant.EventSchedulerLog, func(e eventbus.Event) { + payload, ok := e.Payload.(map[string]interface{}) + if !ok { + return + } + title, _ := payload["title"].(string) + content, _ := payload["content"].(string) + level, _ := payload["level"].(string) + s.AddSchedulerLog(title, content, level) + }) } diff --git a/internal/services/settings_service.go b/internal/services/settings_service.go index 54a1ce3..5ca61e5 100644 --- a/internal/services/settings_service.go +++ b/internal/services/settings_service.go @@ -77,6 +77,8 @@ func (s *SettingsService) InitSettings() error { constant.KeyPushLogMaxCount: "5000", constant.KeyLoginLogDays: "30", constant.KeyLoginLogMaxCount: "1000", + constant.KeySchedulerLogDays: "30", + constant.KeySchedulerLogMaxCount: "10000", } for k, v := range defaultRetention { diff --git a/internal/services/system_ws_service.go b/internal/services/system_ws_service.go index df901ea..ac8b92c 100644 --- a/internal/services/system_ws_service.go +++ b/internal/services/system_ws_service.go @@ -112,6 +112,11 @@ func (m *SystemWSManager) SubscribeEvents(bus *eventbus.EventBus) { bus.Subscribe(constant.EventSystemNotice, func(e eventbus.Event) { m.Broadcast("notice", e.Payload) }) + + // 应用日志新增事件(驱动运行日志下属4大标签页实时流式刷新列表) + bus.Subscribe(constant.EventAppLogAdded, func(e eventbus.Event) { + m.Broadcast(e.Type, e.Payload) + }) } func (c *ClientConnection) Close() { diff --git a/internal/services/tasks/executor_service.go b/internal/services/tasks/executor_service.go index 02c5c54..fad2c4f 100644 --- a/internal/services/tasks/executor_service.go +++ b/internal/services/tasks/executor_service.go @@ -112,6 +112,15 @@ func (es *ExecutorService) initScheduler() { es.scheduler.Start() logger.Infof("[Executor] 调度器已启动: workers=%d, queue=%d, rate=%dms", workerCount, queueSize, rateInterval) + + eventbus.DefaultBus.Publish(eventbus.Event{ + Type: constant.EventSchedulerLog, + Payload: map[string]interface{}{ + "title": "调度器启动", + "content": fmt.Sprintf("主服务任务调度引擎已成功拉起运行。\n配置 Worker 数量: %d\n等待队列容量: %d\n速率限制间隔: %dms", workerCount, queueSize, rateInterval), + "level": constant.LogLevelInfo, + }, + }) } // ServerSchedulerHandler 实现 executor.SchedulerEventHandler @@ -128,6 +137,15 @@ func (h *ServerSchedulerHandler) OnTaskScheduled(req *executor.ExecutionRequest) "status": constant.TaskStatusQueued, }, }) + + eventbus.DefaultBus.Publish(eventbus.Event{ + Type: constant.EventSchedulerLog, + Payload: map[string]interface{}{ + "title": "触发任务", + "content": fmt.Sprintf("任务 [%s] (#%s) 调度类型: %s\n已成功推入后台调度排队队列等待执行。", req.Name, req.TaskID, req.Type), + "level": constant.LogLevelInfo, + }, + }) } } @@ -574,6 +592,15 @@ func (es *ExecutorService) loadCronTasks() { } } logger.Infof("[Executor] 启动调度已加载 %d 个定时任务", count) + + eventbus.DefaultBus.Publish(eventbus.Event{ + Type: constant.EventSchedulerLog, + Payload: map[string]interface{}{ + "title": "加载定时任务", + "content": fmt.Sprintf("后台调度引擎成功解析并载入本地活跃定时计划任务: %d 个。", count), + "level": constant.LogLevelInfo, + }, + }) } // Reload 重新加载配置并重建调度器 @@ -588,6 +615,15 @@ func (es *ExecutorService) Reload() { if es.cronManager != nil { es.cronManager.SetScheduler(es.scheduler) } + + eventbus.DefaultBus.Publish(eventbus.Event{ + Type: constant.EventSchedulerLog, + Payload: map[string]interface{}{ + "title": "调度器重载", + "content": "主服务任务调度引擎配置及执行队列实例已全量重载完毕。", + "level": constant.LogLevelInfo, + }, + }) } // CreateExecutionRequest 统一处理任务到执行请求的转换逻辑(包含指令拼装、脱敏等) diff --git a/web/src/api/index.ts b/web/src/api/index.ts index e5601de..8aea150 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -510,6 +510,8 @@ export interface SiteSettings { push_log_max_count?: string login_log_days?: string login_log_max_count?: string + scheduler_log_days?: string + scheduler_log_max_count?: string } export interface SchedulerSettings { @@ -663,7 +665,8 @@ export interface AppLogListResponse { export const LOG_CATEGORY = { SYSTEM_NOTICE: 'system_notice', PUSH_LOG: 'push_log', - LOGIN_LOG: 'login_log' + LOGIN_LOG: 'login_log', + SCHEDULER_LOG: 'scheduler_log' } as const export const LOG_LEVEL = { diff --git a/web/src/layouts/MainLayout.vue b/web/src/layouts/MainLayout.vue index 25fd37e..f4a139e 100644 --- a/web/src/layouts/MainLayout.vue +++ b/web/src/layouts/MainLayout.vue @@ -62,7 +62,7 @@ const navItems = [ { to: '/languages', icon: Globe, label: '语言依赖', exact: true }, { to: '/terminal', icon: Terminal, label: '终端命令', exact: true }, { to: '/notify', icon: Bell, label: '消息推送', exact: true }, - { to: '/logs', icon: KeyRound, label: '消息日志', exact: true }, + { to: '/logs', icon: KeyRound, label: '运行日志', exact: true }, { to: '/settings', icon: Settings, label: '系统设置', exact: true }, ] diff --git a/web/src/views/logs/MessageLogs.vue b/web/src/views/logs/MessageLogs.vue index 43b192e..34477ab 100644 --- a/web/src/views/logs/MessageLogs.vue +++ b/web/src/views/logs/MessageLogs.vue @@ -8,17 +8,20 @@ import { Search, RefreshCw, Trash2 } from 'lucide-vue-next' import LoginLogTab from './tabs/LoginLogTab.vue' import SystemEventTab from './tabs/SystemEventTab.vue' import PushLogTab from './tabs/PushLogTab.vue' +import SchedulerLogTab from './tabs/SchedulerLogTab.vue' import { LOG_LEVEL, LOG_STATUS } from '@/api' const activeTab = ref('system') const systemTabRef = ref() const pushLogRef = ref() const loginTabRef = ref() +const schedulerTabRef = ref() const filters = ref({ system: { keyword: '', level: 'all' }, push: { keyword: '', status: 'all' }, - login: { username: '' } + login: { username: '' }, + scheduler: { keyword: '', level: 'all' } }) let searchTimer: ReturnType | null = null @@ -39,6 +42,7 @@ async function handleRefresh() { if (activeTab.value === 'system') await systemTabRef.value?.fetchLogs() else if (activeTab.value === 'push') await pushLogRef.value?.fetchLogs() else if (activeTab.value === 'login') await loginTabRef.value?.loadLogs() + else if (activeTab.value === 'scheduler') await schedulerTabRef.value?.fetchLogs() } finally { setTimeout(() => { isRefreshing.value = false @@ -49,6 +53,7 @@ async function handleRefresh() { function handleClear() { if (activeTab.value === 'system' && systemTabRef.value) systemTabRef.value.showClearConfirm = true else if (activeTab.value === 'push' && pushLogRef.value) pushLogRef.value.showClearConfirm = true + else if (activeTab.value === 'scheduler' && schedulerTabRef.value) schedulerTabRef.value.showClearConfirm = true } // 切换标签时重置搜索 @@ -62,17 +67,18 @@ watch(activeTab, () => {
-

消息日志

+

运行日志

{{ activeTab === 'system' ? '查看系统重要运行事件' : - activeTab === 'push' ? '查看消息推送历史记录' : '查看系统用户登录记录' }} + activeTab === 'push' ? '查看消息推送历史记录' : + activeTab === 'scheduler' ? '查看后台调度器执行与配置装载日志' : '查看系统用户登录记录' }}

- +
{ class="h-9 pl-9 w-full bg-muted/20 border-muted-foreground/10 focus:bg-background text-sm" @input="handleSearch" /> +
@@ -146,6 +159,7 @@ watch(activeTab, () => { @@ -159,6 +173,7 @@ watch(activeTab, () => { 系统事件 + 调度日志 推送日志 登录日志 @@ -173,6 +188,10 @@ watch(activeTab, () => {
+
+ +
+
diff --git a/web/src/views/logs/tabs/SchedulerLogTab.vue b/web/src/views/logs/tabs/SchedulerLogTab.vue new file mode 100644 index 0000000..a120440 --- /dev/null +++ b/web/src/views/logs/tabs/SchedulerLogTab.vue @@ -0,0 +1,296 @@ + + + diff --git a/web/src/views/settings/SiteSettings.vue b/web/src/views/settings/SiteSettings.vue index 4b6190e..1a2e53b 100644 --- a/web/src/views/settings/SiteSettings.vue +++ b/web/src/views/settings/SiteSettings.vue @@ -36,7 +36,9 @@ const form = ref({ push_log_days: '15', push_log_max_count: '5000', login_log_days: '30', - login_log_max_count: '1000' + login_log_max_count: '1000', + scheduler_log_days: '30', + scheduler_log_max_count: '10000' }) const loading = ref(false) const showOpenapiConfirmDialog = ref(false) @@ -72,7 +74,9 @@ async function saveSettings() { push_log_days: String(form.value.push_log_days || '15'), push_log_max_count: String(form.value.push_log_max_count || '5000'), login_log_days: String(form.value.login_log_days || '30'), - login_log_max_count: String(form.value.login_log_max_count || '1000') + login_log_max_count: String(form.value.login_log_max_count || '1000'), + scheduler_log_days: String(form.value.scheduler_log_days || '30'), + scheduler_log_max_count: String(form.value.scheduler_log_max_count || '10000') }) await refreshSettings() await loadSettings() @@ -207,6 +211,23 @@ onMounted(loadSettings)
+ +
+ +
+
+ + 天清理 +
+
+ 保留 + + +
+
+
@@ -228,7 +249,7 @@ onMounted(loadSettings)

执行周期说明

- 清理任务在白虎面板后端服务启动时立即执行一次。在运行期间,系统将自动开启后台巡检计数器,每隔 24 小时进行周期性自动清理。 + 清理任务在白虎面板后端服务启动时立即执行一次。在运行期间,系统将自动开启后台巡检计数器,每隔 1 小时进行周期性自动清理。