| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- package service
- import (
- "context"
- "fmt"
- "os"
- "sync"
- "sync/atomic"
- "time"
- )
- const (
- auditLogQueueCapacity = 4096
- auditLogBatchSize = 100
- auditLogFlushInterval = time.Second
- auditRetentionCheckInterval = 24 * time.Hour
- auditRetentionStartupDelay = 5 * time.Minute
- auditRetentionBatchSize = 5000
- )
- // AuditLogService 管理面操作审计日志服务。
- // 写入端为非阻塞异步批量落库(不拖慢管理请求);
- // 读取端提供分页查询;清空端点由 handler 层做 TOTP 强校验后调用 ClearAll。
- type AuditLogService struct {
- repo AuditLogRepository
- settingService *SettingService
- queue chan *AuditLog
- ctx context.Context
- cancel context.CancelFunc
- wg sync.WaitGroup
- droppedCount uint64
- writeFailed uint64
- writtenCount uint64
- }
- func NewAuditLogService(repo AuditLogRepository, settingService *SettingService) *AuditLogService {
- ctx, cancel := context.WithCancel(context.Background())
- return &AuditLogService{
- repo: repo,
- settingService: settingService,
- queue: make(chan *AuditLog, auditLogQueueCapacity),
- ctx: ctx,
- cancel: cancel,
- }
- }
- // Start 启动异步写入与保留期清理协程。
- func (s *AuditLogService) Start() {
- if s == nil || s.repo == nil {
- return
- }
- s.wg.Add(2)
- go s.runWriter()
- go s.runRetentionLoop()
- }
- // Stop 停止服务并尽量落盘队列中剩余记录。
- func (s *AuditLogService) Stop() {
- if s == nil {
- return
- }
- s.cancel()
- s.wg.Wait()
- }
- // Record 非阻塞入队一条审计记录;队列打满时丢弃并计数(管理面流量下几乎不可能发生)。
- func (s *AuditLogService) Record(entry *AuditLog) {
- if s == nil || entry == nil {
- return
- }
- if entry.CreatedAt.IsZero() {
- entry.CreatedAt = time.Now().UTC()
- }
- select {
- case <-s.ctx.Done():
- return
- default:
- }
- select {
- case s.queue <- entry:
- default:
- atomic.AddUint64(&s.droppedCount, 1)
- }
- }
- // List 分页查询审计日志。
- func (s *AuditLogService) List(ctx context.Context, filter *AuditLogFilter) (*AuditLogList, error) {
- return s.repo.List(ctx, filter)
- }
- // GetByID 查询单条详情。
- func (s *AuditLogService) GetByID(ctx context.Context, id int64) (*AuditLog, error) {
- return s.repo.GetByID(ctx, id)
- }
- // ClearAll 全量清空审计日志并写入留痕记录。
- // 调用方(handler)必须先完成 TOTP 验证;本方法负责:
- // 1. 统计并清空全表
- // 2. 同步写入一条 "audit_log.clear" 留痕记录(绕过异步队列,保证落库)
- func (s *AuditLogService) ClearAll(ctx context.Context, trace *AuditLog) (int64, error) {
- deleted, err := s.repo.Count(ctx)
- if err != nil {
- return 0, fmt.Errorf("count audit logs: %w", err)
- }
- if err := s.repo.TruncateAll(ctx); err != nil {
- return 0, fmt.Errorf("truncate audit logs: %w", err)
- }
- if trace != nil {
- trace.Action = AuditActionAuditLogClear
- if trace.CreatedAt.IsZero() {
- trace.CreatedAt = time.Now().UTC()
- }
- if trace.Extra == nil {
- trace.Extra = map[string]any{}
- }
- trace.Extra["deleted_rows"] = deleted
- if err := s.repo.Insert(ctx, trace); err != nil {
- // 留痕失败必须显式暴露:清空已发生,但审计链断裂。
- return deleted, fmt.Errorf("audit logs cleared (%d rows) but failed to persist clear-trace record: %w", deleted, err)
- }
- }
- return deleted, nil
- }
- func (s *AuditLogService) runWriter() {
- defer s.wg.Done()
- ticker := time.NewTicker(auditLogFlushInterval)
- defer ticker.Stop()
- batch := make([]*AuditLog, 0, auditLogBatchSize)
- flush := func() {
- if len(batch) == 0 {
- return
- }
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- inserted, err := s.repo.BatchInsert(ctx, batch)
- cancel()
- if err != nil {
- atomic.AddUint64(&s.writeFailed, uint64(len(batch)))
- _, _ = fmt.Fprintf(os.Stderr, "time=%s level=WARN msg=\"audit log flush failed\" err=%v batch=%d\n",
- time.Now().Format(time.RFC3339Nano), err, len(batch))
- } else {
- atomic.AddUint64(&s.writtenCount, uint64(inserted))
- }
- batch = batch[:0]
- }
- for {
- select {
- case <-s.ctx.Done():
- // 停机前排空队列。
- for {
- select {
- case item := <-s.queue:
- if item == nil {
- continue
- }
- batch = append(batch, item)
- if len(batch) >= auditLogBatchSize {
- flush()
- }
- default:
- flush()
- return
- }
- }
- case item := <-s.queue:
- if item == nil {
- continue
- }
- batch = append(batch, item)
- if len(batch) >= auditLogBatchSize {
- flush()
- }
- case <-ticker.C:
- flush()
- }
- }
- }
- // runRetentionLoop 按保留期定期删除过期审计日志。
- // 删除操作幂等,多实例并发执行无害,因此无需选主。
- func (s *AuditLogService) runRetentionLoop() {
- defer s.wg.Done()
- startupTimer := time.NewTimer(auditRetentionStartupDelay)
- defer startupTimer.Stop()
- select {
- case <-s.ctx.Done():
- return
- case <-startupTimer.C:
- }
- ticker := time.NewTicker(auditRetentionCheckInterval)
- defer ticker.Stop()
- s.runRetentionOnce()
- for {
- select {
- case <-s.ctx.Done():
- return
- case <-ticker.C:
- s.runRetentionOnce()
- }
- }
- }
- func (s *AuditLogService) runRetentionOnce() {
- ctx, cancel := context.WithTimeout(s.ctx, 10*time.Minute)
- defer cancel()
- days := 0
- if s.settingService != nil {
- days = s.settingService.GetAuditLogRetentionDays(ctx)
- }
- if days <= 0 {
- return // 0 或负值表示永久保留,仅支持手动清空
- }
- cutoff := time.Now().UTC().AddDate(0, 0, -days)
- for {
- deleted, err := s.repo.DeleteBefore(ctx, cutoff, auditRetentionBatchSize)
- if err != nil {
- _, _ = fmt.Fprintf(os.Stderr, "time=%s level=WARN msg=\"audit log retention cleanup failed\" err=%v\n",
- time.Now().Format(time.RFC3339Nano), err)
- return
- }
- if deleted == 0 {
- return
- }
- }
- }
|