Implementing user activity audit system on a website
Audit logging is journaling all significant events in the system: who, what, and when did they do something. Essential for incident investigation, compliance with regulatory requirements (152-ФЗ, GDPR), tracking changes to critical data.
What to log
Essential:
- Login / logout / failed login attempts
- Password, email, phone number changes
- Changes to access rights and roles
- Creation, editing, deletion of critical entities
- Payment transactions
- Data exports
As needed:
- Viewing other users' personal data
- API requests to administrative endpoints
- System configuration changes
Audit log 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,
subject_type VARCHAR(100),
subject_id BIGINT,
old_values JSONB,
new_values JSONB,
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);
Implementation via package (Laravel)
Package owen-it/laravel-auditing is the most common choice:
// composer require owen-it/laravel-auditing
use OwenIt\Auditing\Contracts\Auditable;
class User extends Model implements Auditable
{
use \OwenIt\Auditing\Auditable;
protected $auditExclude = ['password', 'remember_token'];
protected $auditEvents = ['created', 'updated', 'deleted'];
}
Custom implementation via Observer
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(),
]);
}
}
User::observe(AuditObserver::class);
Order::observe(AuditObserver::class);
HTTP request audit middleware
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;
}
}
Login/logout audit
Event::listen(Login::class, function (Login $event) {
AuditLog::create([
'user_id' => $event->user->id,
'event' => 'auth.login',
'new_values' => ['guard' => $event->guard],
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
});
Event::listen(Failed::class, function (Failed $event) {
AuditLog::create([
'user_id' => null,
'event' => 'auth.failed',
'new_values' => ['email' => $event->credentials['email'] ?? null],
'ip_address' => request()->ip(),
]);
});
Performance
Synchronous audit logging in each request impacts database load. Solutions:
// Asynchronous logging via queues
dispatch(new WriteAuditLog($data))->onQueue('audit');
// Batch insertion — accumulate in Redis, flush once per minute
Redis::rpush('audit_queue', json_encode($data));
// Separate audit database
'audit' => [
'driver' => 'pgsql',
'database' => 'audit_db',
]
Storage and rotation
// Retention policy — delete records older than N years
// For 152-ФЗ minimum 3 years, for PCI DSS — 1 year
class CleanOldAuditLogs extends Command
{
public function handle(): void
{
AuditLog::where('created_at', '<', now()->subYears(3))->delete();
}
}
Audit viewing interface
Key filters in administrative interface:
- By user
- By event type
- By time range
- By entity (type + ID)
- By IP address
Implementation Timeline
- Basic model + Observer for key entities: 2–3 days
- Full system with queues, rotation, UI: 5–7 days







