add: 新增系统日志

This commit is contained in:
Kerwin
2025-10-28 09:40:55 +08:00
parent 1fe9487833
commit 53aebf2a15
7 changed files with 704 additions and 6 deletions

188
handlers/log_handler.go Normal file
View File

@@ -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": "系统日志清理成功"})
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -341,3 +341,17 @@ export const useApiAccessLogApi = () => {
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
}
}

View File

@@ -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',

View File

@@ -186,7 +186,7 @@
</template>
</AdminPageLayout>
<!-- 请求参数详情模态框 -->
<!-- 请求参数详情模态框 -->
<n-modal v-model:show="showModal" preset="card" title="请求参数详情" style="min-width: 600px;">
<n-code
:code="selectedParams"
@@ -197,6 +197,16 @@
/>
</n-modal>
<!-- 请求参数详情模态框 -->
<n-modal v-model:show="showModal" preset="card" title="请求参数详情" style="min-width: 600px;">
<n-code
:code="selectedParams"
language="json"
:folding="true"
:show-line-numbers="true"
class="bg-gray-100 dark:bg-gray-700 p-4 rounded max-h-96 overflow-auto"
/>
</n-modal>
</template>
<script setup lang="ts">

View File

@@ -0,0 +1,391 @@
<template>
<AdminPageLayout>
<!-- 页面头部 - 标题和按钮 -->
<template #page-header>
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">系统日志</h1>
<p class="text-gray-600 dark:text-gray-400">查看系统运行日志和错误信息</p>
</div>
<div class="flex space-x-3">
<n-button type="primary" @click="refreshSystemLogs" :loading="loading">
<template #icon>
<i class="fas fa-sync-alt"></i>
</template>
刷新
</n-button>
<n-button type="warning" @click="clearSystemLogs" :loading="clearing">
<template #icon>
<i class="fas fa-trash-alt"></i>
</template>
清理日志
</n-button>
</div>
</template>
<!-- 过滤栏 - 搜索和筛选 -->
<template #filter-bar>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<n-select
v-model:value="systemLogLevel"
:options="logLevelOptions"
placeholder="选择日志级别"
clearable
/>
<n-input
v-model:value="systemLogSearch"
placeholder="搜索日志内容..."
@keyup.enter="handleSystemLogSearch"
clearable
>
<template #prefix>
<i class="fas fa-search"></i>
</template>
</n-input>
<n-date-picker
v-model:value="systemStartDate"
type="date"
placeholder="开始日期"
clearable
/>
<n-date-picker
v-model:value="systemEndDate"
type="date"
placeholder="结束日期"
clearable
/>
<n-button type="primary" @click="handleSystemLogSearch">
<template #icon>
<i class="fas fa-search"></i>
</template>
搜索
</n-button>
</div>
</div>
</template>
<!-- 内容区header - 统计信息 -->
<template #content-header>
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<span class="text-lg font-semibold">系统日志列表</span>
<div class="text-sm text-gray-500 dark:text-gray-400">
{{ systemTotal }} 条日志
</div>
</div>
<!-- 统计卡片 -->
<div class="flex space-x-6" v-if="systemLogSummary">
<div class="text-center flex items-base">
<div class="text-2xl font-bold text-blue-600">{{ systemLogSummary.total }}</div>
<div class="text-xs text-gray-500">总日志</div>
</div>
<div class="text-center flex items-base">
<div class="text-2xl font-bold text-gray-500">{{ systemLogSummary.debug }}</div>
<div class="text-xs text-gray-500">调试</div>
</div>
<div class="text-center flex items-base">
<div class="text-2xl font-bold text-green-600">{{ systemLogSummary.info }}</div>
<div class="text-xs text-gray-500">信息</div>
</div>
<div class="text-center flex items-base">
<div class="text-2xl font-bold text-yellow-600">{{ systemLogSummary.warn }}</div>
<div class="text-xs text-gray-500">警告</div>
</div>
<div class="text-center flex items-base">
<div class="text-2xl font-bold text-red-600">{{ systemLogSummary.error }}</div>
<div class="text-xs text-gray-500">错误</div>
</div>
<div class="text-center flex items-base">
<div class="text-2xl font-bold text-purple-600">{{ systemLogSummary.fatal }}</div>
<div class="text-xs text-gray-500">致命</div>
</div>
</div>
</div>
</template>
<!-- 内容区content - 日志列表 -->
<template #content>
<!-- 加载状态 -->
<div v-if="loading" class="flex items-center justify-center py-12">
<n-spin size="large" />
</div>
<!-- 空状态 -->
<div v-else-if="systemLogs.length === 0" class="flex flex-col items-center justify-center py-12">
<i class="fas fa-file-alt text-4xl text-gray-400 mb-4"></i>
<p class="text-gray-500 dark:text-gray-400">暂无系统日志</p>
</div>
<!-- 日志列表 -->
<div v-else class="space-y-2 h-full overflow-y-auto">
<div
v-for="(log, index) in systemLogs"
:key="index"
class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
:class="getLogItemClass(log.level)"
>
<div class="flex items-start">
<div class="flex-shrink-0 w-3 h-3 rounded-full mt-1.5 mr-3" :class="getLogLevelColor(log.level)"></div>
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-2 mb-1">
<n-tag :type="getLogLevelTagType(log.level)" size="small">
{{ log.level }}
</n-tag>
<span class="text-xs text-gray-500 dark:text-gray-400">
{{ formatLogTime(log.timestamp) }}
</span>
<span v-if="log.file" class="text-xs text-gray-500 dark:text-gray-400">
{{ log.file }}:{{ log.line }}
</span>
</div>
<div class="text-sm text-gray-800 dark:text-gray-200 font-mono break-words">
{{ log.message }}
</div>
</div>
</div>
</div>
</div>
</template>
<!-- 内容区footer - 分页组件 -->
<template #content-footer>
<div class="p-4">
<div class="flex justify-center">
<n-pagination
v-model:page="systemCurrentPage"
v-model:page-size="systemPageSize"
:item-count="systemTotal"
:page-sizes="[20, 50, 100]"
show-size-picker
@update:page="handleSystemLogPageChange"
@update:page-size="handleSystemLogPageSizeChange"
/>
</div>
</div>
</template>
</AdminPageLayout>
</template>
<script setup lang="ts">
// 设置页面布局
definePageMeta({
layout: 'admin',
ssr: false
})
import { useSystemLogApi } from '~/composables/useApi'
const notification = useNotification()
const dialog = useDialog()
// 获取API实例
const systemLogApi = useSystemLogApi()
// 响应式数据
const loading = ref(false)
const clearing = ref(false)
const systemLogs = ref<any[]>([])
const systemLogSummary = ref<any>(null)
// 过滤和搜索
const systemLogLevel = ref<string | null>(null)
const systemLogSearch = ref('')
const systemStartDate = ref<number | null>(null)
const systemEndDate = ref<number | null>(null)
// 分页
const systemCurrentPage = ref(1)
const systemPageSize = ref(50)
const systemTotal = ref(0)
// 日志级别选项
const logLevelOptions = [
{ label: 'DEBUG', value: 'debug' },
{ label: 'INFO', value: 'info' },
{ label: 'WARN', value: 'warn' },
{ label: 'ERROR', value: 'error' },
{ label: 'FATAL', value: 'fatal' }
]
// 获取系统日志数据
const fetchSystemLogs = async () => {
loading.value = true
try {
const params: any = {
page: systemCurrentPage.value,
page_size: systemPageSize.value
}
// 添加级别筛选
if (systemLogLevel.value) {
params.level = systemLogLevel.value
}
// 添加日期筛选
if (systemStartDate.value) {
const date = new Date(systemStartDate.value)
params.start_date = date.toISOString().split('T')[0]
}
if (systemEndDate.value) {
const date = new Date(systemEndDate.value)
params.end_date = date.toISOString().split('T')[0]
}
// 添加搜索条件
if (systemLogSearch.value) {
params.search = systemLogSearch.value
}
const response = await systemLogApi.getSystemLogs(params) as any
systemLogs.value = response.data || []
systemTotal.value = response.total || 0
} catch (error) {
console.error('获取系统日志失败:', error)
notification.error({
content: '获取系统日志失败',
duration: 3000
})
systemLogs.value = []
systemTotal.value = 0
} finally {
loading.value = false
}
}
// 获取系统日志统计
const fetchSystemLogSummary = async () => {
try {
const response = await systemLogApi.getSystemLogSummary()
systemLogSummary.value = response.summary || null
} catch (error) {
console.error('获取系统日志统计失败:', error)
}
}
// 刷新系统日志
const refreshSystemLogs = () => {
fetchSystemLogs()
fetchSystemLogSummary()
}
// 系统日志搜索处理
const handleSystemLogSearch = () => {
systemCurrentPage.value = 1
fetchSystemLogs()
}
// 系统日志分页处理
const handleSystemLogPageChange = (page: number) => {
systemCurrentPage.value = page
fetchSystemLogs()
}
const handleSystemLogPageSizeChange = (size: number) => {
systemPageSize.value = size
systemCurrentPage.value = 1
fetchSystemLogs()
}
// 获取日志级别标签类型
const getLogLevelTagType = (level: string): 'default' | 'primary' | 'info' | 'success' | 'warning' | 'error' => {
const levelMap: Record<string, 'default' | 'primary' | 'info' | 'success' | 'warning' | 'error'> = {
'DEBUG': 'default',
'INFO': 'info',
'WARN': 'warning',
'ERROR': 'error',
'FATAL': 'error'
}
return levelMap[level?.toUpperCase()] || 'default'
}
// 获取日志级别颜色
const getLogLevelColor = (level: string): string => {
const colorMap: Record<string, string> = {
'DEBUG': 'bg-gray-400',
'INFO': 'bg-blue-500',
'WARN': 'bg-yellow-500',
'ERROR': 'bg-red-500',
'FATAL': 'bg-purple-500'
}
return colorMap[level?.toUpperCase()] || 'bg-gray-400'
}
// 获取日志项类名
const getLogItemClass = (level: string): string => {
const classMap: Record<string, string> = {
'DEBUG': 'bg-gray-50 dark:bg-gray-800',
'INFO': 'bg-blue-50 dark:bg-blue-900/20',
'WARN': 'bg-yellow-50 dark:bg-yellow-900/20',
'ERROR': 'bg-red-50 dark:bg-red-900/20',
'FATAL': 'bg-purple-50 dark:bg-purple-900/20'
}
return classMap[level?.toUpperCase()] || ''
}
// 格式化日志时间
const formatLogTime = (timestamp: string) => {
if (!timestamp) return '-'
try {
const date = new Date(timestamp)
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
} catch (error) {
console.error('时间格式化错误:', error)
return timestamp
}
}
// 清理系统日志
const clearSystemLogs = async () => {
dialog.warning({
title: '清理系统日志',
content: '确定要清理30天前的系统日志吗此操作不可恢复。',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
clearing.value = true
await systemLogApi.clearSystemLogs(30)
notification.success({
content: '系统日志清理成功',
duration: 3000
})
refreshSystemLogs()
} catch (error) {
console.error('清理系统日志失败:', error)
notification.error({
content: '清理系统日志失败',
duration: 3000
})
} finally {
clearing.value = false
}
}
})
}
// 页面加载时获取数据
onMounted(async () => {
await Promise.all([fetchSystemLogs(), fetchSystemLogSummary()])
})
</script>
<style scoped>
/* 日志条目悬停效果 */
.hover\:bg-gray-50:hover {
background-color: #f9fafb;
}
.dark .hover\:bg-gray-800:hover {
background-color: #1f2937;
}
</style>