mhamed/spatie-activitylog-browse

Auto-log all models, enrich with request/device data, and browse activity logs via a web UI

Maintainers

Package info

github.com/mahmoud-mhamed/spatie-activitylog-browse

Language:Blade

pkg:composer/mhamed/spatie-activitylog-browse

Statistics

Installs: 1 640

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-04-28 21:39 UTC

This package is auto-updated.

Last update: 2026-04-28 21:41:12 UTC


README

Latest Version on Packagist License PHP Version

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 LogsActivity trait โ€” 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

  • 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 install after 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 --force to 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:

  1. Open /activity-log in your browser โ€” protected by web + auth middleware by default.
  2. Trigger any model change โ€” it'll appear with full enrichment.
  3. (Optional) Enable retention in config/activitylog-browse.php so old logs prune themselves.
  4. (Optional) Set ACTIVITYLOG_BROWSE_PASSWORD in .env to 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)

  1. 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.
  2. max_rows / max_size_mb โ€” hard size caps. They win over default_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 the default_days window.
  3. 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 365 days 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 name changed)
  • 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

  1. Disable automatic migrations so they don't run on the central DB:
    'load_migrations' => false,
  2. Publish migrations to your tenant migration path:
    php artisan vendor:publish --tag=activitylog-browse-migrations
    Then move them to database/migrations/tenant/ (or wherever your tenant migrations live).
  3. 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 โ€” LogsActivity trait 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_name if you don't need queue tracking (skips a debug_backtrace per 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.