audit_log_service.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. "os"
  6. "sync"
  7. "sync/atomic"
  8. "time"
  9. )
  10. const (
  11. auditLogQueueCapacity = 4096
  12. auditLogBatchSize = 100
  13. auditLogFlushInterval = time.Second
  14. auditRetentionCheckInterval = 24 * time.Hour
  15. auditRetentionStartupDelay = 5 * time.Minute
  16. auditRetentionBatchSize = 5000
  17. )
  18. // AuditLogService 管理面操作审计日志服务。
  19. // 写入端为非阻塞异步批量落库(不拖慢管理请求);
  20. // 读取端提供分页查询;清空端点由 handler 层做 TOTP 强校验后调用 ClearAll。
  21. type AuditLogService struct {
  22. repo AuditLogRepository
  23. settingService *SettingService
  24. queue chan *AuditLog
  25. ctx context.Context
  26. cancel context.CancelFunc
  27. wg sync.WaitGroup
  28. droppedCount uint64
  29. writeFailed uint64
  30. writtenCount uint64
  31. }
  32. func NewAuditLogService(repo AuditLogRepository, settingService *SettingService) *AuditLogService {
  33. ctx, cancel := context.WithCancel(context.Background())
  34. return &AuditLogService{
  35. repo: repo,
  36. settingService: settingService,
  37. queue: make(chan *AuditLog, auditLogQueueCapacity),
  38. ctx: ctx,
  39. cancel: cancel,
  40. }
  41. }
  42. // Start 启动异步写入与保留期清理协程。
  43. func (s *AuditLogService) Start() {
  44. if s == nil || s.repo == nil {
  45. return
  46. }
  47. s.wg.Add(2)
  48. go s.runWriter()
  49. go s.runRetentionLoop()
  50. }
  51. // Stop 停止服务并尽量落盘队列中剩余记录。
  52. func (s *AuditLogService) Stop() {
  53. if s == nil {
  54. return
  55. }
  56. s.cancel()
  57. s.wg.Wait()
  58. }
  59. // Record 非阻塞入队一条审计记录;队列打满时丢弃并计数(管理面流量下几乎不可能发生)。
  60. func (s *AuditLogService) Record(entry *AuditLog) {
  61. if s == nil || entry == nil {
  62. return
  63. }
  64. if entry.CreatedAt.IsZero() {
  65. entry.CreatedAt = time.Now().UTC()
  66. }
  67. select {
  68. case <-s.ctx.Done():
  69. return
  70. default:
  71. }
  72. select {
  73. case s.queue <- entry:
  74. default:
  75. atomic.AddUint64(&s.droppedCount, 1)
  76. }
  77. }
  78. // List 分页查询审计日志。
  79. func (s *AuditLogService) List(ctx context.Context, filter *AuditLogFilter) (*AuditLogList, error) {
  80. return s.repo.List(ctx, filter)
  81. }
  82. // GetByID 查询单条详情。
  83. func (s *AuditLogService) GetByID(ctx context.Context, id int64) (*AuditLog, error) {
  84. return s.repo.GetByID(ctx, id)
  85. }
  86. // ClearAll 全量清空审计日志并写入留痕记录。
  87. // 调用方(handler)必须先完成 TOTP 验证;本方法负责:
  88. // 1. 统计并清空全表
  89. // 2. 同步写入一条 "audit_log.clear" 留痕记录(绕过异步队列,保证落库)
  90. func (s *AuditLogService) ClearAll(ctx context.Context, trace *AuditLog) (int64, error) {
  91. deleted, err := s.repo.Count(ctx)
  92. if err != nil {
  93. return 0, fmt.Errorf("count audit logs: %w", err)
  94. }
  95. if err := s.repo.TruncateAll(ctx); err != nil {
  96. return 0, fmt.Errorf("truncate audit logs: %w", err)
  97. }
  98. if trace != nil {
  99. trace.Action = AuditActionAuditLogClear
  100. if trace.CreatedAt.IsZero() {
  101. trace.CreatedAt = time.Now().UTC()
  102. }
  103. if trace.Extra == nil {
  104. trace.Extra = map[string]any{}
  105. }
  106. trace.Extra["deleted_rows"] = deleted
  107. if err := s.repo.Insert(ctx, trace); err != nil {
  108. // 留痕失败必须显式暴露:清空已发生,但审计链断裂。
  109. return deleted, fmt.Errorf("audit logs cleared (%d rows) but failed to persist clear-trace record: %w", deleted, err)
  110. }
  111. }
  112. return deleted, nil
  113. }
  114. func (s *AuditLogService) runWriter() {
  115. defer s.wg.Done()
  116. ticker := time.NewTicker(auditLogFlushInterval)
  117. defer ticker.Stop()
  118. batch := make([]*AuditLog, 0, auditLogBatchSize)
  119. flush := func() {
  120. if len(batch) == 0 {
  121. return
  122. }
  123. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  124. inserted, err := s.repo.BatchInsert(ctx, batch)
  125. cancel()
  126. if err != nil {
  127. atomic.AddUint64(&s.writeFailed, uint64(len(batch)))
  128. _, _ = fmt.Fprintf(os.Stderr, "time=%s level=WARN msg=\"audit log flush failed\" err=%v batch=%d\n",
  129. time.Now().Format(time.RFC3339Nano), err, len(batch))
  130. } else {
  131. atomic.AddUint64(&s.writtenCount, uint64(inserted))
  132. }
  133. batch = batch[:0]
  134. }
  135. for {
  136. select {
  137. case <-s.ctx.Done():
  138. // 停机前排空队列。
  139. for {
  140. select {
  141. case item := <-s.queue:
  142. if item == nil {
  143. continue
  144. }
  145. batch = append(batch, item)
  146. if len(batch) >= auditLogBatchSize {
  147. flush()
  148. }
  149. default:
  150. flush()
  151. return
  152. }
  153. }
  154. case item := <-s.queue:
  155. if item == nil {
  156. continue
  157. }
  158. batch = append(batch, item)
  159. if len(batch) >= auditLogBatchSize {
  160. flush()
  161. }
  162. case <-ticker.C:
  163. flush()
  164. }
  165. }
  166. }
  167. // runRetentionLoop 按保留期定期删除过期审计日志。
  168. // 删除操作幂等,多实例并发执行无害,因此无需选主。
  169. func (s *AuditLogService) runRetentionLoop() {
  170. defer s.wg.Done()
  171. startupTimer := time.NewTimer(auditRetentionStartupDelay)
  172. defer startupTimer.Stop()
  173. select {
  174. case <-s.ctx.Done():
  175. return
  176. case <-startupTimer.C:
  177. }
  178. ticker := time.NewTicker(auditRetentionCheckInterval)
  179. defer ticker.Stop()
  180. s.runRetentionOnce()
  181. for {
  182. select {
  183. case <-s.ctx.Done():
  184. return
  185. case <-ticker.C:
  186. s.runRetentionOnce()
  187. }
  188. }
  189. }
  190. func (s *AuditLogService) runRetentionOnce() {
  191. ctx, cancel := context.WithTimeout(s.ctx, 10*time.Minute)
  192. defer cancel()
  193. days := 0
  194. if s.settingService != nil {
  195. days = s.settingService.GetAuditLogRetentionDays(ctx)
  196. }
  197. if days <= 0 {
  198. return // 0 或负值表示永久保留,仅支持手动清空
  199. }
  200. cutoff := time.Now().UTC().AddDate(0, 0, -days)
  201. for {
  202. deleted, err := s.repo.DeleteBefore(ctx, cutoff, auditRetentionBatchSize)
  203. if err != nil {
  204. _, _ = fmt.Fprintf(os.Stderr, "time=%s level=WARN msg=\"audit log retention cleanup failed\" err=%v\n",
  205. time.Now().Format(time.RFC3339Nano), err)
  206. return
  207. }
  208. if deleted == 0 {
  209. return
  210. }
  211. }
  212. }