jacobtims/filament-logger
Filament Logger adds activity logging to your Filament admin panel, recording model creations, updates, and deletions so you can review a clear, searchable history of changes made by your users.
composer require jacobtims/filament-logger, then publish the package assets/config (php artisan vendor:publish --tag="filament-logger-config" and the migrations tag) and run php artisan migrate. The package builds on top of spatie/laravel-activitylog, so you'll see an activity_log table appear alongside its own config.PanelProvider::panel(), add the plugin class (e.g. ->plugin(FilamentLoggerPlugin::make())). This is what wires the Activity Log resource into navigation and applies the package's default authorization.config/filament-logger.php is the contract for everything else: the activity model, the resource class, navigation group/label, which resources get logged by default, and the causer resolver. Read it before you touch any models.use Spatie\Activitylog\Traits\LogsActivity; to a model, implement getActivitylogOptions() with LogOptions::defaults()->logFillable()->logOnlyDirty(), save a record in Filament, then open the Activity Log resource in the panel. You should see the create event, the causer (the logged-in user), and the changed attributes. That single loop — model change → Filament admin edit → activity entry — is the package's core value proposition, and it's worth verifying before building anything custom.If the resource doesn't appear, check panel registration and canAccess()/policy wiring before debugging the logger itself; a missing resource is almost always a panel or authorization problem, not a logging problem.
Trait-on-the-model is the default pattern. For most CRUD resources you don't write logging code at all — you add LogsActivity to the model and let Filament's existing create/update/delete flow generate entries. Prefer logFillable() over logAll() so you're not dumping password hashes, tokens, or relationship blobs into the audit trail.
Use logOnlyDirty() plus dontSubmitEmptyLogs(). Together these keep the log readable: an update that only touches updated_at or a no-op save won't create noise. This matters more in Filament than in a typical app because Filament forms frequently re-save the full model.
Manual logging for non-Eloquent actions. Anything that isn't a model mutation — exports, bulk imports, impersonation, permission changes, queue dispatches — should be logged explicitly:
activity()
->performedOn($record)
->causedBy(auth()->user())
->withProperties(['ip' => request()->ip(), 'changes' => $diff])
->log('exported');
Use a small enum or constants class for the log() string so the Activity Log resource's filters stay consistent and you avoid typo'd event names polluting the filter dropdown.
Custom subject types. If you log things that aren't Eloquent models (or you use a morph map), set the morph map in a service provider and keep names short and stable. The resource renders subject_type directly, so long FQCNs make the UI unreadable.
Attach context with a tap or a helper. Wrap a request-scoped context (tenant ID, impersonator, request ID) in a single helper that calls activity()->withProperties(...) once, rather than repeating property arrays at every call site. The package's extensibility hooks (custom activity model, custom resource) are the right place to add panel-wide defaults — subclass the activity model, point the config at it, and add accessors for anything the UI needs.
Override the resource, don't fork it. Register your own resource class in the config and extend the package's, overriding table(), form(), or getNavigationBadge(). That survives package upgrades; copying the resource into app/Filament does not.
Gate visibility by policy. The activity log is sensitive by nature. Scope the resource's query to the current tenant, and give non-admin roles a read-only or causer-scoped view. Doing this in the resource's getEloquentQuery() is far easier than trying to hide rows in the table.
activity_log table must exist before the package resource queries it. If you're installing into an existing app, publish and run the spatie migration too — the package usually depends on it rather than shipping a duplicate. Check vendor/ before assuming a missing table means a broken install.auth()->user() is null outside HTTP. Either pass the causer explicitly with causedBy(), or set a default causer in a queued job's constructor. Entries with a null causer look like system activity and are easy to misread during incident review.logOnlyDirty() doesn't mean "only changed in this request". It compares against the model's original state; a model you re-hydrate and save may log fields you didn't intend. Test the exact update path in Filament — especially ->save() calls inside afterCreate/afterSave hooks, which can produce double entries if you also have manual logging.activitylog's delete_records_older_than_days and schedule activitylog:clean, or you'll be adding an index-and-archive task six months in. Add an index on (subject_type, subject_id) and created_at early.getActivitylogOptions(); entries but empty properties? check logFillable() against $fillable (guarded fields are skipped); resource missing? check panel plugin registration and policy; wrong causer? check the config's causer resolver.tenant_id column without touching vendor code. Do that before reaching for a global scope on the package's model — global scopes here tend to leak into the activitylog:clean command.How can I help you explore Laravel packages today?