Trail Package
Trail is a comprehensive audit logging and activity tracking system for Laravel applications. It automatically captures model changes, request context, and user activities, providing a complete audit trail for your application's data modifications.
Features Overview
🔍 Comprehensive Audit Logging
- Model Event Tracking: Automatically monitors created, updated, deleted, restored, and force-deleted events
- Change Detection: Captures detailed before/after attribute changes with intelligent diffing
- Snapshot Support: Optionally stores complete model snapshots for point-in-time recovery
- Request Context: Records IP address, user agent, HTTP method, URL, and request ID
🚀 Performance & Scalability
- Queue Integration: Optional background processing to prevent blocking operations
- Smart Caching: Efficient data structure for minimal database impact
- Bulk Operations: Chunked processing for large datasets
- Automatic Pruning: Built-in retention management with configurable policies
🛡️ Security & Privacy
- Sensitive Data Masking: Automatically redacts passwords, tokens, and secrets
- Configurable Attributes: Flexible control over which attributes to track or ignore
- User Association: Links activities to authenticated users across multiple guards
- Request Correlation: Groups related activities using unique request identifiers
🔧 Developer Experience
- Simple Integration: Single trait adds full audit logging to any Eloquent model
- Flexible Configuration: Extensive configuration options for different use cases
- Filament Integration: Built-in admin panel resources for Twist framework users
- Manual Logging: Programmatic API for custom events and activities
Installation
Install the Trail package via Composer:
composer require obelaw/trailConfiguration & Setup
- Publish configuration and migrations:
php artisan vendor:publish --tag=trail-config
php artisan vendor:publish --tag=trail-migrations- Run the migrations:
php artisan migrate- Configure the package (optional):
Edit config/trail.php to customize behavior according to your needs.
Database Schema
The Trail package creates a comprehensive trails table with the following structure:
Schema::create('trails', function (Blueprint $table) {
$table->id();
// Model relationships
$table->nullableMorphs('causer'); // User who caused the change
$table->morphs('trailable'); // Model being tracked
// Event details
$table->string('event'); // Event type (created, updated, etc.)
$table->json('changes')->nullable(); // Attribute changes
$table->json('snapshot')->nullable(); // Full model snapshot
// Request context
$table->string('ip_address', 45)->nullable();
$table->string('user_agent', 512)->nullable();
$table->string('method', 16)->nullable();
$table->string('url', 1024)->nullable();
$table->string('guard', 64)->nullable();
$table->uuid('request_id')->nullable();
$table->timestamps();
// Indexes for performance
$table->index(['trailable_type', 'trailable_id']);
$table->index(['causer_type', 'causer_id']);
$table->index('request_id');
});Basic Usage
Adding Trail to Your Models
The simplest way to enable audit logging is by adding the TracksTrail trait to your Eloquent models:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Obelaw\Trail\Traits\TracksTrail;
class User extends Model
{
use TracksTrail;
protected $fillable = [
'name', 'email', 'password'
];
}Automatic Event Tracking
Once the trait is added, Trail automatically captures all model events:
// Creating a new user
$user = User::create([
'name' => 'John Doe',
'email' => '[email protected]',
'password' => bcrypt('password123')
]);
// Trail record: event='created', changes=['name' => 'John Doe', 'email' => '[email protected]']
// Updating the user
$user->update(['name' => 'John Smith']);
// Trail record: event='updated', changes=['name' => ['old' => 'John Doe', 'new' => 'John Smith']]
// Deleting the user
$user->delete();
// Trail record: event='deleted', changes=[...], snapshot=[complete user data]Alternative: Configuration-Based Watching
Instead of adding traits to each model, you can configure models to watch globally:
// config/trail.php
return [
'watch' => [
App\Models\User::class,
App\Models\Order::class,
App\Models\Product::class,
],
// ... other config
];Manual Logging
Using the Trail Facade
For custom events or manual logging:
use Obelaw\Trail\Facades\Trail;
// Log a custom event
Trail::for($user)
->event('login_attempt')
->changes(['ip' => request()->ip(), 'success' => true])
->save();
// Log with causer (who performed the action)
Trail::for($order)
->by($admin)
->event('status_changed')
->changes([
'status' => ['old' => 'pending', 'new' => 'confirmed'],
'reason' => 'Manual approval by admin'
])
->snapshot($order->toArray())
->save();
// Log application-level events
Trail::event('system_maintenance')
->changes([
'action' => 'backup_started',
'duration_estimate' => '30 minutes'
])
->save();Using Helper Functions
Trail provides convenient helper functions for quick logging:
// Quick event logging
trail_event('user_export', [
'format' => 'csv',
'record_count' => 1500,
'exported_by' => auth()->id()
]);
// Get current causer (authenticated user)
$user = trail_causer(); // Returns auth()->user() safelyManual Trail Builder
For more complex scenarios, use the Trail builder directly:
use Obelaw\Trail\Recording\TrailBuilder;
$trail = TrailBuilder::make()
->for($product)
->by($user)
->event('price_changed')
->changes([
'price' => ['old' => 99.99, 'new' => 89.99],
'discount_applied' => true
])
->props([
'promotion_id' => 123,
'change_reason' => 'Black Friday sale'
])
->context([
'source' => 'admin_panel',
'batch_id' => 'batch_001'
])
->save();Querying Trail Records
Basic Queries
Trail provides convenient methods for querying audit logs:
use Obelaw\Trail\Models\Trail;
// Get all trails for a specific model
$userTrails = Trail::forTrailed(User::class, $user->id)->get();
// Filter by event type
$createdRecords = Trail::event('created')->get();
// Get trails by causer (who performed actions)
$adminActions = Trail::forCauser(Admin::class, $admin->id)->get();
// Combine filters
$userUpdates = Trail::forTrailed(User::class, $user->id)
->event('updated')
->where('created_at', '>=', now()->subDays(30))
->orderBy('created_at', 'desc')
->get();Advanced Queries
// Find all password changes in the last week
$passwordChanges = Trail::whereJsonContains('changes', ['password'])
->where('created_at', '>=', now()->subWeek())
->get();
// Get all actions from a specific IP address
$ipActions = Trail::where('ip_address', '192.168.1.100')
->with(['trailable', 'causer'])
->get();
// Find all failed login attempts
$failedLogins = Trail::event('login_attempt')
->whereJsonPath('changes->success', false)
->get();
// Group activities by request ID
$requestActivities = Trail::where('request_id', $requestId)
->orderBy('created_at')
->get();
// Find bulk operations
$bulkOperations = Trail::select('request_id')
->groupBy('request_id')
->havingRaw('COUNT(*) > 10')
->get();Working with Relationships
// Get trail with related models
$trail = Trail::with(['trailable', 'causer'])->find($id);
echo "User {$trail->causer->name} {$trail->event} {$trail->trailable->name}";
// Access model relationships
if ($trail->trailable instanceof User) {
$user = $trail->trailable;
echo "Modified user: {$user->email}";
}
// Find all trails for models created by specific user
$createdByUser = Trail::whereHas('causer', function ($query) use ($userId) {
$query->where('id', $userId);
})->event('created')->get();Configuration Options
Complete Configuration Reference
// config/trail.php
return [
// Database connection (null uses default)
'connection' => null,
// Table name for trail records
'table' => 'trails',
// Which model events to capture
'events' => [
'created',
'updated',
'deleted',
'restored',
'forceDeleted'
],
// Attributes to ignore when computing changes
'ignore_attributes' => [
'updated_at',
'remember_token',
'last_login_at'
],
// Attributes to redact/mask in logs
'redact_attributes' => [
'password',
'password_confirmation',
'token',
'secret',
'api_key',
'private_key'
],
// Store complete model snapshot
'store_snapshot' => true,
// Use queue for non-blocking writes
'queue' => false,
// Fire TrailRecorded event after saving
'broadcast' => false,
// Retention period in days
'retention_days' => 365,
// Models to auto-watch (alternative to trait)
'watch' => [
// App\Models\User::class,
// App\Models\Order::class,
],
];Event-Specific Configuration
You can customize which events to track per model:
class User extends Model
{
use TracksTrail;
// Override tracked events for this model
protected static function bootTracksTrail(): void
{
static::observe(new TrailObserver(['created', 'updated']));
}
}Custom Attribute Handling
class Product extends Model
{
use TracksTrail;
// Custom attributes to ignore for this model
public function getTrailIgnoreAttributes(): array
{
return array_merge(
config('trail.ignore_attributes', []),
['view_count', 'last_viewed_at']
);
}
// Custom attributes to redact
public function getTrailRedactAttributes(): array
{
return ['internal_cost', 'supplier_price'];
}
}Queue Integration
Enabling Queue Support
For high-traffic applications, enable queue processing to prevent blocking operations:
// config/trail.php
return [
'queue' => true,
// ... other config
];Custom Queue Configuration
// In your service provider
use Obelaw\Trail\Jobs\StoreTrail;
// Configure queue connection and settings
StoreTrail::dispatch($payload)->onQueue('audit-logs');
// Use delayed processing
StoreTrail::dispatch($payload)->delay(now()->addSeconds(30));Processing Trail Jobs
Ensure your queue worker is running:
# Process trail jobs
php artisan queue:work --queue=default,audit-logs
# Monitor queue status
php artisan queue:monitorData Retention & Pruning
Automatic Pruning
Trail includes a built-in pruning command to manage storage:
# Prune records older than configured retention period
php artisan trail:prune
# Prune with custom retention period
php artisan trail:prune --days=180
# Prune only specific events
php artisan trail:prune --days=90 --events=created,updatedScheduled Pruning
Add pruning to your application's scheduler:
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
// Daily pruning using config retention
$schedule->command('trail:prune')->daily();
// Weekly pruning with custom retention
$schedule->command('trail:prune --days=365')->weekly();
// Monthly deep cleanup
$schedule->command('trail:prune --days=90')->monthly();
}Manual Pruning with Queries
For custom pruning logic:
use Obelaw\Trail\Models\Trail;
use Carbon\Carbon;
// Delete trails older than 2 years
Trail::where('created_at', '<', Carbon::now()->subYears(2))->delete();
// Keep only latest 1000 records per model type
Trail::where('trailable_type', User::class)
->orderBy('created_at', 'desc')
->skip(1000)
->delete();
// Prune based on event type
Trail::whereIn('event', ['created', 'updated'])
->where('created_at', '<', Carbon::now()->subDays(30))
->delete();Security & Privacy
Sensitive Data Protection
Trail automatically protects sensitive information:
// These attributes are masked by default
$user = User::create([
'name' => 'John Doe',
'email' => '[email protected]',
'password' => 'secret123',
'api_token' => 'abc123xyz'
]);
// Stored in trail as:
// changes: {
// "name": "John Doe",
// "email": "[email protected]",
// "password": "*****",
// "api_token": "*****"
// }Custom Data Masking
// Add custom sensitive fields
// config/trail.php
return [
'redact_attributes' => [
'password', 'token', 'secret',
'ssn', 'credit_card', 'bank_account',
'private_key', 'encryption_key'
],
];
// Model-specific masking
class PaymentMethod extends Model
{
use TracksTrail;
public function getTrailRedactAttributes(): array
{
return ['card_number', 'cvv', 'account_number'];
}
}GDPR Compliance
For GDPR and privacy compliance:
// Remove all trails for a user (right to be forgotten)
Trail::forTrailed(User::class, $user->id)->delete();
// Or anonymize instead of delete
Trail::forTrailed(User::class, $user->id)
->update([
'ip_address' => null,
'user_agent' => 'anonymized',
'changes' => DB::raw("JSON_SET(changes, '$.email', '[email protected]')")
]);
// Export user's trail data (right to access)
$userTrails = Trail::forTrailed(User::class, $user->id)
->orWhere(function ($query) use ($user) {
$query->forCauser(User::class, $user->id);
})
->get()
->toJson();Integration Examples
E-commerce Order Tracking
class Order extends Model
{
use TracksTrail;
}
class OrderService
{
public function updateOrderStatus(Order $order, string $status, User $user)
{
$oldStatus = $order->status;
$order->update(['status' => $status]);
// Manual trail for business context
Trail::for($order)
->by($user)
->event('status_changed')
->changes([
'old_status' => $oldStatus,
'new_status' => $status,
'changed_by' => $user->name,
'change_reason' => request('reason')
])
->save();
}
}
// Query order history
$orderHistory = Trail::forTrailed(Order::class, $order->id)
->orderBy('created_at')
->get()
->map(function ($trail) {
return [
'event' => $trail->event,
'changes' => $trail->changes,
'user' => $trail->causer?->name,
'date' => $trail->created_at,
'ip' => $trail->ip_address
];
});User Management System
class User extends Model
{
use TracksTrail;
}
class UserController extends Controller
{
public function updateProfile(Request $request, User $user)
{
$changes = $request->only(['name', 'email', 'phone']);
// Update user
$user->update($changes);
// Additional context for profile changes
Trail::for($user)
->by(auth()->user())
->event('profile_updated')
->changes($changes)
->props([
'updated_fields' => array_keys($changes),
'source' => 'web_interface'
])
->save();
}
public function viewAuditLog(User $user)
{
$auditLog = Trail::forTrailed(User::class, $user->id)
->with('causer')
->orderBy('created_at', 'desc')
->paginate(25);
return view('admin.users.audit', compact('user', 'auditLog'));
}
}Content Management System
class Article extends Model
{
use TracksTrail;
}
class ArticleObserver
{
public function updated(Article $article)
{
// Additional business logic for article updates
if ($article->isDirty('status')) {
Trail::for($article)
->by(auth()->user())
->event('status_change')
->changes([
'old_status' => $article->getOriginal('status'),
'new_status' => $article->status,
'visibility' => $article->status === 'published' ? 'public' : 'private'
])
->save();
}
}
}Filament Integration
For Twist framework users, Trail provides built-in Filament resources:
Admin Panel Resources
The Trail package automatically registers Filament resources when used with the Twist framework:
// Automatically available in Filament Admin
// - Trail Resource for viewing audit logs
// - Trail relation managers for models
// - Trail widgets for dashboard insightsCustom Trail Resource
Create custom Filament resources for your trail data:
use Obelaw\Trail\Models\Trail;
use Filament\Resources\Resource;
use Filament\Tables\Table;
use Filament\Tables\Columns\TextColumn;
class TrailResource extends Resource
{
protected static ?string $model = Trail::class;
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('trailable_type')->label('Model'),
TextColumn::make('event')->badge(),
TextColumn::make('causer.name')->label('User'),
TextColumn::make('ip_address'),
TextColumn::make('created_at')->dateTime(),
])
->filters([
SelectFilter::make('event')
->options([
'created' => 'Created',
'updated' => 'Updated',
'deleted' => 'Deleted',
]),
]);
}
}API Reference
Trail Model Methods
scopeForTrailed($query, string $type, $id)
Filter trails for a specific model:
Trail::forTrailed(User::class, $userId)->get();scopeForCauser($query, string $type, $id)
Filter trails by who caused them:
Trail::forCauser(Admin::class, $adminId)->get();scopeEvent($query, string $event)
Filter by event type:
Trail::event('updated')->get();Trail Builder Methods
for(?Model $model): self
Set the model being tracked:
Trail::for($user)->event('login')->save();by(?Model $user): self
Set who caused the action:
Trail::for($order)->by($admin)->event('approved')->save();event(string $event): self
Set the event type:
Trail::event('custom_action')->changes($data)->save();changes(array $changes): self
Set the change data:
Trail::changes(['status' => ['old' => 'pending', 'new' => 'active']])->save();snapshot(?array $snapshot): self
Set model snapshot:
Trail::snapshot($model->toArray())->save();props(array $properties): self
Set additional properties:
Trail::props(['source' => 'api', 'batch_id' => 123])->save();context(array $context): self
Set request context:
Trail::context(['feature_flag' => 'enabled'])->save();Helper Functions
trail_event(string $type, array $properties = [], $model = null)
Quick event logging:
trail_event('backup_completed', ['size' => '2.4GB']);trail_causer(): ?Authenticatable
Get current authenticated user safely:
$user = trail_causer(); // Returns auth()->user() or nullEvents & Broadcasting
TrailRecorded Event
Trail fires a TrailRecorded event after saving each record:
// Listen for trail events
Event::listen(TrailRecorded::class, function ($event) {
$trail = $event->trail;
// Send to external systems
Http::post('https://audit-system.com/webhook', [
'event' => $trail->event,
'model' => $trail->trailable_type,
'user' => $trail->causer?->email,
'timestamp' => $trail->created_at
]);
});Real-time Notifications
// Send real-time notifications for critical changes
Event::listen(TrailRecorded::class, function (TrailRecorded $event) {
if ($event->trail->trailable_type === User::class &&
$event->trail->event === 'deleted') {
Notification::route('slack', config('slack.audit_channel'))
->notify(new UserDeletedNotification($event->trail));
}
});Performance Optimization
Database Indexing
Add custom indexes for your query patterns:
// Migration for custom indexes
Schema::table('trails', function (Blueprint $table) {
// Index for event type queries
$table->index(['event', 'created_at']);
// Index for date range queries
$table->index(['trailable_type', 'created_at']);
// Index for IP-based queries
$table->index(['ip_address', 'created_at']);
});Query Optimization
// Use eager loading for relationships
$trails = Trail::with(['trailable', 'causer'])
->forTrailed(User::class, $userId)
->get();
// Use select() to limit columns
$summaryTrails = Trail::select(['event', 'created_at', 'causer_id'])
->event('login')
->get();
// Use chunking for large datasets
Trail::where('created_at', '<', $cutoffDate)
->chunkById(1000, function ($trails) {
foreach ($trails as $trail) {
// Process each trail
}
});Memory Management
// Process large datasets efficiently
Trail::forTrailed(User::class, $userId)
->lazy()
->each(function (Trail $trail) {
// Process one at a time to save memory
$this->processTrail($trail);
});Best Practices
1. Model Configuration
- Use the
TracksTrailtrait for automatic tracking - Configure
ignore_attributesto exclude timestamp fields - Set up
redact_attributesfor sensitive data protection
2. Performance Considerations
- Enable queuing for high-traffic applications
- Implement regular pruning to manage storage
- Add appropriate database indexes for your query patterns
- Use eager loading when accessing relationships
3. Privacy & Security
- Always redact sensitive information
- Implement data retention policies
- Consider GDPR compliance requirements
- Use IP anonymization for privacy protection
4. Monitoring & Alerting
- Set up alerts for unusual activity patterns
- Monitor trail storage growth
- Track queue processing performance
- Alert on failed audit log writes
Troubleshooting
Common Issues
Trails Not Being Created
- Verify the trait is added to your model:
class YourModel extends Model
{
use TracksTrail; // Make sure this is present
}- Check if the model is in the ignore list
- Verify middleware is registered for request context
Performance Issues
- Enable queue processing:
// config/trail.php
'queue' => true,- Add appropriate indexes:
Schema::table('trails', function (Blueprint $table) {
$table->index(['trailable_type', 'trailable_id']);
});Storage Growing Too Fast
- Implement regular pruning:
$schedule->command('trail:prune --days=90')->daily();- Reduce tracked events:
// config/trail.php
'events' => ['created', 'updated'], // Remove 'deleted', etc.Queue Jobs Failing
- Verify queue worker is running:
php artisan queue:work- Check for serialization issues with complex models
- Monitor queue failure logs
Contributing
We welcome contributions to the Trail package! Please see our contributing guidelines for details.
License
The Trail package is open-source software licensed under the MIT license.
Related Packages
- Attrify - Dynamic attributes system
- Permit System - Role-based permissions
- Twist Framework - Addon system for modular applications