mirror of
https://github.com/tnb-labs/panel.git
synced 2026-09-01 14:55:12 +08:00
f2e41a3364
* 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
|
|
}
|