diff --git a/handlers/log_handler.go b/handlers/log_handler.go new file mode 100644 index 0000000..ed89a20 --- /dev/null +++ b/handlers/log_handler.go @@ -0,0 +1,188 @@ +package handlers + +import ( + "net/http" + "strconv" + "time" + + "github.com/ctwj/urldb/utils" + + "github.com/gin-gonic/gin" +) + +// GetSystemLogs 获取系统日志 +func GetSystemLogs(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50")) + level := c.Query("level") + startDateStr := c.Query("start_date") + endDateStr := c.Query("end_date") + search := c.Query("search") + + var startDate, endDate *time.Time + + if startDateStr != "" { + if parsed, err := time.Parse("2006-01-02", startDateStr); err == nil { + startDate = &parsed + } + } + + if endDateStr != "" { + if parsed, err := time.Parse("2006-01-02", endDateStr); err == nil { + // 设置为当天结束时间 + endOfDay := parsed.Add(24*time.Hour - time.Second) + endDate = &endOfDay + } + } + + // 使用日志查看器获取日志 + logViewer := utils.NewLogViewer("logs") + + // 获取日志文件列表 + logFiles, err := logViewer.GetLogFiles() + if err != nil { + ErrorResponse(c, "获取日志文件失败: "+err.Error(), http.StatusInternalServerError) + return + } + + // 如果指定了日期范围,只选择对应日期的日志文件 + if startDate != nil || endDate != nil { + var filteredFiles []string + for _, file := range logFiles { + fileInfo, err := utils.GetFileInfo(file) + if err != nil { + continue + } + + shouldInclude := true + if startDate != nil { + if fileInfo.ModTime().Before(*startDate) { + shouldInclude = false + } + } + if endDate != nil { + if fileInfo.ModTime().After(*endDate) { + shouldInclude = false + } + } + + if shouldInclude { + filteredFiles = append(filteredFiles, file) + } + } + logFiles = filteredFiles + } + + // 限制读取的文件数量以提高性能 + if len(logFiles) > 10 { + logFiles = logFiles[:10] // 只处理最近的10个文件 + } + + var allLogs []utils.LogEntry + for _, file := range logFiles { + // 读取日志文件 + fileLogs, err := logViewer.ParseLogEntriesFromFile(file, level, search) + if err != nil { + utils.Error("解析日志文件失败 %s: %v", file, err) + continue + } + allLogs = append(allLogs, fileLogs...) + } + + // 按时间排序(最新的在前) + utils.SortLogEntriesByTime(allLogs, false) + + // 应用分页 + start := (page - 1) * pageSize + end := start + pageSize + if start > len(allLogs) { + start = len(allLogs) + } + if end > len(allLogs) { + end = len(allLogs) + } + + pagedLogs := allLogs[start:end] + + SuccessResponse(c, gin.H{ + "data": pagedLogs, + "total": len(allLogs), + "page": page, + "limit": pageSize, + }) +} + +// GetSystemLogFiles 获取系统日志文件列表 +func GetSystemLogFiles(c *gin.Context) { + logViewer := utils.NewLogViewer("logs") + files, err := logViewer.GetLogFiles() + if err != nil { + ErrorResponse(c, "获取日志文件列表失败: "+err.Error(), http.StatusInternalServerError) + return + } + + // 获取每个文件的详细信息 + var fileInfos []gin.H + for _, file := range files { + info, err := utils.GetFileInfo(file) + if err != nil { + continue + } + fileInfos = append(fileInfos, gin.H{ + "name": info.Name(), + "size": info.Size(), + "mod_time": info.ModTime(), + "path": file, + }) + } + + SuccessResponse(c, gin.H{ + "data": fileInfos, + }) +} + +// GetSystemLogSummary 获取系统日志统计摘要 +func GetSystemLogSummary(c *gin.Context) { + logViewer := utils.NewLogViewer("logs") + files, err := logViewer.GetLogFiles() + if err != nil { + ErrorResponse(c, "获取日志文件列表失败: "+err.Error(), http.StatusInternalServerError) + return + } + + // 获取统计信息 + stats, err := logViewer.GetLogStats(files) + if err != nil { + ErrorResponse(c, "获取日志统计信息失败: "+err.Error(), http.StatusInternalServerError) + return + } + + SuccessResponse(c, gin.H{ + "summary": stats, + "files_count": len(files), + }) +} + +// ClearSystemLogs 清理系统日志 +func ClearSystemLogs(c *gin.Context) { + daysStr := c.Query("days") + if daysStr == "" { + ErrorResponse(c, "请提供要清理的天数参数", http.StatusBadRequest) + return + } + + days, err := strconv.Atoi(daysStr) + if err != nil || days < 1 { + ErrorResponse(c, "无效的天数参数", http.StatusBadRequest) + return + } + + logViewer := utils.NewLogViewer("logs") + err = logViewer.CleanOldLogs(days) + if err != nil { + ErrorResponse(c, "清理系统日志失败: "+err.Error(), http.StatusInternalServerError) + return + } + + SuccessResponse(c, gin.H{"message": "系统日志清理成功"}) +} \ No newline at end of file diff --git a/main.go b/main.go index 9c8f917..725e860 100644 --- a/main.go +++ b/main.go @@ -307,6 +307,12 @@ func main() { api.GET("/api-access-logs/stats", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetAPIAccessLogStats) api.DELETE("/api-access-logs", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ClearAPIAccessLogs) + // 系统日志路由 + api.GET("/system-logs", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemLogs) + api.GET("/system-logs/files", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemLogFiles) + api.GET("/system-logs/summary", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemLogSummary) + api.DELETE("/system-logs", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ClearSystemLogs) + // 系统配置路由 api.GET("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemConfig) api.POST("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateSystemConfig) diff --git a/utils/log_viewer.go b/utils/log_viewer.go index 2f85264..43b6373 100644 --- a/utils/log_viewer.go +++ b/utils/log_viewer.go @@ -2,6 +2,7 @@ package utils import ( "bufio" + "encoding/json" "fmt" "os" "path/filepath" @@ -13,11 +14,23 @@ import ( // LogEntry 日志条目 type LogEntry struct { - Timestamp time.Time - Level string - Message string - File string - Line int + Timestamp time.Time `json:"timestamp"` + Level string `json:"level"` + Message string `json:"message"` + File string `json:"file"` + Line int `json:"line"` +} + +// 为LogEntry实现自定义JSON序列化 +func (le LogEntry) MarshalJSON() ([]byte, error) { + type Alias LogEntry + return json.Marshal(&struct { + *Alias + Timestamp string `json:"timestamp"` + }{ + Alias: (*Alias)(&le), + Timestamp: le.Timestamp.Format("2006-01-02T15:04:05Z07:00"), + }) } // LogViewer 日志查看器 @@ -201,6 +214,76 @@ func (lv *LogViewer) GetLogStats(files []string) (map[string]int, error) { return stats, nil } +// ParseLogEntriesFromFile 从文件中解析日志条目 +func (lv *LogViewer) ParseLogEntriesFromFile(filename string, levelFilter string, searchFilter string) ([]LogEntry, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + var results []LogEntry + scanner := bufio.NewScanner(file) + lineNum := 0 + + for scanner.Scan() { + lineNum++ + line := scanner.Text() + + // 如果指定了级别过滤器,检查日志级别 + if levelFilter != "" { + levelPrefix := "[" + strings.ToUpper(levelFilter) + "]" + if !strings.Contains(line, levelPrefix) { + continue + } + } + + // 如果指定了搜索过滤器,检查是否包含搜索词 + if searchFilter != "" { + if !strings.Contains(strings.ToLower(line), strings.ToLower(searchFilter)) { + continue + } + } + + entry := lv.parseLogLine(line) + // 如果解析失败且行不为空,创建一个基本条目 + if entry.Message == line && entry.Level == "" { + // 尝试从行中提取级别 + if strings.Contains(line, "[DEBUG]") { + entry.Level = "DEBUG" + } else if strings.Contains(line, "[INFO]") { + entry.Level = "INFO" + } else if strings.Contains(line, "[WARN]") { + entry.Level = "WARN" + } else if strings.Contains(line, "[ERROR]") { + entry.Level = "ERROR" + } else if strings.Contains(line, "[FATAL]") { + entry.Level = "FATAL" + } else { + entry.Level = "UNKNOWN" + } + } + results = append(results, entry) + } + + return results, scanner.Err() +} + +// SortLogEntriesByTime 按时间对日志条目进行排序 +func SortLogEntriesByTime(entries []LogEntry, ascending bool) { + sort.Slice(entries, func(i, j int) bool { + if ascending { + return entries[i].Timestamp.Before(entries[j].Timestamp) + } + return entries[i].Timestamp.After(entries[j].Timestamp) + }) +} + +// GetFileInfo 获取文件信息 +func GetFileInfo(filepath string) (os.FileInfo, error) { + return os.Stat(filepath) +} + // getFileStats 获取单个文件的统计信息 func (lv *LogViewer) getFileStats(filename string) (map[string]int, error) { file, err := os.Open(filename) diff --git a/web/composables/useApi.ts b/web/composables/useApi.ts index b0dbb5a..d8d5b84 100644 --- a/web/composables/useApi.ts +++ b/web/composables/useApi.ts @@ -340,4 +340,18 @@ export const useApiAccessLogApi = () => { getApiAccessLogStats, clearApiAccessLogs } +} + +// 系统日志管理API +export const useSystemLogApi = () => { + const getSystemLogs = (params?: any) => useApiFetch('/api/system-logs', { params }).then(parseApiResponse) + const getSystemLogFiles = () => useApiFetch('/api/system-logs/files').then(parseApiResponse) + const getSystemLogSummary = () => useApiFetch('/api/system-logs/summary').then(parseApiResponse) + const clearSystemLogs = (days: number) => useApiFetch('/api/system-logs', { method: 'DELETE', body: { days } }).then(parseApiResponse) + return { + getSystemLogs, + getSystemLogFiles, + getSystemLogSummary, + clearSystemLogs + } } \ No newline at end of file diff --git a/web/layouts/admin.vue b/web/layouts/admin.vue index 13035a5..05ca604 100644 --- a/web/layouts/admin.vue +++ b/web/layouts/admin.vue @@ -407,6 +407,12 @@ const userMenuItems = computed(() => [ label: 'API访问日志', type: 'link' }, + { + to: '/admin/system-logs', + icon: 'fas fa-file-alt', + label: '系统日志', + type: 'link' + }, { to: '/admin/version', icon: 'fas fa-code-branch', diff --git a/web/pages/admin/api-access-logs.vue b/web/pages/admin/api-access-logs.vue index daaec75..9e61137 100644 --- a/web/pages/admin/api-access-logs.vue +++ b/web/pages/admin/api-access-logs.vue @@ -186,7 +186,7 @@ - + + + + + + + \ No newline at end of file