Lightweight Laravel activity logging package.
Make this package installable by consumers via Composer and Packagist.
- Create a standalone GitHub repository (e.g.
gottvergessen/activity) and push thisactivity/folder as the repository root. - On Packagist, submit the repository or enable GitHub auto-update.
- In the GitHub repository, add a secret named
PACKAGIST_TOKENwith your Packagist API token. - Create a tag (e.g.
v1.0.0) and push the tag; the workflow will notify Packagist to update.
After publishing to Packagist, consumers can run:
composer require gottvergessen/activity- This package uses PSR-4 autoloading defined in
composer.json. - Tests use
orchestra/testbenchandpest.
The repository includes a GitHub Actions workflow that notifies Packagist on new tags. See .github/workflows/notify-packagist.yml.
Logger is a lightweight, opinionated activity logging package for Laravel that automatically tracks model changes and records who did what, to which model, and when — without polluting your domain logic.
Features:
- Automatic tracking of model changes (create, update, delete, restore)
- Privacy-first with explicit opt-in for sensitive data
- Flexible configuration per model or globally
- Query scopes for easy filtering and analysis
- Batch operations to group related changes
- Lightweight and performant
- Built-in pruning command for cleanup
- Comprehensive audit trails
You can install the package via composer:
composer require gottvergessen/activityPublish the configuration file and migrations:
php artisan activity:installThis will create:
config/activity.php- Configuration filedatabase/migrations/[timestamp]_create_logger_table.php- Database migration
You can also publish assets separately:
# Publish just the config file
php artisan vendor:publish --provider="Gottvergessen\Activity\ActivityServiceProvider" --tag="config"
# Publish just the migrations
php artisan vendor:publish --provider="Gottvergessen\Activity\ActivityServiceProvider" --tag="migrations"After publishing, run the migrations to create the activity_logs table:
php artisan migrateTo keep your database clean, you can prune old activity logs:
# Keep only the last 90 days (default)
php artisan activity:prune
# Keep only the last 30 days
php artisan activity:prune --days=30<?php
return [
/*
|--------------------------------------------------------------------------
| Activity Logging Enabled
|--------------------------------------------------------------------------
|
| This option controls whether activity logging is enabled globally.
| You can disable this to turn off all activity logging.
|
*/
'enabled' => true,
/*
|--------------------------------------------------------------------------
| Ignore Seeding
|--------------------------------------------------------------------------
|
| When enabled, activity events triggered while running database seeders
| are skipped.
|
*/
'ignore_seeding' => true,
/*
|--------------------------------------------------------------------------
| Activity Logs Table Name
|--------------------------------------------------------------------------
|
| The database table name where activity logs will be stored.
|
*/
'table' => 'activity_logs',
/*
|--------------------------------------------------------------------------
| Default Log Category
|--------------------------------------------------------------------------
|
| The default log category to use when a model doesn't specify one.
|
*/
'default_log' => 'default',
/*
|--------------------------------------------------------------------------
| Tracked Events
|--------------------------------------------------------------------------
|
| The default events that will be tracked for all models.
| Models can override this using the $trackEvents property.
|
*/
'events' => [
'created',
'updated',
'deleted',
'restored',
],
/*
|--------------------------------------------------------------------------
| Ignored Attributes
|--------------------------------------------------------------------------
|
| Attributes that should be ignored when logging changes globally.
| Models can add more via the $ignoredAttributes property.
|
*/
'ignore_attributes' => [
'created_at',
'updated_at',
'deleted_at',
'remember_token',
],
/*
|--------------------------------------------------------------------------
| Capture Causer
|--------------------------------------------------------------------------
|
| Automatically capture the authenticated user who made the change.
|
*/
'capture_causer' => true,
/*
|--------------------------------------------------------------------------
| Capture Request Metadata
|--------------------------------------------------------------------------
|
| Capture HTTP request metadata (method, host).
| Only applies to web requests, not console commands.
|
*/
'capture_request_meta' => false,
/*
|--------------------------------------------------------------------------
| Capture IP Address
|--------------------------------------------------------------------------
|
| Capture the IP address of the user making the change.
| Only applies to web requests. Requires explicit opt-in due to privacy.
|
*/
'capture_ip' => false,
/*
|--------------------------------------------------------------------------
| Auto Batch
|--------------------------------------------------------------------------
|
| Automatically assign a unique batch ID to each request's activities.
| When enabled, all activities within a single request share a batch_id.
|
*/
'auto_batch' => false,
];use Gottvergessen\Activity\Traits\TracksModelActivity;
class User extends Authenticatable
{
use TracksModelActivity;
}Now all changes to the User model are automatically logged:
$user = User::create(['name' => 'John', 'email' => 'john@example.com']);
// ✓ Creates activity log with event='created'
$user->update(['name' => 'Jane']);
// ✓ Creates activity log with event='updated' showing the change
$user->delete();
// ✓ Creates activity log with event='deleted'Pivot operations like syncWithoutDetaching() and detach() do not mutate
the tracked model's own attributes, so they won't produce automatic updated
activity entries by themselves.
Use logActivity() in your model methods to explicitly record those changes:
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Gottvergessen\Activity\Traits\TracksModelActivity;
class Project extends Model
{
use TracksModelActivity;
public function members(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
public function assignMember(User $user): void
{
$this->members()->syncWithoutDetaching([$user->getKey()]);
$this->logActivity('updated', [
'relations' => [
'members' => ['attached' => [$user->getKey()]],
],
]);
}
public function removeMember(User $user): void
{
$this->members()->detach($user->getKey());
$this->logActivity('updated', [
'relations' => [
'members' => ['detached' => [$user->getKey()]],
],
]);
}
}use Gottvergessen\Activity\Traits\TracksModelActivity;
class User extends Authenticatable
{
use TracksModelActivity;
protected array $ignoredAttributes = [
'password',
'remember_token',
];
/*
* Changes to the listed attributes will not appear in the activity properties
* If only ignored attributes change, no activity log entry is created
*/
}If you seed data during local development or tests, you can keep the package from
recording those model events by leaving activity.ignore_seeding enabled.
config(['activity.ignore_seeding' => true]);Set it to false if you want seeder-driven model changes to be logged too.
The activity_logs table tracks the following information:
| Column | Type | Description |
|---|---|---|
| id | ID | Primary key |
| event | string | The event type (created, updated, deleted, restored). Events can be resolved into custom actions and descriptions |
| action | string | Custom semantic action defined by the model (optional) |
| log | string | Log category for grouping activities |
| description | string | Description of the activity (optional) |
| subject_type | string | The model class that was changed |
| subject_id | int | The ID of the model that was changed |
| causer_type | string | The model class of the user who made the change (optional) |
| causer_id | int | The ID of the user who made the change (optional) |
| properties | json | The model attributes that changed |
| meta | json | Metadata including IP, user agent, HTTP method, and host |
| batch_id | string | Batch ID for grouping related changes (optional) |
| created_at | timestamp | When the activity was recorded |
| updated_at | timestamp | When the activity record was last updated |
Add the InteractsWithActivity trait to easily query a model's activities:
use Gottvergessen\Activity\Traits\TracksModelActivity;
use Gottvergessen\Activity\Traits\InteractsWithActivity;
class User extends Authenticatable
{
use TracksModelActivity, InteractsWithActivity;
}Now you can access activities directly from your models:
// Eager load activities
$users = User::with('activities')->get();
// Get all activities for a user
$user->activities()->get();
// Filter activities
$user->activities()->where('event', 'updated')->get();
// Get the most recent activity
$latest = $user->latestActivity();
// Check if user has any activities
if ($user->hasActivities()) {
echo "This user has activity history";
}The InteractsWithActivity trait is optional and only required if you want to access $model->activities().
use Gottvergessen\Activity\Traits\TracksModelActivity;
class User extends Authenticatable
{
use TracksModelActivity;
protected array $trackEvents = [
'created',
'updated',
];
/*
* Limit which events are tracked for this model
*/
}use Gottvergessen\Activity\Traits\TracksModelActivity;
class Invoice extends Model
{
use TracksModelActivity;
public function activityLog(): string
{
return 'invoices';
}
}use Gottvergessen\Activity\Traits\TracksModelActivity;
class Appointment extends Model
{
use TracksModelActivity;
public function activityAction(string $event): string
{
return match ($event) {
'created' => 'blog created',
'updated' => 'blog updated',
'deleted' => 'cancelled',
};
}
}use Gottvergessen\Activity\Traits\TracksModelActivity;
class Appointment extends Model
{
use TracksModelActivity;
public function activityDescription(string $event): string
{
return match ($event) {
'created' => "Appointment was scheduled for {$this->scheduled_at}",
'updated' => "Appointment has been updated to {$this->new_appointment_date}",
default => $event,
};
}
}Group multiple model changes under a single batch ID:
use Gottvergessen\Activity\Activity;
Activity::batch(function () {
$user->update(['name' => 'John']);
$user->profile()->update(['bio' => 'Updated bio']);
// Both changes share the same batch_id
});You can temporarily disable activity logging when needed:
use Gottvergessen\Activity\Support\ActivityContext;
ActivityContext::withoutLogging(function () {
// These changes won't be logged
User::create(['name' => 'John']);
$user->update(['email' => 'john@example.com']);
});
// Or manually control logging
ActivityContext::disable();
User::create(['name' => 'Jane']); // Not logged
ActivityContext::enable();
User::create(['name' => 'Bob']); // LoggedThe Activity model provides convenient query scopes:
use Gottvergessen\Activity\Models\Activity;
// Filter by event type
Activity::forEvent('created')->get();
// Filter by subject model
Activity::forSubject($user)->get();
// Filter by causer
Activity::causedBy($admin)->get();
// Filter by batch
Activity::inBatch($batchId)->get();
// Filter by log category
Activity::inLog('invoices')->get();
// Filter by date range
Activity::betweenDates($startDate, $endDate)->get();$deletions = Activity::forEvent('deleted')
->forSubject($user)
->with('causer')
->latest()
->get();class Document extends Model
{
use TracksModelActivity, InteractsWithActivity;
}
@foreach($document->activities()->latest()->get() as $activity)
{{ $activity->causer?->name }}: {{ $activity->description }}
@endforeachFor more examples and patterns, see EXAMPLES.md.
The MIT License (MIT). Please see License File for more information.