Audit Log

The audit log records sensitive operations as JSON Lines, one event per line, append-only. Configure the path via audit.file in horizon.yaml (see Setup → audit).

Event schema

Source: apps/bff/src/audit/logger.ts.

interface AuditEvent {
  ts: string;                        // ISO-8601 timestamp
  actor: string | null;              // username; null for system events
  action: string;                    // e.g. 'auth.login', 'rule.addOrUpdate'
  verb?: string;                     // RBAC verb checked, if any
  target?: string;                   // resource id / name
  outcome: string;                   // 'success' | 'failure' | 'break-glass' | HTTP status | OAP status
  details?: Record<string, unknown>; // free-form context
  fromIp?: string;                   // requester IP
  sessionId?: string;
}

One event per line, \n-terminated. Use jq -c to filter:

tail -f horizon-audit.jsonl | jq -c 'select(.action | startswith("auth."))'

Recorded actions

The recorded set evolves with the codebase. As of the current build:

Action Outcome values Notes
auth.login success, failure Standard login. details.backend is local or ldap. On success details.roles carries the resolved role list.
auth.login.break-glass break-glass Emergency admin login. Logged in addition to a WARN application log line.
auth.logout success Explicit logout (cookie cleared). Sessions that simply expire are not logged.
rule.addOrUpdate success, HTTP status on failure DSL Management create / update. target is the rule name; details carries the diff summary.
rule.delete success, HTTP status on failure DSL Management delete.
alarm-rule.addOrUpdate success, HTTP status on failure Alarm Rule editor write.
setup.write success, HTTP status on failure Per-user setup state write.
overview-template.write success Overview template admin edits.
layer-template.write success Layer template admin edits.

outcome is the literal string for normal flows (success, failure, break-glass) and a stringified HTTP / OAP status code when the underlying call failed. This makes audit-time error correlation straightforward: an entry with outcome: "503" tells you OAP returned a server error.

Example entries

Successful local login

{
  "ts": "2026-05-18T09:14:02.118Z",
  "actor": "alice",
  "action": "auth.login",
  "outcome": "success",
  "fromIp": "10.0.5.12",
  "sessionId": "k7r...",
  "details": { "backend": "local", "roles": ["operator"] }
}

Failed LDAP login

{
  "ts": "2026-05-18T09:14:08.221Z",
  "actor": "alice",
  "action": "auth.login",
  "outcome": "failure",
  "fromIp": "10.0.5.12",
  "details": { "backend": "ldap" }
}

(No sessionId — no session was created.)

Break-glass login

{
  "ts": "2026-05-18T14:29:33.456Z",
  "actor": "emergency-admin",
  "action": "auth.login.break-glass",
  "outcome": "break-glass",
  "fromIp": "192.0.2.10",
  "sessionId": "z3a...",
  "details": { "backend": "ldap" }
}

Rule write

{
  "ts": "2026-05-18T15:02:11.004Z",
  "actor": "alice",
  "action": "rule.addOrUpdate",
  "verb": "rule:write",
  "target": "service_resp_time_rule",
  "outcome": "success",
  "fromIp": "10.0.5.12",
  "sessionId": "k7r..."
}

File format

  • JSON Lines. One JSON object per line, \n-terminated.
  • Append-only. The BFF opens the file in append mode and never truncates / rotates.
  • No rotation built in. Pair with logrotate, vector, fluent-bit, or a sidecar shipper.

Storage placement

  • Durable storage required. Break-glass logins, rule edits, and setup changes should outlive the container.
  • Filesystem perms matter for forensic integrity — typically 0640 (BFF user write, ops group read), not world-readable. Adjust to your operations posture.
  • Encrypted at rest if your compliance posture requires it. Use disk-level encryption (LUKS, EBS encryption) — the BFF itself does not encrypt.

What is NOT in the audit log

  • Read operations. Dashboard fetches, alarm queries, MQE reads are not logged (volume would be unworkable, and reads have no side effects). Configure debugLog.enabled for wire-level read logging.
  • Typed passwords. Never logged. Failed logins show the actor but not the attempted password.
  • OAP response bodies. Audit entries reference what was attempted, not the underlying response payload. For payload-level visibility, use debugLog (./horizon-wire.jsonl).

In-memory “seen cache”

In addition to the on-disk audit log, the BFF maintains an in-memory UserSeenCache of successful logins:

  • Records: username, source (local / ldap / break-glass), roles, last-seen timestamp, last IP.
  • Reset on BFF restart.
  • Exposed via GET /api/admin/users and visible on the Users admin page.

This is a UX convenience — it lets the Users page show “who has logged in to this BFF instance recently” without parsing the audit log. For historical / cluster-wide analysis, parse the JSONL file directly.

Wire-up to log pipelines

JSON Lines is ingestable by almost everything. Common pipelines:

Pipeline Configuration
Vector [sources.horizon] type = "file", format = "json"
Fluent Bit INPUT tail, Parser json
Promtail / Loki pipeline_stages: - json
Elastic Filebeat filebeat.inputs: - type: filestream, parsers: - ndjson
Splunk Universal Forwarder INDEXED_EXTRACTIONS=JSON

Index on actor, action, outcome for the common queries (“all admin actions by user X in the last 24h”).