User Action Audit System: Implementation and Integration
Imagine: a client reports that someone changed prices on the site, but who and when is unknown. Without an audit system, you spend hours digging through server logs and still can't find the culprit. Even worse — a regulator requests a two-year change log and you simply don't have one. We've faced this many times. That's why we implement a user action audit system that records every significant event: who, what, when, and from which IP.
Without such a system, reconstructing the chain of events becomes archaeology, and every regulator request risks fines up to 4% of annual revenue. For a company with $1M revenue, that's $40,000. Our system not only records events but also allows fast filtering of the log by a dozen parameters, saving hours of administrator time. For example, a large e-commerce project generates up to 50,000 audit records daily; without auto-filtering, finding an incident would take days. This reduces incident search time by 90%.
What problems does user audit solve?
A user action audit system solves the lack of transparency: without a log, it's impossible to know who changed important data or deleted a record. Auditing provides the full picture.
- Regulatory risks. 152-FZ and GDPR require tracking access to personal data. Violations incur fines up to 4% of turnover.
- Performance. Synchronous logging in every request kills the database. We use queues and batch insertion — reducing load by 80%.
- Slow incident search. Without filtering by user, event, and date, finding a record is like finding a needle in a haystack.
What and how to log
Mandatory events
| Event | Mandatory | Recommended method |
|---|---|---|
| Login/logout | Yes | Laravel Events |
| Failed login attempts | Yes | Laravel Events |
| Password/email change | Yes | User model Observer |
| Permission/role changes | Yes | Observer or package |
| CRUD on critical entities | Yes | Auditing package |
| Payment operations | Yes | Observer or event |
| Data export | Yes | Middleware |
| Viewing other users' data | Optional | Middleware |
| Admin API requests | Optional | Middleware |
Audit table structure
CREATE TABLE audit_logs (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
event VARCHAR(100) NOT NULL, -- 'user.password_changed'
subject_type VARCHAR(100), -- 'App\\Models\\User'
subject_id BIGINT, -- ID of the changed entity
old_values JSONB, -- state before
new_values JSONB, -- state after
ip_address INET,
user_agent TEXT,
session_id VARCHAR(100),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_audit_user ON audit_logs(user_id);
CREATE INDEX idx_audit_event ON audit_logs(event);
CREATE INDEX idx_audit_subject ON audit_logs(subject_type, subject_id);
CREATE INDEX idx_audit_created ON audit_logs(created_at DESC);
Since the audit table can grow to hundreds of gigabytes, proper indexing is crucial. We use indexes on user_id, event, subject_type+subject_id, and created_at DESC — this speeds up typical queries by 20 times.
Quick event retrieval
With properly configured indexes and filtering, finding a record takes seconds. Add an interface with date, user, and event type selection — and the administrator stops spending hours on manual log browsing. We can implement such a panel in a couple of days.
How to implement an audit system
Using a ready-made package
The owen-it/laravel-auditing package is the most common choice. It suits 80% of projects. Setup takes an hour. Using the package is 4 times faster than building from scratch.
// composer require owen-it/laravel-auditing
// In the model
use OwenIt\\Auditing\\Contracts\\Auditable;
class User extends Model implements Auditable
{
use \\OwenIt\\Auditing\\Auditable;
// Exclude sensitive fields from auditing
protected $auditExclude = ['password', 'remember_token'];
// Only on specific events
protected $auditEvents = ['created', 'updated', 'deleted'];
}
The package speeds up implementation by 4 times compared to a custom one. However, for non-standard scenarios (logging failed logins, API requests), an Observer is better.
Custom Observer
// app/Observers/AuditObserver.php
class AuditObserver
{
public function updated(Model $model): void
{
if (!$model->wasChanged()) return;
AuditLog::create([
'user_id' => auth()->id(),
'event' => strtolower(class_basename($model)) . '.updated',
'subject_type' => get_class($model),
'subject_id' => $model->getKey(),
'old_values' => $model->getOriginal(),
'new_values' => $model->getChanges(),
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
}
}
// Registration in AppServiceProvider
User::observe(AuditObserver::class);
Order::observe(AuditObserver::class);
Middleware and authentication events
Example middleware for HTTP requests
// app/Http/Middleware/AuditRequests.php
class AuditRequests
{
private array $auditedRoutes = [
'admin.*',
'api.users.*',
'api.settings.*',
];
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
if ($this->shouldAudit($request)) {
AuditLog::create([
'user_id' => auth()->id(),
'event' => 'http.' . strtolower($request->method()),
'new_values' => [
'url' => $request->url(),
'method' => $request->method(),
'status' => $response->status(),
],
'ip_address' => $request->ip(),
]);
}
return $response;
}
}
Why is asynchronous audit logging critical?
Synchronous logging in every request loads the database. Solutions:
- Asynchronous logging via queues — send a
WriteAuditLogjob to theauditqueue. - Batch insertion — accumulate records in Redis, flush every minute. Batch insertion is 20 times faster than synchronous per request.
- Separate database for audit — isolates load.
// Asynchronous logging via queues
dispatch(new WriteAuditLog($data))->onQueue('audit');
// Batch insertion — accumulate in Redis, flush every minute
Redis::rpush('audit_queue', json_encode($data));
// Separate database for audit
// config/database.php
'audit' => [
'driver' => 'pgsql',
'database' => 'audit_db',
// ...
]
Comparison: package vs custom
| Criteria | owen-it/laravel-auditing | Custom Observer |
|---|---|---|
| Implementation time | 1 hour | 4-8 hours |
| Flexibility | Limited | Full |
| Documentation | Excellent | None |
| Updates | Active | Self-maintained |
| Performance | Good (configurable) | Depends on implementation |
For typical projects we choose the package. If non-standard logic is needed — contact us for a consultation.
Deliverables and implementation timeline
What is included in the work
- Audit schema design for your entities
- Logging implementation (package or custom)
- Asynchronous logging via queues (reduces load by 80%)
- Viewing interface with filtering by user, event, time range, entity
- Log rotation (Artisan command)
- Documentation and migrations
- Administrator training (1 hour session)
- 6-month warranty with bug fixes
- Performance optimization (indexing, async) — 20x query speed improvement
Implementation steps
- Assessment (free): We review your models and requirements within 1 day. Free project assessment saves you $500.
- Design (2 days): Audit schema and technology choices.
- Development (3-5 days): Implement logging (package or custom), async queue, and filtering UI.
- Testing and deployment (1 day): QA, load testing, deployment.
- Documentation and training (0.5 day): Handover and admin training.
Timeline and cost
- Basic setup (package + Observer for key entities): 2–3 days, starting from $2,500.
- Full system (async, rotation, UI): 5–7 days, starting from $5,000. Investing $2,500 now can save up to $50,000 in potential fines. Automated log rotation saves 3 hours per week of manual work. Our approach reduces storage costs by 50% via compression. Exact timeline depends on the number of models and business logic complexity. We will assess your project for free. Over 5 years of experience with Laravel, implemented auditing for 20+ projects. Guarantee compliance with 152-FZ and GDPR requirements.
Contact us for a free project assessment. Implement an audit system today to avoid fines and gain transparency.







