| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306 |
- package store
- import (
- "context"
- "database/sql"
- "errors"
- "fmt"
- "strings"
- "time"
- )
- func (s *Store) AppendAuditEvent(ctx context.Context, value AuditEvent) (AuditEvent, error) {
- value.Action = strings.TrimSpace(value.Action)
- if value.Action == "" {
- return AuditEvent{}, errors.New("audit action is required")
- }
- details, err := normalizeJSONObject(value.Details)
- if err != nil {
- return AuditEvent{}, fmt.Errorf("normalize audit details: %w", err)
- }
- if value.CreatedAt.IsZero() {
- value.CreatedAt = time.Now().UTC()
- }
- result, err := s.db.ExecContext(ctx, `
- INSERT INTO audit_events (
- actor, action, entity_type, entity_id, outcome, remote_addr,
- details_json, created_at
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- `,
- value.Actor, value.Action, value.EntityType, value.EntityID,
- value.Outcome, value.RemoteAddr, string(details), value.CreatedAt.Unix(),
- )
- if err != nil {
- return AuditEvent{}, fmt.Errorf("append audit event: %w", err)
- }
- value.ID, err = result.LastInsertId()
- if err != nil {
- return AuditEvent{}, fmt.Errorf("read audit event id: %w", err)
- }
- value.Details = details
- return value, nil
- }
- func (s *Store) ListAuditEvents(ctx context.Context, filter AuditFilter) ([]AuditEvent, error) {
- clauses := make([]string, 0, 7)
- args := make([]any, 0, 8)
- if filter.Actor != "" {
- clauses = append(clauses, `actor = ?`)
- args = append(args, filter.Actor)
- }
- if filter.Action != "" {
- clauses = append(clauses, `action = ?`)
- args = append(args, filter.Action)
- }
- if filter.EntityType != "" {
- clauses = append(clauses, `entity_type = ?`)
- args = append(args, filter.EntityType)
- }
- if filter.EntityID != "" {
- clauses = append(clauses, `entity_id = ?`)
- args = append(args, filter.EntityID)
- }
- if !filter.Since.IsZero() {
- clauses = append(clauses, `created_at >= ?`)
- args = append(args, filter.Since.UTC().Unix())
- }
- if !filter.Until.IsZero() {
- clauses = append(clauses, `created_at < ?`)
- args = append(args, filter.Until.UTC().Unix())
- }
- if filter.BeforeID > 0 {
- clauses = append(clauses, `id < ?`)
- args = append(args, filter.BeforeID)
- }
- query := auditEventSelect
- if len(clauses) > 0 {
- query += ` WHERE ` + strings.Join(clauses, ` AND `)
- }
- query += ` ORDER BY created_at DESC, id DESC LIMIT ?`
- args = append(args, normalizedLimit(filter.Limit))
- rows, err := s.db.QueryContext(ctx, query, args...)
- if err != nil {
- return nil, fmt.Errorf("list audit events: %w", err)
- }
- defer rows.Close()
- values := make([]AuditEvent, 0)
- for rows.Next() {
- value, err := auditEvent(rows)
- if err != nil {
- return nil, fmt.Errorf("scan audit event: %w", err)
- }
- values = append(values, value)
- }
- if err := rows.Err(); err != nil {
- return nil, fmt.Errorf("iterate audit events: %w", err)
- }
- return values, nil
- }
- const auditEventSelect = `
- SELECT id, actor, action, entity_type, entity_id, outcome,
- remote_addr, details_json, created_at
- FROM audit_events`
- func auditEvent(row rowScanner) (AuditEvent, error) {
- var value AuditEvent
- var details string
- var createdAt int64
- err := row.Scan(
- &value.ID, &value.Actor, &value.Action, &value.EntityType,
- &value.EntityID, &value.Outcome, &value.RemoteAddr, &details,
- &createdAt,
- )
- if errors.Is(err, sql.ErrNoRows) {
- return AuditEvent{}, ErrNotFound
- }
- if err != nil {
- return AuditEvent{}, err
- }
- value.Details = []byte(details)
- value.CreatedAt = time.Unix(createdAt, 0).UTC()
- return value, nil
- }
- func (s *Store) AppendLogEvent(ctx context.Context, value LogEvent) (LogEvent, error) {
- value.Level = strings.ToLower(strings.TrimSpace(value.Level))
- if value.Level == "" {
- return LogEvent{}, errors.New("log level is required")
- }
- if strings.TrimSpace(value.Message) == "" {
- return LogEvent{}, errors.New("log message is required")
- }
- fields, err := normalizeJSONValue(value.Fields)
- if err != nil {
- return LogEvent{}, fmt.Errorf("normalize log fields: %w", err)
- }
- if value.Time.IsZero() {
- value.Time = time.Now().UTC()
- }
- result, err := s.db.ExecContext(ctx, `
- INSERT INTO log_events (event_time, level, message, caller, fields_json)
- VALUES (?, ?, ?, ?, ?)
- `, value.Time.Unix(), value.Level, value.Message, value.Caller, string(fields))
- if err != nil {
- return LogEvent{}, fmt.Errorf("append log event: %w", err)
- }
- value.ID, err = result.LastInsertId()
- if err != nil {
- return LogEvent{}, fmt.Errorf("read log event id: %w", err)
- }
- value.Fields = fields
- return value, nil
- }
- func (s *Store) ListLogEvents(ctx context.Context, filter LogFilter) ([]LogEvent, error) {
- clauses := make([]string, 0, 4)
- args := make([]any, 0, 5)
- if filter.Level != "" {
- clauses = append(clauses, `level = ?`)
- args = append(args, strings.ToLower(filter.Level))
- }
- if !filter.Since.IsZero() {
- clauses = append(clauses, `event_time >= ?`)
- args = append(args, filter.Since.UTC().Unix())
- }
- if !filter.Until.IsZero() {
- clauses = append(clauses, `event_time < ?`)
- args = append(args, filter.Until.UTC().Unix())
- }
- if filter.BeforeID > 0 {
- clauses = append(clauses, `id < ?`)
- args = append(args, filter.BeforeID)
- }
- query := logEventSelect
- if len(clauses) > 0 {
- query += ` WHERE ` + strings.Join(clauses, ` AND `)
- }
- query += ` ORDER BY event_time DESC, id DESC LIMIT ?`
- args = append(args, normalizedLimit(filter.Limit))
- rows, err := s.db.QueryContext(ctx, query, args...)
- if err != nil {
- return nil, fmt.Errorf("list log events: %w", err)
- }
- defer rows.Close()
- values := make([]LogEvent, 0)
- for rows.Next() {
- value, err := logEvent(rows)
- if err != nil {
- return nil, fmt.Errorf("scan log event: %w", err)
- }
- values = append(values, value)
- }
- if err := rows.Err(); err != nil {
- return nil, fmt.Errorf("iterate log events: %w", err)
- }
- return values, nil
- }
- const logEventSelect = `
- SELECT id, event_time, level, message, caller, fields_json
- FROM log_events`
- func logEvent(row rowScanner) (LogEvent, error) {
- var value LogEvent
- var eventTime int64
- var fields string
- err := row.Scan(
- &value.ID, &eventTime, &value.Level, &value.Message,
- &value.Caller, &fields,
- )
- if errors.Is(err, sql.ErrNoRows) {
- return LogEvent{}, ErrNotFound
- }
- if err != nil {
- return LogEvent{}, err
- }
- value.Time = time.Unix(eventTime, 0).UTC()
- value.Fields = []byte(fields)
- return value, nil
- }
- func (s *Store) PruneAuditEvents(ctx context.Context, before time.Time) (int64, error) {
- return deleteEventsBefore(ctx, s.db, `audit_events`, `created_at`, before)
- }
- func (s *Store) PruneLogEvents(ctx context.Context, before time.Time) (int64, error) {
- return deleteEventsBefore(ctx, s.db, `log_events`, `event_time`, before)
- }
- // CountLogEvents returns how many application log rows are persisted.
- func (s *Store) CountLogEvents(ctx context.Context) (int64, error) {
- var count int64
- if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM log_events`).Scan(&count); err != nil {
- return 0, fmt.Errorf("count log events: %w", err)
- }
- return count, nil
- }
- // PruneLogEventsToCount keeps only the newest `keep` log rows, deleting the
- // rest. keep <= 0 deletes everything.
- func (s *Store) PruneLogEventsToCount(ctx context.Context, keep int) (int64, error) {
- if keep < 0 {
- keep = 0
- }
- result, err := s.db.ExecContext(ctx, `
- DELETE FROM log_events WHERE id NOT IN (
- SELECT id FROM log_events ORDER BY id DESC LIMIT ?
- )
- `, keep)
- if err != nil {
- return 0, fmt.Errorf("prune log events to count: %w", err)
- }
- affected, err := result.RowsAffected()
- if err != nil {
- return 0, fmt.Errorf("read pruned log count: %w", err)
- }
- return affected, nil
- }
- func deleteEventsBefore(
- ctx context.Context,
- executor contextExecer,
- table string,
- column string,
- before time.Time,
- ) (int64, error) {
- // table and column are internal constants from the callers above.
- result, err := executor.ExecContext(
- ctx,
- `DELETE FROM `+table+` WHERE `+column+` < ?`,
- before.UTC().Unix(),
- )
- if err != nil {
- return 0, fmt.Errorf("prune %s: %w", table, err)
- }
- affected, err := result.RowsAffected()
- if err != nil {
- return 0, fmt.Errorf("read pruned %s count: %w", table, err)
- }
- return affected, nil
- }
- // PruneEvents removes audit and application logs atomically.
- func (s *Store) PruneEvents(
- ctx context.Context,
- auditBefore time.Time,
- logBefore time.Time,
- ) (auditCount int64, logCount int64, err error) {
- tx, err := s.db.BeginTx(ctx, nil)
- if err != nil {
- return 0, 0, fmt.Errorf("begin event pruning: %w", err)
- }
- defer tx.Rollback()
- auditCount, err = deleteEventsBefore(ctx, tx, `audit_events`, `created_at`, auditBefore)
- if err != nil {
- return 0, 0, err
- }
- logCount, err = deleteEventsBefore(ctx, tx, `log_events`, `event_time`, logBefore)
- if err != nil {
- return 0, 0, err
- }
- if err := tx.Commit(); err != nil {
- return 0, 0, fmt.Errorf("commit event pruning: %w", err)
- }
- return auditCount, logCount, nil
- }
|