felipearnold / laravel-health-digest
Logs slow queries and slow requests and groups them, together with application errors, into a ranked JSON digest.
Package info
github.com/felipeArnold/laravel-health-digest
pkg:composer/felipearnold/laravel-health-digest
Requires
- php: ^8.3
- illuminate/console: ^12.0|^13.0
- illuminate/database: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/log: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- laravel/pint: ^1.18
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
- pestphp/pest-plugin-laravel: ^3.0|^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Logs slow queries and slow requests as JSON and groups them, together with the errors in your application log, into a ranked digest that a human or an AI agent can turn into tickets.
- Slow queries — SQL above a threshold, without bindings, with the origin that ran it (route, Livewire component and method, queued job or command).
- Slow requests — HTTP and Livewire requests above a threshold.
- Errors —
ERRORand above fromlaravel.log(single or daily), grouped by exception class and location. - Digest —
php artisan health:digestgroups entries that differ only by literals, masks numbers, quoted strings and e-mails, and ranks bycount × p95.
Requirements
PHP 8.3+, Laravel 12 or 13.
Installation
composer require felipearnold/laravel-health-digest
php artisan vendor:publish --tag=health-digest-config # optional
The service provider is auto-discovered. It:
- registers a daily JSON log channel named
health-digest(storage/logs/health-digest-YYYY-MM-DD.log) unless you define one; - listens to
QueryExecuted,JobProcessingandCommandStarting; - prepends
LogSlowRequeststo thewebmiddleware group.
Identify the tenant (optional)
use FelipeArnold\HealthDigest\HealthDigest; public function boot(): void { HealthDigest::resolveTenantUsing(fn () => auth()->user()?->company_id); }
The value appears as tenant in each log entry and as tenants in the digest.
A resolver that throws is treated as null; it never breaks the request.
Schedule the digest
Schedule::command('health:digest')->twiceDailyAt(7, 13, 30)->withoutOverlapping();
Configuration
| Key | Env | Default |
|---|---|---|
enabled |
HEALTH_DIGEST_ENABLED |
true |
channel |
— | health-digest |
log_path |
HEALTH_DIGEST_LOG_PATH |
storage/logs/health-digest.log |
retention_days |
HEALTH_DIGEST_RETENTION_DAYS |
14 |
query_threshold_ms |
HEALTH_DIGEST_QUERY_MS |
500 (0 disables) |
request_threshold_ms |
HEALTH_DIGEST_REQUEST_MS |
3000 (0 disables) |
middleware_groups |
— | ['web'] |
error_log_path |
HEALTH_DIGEST_ERROR_LOG_PATH |
storage/logs/laravel.log |
output_path |
HEALTH_DIGEST_OUTPUT_PATH |
storage/app/health-digest/latest.json |
Digest format
php artisan health:digest --hours=12 --limit=20
{
"generated_at": "2026-10-05 13:30:00",
"since": "2026-10-05 01:30:00",
"slow_queries": [
{
"fingerprint": "3f9a1c0b7d2e",
"sample": "select * from orders where customer_id = ?",
"count": 3,
"p95_ms": 1000,
"max_ms": 1000,
"tenants": ["ACME", "GLOBEX"],
"origins": ["GET orders", "livewire orders.list@save"],
"first_seen": "2026-10-05 09:00:00",
"last_seen": "2026-10-05 11:00:00"
}
],
"slow_requests": [],
"errors": [
{
"fingerprint": "a81c22e09f41",
"sample": "Inconsistent balance for account ?",
"exception": "App\\Exceptions\\BalanceException",
"location": "app/Actions/Foo.php:42",
"level": "ERROR",
"count": 2,
"first_seen": "2026-10-05 10:00:00",
"last_seen": "2026-10-05 11:00:00"
}
]
}
fingerprint is stable across runs, so it can deduplicate tickets.
Privacy
Query bindings are never logged. Error samples drop the (Connection: …, SQL: …)
suffix of query exceptions and mask quoted strings, numbers, dates, documents
and e-mails. Livewire component and method names are accepted only when they
look like identifiers, so free text from the request payload never reaches the
digest.
Testing
composer test
License
MIT