alifcoder / activity-log
A simple action activity log package for Laravel.
Requires
- php: >=8.2
- illuminate/support: ^11.0 || ^12.0 || ^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/framework: ^13.0
- laravel/pint: ^1.29
- orchestra/testbench: ^11.0
- phpstan/phpstan: ^2.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v1.1.16
- v1.1.15
- v1.1.14
- v1.1.13
- v1.1.12
- v1.1.11
- v1.1.10
- v1.1.9
- v1.1.8
- v1.1.7
- v1.1.6
- v1.1.5
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.15
- v1.0.14
- v1.0.13
- v1.0.12
- v1.0.11
- v1.0.10
- v1.0.9
- v1.0.8
- v1.0.7
- v1.0.6
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- dev-feature/activity-log-hardening
- dev-enabled
- dev-version
This package is auto-updated.
Last update: 2026-10-22 11:54:02 UTC
README
A simple, customizable Laravel package to log and store user activity across your application. Perfect for auditing, tracking changes, and monitoring actions within modules.
β¨ Features
- Logs authenticated user actions and request details
- Stores activity in a dedicated
activity_logstable - Supports parent-child log relationships
- Customizable logging behavior
- Easily extendable and minimalistic
π¦ Requirements
- PHP
>=8.2 - Laravel
^11.0 || ^12.0 || ^13.0
π Installation
composer require alifcoder/activity-log
βοΈ Configuration & Migration
Publish the configuration and migration with:
php artisan vendor:publish --tag=activity-log
This will publish:
config/activity-log.phpdatabase/migrations/xxxx_xx_xx_xxxxxx_create_activity_logs_table.php
Then run the migration:
php artisan migrate
π§βπ» Usage
Logging a manual activity
ActivityLogger::log(new ActivityLogCreateDTO( log_type: 'custom', // optional: log type user_id: 1, // optional: user_id url: 'http://example.com', // optional: URL method: 'GET', // optional: HTTP method request_body: '{}', // optional: request body response_body: '{}', // optional: response body ... // other parameters ));
π Logging Requests (Route Middleware)
Request logging is opt-in per route or route group via the log.activity alias:
Route::middleware(['api', 'log.activity'])->group(function () { Route::post('/example', ExampleController::class); });
The package does not register the middleware globally. Each request is logged once, by the routes that use the alias.
π Encrypted Request & Response Storage
All logged request and response bodies are stored as encrypted files for maximum data protection.
To read an encrypted stored file:
use Alif\ActivityLog\Facades\FileStorage; $content = FileStorage::readEncrypted($log->request_body);
Format. New files use AES-256-GCM (authenticated) with a v2: prefix. Files written by older versions (AES-256-CBC, no prefix) are still read, so no data migration is needed. A tampered or wrongly keyed file raises an error, and readEncrypted() returns null.
Names. Files are named after the additional_id (request_<sha1>.log, and so on). A retried job overwrites its own files, so it does not leave orphans behind.
π Generating an Encryption Secret Key
Since request/response data is encrypted, the package requires a secret key.
Generate it using:
php artisan activity-log:generate-key
The key is written to .env. If a key already exists, the command refuses to change it. Use --force only when you intentionally discard the old key: logs encrypted with it can no longer be read.
π IP Address Details Sync
The package supports collecting additional information about user IP addresses, such as:
- Country
- City
- Coordinates
- ISP
- ASN
- and more
After installation, write scheduler code to sync IP details:
$schedule->command('activity-log:sync-ip-details')->hourly();
- Lookups run in parallel batches of 10.
- IPs from
ip_ignore.exact(e.g.127.0.0.1,::1) are filtered in SQL, so they are never selected. - Private and reserved addresses (
10.x,192.168.x, β¦) are marked with{"success": false}without any request, so they never leave the server. - Each unique public IP is requested once. Bad IPs get the same marker, so they are not requested again.
- Failed requests (timeouts, connection errors) keep
ip_detailsempty and are retried on the next run.
π§Ή Pruning Old Logs
Delete logs older than log_keep_days, together with their encrypted files:
$schedule->command('activity-log:prune')->daily();
The command works in chunks (--chunk=1000): one DB delete and one file delete per chunk. Newer logs that pointed at a deleted parent are detached (parent_id set to null), so no dangling links remain. Date folders (activity-log/YYYY-MM-DD/) older than the retention window are removed as well, with one day of margin. It is much faster than model:prune on large tables (for example, 3000 rows: ~0.35 s with ~14 queries, vs ~1 s with ~6000 queries for model:prune).
π‘οΈ Failure Behavior
- Logging never breaks the request. Errors (missing key, queue or DB failure) are reported through Laravel's exception handler and the request continues.
- If the encryption key changed, stored files can no longer be decrypted.
FileStorage::readEncrypted()reports the error and returnsnullinstead of an empty string.
π§© One Row per Request
Each additional_id maps to exactly one row. Writes are a single atomic INSERT ... ON CONFLICT, so the middleware row and the model rows of one request merge safely, even when several queue workers write at the same time. Manual logs without an additional_id get their own id and their own row.
The unique index on additional_id is part of the activity_logs migration, so fresh installs have it. Tables created by an older version need it added by hand, and only after their duplicates are removed:
-- 1. find the duplicates SELECT additional_id, COUNT(*) FROM activity_logs WHERE additional_id IS NOT NULL GROUP BY additional_id HAVING COUNT(*) > 1; -- 2. after merging or deleting them ALTER TABLE activity_logs ADD UNIQUE INDEX activity_logs_additional_id_unique (additional_id);
β‘ Model Change Logging
Models using ActivityLogTrait are logged on create, delete, and on update only when an attribute actually changed. The first model saved in a request is linked to that request's log row. Each model change is linked to the previous change of the same model through parent_id.
π§± Table Structure: activity_logs
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
parent_id |
UUID | Link to the previous log of the same model (optional) |
additional_id |
String | Request ID, groups all logs of one request/HTTP call |
log_type |
String | middleware, http, or custom type |
user_id |
String | Authenticated user ID |
module |
String | App module, taken from the controller namespace |
route |
String | Controller action name |
url |
Text | Full URL accessed |
model_id |
String | Related model ID (optional) |
model_type |
String | Related model class (optional) |
user_agent |
String | Browser/user-agent string |
app_type |
String | App-Type request header |
app_version |
String | App-Version request header |
ip |
String | Client IP address |
method |
String | HTTP method |
status_code |
String | HTTP status code |
request_body |
String | Path to the encrypted request file (not the body itself) |
response_body |
String | Path to the encrypted response file |
curl |
String | Path to the encrypted curl file |
description |
Text | Log description |
request_duration |
Decimal | Request duration in milliseconds |
peak_memory_usage |
BigInt | Peak memory in bytes |
ip_details |
JSON | IP geo/ISP data filled by activity-log:sync-ip-details |
created_at |
DateTime | When the log was created |
updated_at |
DateTime | When the log was last updated |
Indexes: every column except the encrypted file paths (request_body, response_body, curl) is indexed, because the ERP backend filters by the others. additional_id is UNIQUE, created_at has its own index, and (model_id, created_at) is composite.
βοΈ Environment Variables
| Variable | Default | Description |
|---|---|---|
ACTIVITY_LOG_ENABLED |
true |
Turn logging on/off |
ACTIVITY_LOG_DB_CONNECTION |
DB_CONNECTION |
Connection for activity_logs |
ACTIVITY_LOG_USE_QUEUE |
true |
Write logs through a queued job |
ACTIVITY_LOG_QUEUE_NAME |
default |
Queue name for log jobs |
ACTIVITY_LOG_FILE_STORAGE_DISK |
local |
Filesystem disk for encrypted files |
ACTIVITY_LOG_ENCRYPT_KEY |
β | Encryption key (activity-log:generate-key) |
ACTIVITY_LOG_KEEP_DAYS |
90 |
Days to keep logs before pruning |
π§Ή Remove Package
composer remove alifcoder/activity-log
π€ Contributing
Pull requests are welcome! For major changes, please open an issue first.
πͺͺ License
MIT License Β© Shukhratjon Yuldashev
π‘ Logging Incoming Requests and Outgoing HTTP Calls
β 1. Incoming requests
Add the log.activity alias to the routes you want logged (see Logging Requests). Logged requests include route, URL, method, user info, status code (including failed requests: HttpException status or 500), and request/response bodies.
To turn logging off everywhere, set ACTIVITY_LOG_ENABLED=false.
β 2. Log Outgoing HTTP Requests
The package extends Laravelβs Http client with a loggable() macro.
Example usage:
use Illuminate\Support\Facades\Http; $response = Http::loggable()->get('https://example.com/api/data');
This logs outgoing HTTP calls made by your application.
π§© Customization
You can customize what gets logged, ignored routes/methods, and the database connection via:
config/activity-log.php
π« Support
If you need help, feel free to contact Shukhratjon Yuldashev on Telegram.
π§ͺ Development
composer format # Laravel Pint: apply code style composer format:test # Laravel Pint: check only composer phpstan # PHPStan level 5 over src/
Git hooks are not versioned. After cloning, install the pre-commit hook once:
printf '#!/bin/sh\nset -eu\nexec "$(git rev-parse --show-toplevel)/scripts/pre-commit"\n' > .git/hooks/pre-commit chmod +x .git/hooks/pre-commit
On commit it formats the staged PHP files with Pint and then runs PHPStan. Any error stops the commit.