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