mradder/filament-logger
Filament Logger is an audit and activity log package for Filament admin panels, built on spatie/laravel-activitylog. It adds a ready-made activity resource, CSV/JSON exports, dashboard widgets, and automatic logging for models, resources, auth events, and custom events.
Three commands and one panel-provider edit get you a working audit UI:
composer require mradder/filament-logger
php artisan filament-logger:install # publishes config + spatie activity_log migration
php artisan migrate
Then register the ready-made resource in the panel you want it on:
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
->resources([
config('filament-logger.activity_resource'),
]);
}
Where to look first: open config/filament-logger.php after publishing — it is the single source of truth for what is logged (access events, notifications, redaction, ignored fields, retention) and it's where you'll spend most of your tuning time. Second stop is the Activity Review UI doc page, which explains the tabs, filters, and diff viewer you're about to see. Third is the resource class referenced by config('filament-logger.activity_resource') — you'll extend it sooner or later.
First real use case: "who changed this record, and from what to what?" Add LogsActivity to a single high-value model (an Invoice, a User, a Setting), perform an edit in the admin, and open the activity resource. You should see the causer, the subject, an updated event, and a structured old/new diff. That one loop — edit, check diff, confirm nothing sensitive leaked — validates the whole install and is worth doing before you enable resource-wide or auth logging.
A worthwhile second step: turn on access-event logging (access.guards => ['web'] in a multi-guard app) and confirm logins/failed logins appear in the "Auth issues" review tab. That's the security half of the package and it's the part auditors ask about.
Choose one logging layer per model. The package supports logging from the Filament resource layer and from the Eloquent model layer. Doing both on the same model gives you duplicate rows for every create/update. Rule of thumb: use the model trait when writes happen outside the panel too (imports, API, artisan commands); use resource-level logging only when the panel is the sole write path. Never both.
Trait configuration beats blanket logging. On the model:
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly(['status', 'total', 'assigned_to'])
->logOnlyDirty()
->dontSubmitEmptyLogs()
->useLogName('invoice');
}
logOnly + logOnlyDirty is what keeps the audit table readable and the diffs meaningful. A model with logAll() and a 40-column table produces diffs nobody reads.
Custom domain events are the highest-value feature. Most mature uses of this package aren't "log every update" — they're "log the ten decisions that matter":
activity('security')
->event('export_requested')
->performedOn($report)
->causedBy(auth()->user())
->withProperties(['filters' => $filters, 'rows' => $count])
->log('Exported customer report');
Give these a consistent log_name (e.g. security, billing) so saved tabs and the high-risk widget can slice them cleanly. This is also the natural hook for alert fan-out to mail/Slack/Discord.
Saved review tabs are a workflow, not a UI nicety. Pre-build tabs matching how your team actually investigates: "High risk" (deletes, force-deletes, permission changes), "Auth issues" (failed logins, lockouts), "Impersonation", "Billing". Then the daily routine is opening a tab and scanning — not constructing filters from scratch each morning. Combine with date presets to make "last 24h" and "since last release" one-click.
Exports belong in a runbook. CSV/JSON exports operate on the current filter set, so the pattern is: apply filters → export → attach to the ticket. Document the exact filter recipe for your compliance export so it's reproducible; a filtered export with an undocumented filter set is not evidence.
Pruning is part of the implementation, not an afterthought. Schedule the retention command (age + log name) from the start. Audit tables grow faster than any other table in the app, and retrofitting retention onto a million-row table on a Friday afternoon is unpleasant.
Multi-panel and tenancy: each panel registers its own resource instance; scope access via the activity policy and, if you're multi-tenant, verify that subject lookups don't leak across tenants. The recipes page covers both patterns explicitly — read it before you register the resource on a second panel.
Double-check the Filament version floor. Filament 4 support starts at 4.3.1 for an upstream security reason. If you're pinned to an earlier 4.x, upgrade before installing — don't try to work around it.
The install command publishes the Spatie migration. If activity_log already exists from a direct spatie/laravel-activitylog install, don't re-run the migration; the install command is a convenience wrapper, not a replacement for reviewing what it publishes. Check the published migration against your existing schema before migrate on a production-adjacent database.
Re-run config:cache after touching the config. Filament reads config('filament-logger.activity_resource') at panel boot; a stale config cache produces a confusing "resource won't appear / wrong resource appears" symptom. Same for any config-gated behaviour (access guards, redaction, ignored fields).
Hidden attributes and redaction solve different problems. $hidden on the model prevents certain attributes being logged at all (a fix landed for this in the 0.5.x line). Redaction replaces values in the displayed diff while retaining the record. Use $hidden/ignored-fields for "must never hit the database", and redaction for "auditors can see a change happened, but not the value" — passwords, tokens, PII, card data. Verify with a real edit and look at the redacted diff screenshot behaviour, don't assume.
Query-builder and mass updates log nothing. Model::where(...)->update(...), raw SQL, and upsert bypass model events entirely. If a bulk admin action must be auditable, log an explicit custom event alongside it rather than expecting the trait to catch it.
Soft-delete lifecycle events need soft deletes. Restore, force-delete, and replicate flows only produce activity when the model actually supports them. Plain deletes give you deleted and nothing else.
Morph maps quietly change subject_type. Introduce Relation::enforceMorphMap() after you have audit history and your existing rows point at FQCNs. Historical rows keep the old value, so subject filters look empty for old records. Decide on the morph map before going live, or write a one-off backfill.
Encrypted and cast columns show ciphertext in diffs. encrypted, encrypted:array, and some custom casts mean the "new value" is unreadable in the viewer. Redaction is the honest answer; a raw ciphertext blob in a diff just confuses reviewers.
Watch the write cost on chatty models. Every logged update is an extra insert plus JSON serialisation. Don't put the trait on high-frequency models (sessions, counters, job bookkeeping, read-tracking) — you'll add latency and blow up the table. Log those as aggregated custom events at most.
Alert hooks need thresholds and deduplication. Wiring Slack to every delete is how alerting dies. Alert on named custom events or on high-risk categories with a rate limit; put the "spike" signal in the dashboard widget where someone will actually look at it.
Testing: assertions against activity_log are straightforward, but disable noisy logging in your test config (or use a narrower logOnly) so unrelated tests don't fail on unexpected activity rows. RefreshDatabase plus an explicit custom event assertion is the cheapest, most durable test.
Extension points, in order of usefulness: (1) subclass the activity resource and point config('filament-logger.activity_resource') at your class to change navigation, columns, or filters; (2) add your own widgets next to the bundled ones; (3) swap the underlying Activity model if you need extra columns — the package resolves it automatically, which is the cleanest place to attach a tenant_id; (4) policies on the activity model gate the whole surface, and the package ships stricter authorization defaults than the original — confirm your non-admin roles are either intentionally excluded or intentionally granted, because a silently empty audit screen is worse than a locked one.
How can I help you explore Laravel packages today?