Audit Log

Who did what to which thing, when. Denied and errored attempts are kept and marked rather than filtered out — a log of successes only tells you nothing about the attack you are looking for.

src/components/ui/audit-log.tsx

About

From the component's own source, where it was written.

An append-only record of who did what.

There is no edit or delete affordance anywhere in this component, by design. An audit log that can be changed from the interface that displays it is not evidence, and the absence of those buttons is the feature.

Timestamps show both the relative and the absolute form. Relative is how people scan; absolute is what gets quoted in an incident review, and having to hover for it wastes the reviewer's time.

A failed action is recorded and shown. An attempt that was denied is often the most interesting row on the page.

Install

npx astralyx-ui add audit-log

Usage

import { AuditLog } from '@/components/ui/audit-log'

<AuditLog events={events} searchable />

Composer

Change the props and the source below follows.

  • a.okafor@example.com

    a.okafor@example.com updated role “Support agent”

    81.2.69.142

    OK
  • system

    system rotated API key sk_live_••4f21

    OK
  • m.laurent@example.com

    m.laurent@example.com attempted to delete account 88213

    45.83.220.11

    Denied
  • a.okafor@example.com

    a.okafor@example.com exported 12,408 customer records

    81.2.69.142

    OK
  • billing-worker

    billing-worker failed to charge subscription sub_9912

    Error
Props
tsx
<AuditLog events={events} searchable />

Examples

Who did what

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

AuditLog

Extends ComponentProps<'div'>.

PropTypeDefaultDescription
eventsAuditEvent[]required{ id, actor, action, target?, at, ip?, outcome?, detail? }. The action is a verb, so a row reads as a sentence.
searchablebooleantrueFilters across actor, action and target at once. Which field a term belongs to is not something the reader should have to decide.
nowDate——
localestring'en-GB'—
filterPlaceholderstring'Filter by actor, action or IP'—
filterLabelstring'Filter audit log'Accessible name for the filter field.
toggleDetailLabel(open: boolean) => stringDEFAULT_TOGGLE_DETAIL_LABELAccessible name for a row's detail toggle.
emptyMessageReactNode'No events match.'—

Types

AuditEvent

An append-only record of who did what.

FieldTypeDescription
idstring—
actorReactNode—
actorAvatar?ReactNode—
actionstringVerb — "updated", "deleted", "signed in".
target?ReactNode—
atDate—
ip?string—
outcome?'success' | 'denied' | 'error'Marked, never filtered. A denied attempt is often the most interesting row on the page.
detail?unknownAnything structured — a before/after pair, a payload. Rendered as an expandable JSON view, collapsed by default.

Behaviour

TopicDetailDescription
timestampsbothRelative for scanning, exact on hover and in the <time> element — an audit log with only "2 hours ago" is not evidence.