2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 12:40:25 +08:00
Files
panel/internal/data/log.go
Copilot 4f19a58bba feat: add date selector for viewing archived panel logs (#1316)
* Initial plan

* feat: add date selector for log viewing

- Add date parameter to log list API
- Add log dates listing API
- Update frontend with date selector in all log views
- Add translations for Date field

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* refactor: cache regex patterns to avoid recompilation overhead

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* refactor: use predefined regex constants instead of sync.Map

Simplified the regex caching by using predefined package-level constants
for the three log types (app, db, http) instead of sync.Map.

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* fix: lint

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>
Co-authored-by: 耗子 <haozi@loli.email>
2026-02-03 00:04:43 +08:00

256 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package data
import (
"bufio"
"encoding/json"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"gorm.io/gorm"
"github.com/acepanel/panel/internal/app"
"github.com/acepanel/panel/internal/biz"
)
var (
logArchivePatternApp = regexp.MustCompile(`^app-(\d{4}-\d{2}-\d{2})T.*\.log$`)
logArchivePatternDB = regexp.MustCompile(`^db-(\d{4}-\d{2}-\d{2})T.*\.log$`)
logArchivePatternHTTP = regexp.MustCompile(`^http-(\d{4}-\d{2}-\d{2})T.*\.log$`)
)
type logRepo struct {
db *gorm.DB
}
func NewLogRepo(db *gorm.DB) biz.LogRepo {
return &logRepo{
db: db,
}
}
// List 获取日志列表
// date 格式为 YYYY-MM-DD空字符串表示当天日志
func (r *logRepo) List(logType string, limit int, date string) ([]biz.LogEntry, error) {
logDir := filepath.Join(app.Root, "panel/storage/logs")
var logPath string
if date == "" {
// 无日期参数,读取当前日志文件
logPath = filepath.Join(logDir, logType+".log")
} else {
// 有日期参数,查找对应的归档日志文件
pattern := getLogArchivePattern(logType)
entries, err := os.ReadDir(logDir)
if err != nil {
if os.IsNotExist(err) {
return []biz.LogEntry{}, nil
}
return nil, err
}
found := false
for _, entry := range entries {
if entry.IsDir() {
continue
}
matches := pattern.FindStringSubmatch(entry.Name())
if len(matches) == 2 && matches[1] == date {
logPath = filepath.Join(logDir, entry.Name())
found = true
break
}
}
if !found {
return []biz.LogEntry{}, nil
}
}
file, err := os.Open(logPath)
if err != nil {
if os.IsNotExist(err) {
return []biz.LogEntry{}, nil
}
return nil, err
}
defer func(file *os.File) { _ = file.Close() }(file)
// 读取所有行
var lines []string
scanner := bufio.NewScanner(file)
// 增加缓冲区大小以处理较长的日志行
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) != "" {
lines = append(lines, line)
}
}
if err = scanner.Err(); err != nil {
return nil, err
}
// 从末尾取指定数量的行
start := 0
if len(lines) > limit {
start = len(lines) - limit
}
lines = lines[start:]
// 倒序处理,最新的在前面
entries := make([]biz.LogEntry, 0, len(lines))
for i := len(lines) - 1; i >= 0; i-- {
entry, err := r.parseLine(lines[i], logType)
if err != nil {
continue
}
entries = append(entries, entry)
}
// 如果是app日志查询用户名
if logType == biz.LogTypeApp {
r.fillOperatorNames(entries)
}
return entries, nil
}
// ListDates 获取可用的日志日期列表
func (r *logRepo) ListDates(logType string) ([]string, error) {
logDir := filepath.Join(app.Root, "panel/storage/logs")
entries, err := os.ReadDir(logDir)
if err != nil {
if os.IsNotExist(err) {
return []string{}, nil
}
return nil, err
}
pattern := getLogArchivePattern(logType)
dates := make([]string, 0)
for _, entry := range entries {
if entry.IsDir() {
continue
}
matches := pattern.FindStringSubmatch(entry.Name())
if len(matches) == 2 {
dates = append(dates, matches[1])
}
}
// 按日期倒序排列,最新的在前面
sort.Sort(sort.Reverse(sort.StringSlice(dates)))
return dates, nil
}
// fillOperatorNames 填充操作员用户名
func (r *logRepo) fillOperatorNames(entries []biz.LogEntry) {
// 收集所有用户ID
userIDs := make(map[uint]bool)
for _, entry := range entries {
if entry.OperatorID > 0 {
userIDs[entry.OperatorID] = true
}
}
if len(userIDs) == 0 {
return
}
// 批量查询用户名
ids := make([]uint, 0, len(userIDs))
for id := range userIDs {
ids = append(ids, id)
}
var users []biz.User
r.db.Select("id", "username").Where("id IN ?", ids).Find(&users)
// 构建ID到用户名的映射
userMap := make(map[uint]string)
for _, user := range users {
userMap[user.ID] = user.Username
}
// 填充用户名
for i := range entries {
if entries[i].OperatorID > 0 {
if username, ok := userMap[entries[i].OperatorID]; ok {
entries[i].OperatorName = username
}
}
}
}
// parseLine 解析日志行
func (r *logRepo) parseLine(line string, logType string) (biz.LogEntry, error) {
var rawEntry map[string]any
if err := json.Unmarshal([]byte(line), &rawEntry); err != nil {
return biz.LogEntry{}, err
}
entry := biz.LogEntry{
Extra: make(map[string]any),
}
// 解析通用字段
if t, ok := rawEntry["time"].(string); ok {
if parsed, err := time.Parse(time.RFC3339Nano, t); err == nil {
entry.Time = parsed
}
}
if level, ok := rawEntry["level"].(string); ok {
entry.Level = level
}
if msg, ok := rawEntry["msg"].(string); ok {
entry.Msg = msg
}
// 解析操作日志特有字段
if logType == biz.LogTypeApp {
if t, ok := rawEntry["type"].(string); ok {
entry.Type = t
}
if opID, ok := rawEntry["operator_id"].(float64); ok {
entry.OperatorID = uint(opID)
}
}
// 其他字段放入Extra
excludeKeys := map[string]bool{
"time": true, "level": true, "msg": true, "type": true, "operator_id": true,
}
for k, v := range rawEntry {
if !excludeKeys[k] {
entry.Extra[k] = v
}
}
return entry, nil
}
// getLogArchivePattern 获取归档日志文件名匹配正则表达式
func getLogArchivePattern(logType string) *regexp.Regexp {
switch logType {
case biz.LogTypeApp:
return logArchivePatternApp
case biz.LogTypeDB:
return logArchivePatternDB
case biz.LogTypeHTTP:
return logArchivePatternHTTP
default:
return logArchivePatternApp
}
}