events.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. package store
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "strings"
  8. "time"
  9. )
  10. func (s *Store) AppendAuditEvent(ctx context.Context, value AuditEvent) (AuditEvent, error) {
  11. value.Action = strings.TrimSpace(value.Action)
  12. if value.Action == "" {
  13. return AuditEvent{}, errors.New("audit action is required")
  14. }
  15. details, err := normalizeJSONObject(value.Details)
  16. if err != nil {
  17. return AuditEvent{}, fmt.Errorf("normalize audit details: %w", err)
  18. }
  19. if value.CreatedAt.IsZero() {
  20. value.CreatedAt = time.Now().UTC()
  21. }
  22. result, err := s.db.ExecContext(ctx, `
  23. INSERT INTO audit_events (
  24. actor, action, entity_type, entity_id, outcome, remote_addr,
  25. details_json, created_at
  26. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  27. `,
  28. value.Actor, value.Action, value.EntityType, value.EntityID,
  29. value.Outcome, value.RemoteAddr, string(details), value.CreatedAt.Unix(),
  30. )
  31. if err != nil {
  32. return AuditEvent{}, fmt.Errorf("append audit event: %w", err)
  33. }
  34. value.ID, err = result.LastInsertId()
  35. if err != nil {
  36. return AuditEvent{}, fmt.Errorf("read audit event id: %w", err)
  37. }
  38. value.Details = details
  39. return value, nil
  40. }
  41. func (s *Store) ListAuditEvents(ctx context.Context, filter AuditFilter) ([]AuditEvent, error) {
  42. clauses := make([]string, 0, 7)
  43. args := make([]any, 0, 8)
  44. if filter.Actor != "" {
  45. clauses = append(clauses, `actor = ?`)
  46. args = append(args, filter.Actor)
  47. }
  48. if filter.Action != "" {
  49. clauses = append(clauses, `action = ?`)
  50. args = append(args, filter.Action)
  51. }
  52. if filter.EntityType != "" {
  53. clauses = append(clauses, `entity_type = ?`)
  54. args = append(args, filter.EntityType)
  55. }
  56. if filter.EntityID != "" {
  57. clauses = append(clauses, `entity_id = ?`)
  58. args = append(args, filter.EntityID)
  59. }
  60. if !filter.Since.IsZero() {
  61. clauses = append(clauses, `created_at >= ?`)
  62. args = append(args, filter.Since.UTC().Unix())
  63. }
  64. if !filter.Until.IsZero() {
  65. clauses = append(clauses, `created_at < ?`)
  66. args = append(args, filter.Until.UTC().Unix())
  67. }
  68. if filter.BeforeID > 0 {
  69. clauses = append(clauses, `id < ?`)
  70. args = append(args, filter.BeforeID)
  71. }
  72. query := auditEventSelect
  73. if len(clauses) > 0 {
  74. query += ` WHERE ` + strings.Join(clauses, ` AND `)
  75. }
  76. query += ` ORDER BY created_at DESC, id DESC LIMIT ?`
  77. args = append(args, normalizedLimit(filter.Limit))
  78. rows, err := s.db.QueryContext(ctx, query, args...)
  79. if err != nil {
  80. return nil, fmt.Errorf("list audit events: %w", err)
  81. }
  82. defer rows.Close()
  83. values := make([]AuditEvent, 0)
  84. for rows.Next() {
  85. value, err := auditEvent(rows)
  86. if err != nil {
  87. return nil, fmt.Errorf("scan audit event: %w", err)
  88. }
  89. values = append(values, value)
  90. }
  91. if err := rows.Err(); err != nil {
  92. return nil, fmt.Errorf("iterate audit events: %w", err)
  93. }
  94. return values, nil
  95. }
  96. const auditEventSelect = `
  97. SELECT id, actor, action, entity_type, entity_id, outcome,
  98. remote_addr, details_json, created_at
  99. FROM audit_events`
  100. func auditEvent(row rowScanner) (AuditEvent, error) {
  101. var value AuditEvent
  102. var details string
  103. var createdAt int64
  104. err := row.Scan(
  105. &value.ID, &value.Actor, &value.Action, &value.EntityType,
  106. &value.EntityID, &value.Outcome, &value.RemoteAddr, &details,
  107. &createdAt,
  108. )
  109. if errors.Is(err, sql.ErrNoRows) {
  110. return AuditEvent{}, ErrNotFound
  111. }
  112. if err != nil {
  113. return AuditEvent{}, err
  114. }
  115. value.Details = []byte(details)
  116. value.CreatedAt = time.Unix(createdAt, 0).UTC()
  117. return value, nil
  118. }
  119. func (s *Store) AppendLogEvent(ctx context.Context, value LogEvent) (LogEvent, error) {
  120. value.Level = strings.ToLower(strings.TrimSpace(value.Level))
  121. if value.Level == "" {
  122. return LogEvent{}, errors.New("log level is required")
  123. }
  124. if strings.TrimSpace(value.Message) == "" {
  125. return LogEvent{}, errors.New("log message is required")
  126. }
  127. fields, err := normalizeJSONValue(value.Fields)
  128. if err != nil {
  129. return LogEvent{}, fmt.Errorf("normalize log fields: %w", err)
  130. }
  131. if value.Time.IsZero() {
  132. value.Time = time.Now().UTC()
  133. }
  134. result, err := s.db.ExecContext(ctx, `
  135. INSERT INTO log_events (event_time, level, message, caller, fields_json)
  136. VALUES (?, ?, ?, ?, ?)
  137. `, value.Time.Unix(), value.Level, value.Message, value.Caller, string(fields))
  138. if err != nil {
  139. return LogEvent{}, fmt.Errorf("append log event: %w", err)
  140. }
  141. value.ID, err = result.LastInsertId()
  142. if err != nil {
  143. return LogEvent{}, fmt.Errorf("read log event id: %w", err)
  144. }
  145. value.Fields = fields
  146. return value, nil
  147. }
  148. func (s *Store) ListLogEvents(ctx context.Context, filter LogFilter) ([]LogEvent, error) {
  149. clauses := make([]string, 0, 4)
  150. args := make([]any, 0, 5)
  151. if filter.Level != "" {
  152. clauses = append(clauses, `level = ?`)
  153. args = append(args, strings.ToLower(filter.Level))
  154. }
  155. if !filter.Since.IsZero() {
  156. clauses = append(clauses, `event_time >= ?`)
  157. args = append(args, filter.Since.UTC().Unix())
  158. }
  159. if !filter.Until.IsZero() {
  160. clauses = append(clauses, `event_time < ?`)
  161. args = append(args, filter.Until.UTC().Unix())
  162. }
  163. if filter.BeforeID > 0 {
  164. clauses = append(clauses, `id < ?`)
  165. args = append(args, filter.BeforeID)
  166. }
  167. query := logEventSelect
  168. if len(clauses) > 0 {
  169. query += ` WHERE ` + strings.Join(clauses, ` AND `)
  170. }
  171. query += ` ORDER BY event_time DESC, id DESC LIMIT ?`
  172. args = append(args, normalizedLimit(filter.Limit))
  173. rows, err := s.db.QueryContext(ctx, query, args...)
  174. if err != nil {
  175. return nil, fmt.Errorf("list log events: %w", err)
  176. }
  177. defer rows.Close()
  178. values := make([]LogEvent, 0)
  179. for rows.Next() {
  180. value, err := logEvent(rows)
  181. if err != nil {
  182. return nil, fmt.Errorf("scan log event: %w", err)
  183. }
  184. values = append(values, value)
  185. }
  186. if err := rows.Err(); err != nil {
  187. return nil, fmt.Errorf("iterate log events: %w", err)
  188. }
  189. return values, nil
  190. }
  191. const logEventSelect = `
  192. SELECT id, event_time, level, message, caller, fields_json
  193. FROM log_events`
  194. func logEvent(row rowScanner) (LogEvent, error) {
  195. var value LogEvent
  196. var eventTime int64
  197. var fields string
  198. err := row.Scan(
  199. &value.ID, &eventTime, &value.Level, &value.Message,
  200. &value.Caller, &fields,
  201. )
  202. if errors.Is(err, sql.ErrNoRows) {
  203. return LogEvent{}, ErrNotFound
  204. }
  205. if err != nil {
  206. return LogEvent{}, err
  207. }
  208. value.Time = time.Unix(eventTime, 0).UTC()
  209. value.Fields = []byte(fields)
  210. return value, nil
  211. }
  212. func (s *Store) PruneAuditEvents(ctx context.Context, before time.Time) (int64, error) {
  213. return deleteEventsBefore(ctx, s.db, `audit_events`, `created_at`, before)
  214. }
  215. func (s *Store) PruneLogEvents(ctx context.Context, before time.Time) (int64, error) {
  216. return deleteEventsBefore(ctx, s.db, `log_events`, `event_time`, before)
  217. }
  218. // CountLogEvents returns how many application log rows are persisted.
  219. func (s *Store) CountLogEvents(ctx context.Context) (int64, error) {
  220. var count int64
  221. if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM log_events`).Scan(&count); err != nil {
  222. return 0, fmt.Errorf("count log events: %w", err)
  223. }
  224. return count, nil
  225. }
  226. // PruneLogEventsToCount keeps only the newest `keep` log rows, deleting the
  227. // rest. keep <= 0 deletes everything.
  228. func (s *Store) PruneLogEventsToCount(ctx context.Context, keep int) (int64, error) {
  229. if keep < 0 {
  230. keep = 0
  231. }
  232. result, err := s.db.ExecContext(ctx, `
  233. DELETE FROM log_events WHERE id NOT IN (
  234. SELECT id FROM log_events ORDER BY id DESC LIMIT ?
  235. )
  236. `, keep)
  237. if err != nil {
  238. return 0, fmt.Errorf("prune log events to count: %w", err)
  239. }
  240. affected, err := result.RowsAffected()
  241. if err != nil {
  242. return 0, fmt.Errorf("read pruned log count: %w", err)
  243. }
  244. return affected, nil
  245. }
  246. func deleteEventsBefore(
  247. ctx context.Context,
  248. executor contextExecer,
  249. table string,
  250. column string,
  251. before time.Time,
  252. ) (int64, error) {
  253. // table and column are internal constants from the callers above.
  254. result, err := executor.ExecContext(
  255. ctx,
  256. `DELETE FROM `+table+` WHERE `+column+` < ?`,
  257. before.UTC().Unix(),
  258. )
  259. if err != nil {
  260. return 0, fmt.Errorf("prune %s: %w", table, err)
  261. }
  262. affected, err := result.RowsAffected()
  263. if err != nil {
  264. return 0, fmt.Errorf("read pruned %s count: %w", table, err)
  265. }
  266. return affected, nil
  267. }
  268. // PruneEvents removes audit and application logs atomically.
  269. func (s *Store) PruneEvents(
  270. ctx context.Context,
  271. auditBefore time.Time,
  272. logBefore time.Time,
  273. ) (auditCount int64, logCount int64, err error) {
  274. tx, err := s.db.BeginTx(ctx, nil)
  275. if err != nil {
  276. return 0, 0, fmt.Errorf("begin event pruning: %w", err)
  277. }
  278. defer tx.Rollback()
  279. auditCount, err = deleteEventsBefore(ctx, tx, `audit_events`, `created_at`, auditBefore)
  280. if err != nil {
  281. return 0, 0, err
  282. }
  283. logCount, err = deleteEventsBefore(ctx, tx, `log_events`, `event_time`, logBefore)
  284. if err != nil {
  285. return 0, 0, err
  286. }
  287. if err := tx.Commit(); err != nil {
  288. return 0, 0, fmt.Errorf("commit event pruning: %w", err)
  289. }
  290. return auditCount, logCount, nil
  291. }