mhamed / spatie-activitylog-browse
Auto-log all models, enrich with request/device data, and browse activity logs via a web UI
Package info
github.com/mahmoud-mhamed/spatie-activitylog-browse
Language:Blade
pkg:composer/mhamed/spatie-activitylog-browse
Requires
- php: ^8.1
- illuminate/support: ^10.0||^11.0||^12.0
- spatie/laravel-activitylog: ^4.0
README
A Laravel package that extends spatie/laravel-activitylog v4 with automatic model logging, rich contextual enrichment, a web-based log browser, a statistics dashboard, automatic retention/cleanup, and a deletion audit trail โ all with English/Arabic UI, dark mode, and an optional password gate.
Arabic version: README.ar.md
Features
Logging
- ๐ Auto-log all models without the
LogsActivitytrait โ opt-out via excluded list - ๐ฆ Rich enrichment โ request, device, performance, app, session, and execution context attached to every log entry
- ๐ UUID-friendly โ morph ID columns automatically migrated to support UUIDs
- โก Performance-optimized โ per-class caching, request-scoped collectors, no per-event reflection
Browsing & Analytics
- ๐ Browse UI โ filter, search, popovers, color-coded diffs, related-model navigation
- ๐ Statistics dashboard โ charts for hourly/daily/monthly activity, peak times, top models/causers/attributes
- ๐ Localized โ English & Arabic with automatic RTL layout
- ๐ Dark mode โ system-aware with manual toggle, persisted in localStorage
- ๐ Attribute translation โ uses Laravel's
validation.attributes
Cleanup & Audit
- ๐งน Manual cleanup page โ preview-then-delete with model and date filters
- โฑ Automatic retention โ age + size limits, per-model overrides (incl.
'forever'), Laravel-scheduler integration - ๐ Deletion history โ JSON audit log of every cleanup with row-level diff (before/after, duration, trigger, user)
Security
- ๐ก Optional password gate with rate-limited login (5/min)
- ๐ช Authorization gate support for fine-grained permissions
- ๐ข Multi-tenancy aware (works out of the box with stancl/tenancy)
Table of Contents
- Requirements
- Installation
- Quick Start
- Configuration
- Usage
- Browse UI
- Statistics Dashboard
- Deletion History Page
- Artisan Commands
- Localization
- Multi-Tenancy
- Performance Notes
- Architecture
- License
Requirements
- PHP 8.1+
- Laravel 10, 11, or 12
- spatie/laravel-activitylog ^4.0
Installation
composer require mhamed/spatie-activitylog-browse
If auto-discovery doesn't work, register the provider manually in bootstrap/providers.php (Laravel 11+) or config/app.php:
Mhamed\SpatieActivitylogBrowse\ActivitylogBrowseServiceProvider::class,
Then run the install command โ publishes the spatie migration, the package config, runs migrations, fixes UUID-friendly morph columns, adds performance indexes, and prepares the deletion-history storage:
php artisan activitylog-browse:install
Re-running
installafter upgrading the package will offer to refresh your config so new options (e.g.retention,deletion_history) are picked up.
Publishing individual assets
# Spatie migration php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations" php artisan migrate # Package config php artisan vendor:publish --tag=activitylog-browse-config # Views (optional) php artisan vendor:publish --tag=activitylog-browse-views # Language files (optional) php artisan vendor:publish --tag=activitylog-browse-lang # Migrations (optional โ for multi-tenancy setups) php artisan vendor:publish --tag=activitylog-browse-migrations
Tip: Use
--forceto overwrite previously published files after upgrading the package:php artisan vendor:publish --tag=activitylog-browse-config --force
Local Development
To install as a local path repository, add the following to your Laravel app's composer.json:
"repositories": [ { "type": "path", "url": "../spatie-activitylog-browse" } ]
composer require mhamed/spatie-activitylog-browse:@dev php artisan activitylog-browse:install
Quick Start
After running activitylog-browse:install:
- Open
/activity-login your browser โ protected byweb+authmiddleware by default. - Trigger any model change โ it'll appear with full enrichment.
- (Optional) Enable retention in
config/activitylog-browse.phpso old logs prune themselves. - (Optional) Set
ACTIVITYLOG_BROWSE_PASSWORDin.envto add a password gate.
Configuration
The published config file is at config/activitylog-browse.php. Key sections below.
Auto-Log
'auto_log' => [ 'enabled' => true, 'events' => ['created', 'updated', 'deleted'], 'models' => '*', // '*' = all models, or array of specific classes 'excluded_models' => [], 'log_name' => 'default', 'log_only_dirty' => true, 'excluded_attributes' => [ 'password', 'remember_token', 'two_factor_secret', 'two_factor_recovery_codes', ], 'submit_empty_logs' => false, 'exclude_null_on_create' => false, ],
Models that already use the LogsActivity trait are automatically skipped to prevent duplicate entries.
Enrichment
Each enrichment section can be enabled/disabled and has per-field toggles. Disabling an entire section removes all per-event overhead for that section.
Show all enrichment options
'request_data' => [ 'enabled' => true, 'fields' => [ 'url' => true, 'previous_url' => true, 'method' => true, 'route_name' => true, ], ], 'device_data' => [ 'enabled' => true, 'fields' => ['ip' => true, 'user_agent' => true, 'referrer' => true], ], 'performance_data' => [ 'enabled' => true, 'fields' => [ 'request_duration' => true, // ms since LARAVEL_START 'memory_peak' => true, // bytes 'db_query_count' => true, // requires DB::enableQueryLog() to be useful ], ], 'app_data' => [ 'enabled' => true, 'fields' => [ 'environment' => true, 'php_version' => true, 'server_hostname' => true, ], ], 'session_data' => [ 'enabled' => true, 'fields' => ['auth_guard' => true], ], 'execution_context' => [ 'enabled' => true, 'fields' => [ 'source' => true, // "web" | "console" | "queue" | "schedule" 'job_name' => true, // queue job class name 'command_name' => true, // artisan command name ], ],
All collectors gracefully return empty data when running outside their context (e.g. request data in console).
Browse UI Config
'browse' => [ 'enabled' => true, 'prefix' => 'activity-log', 'middleware' => ['web', 'auth'], 'per_page' => 25, 'gate' => null, // e.g. 'view-activity-log' 'password' => env('ACTIVITYLOG_BROWSE_PASSWORD'), 'available_locales' => ['en', 'ar'], ],
Set gate to a Laravel Gate name to restrict access; the package will call Gate::authorize($name) on every browse request.
Password Gate
For environments where you want a shared password protecting the browse UI (in addition to whatever auth/middleware you've configured):
# .env
ACTIVITYLOG_BROWSE_PASSWORD=your-secret-here
When set, users hitting /activity-log are redirected to a login screen. The form is rate-limited to 5 attempts per minute per IP. Authentication is stored in the session โ a logout button appears in the navbar when a user is signed in via password.
Set the env variable to an empty value (or remove it) to disable the gate entirely.
Retention / Auto-Cleanup
Automatically prune old activity log entries based on age and table size limits, with per-model overrides for sensitive data that should be kept longer (or forever).
'retention' => [ 'enabled' => true, 'default_days' => 90, // catch-all age limit 'max_rows' => 1_000_000, // null to disable 'max_size_mb' => 500, // null to disable 'per_model' => [ App\Models\AuditLog::class => 'forever', App\Models\User::class => 365, ], 'per_log_name' => [ 'security' => 365, ], 'chunk_size' => 1000, 'optimize_after' => true, 'schedule' => 'daily', // 'daily' | 'weekly' | 'monthly' | null 'schedule_time' => '03:00', // 24-hour HH:MM ],
Priority hierarchy (strongest โ weakest)
per_model/per_log_nameโ always win.'forever'is fully protected from both age and size pruning.- An int day count protects records younger than the configured days from BOTH age and size pruning.
max_rows/max_size_mbโ hard size caps. They win overdefault_days: when the table is over the cap, the oldest records (not protected by a per-model rule) are deleted even if they are still inside thedefault_dayswindow.default_daysโ the catch-all rule. Applies only to records not covered by a higher-priority rule.
What happens at the size limit?
| Per-model rule | Age-based prune | Size-based prune (when max_rows / max_size_mb is hit) |
|---|---|---|
| Not configured | Deleted after default_days |
Can be deleted (oldest first) |
365 (any int days) |
Deleted after 365 days | Protected while younger than 365 days; older records can be deleted |
'forever' |
Never deleted | Never deleted (fully protected) |
TL;DR: Per-model retention is the authoritative rule. A model set to
365days will keep its rows for the full 365 days even if the table is over its size cap โ they only become eligible for size pruning after their own retention window expires. The size cap is therefore best-effort: if every record is still inside its per-model retention window, nothing is deleted and the table stays over the cap until the protections expire. Set realistic per-model values to keep the size cap effective.
How it runs
| Trigger | When |
|---|---|
| Schedule | Automatically at schedule_time (frequency = daily/weekly/monthly). Requires schedule:work or a cron entry calling schedule:run. |
| CLI | php artisan activitylog-browse:prune โ see Artisan Commands |
| UI | A Run Cleanup Now button on the cleanup page. |
Deletion History Config
Every cleanup operation (manual, scheduled, CLI, dry-run) is recorded in an append-only JSON file:
'deletion_history' => [ 'enabled' => true, 'path' => storage_path('activitylog-browse/deletion-history.json'), 'max_entries' => 500, // oldest are dropped first 'max_size_mb' => 3, // file is reset if exceeded ],
Each entry captures: timestamp, trigger (schedule/cli/ui/manual), operation type, deleted count + breakdown, duration, table state before/after (rows + size MB), config snapshot, and user/IP context. Empty operations (0 rows deleted) are skipped.
The package automatically creates the storage directory and a .gitignore to prevent committing the JSON file.
Usage
Auto-Logging
Once installed, all Eloquent model events are logged automatically:
$user = User::create(['name' => 'John']); // Logged $user->update(['name' => 'Jane']); // Logged $user->delete(); // Logged
To exclude specific models:
'excluded_models' => [ App\Models\TemporaryFile::class, ],
Enrichment payload
Every activity log entry โ whether from auto-logging, the LogsActivity trait, or manual activity() calls โ is enriched with contextual data:
Example enriched properties
{
"attributes": { "name": "Jane" },
"old": { "name": "John" },
"request_data": {
"url": "https://example.com/users/1",
"method": "PUT",
"route_name": "users.update"
},
"device_data": {
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0 ..."
},
"performance_data": {
"request_duration": 142,
"memory_peak": 12582912,
"db_query_count": 8
},
"app_data": {
"environment": "production",
"php_version": "8.3.0",
"server_hostname": "web-01"
},
"session_data": { "auth_guard": "web" },
"execution_context": {
"source": "web",
"job_name": null,
"command_name": null
}
}
Browse UI
Visit /activity-log (or your configured prefix). Top navigation includes: Activity Log, Statistics, Cleanup, Deletion History, About โ plus a language switcher, theme toggle, and (when password gate is on) logout.
The list view provides:
- Filtering โ log name, event type, model type, model ID, causer, date range, description search
- Changed-attribute filter โ select a model type, then filter by a specific attribute (e.g. only show logs where
namechanged) - Quick preview popover โ hover the info icon on a row to see the old/new diff inline
- Current-attributes popover โ view the subject's live model data without leaving the list
- Model info sidebar โ when a model type is selected, shows total logs, unique records, table name, table size, event-breakdown badges, and clickable attribute chips for quick filtering
- Related model navigation โ jump to all logs for a related model instance
- Detail view โ color-coded diff, request/device/performance/app/session/execution metadata, raw JSON
Attribute translation
Attribute names (column names like first_name, email_verified_at) are auto-translated using lang/{locale}/validation.php:
- If
validation.attributes.{key}exists โ "First Name" (first_name) - Otherwise โ "Email Verified At" (auto-headlined) with the original key in small text
Define translations once and they appear everywhere in the UI:
'attributes' => [ 'first_name' => 'First Name', 'email' => 'Email Address', 'created_at' => 'Creation Date', ],
Statistics Dashboard
Visit /activity-log/statistics. Each section loads independently via AJAX with skeleton states for fast initial render.
A date-range filter at the top applies to all sections (cached for 60s when filtered, 120s for all-time).
Sections: Overview cards ยท Peak Hour chart ยท Daily Activity (30 days) ยท Activity by Day of Week ยท Peak Times ยท Monthly Activity ยท System vs User Actions ยท Events Breakdown ยท Log Names ยท Top Models ยท Top Causers ยท Most Changed Attributes (last 1000 updates).
Deletion History Page
/activity-log/deletion-history โ auditable record of every cleanup operation:
- Stats cards โ total entries, file size, current path
- Per-row โ when, trigger badge (color-coded: schedule/cli/ui/manual + dry-run flag), operation, deleted count + breakdown (age vs size), table size before โ after with diff, duration in ms, user/IP/command
- Expandable JSON โ click a row to see the full entry payload (config snapshot, table state, context)
- Pagination โ 25 per page
- Clear button โ wipes the JSON file (with confirmation)
Artisan Commands
# Install / upgrade php artisan activitylog-browse:install # Retention / cleanup php artisan activitylog-browse:prune # full prune (age + size) php artisan activitylog-browse:prune --dry-run # report what would be deleted php artisan activitylog-browse:prune --age # age-based only php artisan activitylog-browse:prune --size # size-based only
The prune command is automatically registered with the Laravel scheduler when retention.schedule is set (and you have schedule:work or cron running).
Localization
The package ships with English and Arabic translations. The UI auto-adapts to RTL when locale is ar.
// config/app.php 'locale' => 'ar',
Or at runtime:
App::setLocale('ar');
The browse UI also includes a language switcher button that saves the preference in the session.
To customize translations:
php artisan vendor:publish --tag=activitylog-browse-lang
# Use --force to overwrite previously published files
php artisan vendor:publish --tag=activitylog-browse-lang --force
This copies the files to lang/vendor/activitylog-browse/ where you can edit them or add new languages โ then update available_locales in the config.
Multi-Tenancy
Works out of the box with stancl/tenancy (multi-database tenancy):
- Cache isolation โ keys are prefixed with the tenant ID (e.g.
activitylog-browse:t:1:stats:overview). - Database connection โ queries use whatever connection your Activity model defines.
- No hard dependency โ tenant detection uses
function_exists('tenant').
Setup for multi-database tenancy
- Disable automatic migrations so they don't run on the central DB:
'load_migrations' => false, - Publish migrations to your tenant migration path:
php artisan vendor:publish --tag=activitylog-browse-migrations
Then move them todatabase/migrations/tenant/(or wherever your tenant migrations live). - Add tenancy middleware to the browse routes:
'browse' => [ 'middleware' => ['web', 'auth', \Stancl\Tenancy\Middleware\InitializeTenancyByDomain::class], ],
Without tenancy, no extra setup is needed โ everything works as expected.
Performance Notes
The package is designed for low-overhead operation even on high-traffic apps with auto-logging enabled:
- Per-class caching in
GlobalModelLoggerโLogsActivitytrait detection runs once per model class, not per event - Request-scoped collectors โ
debug_backtrace, auth guard enumeration, and source detection run once per request, results cached as static properties - Disabled-aware enrichment โ the observer only invokes collectors that are enabled in config; disabled sections add zero per-event overhead
- Console-only schedule registration โ HTTP requests skip scheduler binding entirely
- Bulk-friendly delete chunks โ retention pruning runs in chunks of 1000 (configurable) with
set_time_limit(30)to avoid table-lock storms
For best results on high-throughput apps:
- Add high-frequency models to
excluded_models - Disable
execution_context.fields.job_nameif you don't need queue tracking (skips adebug_backtraceper request) - Use
Model::withoutEvents(...)around bulk imports
Architecture
| Component | Role |
|---|---|
ActivitylogBrowseServiceProvider |
Registers everything: listener, observer, routes, scheduler |
GlobalModelLogger |
Listens to global Eloquent events; logs activity for models without LogsActivity |
ActivityEnrichmentObserver |
Observes the Activity model's creating event; merges enrichment data into properties |
RequestDataCollector / DeviceDataCollector / ... |
Individual data collectors invoked by the observer |
RelationDiscovery |
Reflection-based auto-discovery of Eloquent relationships for related-model browsing |
RetentionPruner |
Implements the priority hierarchy โ age, size, per-model, per-log-name pruning |
DeletionLogger |
Writes deletion entries to the JSON history file with size/count caps |
ActivityLogHelpers |
Shared helpers โ connection name, table size, cache key prefix, stats cache invalidation |
ActivityLogController |
Handles the browse UI: filtering, AJAX endpoints, statistics API, attribute inspection, cleanup, deletion history |
RequirePassword middleware |
Enforces the optional password gate (rate-limited login) |
SetLocale middleware |
Applies the user's locale preference from the session |
InstallCommand |
activitylog-browse:install โ publishes assets, fixes UUID columns, adds indexes |
PruneCommand |
activitylog-browse:prune โ manual / scheduled retention runs |
License
MIT โ see LICENSE.