mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 04:22:33 +08:00
* Initial plan * feat: 添加操作日志记录功能和前端日志查看页面 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: 修复 HttpLog 中 IP 字符串处理的类型安全问题 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: lint * feat: 完善面板内日志记录,添加网站/数据库/项目/计划任务/备份CRUD日志 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 完善更多模块的日志记录(证书/SSH/Webhook/用户/设置/安全) Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: 改进cert_dns删除方法,在删除前获取实体信息用于日志记录 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * fix: lint * feat: 从session获取操作员ID并在前端显示用户名 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>
180 lines
3.6 KiB
Go
180 lines
3.6 KiB
Go
package data
|
||
|
||
import (
|
||
"bufio"
|
||
"encoding/json"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
|
||
"github.com/acepanel/panel/internal/app"
|
||
"github.com/acepanel/panel/internal/biz"
|
||
)
|
||
|
||
type logRepo struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
func NewLogRepo(db *gorm.DB) biz.LogRepo {
|
||
return &logRepo{
|
||
db: db,
|
||
}
|
||
}
|
||
|
||
// List 获取日志列表
|
||
func (r *logRepo) List(logType string, limit int) ([]biz.LogEntry, error) {
|
||
var filename string
|
||
switch logType {
|
||
case biz.LogTypeApp:
|
||
filename = "app.log"
|
||
case biz.LogTypeDB:
|
||
filename = "db.log"
|
||
case biz.LogTypeHTTP:
|
||
filename = "http.log"
|
||
default:
|
||
filename = "app.log"
|
||
}
|
||
|
||
logPath := filepath.Join(app.Root, "panel/storage/logs", filename)
|
||
|
||
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
|
||
}
|
||
|
||
// 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
|
||
}
|