159 lines
4.6 KiB
Go
159 lines
4.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
|
|
"oci-portal/internal/model"
|
|
)
|
|
|
|
// 系统日志保留策略与异步写入超时。
|
|
const (
|
|
systemLogRetention = 90 * 24 * time.Hour // 过期删除阈值
|
|
systemLogMaxRows = 50000 // 总量兜底上限,超出删最旧
|
|
systemLogCleanupTick = 24 * time.Hour // 周期清理间隔
|
|
systemLogWriteWait = 3 * time.Second // 单条异步写入超时
|
|
)
|
|
|
|
// 系统日志查询分页默认值与上限。
|
|
const (
|
|
systemLogDefaultPageSize = 20
|
|
systemLogMaxPageSize = 100
|
|
)
|
|
|
|
// SystemLogService 记录并查询面板自身的操作日志(区别于 OCI 云端审计事件)。
|
|
type SystemLogService struct {
|
|
db *gorm.DB
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// NewSystemLogService 组装依赖;调用 StartCleanup 后开始周期清理。
|
|
func NewSystemLogService(db *gorm.DB) *SystemLogService {
|
|
return &SystemLogService{db: db}
|
|
}
|
|
|
|
// Record 异步落一条日志,不阻塞业务请求;写入失败只记日志。
|
|
func (s *SystemLogService) Record(entry model.SystemLog) {
|
|
s.wg.Add(1)
|
|
go func() {
|
|
defer s.wg.Done()
|
|
ctx, cancel := context.WithTimeout(context.Background(), systemLogWriteWait)
|
|
defer cancel()
|
|
if err := s.record(ctx, entry); err != nil {
|
|
log.Printf("system log record: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// record 同步落库,供 Record 内部与测试直调。
|
|
func (s *SystemLogService) record(ctx context.Context, entry model.SystemLog) error {
|
|
if entry.CreatedAt.IsZero() {
|
|
entry.CreatedAt = time.Now()
|
|
}
|
|
if err := s.db.WithContext(ctx).Create(&entry).Error; err != nil {
|
|
return fmt.Errorf("create system log: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Wait 等待在途异步写入与清理 goroutine 退出,供进程收尾与测试同步。
|
|
func (s *SystemLogService) Wait() {
|
|
s.wg.Wait()
|
|
}
|
|
|
|
// SystemLogQuery 是日志查询参数;Keyword 对路径与用户名模糊匹配。
|
|
type SystemLogQuery struct {
|
|
Page int
|
|
PageSize int
|
|
Keyword string
|
|
}
|
|
|
|
// normalize 补分页默认值并钳制上限。
|
|
func (q SystemLogQuery) normalize() SystemLogQuery {
|
|
if q.Page < 1 {
|
|
q.Page = 1
|
|
}
|
|
if q.PageSize < 1 {
|
|
q.PageSize = systemLogDefaultPageSize
|
|
}
|
|
if q.PageSize > systemLogMaxPageSize {
|
|
q.PageSize = systemLogMaxPageSize
|
|
}
|
|
return q
|
|
}
|
|
|
|
// List 按时间倒序分页查询日志,返回当前页与总数。
|
|
func (s *SystemLogService) List(ctx context.Context, q SystemLogQuery) ([]model.SystemLog, int64, error) {
|
|
q = q.normalize()
|
|
tx := s.db.WithContext(ctx).Model(&model.SystemLog{})
|
|
if q.Keyword != "" {
|
|
kw := "%" + q.Keyword + "%"
|
|
tx = tx.Where("path LIKE ? OR username LIKE ?", kw, kw)
|
|
}
|
|
var total int64
|
|
if err := tx.Count(&total).Error; err != nil {
|
|
return nil, 0, fmt.Errorf("count system logs: %w", err)
|
|
}
|
|
items := make([]model.SystemLog, 0, q.PageSize)
|
|
err := tx.Order("created_at DESC, id DESC").
|
|
Offset((q.Page - 1) * q.PageSize).Limit(q.PageSize).
|
|
Find(&items).Error
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("list system logs: %w", err)
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
// StartCleanup 启动周期清理:启动即清一次,之后每 24h 一次,随 ctx 取消退出。
|
|
func (s *SystemLogService) StartCleanup(ctx context.Context) {
|
|
s.wg.Add(1)
|
|
go func() {
|
|
defer s.wg.Done()
|
|
s.cleanupOnce(ctx)
|
|
ticker := time.NewTicker(systemLogCleanupTick)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
s.cleanupOnce(ctx)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// cleanupOnce 执行一轮清理,失败只记日志、不中断周期调度。
|
|
func (s *SystemLogService) cleanupOnce(ctx context.Context) {
|
|
if err := s.cleanup(ctx, systemLogRetention, systemLogMaxRows); err != nil {
|
|
log.Printf("system log cleanup: %v", err)
|
|
}
|
|
}
|
|
|
|
// cleanup 先删过期记录,再对超量部分删最旧;阈值参数化便于测试。
|
|
func (s *SystemLogService) cleanup(ctx context.Context, retention time.Duration, maxRows int) error {
|
|
cutoff := time.Now().Add(-retention)
|
|
if err := s.db.WithContext(ctx).Where("created_at < ?", cutoff).Delete(&model.SystemLog{}).Error; err != nil {
|
|
return fmt.Errorf("delete expired system logs: %w", err)
|
|
}
|
|
var total int64
|
|
if err := s.db.WithContext(ctx).Model(&model.SystemLog{}).Count(&total).Error; err != nil {
|
|
return fmt.Errorf("count system logs: %w", err)
|
|
}
|
|
overflow := int(total) - maxRows
|
|
if overflow <= 0 {
|
|
return nil
|
|
}
|
|
oldest := s.db.Model(&model.SystemLog{}).Select("id").
|
|
Order("created_at ASC, id ASC").Limit(overflow)
|
|
if err := s.db.WithContext(ctx).Where("id IN (?)", oldest).Delete(&model.SystemLog{}).Error; err != nil {
|
|
return fmt.Errorf("trim system logs over cap: %w", err)
|
|
}
|
|
return nil
|
|
}
|