Home

05 audit log

3a.5 Audit log

Implementation status: shipped (M2). The audit log uses Redis Streams as its storage medium. The planned SQL-based implementation described below was superseded by this lighter-weight operational log.


Storage: Redis Stream

All audit entries are appended to a single global Redis Stream under the key bigfred:audit. Entries are serialised as JSON and stored in the stream's payload field. The stream is trimmed to ≈ 5 000 entries (MAXLEN ~ 5000 APPROX) and its TTL is refreshed to 24 hours on every write, so an idle installation automatically evicts old data without a janitor.

The AuditService lives in pkgs/bigfred/server/service/audit.go and exposes two methods:

// Publish implements cmd.AuditPublisher.
// layoutID = 0 means the event is not scoped to a specific layout.
func (s *AuditService) Publish(
    ctx      context.Context,
    layoutID uint,
    actor    cmd.AuditActor, // {UserID, Login}
    msg      string,         // i18n key, e.g. "audit_radio_stop"
    vars     map[string]string, // template variables interpolated on the frontend
) error

// List returns up to `limit` entries in newest-first order.
func (s *AuditService) List(ctx context.Context, limit int) ([]contract.AuditEntryWire, error)

cmd.AuditPublisher is the narrow interface injected into every cmd struct that emits events. Passing nil is always safe — every call site nil-guards the publisher.


Wire format (stored in each stream entry)

{
  "layoutId":   2,
  "actorId":    42,
  "actorLogin": "Damian",
  "msg":        "audit_radio_stop",
  "vars":       { "layout": "Makieta główna" },
  "occurredAt": 1718784023000
}

The streamId field is the Redis entry ID and is not stored inside the payload — it is populated at read time.


Audited events

i18n key Emitted by Vars
audit_radio_stop cmd.RadioStop.Trigger
audit_estop_target cmd.EStopTarget.Trigger target, targetId
audit_layout_updated httpapi.LayoutHandler.Update name
audit_layout_locked httpapi.LayoutHandler.Lock name
audit_layout_unlocked httpapi.LayoutHandler.Unlock name
audit_command_station_updated httpapi.CommandStationHandler.Update name
audit_command_station_deleted httpapi.CommandStationHandler.Delete name
audit_takeover_granted cmd.Takeover.autoGrant driver, target, vehicle
audit_user_created httpapi.UserHandler.Create target
audit_user_updated httpapi.UserHandler.Update target
audit_user_deleted httpapi.UserHandler.Delete target
audit_user_activated httpapi.UserHandler.Activate target
audit_user_deactivated httpapi.UserHandler.Deactivate target
audit_roster_vehicle_added httpapi.LayoutRosterHandler.AddVehicle vehicle
audit_roster_vehicle_removed httpapi.LayoutRosterHandler.RemoveVehicle vehicle
audit_roster_train_added httpapi.LayoutRosterHandler.AddTrain train
audit_roster_train_removed httpapi.LayoutRosterHandler.RemoveTrain train

REST API

GET /api/v1/audit-log?limit=<n>

Frontend

The Audit log view is accessible to all logged-in users via the "My" menu in the top navigation bar (/audit-log). Refresh is manual only (a "Refresh" button invalidates the React-Query cache and re-fetches).

Messages are translated on the frontend using the audit i18n namespace (web/src/i18n/locales/{pl,en}/audit.json). The msg field is used as the translation key under the events namespace group:

// example — audit:events.audit_radio_stop
t(`events.${entry.msg}`, { actorLogin: entry.actorLogin, ...entry.vars })

Write-path discipline


Retention & limitations compared to SQL

Property Redis Streams (current) SQL (future option)
Ordering Guaranteed (stream ID is monotonic) Guaranteed
TTL 24 h idle, auto-trimmed Configurable (90 d in original design)
Max entries ≈ 5 000 Unlimited
Filtering Client-side only SQL WHERE
Persistence after restart RDB by default (save 60 100; disable with --redis-no-persist) Always
Access control Any authenticated user Admin-only (original design)