3 · Activity logging
Objective — give security-relevant events a durable audit trail so who did what, when always has an answer: install Spatie Activity Log, capture model changes, log auth and GDPR actions, and auto-prune old records on a schedule.
Background
Section titled “Background”When something goes wrong — a permission change, a mass deletion, a suspicious login — the first question is who did what, when. Activity logging answers it. Three sources feed one activity_log table; a scheduled job keeps it bounded.
flowchart LR M["Model changes<br/>(LogsActivity trait)"] --> L[("activity_log table")] A["Auth events<br/>login · logout · failed"] --> L G["GDPR actions<br/>export · deletion request"] --> L L --> C["activitylog:clean<br/>daily · 90-day retention"]1. Audit, then install Spatie Activity Log
Section titled “1. Audit, then install Spatie Activity Log”A CodeCanyon app may already ship its own logging. Check first — installing Spatie on top of a built-in logger just creates duplicate, conflicting trails.
-
Check for existing logging before you install.
Terminal window composer show | grep spatie/laravel-activitylogphp artisan tinker --execute="echo Schema::hasTable('activity_log') ? 'EXISTS' : 'NONE';"grep -r "LogsActivity" app/Models/# Expected: shows whether Spatie, the activity_log table, or LogsActivity already exist- ✅ You know which of the three states you’re in.
Finding Action No logging exists Full install (substep 2) Spatie installed, not configured Skip to section 2 Vendor has built-in logging Hybrid — add Spatie only for the gaps -
Install and configure Spatie (only when no logger exists).
Terminal window composer require spatie/laravel-activitylogphp artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-config"php artisan migrate# Expected: package installs, config publishes, activity_log table is createdIn
config/activitylog.php, gate it behind an env flag and set retention, then addACTIVITY_LOG_ENABLED=trueto.env:'enabled' => env('ACTIVITY_LOG_ENABLED', true),'delete_records_older_than_days' => 90,- ✅ Spatie is installed, env-gated, and migrated.
2. Log model changes on key models
Section titled “2. Log model changes on key models”Add the LogsActivity trait to the models that matter for an audit — User, Invoice, Subscription, Settings. Log only the fields you care about, and only when they actually change.
-
Add the
LogsActivitytrait with scoped, dirty-only options.app/Models/User.php use Spatie\Activitylog\Traits\LogsActivity;use Spatie\Activitylog\LogOptions;class User extends Authenticatable{use LogsActivity;public function getActivitylogOptions(): LogOptions{return LogOptions::defaults()->logOnly(['name', 'email', 'role', 'status'])->logOnlyDirty()->dontSubmitEmptyLogs();}}- ✅ Key models log only the selected fields, only when they change.
3. Log authentication events
Section titled “3. Log authentication events”Create listeners for Laravel’s auth events and capture IP + user agent — the context you need to spot credential stuffing or session hijacking.
-
Write a login listener that records the user, IP, and user agent.
app/Listeners/LogSuccessfulLogin.php activity('auth')->causedBy($event->user)->withProperties(['ip' => request()->ip(), 'user_agent' => request()->userAgent()])->log('User logged in');- ✅ Login activity records the actor + request context.
-
Write a failed-login listener —
Failedevents have no$event->user; log the attempted email instead.app/Listeners/LogFailedLogin.php activity('auth')->withProperties(['email' => $event->credentials['email'] ?? 'unknown','ip' => request()->ip(),'user_agent' => request()->userAgent(),])->log('Failed login attempt');- ✅ Failed-login activity captures email + request context (not
causedBy($event->user)).
- ✅ Failed-login activity captures email + request context (not
-
Register the listeners in
EventServiceProvider.protected $listen = [\Illuminate\Auth\Events\Login::class => [LogSuccessfulLogin::class],\Illuminate\Auth\Events\Failed::class => [LogFailedLogin::class],\Illuminate\Auth\Events\Logout::class => [LogLogout::class],];- ✅ Login, failed-login, and logout each fire their listener.
4. Log sensitive GDPR actions
Section titled “4. Log sensitive GDPR actions”Data export and account deletion are exactly the events a regulator (or a dispute) will ask about. Log deletion before the data is gone.
-
Log export and deletion-request events (deletion logged before the data is removed).
// AccountController::exportData()activity('gdpr')->causedBy(auth()->user())->log('User exported personal data');// AccountController::deleteAccount() — log BEFORE deletion runsactivity('gdpr')->causedBy($user)->log('User requested account deletion');- ✅ Both GDPR actions land in the log, with deletion logged pre-removal.
These wire directly into the GDPR endpoints built in Legal, privacy & GDPR.
5. Prune old records on a schedule
Section titled “5. Prune old records on a schedule”In app/Console/Kernel.php, clean the log nightly so it never balloons.
-
Schedule the nightly clean.
$schedule->command('activitylog:clean')->dailyAt('04:00');- ✅
activitylog:cleanis scheduled daily.
- ✅
-
Confirm the server cron drives the scheduler — install once per server in Phase 5 · Secure & verify schema (idempotent
grep -v 'schedule:run'pattern). Here, only verify:Terminal window ssh <deploy-alias> 'crontab -l 2>/dev/null | grep -c "schedule:run"' # must print exactly 1# Expected: 1 — if 0, go back to Phase 5; if 2+, dedupe with the Phase 5 idempotent install- ✅ Exactly one
schedule:runline is present on the server cron.
- ✅ Exactly one
6. Verify
Section titled “6. Verify”Confirm entries are actually landing in the table.
-
Inspect the most recent entries in tinker.
Terminal window php artisan tinker>>> Activity::latest()->first(); // shows the most recent entry>>> Activity::where('log_name', 'auth')->count(); // auth events recorded# Expected: a recent Activity row, and a non-zero auth count- ✅ The latest activity row returns and the auth count is non-zero.
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 🤖 Activity log installed — Spatie installed (or vendor logger confirmed);
ACTIVITY_LOG_ENABLED=true. - 🤖 Model changes logged —
LogsActivityon key models, logging only-dirty selected fields. - 🤖 Auth events logged — login / logout / failed-login listeners registered, capturing IP + user agent.
- 🤖 GDPR actions logged — export + deletion-request recorded (deletion logged before data removal).
- 🤖 Retention scheduled —
activitylog:cleandaily; server cron drivesschedule:run.