laravel-audit-log maintained by campelo
Laravel Audit Log
Automatic audit logging for Laravel applications. Track who did what, when, where, and what changed.
Features
- Automatic Request Logging - Middleware logs all HTTP requests (POST, PUT, PATCH, DELETE by default)
- Model Event Logging - Trait to automatically log model changes (created, updated, deleted)
- Error Logging - Capture and log exceptions and errors (4xx, 5xx) with stack traces
- Performance Logging - Track slow database queries and slow HTTP requests
- Rollback - Revert model changes based on audit log history
- Detailed Information - Captures user, IP, user agent, URL, method, table, old/new values
- Built-in API - Query audit logs with filters via REST API
- Sensitive Data Protection - Automatically redacts passwords and sensitive fields
- Queue Support - Offload logging to queues for better performance
- Customizable - Configure which methods, routes, events, and error types to log
- Notifications - Send email/Slack alerts when critical errors occur
Installation
composer require campelo/laravel-audit-log
Publish the config and migrations:
php artisan vendor:publish --tag=audit-log-config
php artisan vendor:publish --tag=audit-log-migrations
php artisan migrate
Quick Start
1. Automatic Request Logging
The middleware is automatically registered. All POST, PUT, PATCH, DELETE requests will be logged.
// config/audit-log.php
'log_methods' => [
'POST',
'PUT',
'PATCH',
'DELETE',
// 'GET', // Uncomment to also log read operations
],
2. Model Event Logging
Add the Auditable trait to your models:
use Campelo\AuditLog\Traits\Auditable;
class User extends Model
{
use Auditable;
// Optional: exclude sensitive fields
protected array $auditExclude = ['password', 'remember_token'];
// Optional: only include specific fields
protected array $auditInclude = ['name', 'email', 'role'];
}
3. Manual Logging
use Campelo\AuditLog\Facades\AuditLog;
// Log a custom event
AuditLog::log(
event: 'user_promoted',
model: $user,
oldValues: ['role' => 'user'],
newValues: ['role' => 'admin'],
description: 'User was promoted to admin'
);
4. Error Logging
Automatically log all exceptions and errors that occur in your application.
Step 1: Configure in your .env:
AUDIT_LOG_ERRORS_ENABLED=true
AUDIT_LOG_ERRORS_4XX=false # Log client errors (400-499)
AUDIT_LOG_ERRORS_5XX=true # Log server errors (500-599)
Step 2: Integrate with your Exception Handler:
// app/Exceptions/Handler.php
use Campelo\AuditLog\Exceptions\AuditLogExceptionHandler;
class Handler extends ExceptionHandler
{
use AuditLogExceptionHandler;
public function register(): void
{
$this->reportable(function (Throwable $e) {
$this->auditLogException($e);
});
}
}
Manual error logging:
use Campelo\AuditLog\Facades\AuditLog;
try {
// Some operation that might fail
$this->processPayment($order);
} catch (PaymentException $e) {
// Log the error with additional context
AuditLog::logError($e, request(), [
'order_id' => $order->id,
'amount' => $order->total,
]);
throw $e;
}
API Endpoints
The package provides built-in API endpoints to query audit logs:
List Audit Logs
GET /api/audit-logs
Query Parameters:
| Parameter | Description | Example |
|---|---|---|
user_id |
Filter by user ID | ?user_id=1 |
event |
Filter by event type | ?event=updated or ?event=error |
events |
Multiple events (comma-separated) | ?events=created,updated |
table |
Filter by table name | ?table=users |
model |
Filter by model class | ?model=App\Models\User |
model_id |
Filter by model ID (requires model) | ?model=App\Models\User&model_id=1 |
method |
Filter by HTTP method | ?method=POST |
ip |
Filter by IP address | ?ip=192.168.1.1 |
route |
Filter by route name | ?route=users.update |
response_code |
Filter by HTTP response code | ?response_code=500 |
date_from |
Filter from date | ?date_from=2024-01-01 |
date_to |
Filter to date | ?date_to=2024-12-31 |
search |
Search in description, URL, user name/email | ?search=john |
per_page |
Items per page (max 100) | ?per_page=50 |
sort |
Sort field | ?sort=performed_at |
Examples:
# Get all error logs
GET /api/audit-logs?event=error
# Get only server errors (500)
GET /api/audit-logs?event=error&response_code=500
# Get errors from today
GET /api/audit-logs?event=error&date_from=2024-01-15
# Get all CRUD operations (exclude errors)
GET /api/audit-logs?events=created,updated,deleted
| order | Sort order (asc/desc) | ?order=desc |
Get Single Entry
GET /api/audit-logs/{id}
Get Logs for Model
GET /api/audit-logs/model/{model}/{id}
GET /api/audit-logs/model/App%5CModels%5CUser/1
Get Logs for User
GET /api/audit-logs/user/{userId}
Get Statistics
GET /api/audit-logs/stats
GET /api/audit-logs/stats?date_from=2024-01-01&date_to=2024-01-31
Returns:
- Total count
- Count by event type
- Count by table
- Count by HTTP method
- Top 10 users by activity
- Daily activity for last 30 days
Get Filter Options
GET /api/audit-logs/filters
Returns available values for events, tables, methods, and users.
Cleanup Old Logs
DELETE /api/audit-logs/cleanup?days=365
Automatic Cleanup
The package includes a command to clean up old audit logs based on retention policy.
Configuration
# Global retention (days)
AUDIT_LOG_RETENTION_DAYS=365
# Error logs retention (if different from global)
AUDIT_LOG_ERRORS_RETENTION_DAYS=90
# Enable automatic cleanup
AUDIT_LOG_CLEANUP_ENABLED=true
AUDIT_LOG_CLEANUP_SCHEDULE=02:00
Automatic Cleanup (Recommended)
Enable automatic cleanup in your .env:
AUDIT_LOG_CLEANUP_ENABLED=true
AUDIT_LOG_CLEANUP_SCHEDULE=02:00
The package will automatically register the cleanup command in Laravel's scheduler. Make sure your server has cron configured:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
Manual Cleanup
Run the cleanup command manually:
# Clean all logs based on config retention
php artisan audit-log:cleanup
# Preview what would be deleted (dry run)
php artisan audit-log:cleanup --dry-run
# Override retention days
php artisan audit-log:cleanup --days=30
# Different retention for errors
php artisan audit-log:cleanup --days=365 --error-days=30
# Clean only error logs
php artisan audit-log:cleanup --type=errors
# Clean only operation logs (exclude errors)
php artisan audit-log:cleanup --type=operations
Manual Scheduler Registration
If you prefer to register the command manually in your app/Console/Kernel.php:
protected function schedule(Schedule $schedule): void
{
// Daily cleanup at 2 AM
$schedule->command('audit-log:cleanup')->dailyAt('02:00');
// Or weekly on Sunday
$schedule->command('audit-log:cleanup')->weeklyOn(0, '03:00');
// With custom retention
$schedule->command('audit-log:cleanup --days=90 --error-days=30')->daily();
}
Notifications
Get notified via Email and/or Slack when critical errors (5xx) occur.
Configuration
# Enable notifications
AUDIT_LOG_NOTIFICATIONS_ENABLED=true
# Choose channels: mail, slack, or both
AUDIT_LOG_NOTIFY_CHANNELS=mail # Only email
AUDIT_LOG_NOTIFY_CHANNELS=slack # Only Slack
AUDIT_LOG_NOTIFY_CHANNELS=mail,slack # Both
# Email recipient (uses Laravel's mail config)
AUDIT_LOG_NOTIFY_EMAIL=admin@example.com
# Slack webhook URL
AUDIT_LOG_SLACK_WEBHOOK=https://hooks.slack.com/services/xxx/yyy/zzz
Requirements
- Email: Uses Laravel's built-in mail configuration (
config/mail.php) - Slack: Requires the Slack notification channel package:
composer require laravel/slack-notification-channel
Throttling
To prevent notification spam, the package throttles repeated errors:
- Same error (same exception class + file + line) will only notify 5 times per hour
- Configurable in
config/audit-log.php:
'notifications' => [
'throttle' => [
'enabled' => true,
'max_notifications' => 5, // Max notifications per error type
'decay_minutes' => 60, // Time window
],
],
Which errors trigger notifications
By default, only server errors (5xx) trigger notifications. You can customize via .env:
# Default: 500,501,502,503,504
AUDIT_LOG_NOTIFY_ON_CODES=500,502,503
# Include all server errors
AUDIT_LOG_NOTIFY_ON_CODES=500,501,502,503,504,505,506,507,508,510,511
# Include some client errors too
AUDIT_LOG_NOTIFY_ON_CODES=401,403,500,502,503
Or in config file:
'notifications' => [
'notify_on_codes' => [500, 501, 502, 503, 504],
],
Performance Logging
Track slow database queries and slow HTTP requests for performance monitoring.
Configuration
# Enable performance logging
AUDIT_LOG_PERFORMANCE_ENABLED=true
# Slow query threshold (milliseconds)
AUDIT_LOG_SLOW_QUERY_THRESHOLD=1000
# Include query bindings in logs
AUDIT_LOG_SLOW_QUERY_BINDINGS=true
# Slow request threshold (milliseconds)
AUDIT_LOG_SLOW_REQUEST_THRESHOLD=2000
Querying Performance Logs
use Campelo\AuditLog\Models\AuditLog;
// Get all slow queries
$slowQueries = AuditLog::slowQueries()->get();
// Get all slow requests
$slowRequests = AuditLog::slowRequests()->get();
// Get all performance logs
$performance = AuditLog::performance()->get();
// Filter by duration (in metadata)
$verySlowQueries = AuditLog::slowQueries()
->whereRaw("JSON_EXTRACT(metadata, '$.execution_time_ms') > 5000")
->get();
API Endpoints
# Get slow query logs
GET /api/audit-logs?event=slow_query
# Get slow request logs
GET /api/audit-logs?event=slow_request
# Get all performance logs
GET /api/audit-logs?events=slow_query,slow_request
Response Format
Slow Query:
{
"id": 100,
"event": "slow_query",
"performed_at": "2024-01-15T10:30:00+00:00",
"description": "Slow query (1523.45 ms): SELECT * FROM orders WHERE...",
"metadata": {
"query": "SELECT * FROM orders WHERE status = ? AND created_at > ?",
"execution_time_ms": 1523.45,
"connection": "mysql",
"bindings": ["pending", "2024-01-01"]
}
}
Slow Request:
{
"id": 101,
"event": "slow_request",
"performed_at": "2024-01-15T10:31:00+00:00",
"description": "Slow request (3245.67 ms): GET /api/reports/sales",
"metadata": {
"duration_ms": 3245.67,
"memory_usage_bytes": 52428800,
"memory_usage_mb": 50.0,
"peak_memory_bytes": 67108864,
"peak_memory_mb": 64.0
}
}
Rollback
Revert model changes based on audit log history. Only authorized users can perform rollback.
Configuration
# Enable rollback feature
AUDIT_LOG_ROLLBACK_ENABLED=true
# User IDs authorized to perform rollback (comma-separated)
AUDIT_LOG_ROLLBACK_ALLOWED_USERS=1,5,10
# Maximum chain length for rollback (0 = unlimited)
AUDIT_LOG_ROLLBACK_MAX_CHAIN=10
Basic Rollback
use Campelo\AuditLog\Facades\AuditLog;
use Campelo\AuditLog\Models\AuditLog as AuditLogModel;
// Rollback by audit log ID
$rollbackLog = AuditLog::rollback($auditLogId);
// Check if rollback is possible
$result = AuditLog::canRollback($auditLogId);
// Returns: ['can_rollback' => true/false, 'reason' => '...']
// Rollback via model instance
$auditLog = AuditLogModel::find($id);
$rollbackLog = $auditLog->rollback();
Rollback from Model
// Rollback to a specific audit log
$user->rollbackTo($auditLogId);
// Rollback to previous state
$user->rollbackToPrevious();
Rollback Chain (Multiple Undos)
// Rollback the last 3 changes
$rollbackLogs = AuditLog::rollbackChain($auditLogId, steps: 3);
API Endpoints
# Check if rollback is possible
GET /api/audit-logs/{id}/can-rollback
# Response
{
"can_rollback": true,
"reason": null
}
# Perform rollback
POST /api/audit-logs/{id}/rollback
# Response
{
"success": true,
"message": "Rollback completed successfully.",
"rollback_log": { ... }
}
# Rollback chain
POST /api/audit-logs/rollback-chain/{id}
Body: { "steps": 3 }
# Response
{
"success": true,
"message": "Rolled back 3 changes.",
"rollback_logs": [...]
}
Rollback Events
Each rollback creates a new audit log with event rollback:
{
"id": 150,
"event": "rollback",
"description": "Rolled back updated on User #1 (from audit log #42)",
"old_values": { "name": "New Name" },
"new_values": { "name": "Original Name" },
"metadata": {
"rolled_back_audit_log_id": 42,
"rolled_back_event": "updated",
"rolled_back_at": "2024-01-15T10:00:00+00:00"
}
}
Querying Rollbacks
// Get all rollback events
$rollbacks = AuditLog::rollbacks()->get();
// Get rollbackable events only
$rollbackable = AuditLog::rollbackable()->get();
// Check if an audit log was rolled back
$auditLog = AuditLogModel::find($id);
if ($auditLog->isRolledBack()) {
$rollbackLog = $auditLog->getRollbackLog();
}
Limitations
- created: Rollback deletes the model
- updated: Rollback restores old values
- deleted: Rollback restores soft-deleted models or recreates with old_values
- Cannot rollback events without
old_values(e.g., if not stored) - Cannot rollback non-model events (e.g.,
error,slow_query) - Each audit log can only be rolled back once
Query Using Model
use Campelo\AuditLog\Models\AuditLog;
// Get all logs for a model
$logs = AuditLog::forModel($user)->get();
// Get logs for a specific user
$logs = AuditLog::byUser($user)->get();
// Get logs for a specific event
$logs = AuditLog::event('updated')->get();
// Get logs for a table
$logs = AuditLog::forTable('users')->get();
// Get logs between dates
$logs = AuditLog::between('2024-01-01', '2024-01-31')->get();
// Get logs from IP
$logs = AuditLog::fromIp('192.168.1.1')->get();
// Get only write operations
$logs = AuditLog::writeOperations()->get();
// Combine scopes
$logs = AuditLog::byUser($user)
->event(['created', 'updated'])
->between($startDate, $endDate)
->get();
Query Examples
Normal Operation Logs (CRUD)
use Campelo\AuditLog\Models\AuditLog;
// All create operations
$created = AuditLog::event('created')->latest('performed_at')->get();
// All update operations for a specific table
$userUpdates = AuditLog::event('updated')
->forTable('users')
->get();
// All delete operations by a specific user
$deletedByAdmin = AuditLog::event('deleted')
->byUser($adminId)
->get();
// All write operations (POST, PUT, PATCH, DELETE) today
$todayWrites = AuditLog::writeOperations()
->whereDate('performed_at', today())
->get();
// All read operations (GET) - if enabled in config
$reads = AuditLog::readOperations()->get();
// History of a specific record
$orderHistory = AuditLog::forModel(Order::class, $orderId)
->oldest('performed_at')
->get();
// Activity by user in a date range
$userActivity = AuditLog::byUser($userId)
->between('2024-01-01', '2024-01-31')
->get();
Error Logs
use Campelo\AuditLog\Models\AuditLog;
// All errors
$allErrors = AuditLog::errors()->latest('performed_at')->get();
// Only server errors (500-599)
$serverErrors = AuditLog::serverErrors()->get();
// Only client errors (400-499)
$clientErrors = AuditLog::clientErrors()->get();
// Errors by response code
$notFound = AuditLog::errors()->responseCode(404)->get();
$forbidden = AuditLog::errors()->responseCode(403)->get();
// Errors in a specific route
$apiErrors = AuditLog::errors()
->where('url', 'like', '%/api/payments%')
->get();
// Errors by a specific user
$userErrors = AuditLog::errors()
->byUser($userId)
->get();
// Recent errors (last 24 hours)
$recentErrors = AuditLog::errors()
->where('performed_at', '>=', now()->subDay())
->get();
// Errors with specific exception class
$paymentErrors = AuditLog::errors()
->whereJsonContains('metadata->exception_class', 'App\\Exceptions\\PaymentException')
->get();
// Error statistics by day
$errorsByDay = AuditLog::errors()
->selectRaw('DATE(performed_at) as date, COUNT(*) as count')
->groupBy('date')
->orderBy('date', 'desc')
->get();
// Top error types
$topErrors = AuditLog::errors()
->selectRaw("JSON_EXTRACT(metadata, '$.exception_class') as exception, COUNT(*) as count")
->groupBy('exception')
->orderBy('count', 'desc')
->limit(10)
->get();
Combined Queries
// All activity (normal + errors) by a user
$allActivity = AuditLog::byUser($userId)
->latest('performed_at')
->get();
// Separate normal logs from errors
$normalLogs = AuditLog::where('event', '!=', 'error')->get();
$errorLogs = AuditLog::errors()->get();
// Dashboard statistics
$stats = [
'total_operations' => AuditLog::where('event', '!=', 'error')->count(),
'total_errors' => AuditLog::errors()->count(),
'server_errors' => AuditLog::serverErrors()->count(),
'client_errors' => AuditLog::clientErrors()->count(),
'creates_today' => AuditLog::event('created')->whereDate('performed_at', today())->count(),
'updates_today' => AuditLog::event('updated')->whereDate('performed_at', today())->count(),
'deletes_today' => AuditLog::event('deleted')->whereDate('performed_at', today())->count(),
'errors_today' => AuditLog::errors()->whereDate('performed_at', today())->count(),
];
Accessing Audit Logs from Models
// Get all audit logs
$user->auditLogs;
// Get last audit log
$user->lastAuditLog();
// Get logs for specific event
$user->getAuditLogsForEvent('updated');
Configuration
// config/audit-log.php
return [
// Enable/disable globally
'enabled' => env('AUDIT_LOG_ENABLED', true),
// Database connection (null = default)
'connection' => null,
// Table name
'table' => 'audit_logs',
// HTTP methods to log
'log_methods' => ['POST', 'PUT', 'PATCH', 'DELETE'],
// Model events to log
'log_events' => ['created', 'updated', 'deleted', 'restored'],
// Routes to exclude
'excluded_routes' => [
'telescope/*',
'horizon/*',
'_debugbar/*',
],
// Fields to redact
'excluded_fields' => [
'password',
'password_confirmation',
'secret',
'token',
'api_key',
],
// Queue configuration
'queue' => [
'enabled' => env('AUDIT_LOG_QUEUE', false),
'connection' => 'default',
'queue' => 'audit-logs',
],
// Data retention (days, null = forever)
'retention_days' => 365,
// API routes
'routes_enabled' => true,
'route_prefix' => 'api/audit-logs',
'route_middleware' => ['api', 'auth'],
// Automatic cleanup
'cleanup' => [
'enabled' => env('AUDIT_LOG_CLEANUP_ENABLED', false),
'schedule' => env('AUDIT_LOG_CLEANUP_SCHEDULE', '02:00'),
],
// Error logging configuration
'errors' => [
'enabled' => env('AUDIT_LOG_ERRORS_ENABLED', true),
'log_4xx' => env('AUDIT_LOG_ERRORS_4XX', false),
'log_5xx' => env('AUDIT_LOG_ERRORS_5XX', true),
'log_stack_trace' => true,
'max_stack_trace_length' => 5000,
'excluded_exceptions' => [
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Validation\ValidationException::class,
\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class,
],
'retention_days' => env('AUDIT_LOG_ERRORS_RETENTION_DAYS', null),
],
// Notifications
'notifications' => [
'enabled' => env('AUDIT_LOG_NOTIFICATIONS_ENABLED', false),
'channels' => explode(',', env('AUDIT_LOG_NOTIFY_CHANNELS', 'mail')),
'mail' => [
'to' => env('AUDIT_LOG_NOTIFY_EMAIL', null),
],
'slack' => [
'webhook_url' => env('AUDIT_LOG_SLACK_WEBHOOK', null),
],
'throttle' => [
'enabled' => true,
'max_notifications' => 5,
'decay_minutes' => 60,
],
// Configurable via AUDIT_LOG_NOTIFY_ON_CODES=500,502,503
'notify_on_codes' => env('AUDIT_LOG_NOTIFY_ON_CODES')
? array_map('intval', explode(',', env('AUDIT_LOG_NOTIFY_ON_CODES')))
: [500, 501, 502, 503, 504],
],
// Performance logging
'performance' => [
'enabled' => env('AUDIT_LOG_PERFORMANCE_ENABLED', false),
'slow_queries' => [
'enabled' => env('AUDIT_LOG_SLOW_QUERIES_ENABLED', true),
'threshold' => env('AUDIT_LOG_SLOW_QUERY_THRESHOLD', 1000),
'log_bindings' => env('AUDIT_LOG_SLOW_QUERY_BINDINGS', true),
],
'slow_requests' => [
'enabled' => env('AUDIT_LOG_SLOW_REQUESTS_ENABLED', true),
'threshold' => env('AUDIT_LOG_SLOW_REQUEST_THRESHOLD', 2000),
'log_memory' => true,
],
],
// Rollback
'rollback' => [
'enabled' => env('AUDIT_LOG_ROLLBACK_ENABLED', true),
// Configurable via AUDIT_LOG_ROLLBACK_ALLOWED_USERS=1,5,10
'allowed_users' => env('AUDIT_LOG_ROLLBACK_ALLOWED_USERS')
? array_map('intval', explode(',', env('AUDIT_LOG_ROLLBACK_ALLOWED_USERS')))
: [],
'rollbackable_events' => ['created', 'updated', 'deleted'],
'max_chain_length' => env('AUDIT_LOG_ROLLBACK_MAX_CHAIN', 10),
'log_rollback' => true,
],
];
Customization
Custom User Resolver
// config/audit-log.php
'user_resolver' => App\Services\CustomUserResolver::class,
// App/Services/CustomUserResolver.php
class CustomUserResolver
{
public function resolve(): ?int
{
return auth('admin')->id() ?? auth()->id();
}
}
Custom Audit Data
class Order extends Model
{
use Auditable;
public function getAuditCustomData(): array
{
return [
'total' => $this->total,
'items_count' => $this->items->count(),
];
}
public function getAuditDescription(string $event): ?string
{
return "Order #{$this->id} was {$event}";
}
public function shouldBeAudited(): bool
{
// Don't audit draft orders
return $this->status !== 'draft';
}
}
Response Format
Normal Operation Log
{
"id": 1,
"user": {
"id": 1,
"type": "App\\Models\\User",
"name": "John Doe",
"email": "john@example.com"
},
"performed_at": "2024-01-15T10:30:00+00:00",
"performed_at_human": "2 hours ago",
"request": {
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"url": "https://example.com/api/users/1",
"method": "PUT",
"route": "users.update"
},
"event": "updated",
"model": {
"type": "App\\Models\\User",
"id": 1,
"table": "users"
},
"changes": {
"old": { "name": "John" },
"new": { "name": "John Doe" },
"fields": ["name"],
"diff": {
"name": { "old": "John", "new": "John Doe" }
}
},
"summary": "Updated User #1 by John Doe"
}
Error Log
{
"id": 42,
"user": {
"id": 1,
"type": "App\\Models\\User",
"name": "John Doe",
"email": "john@example.com"
},
"performed_at": "2024-01-15T14:22:00+00:00",
"performed_at_human": "5 minutes ago",
"request": {
"ip": "192.168.1.1",
"user_agent": "Mozilla/5.0...",
"url": "https://example.com/api/orders/process",
"method": "POST",
"route": "orders.process"
},
"event": "error",
"response_code": 500,
"description": "[500] PaymentException: Payment gateway timeout",
"metadata": {
"exception_class": "App\\Exceptions\\PaymentException",
"exception_code": 0,
"file": "/var/www/app/Services/PaymentService.php",
"line": 142,
"stack_trace": "#0 /var/www/app/Http/Controllers/OrderController.php(85): App\\Services\\PaymentService->process()...",
"context": {
"order_id": 123,
"amount": 99.99
}
},
"summary": "Error by John Doe"
}
License
MIT