parvion / laravel-log-pruner
A robust Laravel package for atomic log rotation, dynamic database table pruning, queue worker restart, and multi-recipient email reporting.
Requires
- php: ^8.0
- illuminate/console: ^10.0|^11.0|^12.0|^13.0
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/mail: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
- illuminate/validation: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0
README
A zero-dependency, enterprise-ready Laravel package that atomically rotates your log file, prunes old backups and database rows, restarts queue workers, and emails a detailed report — all from one Artisan command, fully controlled from your
config/log-pruner.phpand.env.
Table of Contents
- Features
- How It Works — The 5 Phases
- Requirements
- Installation
- Configuration
- Usage
- Scheduling
- Email Report Example
- Backup Info Table Example
- .env Quick Reference
- Disabling Features
- License
Features
| Feature | Details |
|---|---|
| 🔄 Atomic log rotation | Uses OS-level rename() — zero window where log is missing |
| 📋 Backup info table | Shows every backup: size, age, expiry date, and SAFE/EXPIRED status |
| 🗑️ Backup file pruning | Auto-deletes backups older than your configured retention period |
| 🔧 Queue worker restart | Gracefully signals workers to release old file handles |
| 🗃️ Dynamic DB pruning | Safely prunes any database table with schema existence checks |
| 📧 Multi-recipient email | Plain-text report via Mail::raw() — works with ZeptoMail, Postmark, Mailgun |
| 🛡️ Schema safety checks | Verifies table + created_at column exist before any DELETE |
| ⚙️ Fully config-driven | Every feature toggled from config/log-pruner.php and .env |
| 🎛️ CLI overrides config | Pass --days, --tables, --email to override config on the fly |
How It Works — The 5 Phases
Each phase can be individually enabled or disabled in the config.
┌─────────────────────────────────────────────────────────────────┐
│ php artisan logs:rotate-and-prune │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 1 — Log Rotation (features.log_rotation) │
│ rename(laravel.log → laravel-backup-2026-05-23-020000.log) │
│ touch(laravel.log) chmod(0664) │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 2 — Backup Pruning (features.backup_pruning) │
│ Scan storage/logs/ for laravel-backup-*.log │
│ Print info table (name, size, age, expiry, status) │
│ Delete files older than retention threshold │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 3 — Queue Restart (features.queue_restart) │
│ Artisan::call('queue:restart') │
│ Workers gracefully restart → open fresh laravel.log │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 4 — Database Pruning (features.db_pruning) │
│ For each table → check exists → check created_at column │
│ DELETE WHERE created_at < cutoff_date │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 5 — Email Report (features.email_report) │
│ Validate each recipient → Mail::raw() → one email per addr │
│ Report includes all phases + backup file table │
└─────────────────────────────────────────────────────────────────┘
Requirements
| Dependency | Version |
|---|---|
| PHP | ^8.0 (8.0, 8.1, 8.2, 8.3+) |
| Laravel | 10.x, 11.x, 12.x, 13.x |
| Carbon | Included with Laravel |
Installation
Step 1 — Add the package
Option A — Local development (path repository)
Add to your Laravel app's composer.json:
{
"repositories": [
{
"type": "path",
"url": "../laravel-log-pruner"
}
],
"require": {
"parvion/laravel-log-pruner": "*"
}
}
Then run:
composer require parvion/laravel-log-pruner
Option B — From Packagist (once published)
composer require parvion/laravel-log-pruner
Laravel's auto-discovery registers the service provider automatically. No manual
config/app.phpedit required.
Step 2 — Publish the config
php artisan vendor:publish --tag=log-pruner-config
This copies the config to your app at config/log-pruner.php.
You can now customise every setting without touching the package source.
Step 3 — Set your .env values
Open your .env and add:
# How many days to keep backup files and DB rows LOG_PRUNER_DAYS=15 # Tables to prune (comma-separated) LOG_PRUNER_TABLES=system_logs,audit_logs # Email recipients for the report (comma-separated) LOG_PRUNER_MAIL_ENABLED=true LOG_PRUNER_MAIL_RECIPIENTS=admin@yourdomain.com,devops@yourdomain.com
Step 4 — Verify the command is registered
php artisan list | grep logs # Expected output: # logs:rotate-and-prune Atomically rotates laravel.log, prunes old backups...
Step 5 — Run it manually to test
php artisan logs:rotate-and-prune
Configuration
After publishing, edit config/log-pruner.php. Every key maps to a .env variable:
return [ // ── Master on/off switch ────────────────────────────────────────────── 'enabled' => env('LOG_PRUNER_ENABLED', true), // ── Retention period (days) ─────────────────────────────────────────── 'days' => env('LOG_PRUNER_DAYS', 15), // ── Enable / disable each phase independently ───────────────────────── 'features' => [ 'log_rotation' => env('LOG_PRUNER_FEATURE_ROTATION', true), 'backup_pruning' => env('LOG_PRUNER_FEATURE_BACKUP_PRUNING', true), 'queue_restart' => env('LOG_PRUNER_FEATURE_QUEUE_RESTART', true), 'db_pruning' => env('LOG_PRUNER_FEATURE_DB_PRUNING', true), 'email_report' => env('LOG_PRUNER_FEATURE_EMAIL', true), ], // ── Tables to DELETE old rows from ──────────────────────────────────── 'tables' => explode(',', env('LOG_PRUNER_TABLES', 'system_logs')), // ── Backup file display settings ────────────────────────────────────── 'backup' => [ 'show_info' => env('LOG_PRUNER_BACKUP_SHOW_INFO', true), 'show_info_in_email' => env('LOG_PRUNER_BACKUP_SHOW_INFO_EMAIL', true), 'date_format' => env('LOG_PRUNER_BACKUP_DATE_FORMAT', 'Y-m-d H:i'), ], // ── Email report settings ───────────────────────────────────────────── 'mail' => [ 'enabled' => env('LOG_PRUNER_MAIL_ENABLED', true), 'recipients' => array_filter(array_map('trim', explode(',', env('LOG_PRUNER_MAIL_RECIPIENTS', '')))), 'subject_prefix' => env('LOG_PRUNER_MAIL_SUBJECT_PREFIX', '[Log Pruner]'), ], ];
Usage
Basic — uses all config defaults
php artisan logs:rotate-and-prune
Override retention period
# Keep 30 days instead of config default
php artisan logs:rotate-and-prune --days=30
Override which tables to prune
# Prune three tables in one run
php artisan logs:rotate-and-prune --tables=system_logs,audit_logs,api_request_logs
Override email recipients
# Send report to specific people for this run only
php artisan logs:rotate-and-prune --email=cto@example.com,sre@example.com
Full override example
php artisan logs:rotate-and-prune \ --days=7 \ --tables=system_logs,audit_logs \ --email=admin@example.com,devops@example.com
Priority: CLI option → config/log-pruner.php → built-in default
Scheduling
Laravel 11+ (routes/console.php)
<?php use Illuminate\Support\Facades\Schedule; // All settings come from config/log-pruner.php automatically. // Pass CLI options here only if you need to override config for this schedule. Schedule::command('logs:rotate-and-prune') ->dailyAt('02:00') ->timezone('Asia/Kolkata') ->withoutOverlapping() ->runInBackground();
With explicit overrides (optional):
Schedule::command('logs:rotate-and-prune', [ '--days' => 15, '--tables' => 'system_logs,audit_logs', '--email' => 'devops@yourdomain.com', ]) ->dailyAt('02:00') ->timezone('Asia/Kolkata') ->withoutOverlapping() ->runInBackground();
Laravel 10 (app/Console/Kernel.php)
<?php namespace App\Console; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; class Kernel extends ConsoleKernel { protected function schedule(Schedule $schedule): void { $schedule->command('logs:rotate-and-prune') ->dailyAt('02:00') ->timezone('Asia/Kolkata') ->withoutOverlapping() ->runInBackground(); } }
Server Cron Entry
Add this to your server (crontab -e) — this is the only cron entry you need:
* * * * * cd /var/www/your-laravel-app && php artisan schedule:run >> /dev/null 2>&1
The cron runs every minute. Laravel's scheduler handles the
02:00 Asia/Kolkatatiming internally via->dailyAt()and->timezone(). Never put thelogs:rotate-and-prunecommand directly in cron — you would bypasswithoutOverlapping()and timezone support.
Email Report Example
=========================================================
PARVION LARAVEL LOG PRUNER — AUTOMATED REPORT
=========================================================
Application : My Laravel App
URL : https://myapp.com
Report Time : 2026-05-23 02:00:05 IST
---------------------------------------------------------
CONFIGURATION
---------------------------------------------------------
Retention Period : 15 days
Cutoff Date : 2026-05-08 02:00:05 IST
Tables : system_logs, audit_logs
Recipients : admin@myapp.com, devops@myapp.com
---------------------------------------------------------
FEATURE STATUS
---------------------------------------------------------
Log Rotation : Enabled
Backup Pruning : Enabled
Queue Restart : Enabled
DB Pruning : Enabled
Email Report : Enabled
---------------------------------------------------------
LOG FILE ROTATION
---------------------------------------------------------
New Backup File : laravel-backup-2026-05-23-020005.log
Old Backups Deleted : 2
---------------------------------------------------------
BACKUP FILES STATUS (retention: 15 days)
---------------------------------------------------------
------------------------------------------------------------------------
File Name Size Created Age Status
------------------------------------------------------------------------
laravel-backup-2026-05-20-020005 2.34 MB 2026-05-20 02:00 3d SAFE (expires 2026-06-04, in 12d)
laravel-backup-2026-05-07-020002 1.12 MB 2026-05-07 02:00 16d DELETED
laravel-backup-2026-04-30-020001 987 KB 2026-04-30 02:00 23d DELETED
------------------------------------------------------------------------
---------------------------------------------------------
QUEUE WORKERS
---------------------------------------------------------
Restart Signaled : Yes
---------------------------------------------------------
DATABASE TABLE PRUNING
---------------------------------------------------------
• `system_logs` 142 rows deleted
• `audit_logs` 87 rows deleted
---------------------------------------------------------
This is an automated message from the Log Pruner.
Please do not reply to this email.
=========================================================
Backup Info Table Example
When LOG_PRUNER_BACKUP_SHOW_INFO=true, Phase 2 prints this in the console:
Backup Files Status (retention: 15 days)
────────────────────────────────────────────────────────────────────────────────
File Name Size Created Days Kept Expires On Status
────────────────────────────────────────────────────────────────────────────────
laravel-backup-2026-05-20-020005.log 2.34 MB 2026-05-20 02:00 3d 2026-06-04 02:00 SAFE (expires in 12d)
laravel-backup-2026-05-10-020003.log 1.87 MB 2026-05-10 02:00 13d 2026-05-25 02:00 SAFE (expires in 2d)
laravel-backup-2026-05-07-020002.log 1.12 MB 2026-05-07 02:00 16d 2026-05-22 02:00 EXPIRED → deleted
laravel-backup-2026-04-30-020001.log 987 KB 2026-04-30 02:00 23d 2026-05-15 02:00 EXPIRED → deleted
────────────────────────────────────────────────────────────────────────────────
.env Quick Reference
# ╔══════════════════════════════════════════════════════════════╗ # ║ LOG PRUNER CONFIGURATION ║ # ╚══════════════════════════════════════════════════════════════╝ # Master on/off switch (set false to pause everything) LOG_PRUNER_ENABLED=true # Default retention in days (can be overridden with --days=N) LOG_PRUNER_DAYS=15 # ── Feature Toggles ────────────────────────────────────────── LOG_PRUNER_FEATURE_ROTATION=true # Phase 1 — atomic log rename + touch LOG_PRUNER_FEATURE_BACKUP_PRUNING=true # Phase 2 — delete expired backup files LOG_PRUNER_FEATURE_QUEUE_RESTART=true # Phase 3 — graceful queue:restart LOG_PRUNER_FEATURE_DB_PRUNING=true # Phase 4 — delete old DB rows LOG_PRUNER_FEATURE_EMAIL=true # Phase 5 — send email report # ── Database Tables ─────────────────────────────────────────── LOG_PRUNER_TABLES=system_logs,audit_logs,api_request_logs # ── Backup Info Display ─────────────────────────────────────── LOG_PRUNER_BACKUP_SHOW_INFO=true # Show table in console LOG_PRUNER_BACKUP_SHOW_INFO_EMAIL=true # Include table in email LOG_PRUNER_BACKUP_DATE_FORMAT="Y-m-d H:i" # ── Email Report ────────────────────────────────────────────── LOG_PRUNER_MAIL_ENABLED=true LOG_PRUNER_MAIL_RECIPIENTS=admin@yourdomain.com,devops@yourdomain.com LOG_PRUNER_MAIL_SUBJECT_PREFIX=[Log Pruner]
Disabling Features
| What you want to stop | .env setting |
|---|---|
| Stop everything (maintenance) | LOG_PRUNER_ENABLED=false |
| Stop rotating the log file | LOG_PRUNER_FEATURE_ROTATION=false |
| Stop deleting old backup files | LOG_PRUNER_FEATURE_BACKUP_PRUNING=false |
| Skip queue worker restart | LOG_PRUNER_FEATURE_QUEUE_RESTART=false |
| Stop deleting old DB rows | LOG_PRUNER_FEATURE_DB_PRUNING=false |
| Stop sending email reports | LOG_PRUNER_MAIL_ENABLED=false |
| Disable email feature entirely | LOG_PRUNER_FEATURE_EMAIL=false |
Mail Driver Compatibility
The package uses Mail::raw() which works with any mail driver — no Blade
templates are required:
| Driver | Compatible |
|---|---|
| SMTP | ✅ |
| ZeptoMail | ✅ |
| Mailgun | ✅ |
| Postmark | ✅ |
| Resend | ✅ |
| Amazon SES | ✅ |
| Log (local dev) | ✅ |
License
MIT — © Anand Kumar (Parvion) — see LICENSE
Contributing
Bug reports, feature requests, and pull requests are welcome!
Please read CONTRIBUTING.md before submitting a PR.
Changelog
All notable changes between versions are documented in CHANGELOG.md.
This project follows Semantic Versioning and Keep a Changelog.