laravel maintained by daemon8
Active development. This package is in public alpha. Tracked work for this SDK lives as GitHub Issues; the broader roadmap is maintained on the primary daemon8 repo.
Daemon8 for Laravel
Laravel 11 / 12 / 13. PHP 8.4+. Zero performance cost when the daemon is offline. Octane and long-running workers are under active hardening — see the roadmap on the primary repo.
Getting Started
Install the package:
composer require daemon8/laravel
php artisan vendor:publish --tag=daemon8-config
Start the local daemon (if you haven't already):
daemon8 install # one-time setup + start as a system service
Every HTTP request, slow query, exception, and queued job now streams into Daemon8 automatically. Emit an observation manually from anywhere:
use Daemon8\Laravel\Facades\Daemon8;
Daemon8::log('checkout completed', severity: 'info');
Daemon8::warn('payment retry threshold reached');
Daemon8::exception($e);
Query from your terminal or agent:
daemon8 tail --kinds log,exception --severity warn
Or from code, against the local daemon's HTTP API:
curl 'http://127.0.0.1:9077/api/observe?kinds=log,exception&severity_min=warn&limit=20'
Manual observations
Inject Daemon8Client for testable classes (recommended):
use Daemon8\Daemon8Client;
final class CheckoutService
{
public function __construct(private readonly Daemon8Client $daemon8) {}
public function process(Order $order): void
{
$this->daemon8->log('processing order #' . $order->id);
if ($order->needsManualReview()) {
$this->daemon8->send(
['order_id' => $order->id, 'reason' => $order->reviewReason()],
severity: 'warn',
channel: 'orders.review',
);
}
}
}
Or reach for the facade when a one-off emission is simplest:
use Daemon8\Laravel\Facades\Daemon8;
Daemon8::send(['event' => 'deploy', 'version' => '2.4.1'], severity: 'info', channel: 'deploys');
Sensitive data
Default redaction covers passwords, tokens, API keys, authorization headers, JWTs, Stripe keys, AWS access keys, GitHub PATs, and common credential shapes. Three lanes configured in config/daemon8.php:
'sensitive' => [
'fields' => ['password', 'api_key', 'custom_secret'],
'headers' => ['authorization', 'x-api-key'],
'patterns' => ['/sk_(?:live|test)_[A-Za-z0-9]{24,}/'],
],
Plus two freebies: #[\SensitiveParameter]-marked arguments are redacted in captured backtraces, and Eloquent's $hidden is honored when models are serialized.
Wrap sensitive values at their origin when key-name matching isn't enough:
use function Daemon8\sensitive;
Daemon8::send(['token' => sensitive($token)]);
Respondents — the reactive runtime
Watchers push observations into Daemon8. Respondents react to them. A respondent is a first-class Laravel class with full container access: Eloquent, queues, cache, mail — everything you'd have in a job or controller.
use Daemon8\Contracts\Respondent;
use Daemon8\Filter;
use Daemon8\Observation;
use Daemon8\Severity;
final class SlowQueryAnalyst implements Respondent
{
public function interest(): Filter
{
return new Filter(kinds: ['query'], severityMin: Severity::Warn);
}
public function respond(Observation $observation): void
{
$sql = (string) ($observation->data['sql'] ?? '');
$duration = (float) ($observation->data['duration_ms'] ?? 0);
SlowQuery::create([
'sql' => $sql,
'duration_ms' => $duration,
'captured_at' => now(),
]);
}
}
Register in config/daemon8.php:
'respondents' => [
App\Daemon8\Respondents\SlowQueryAnalyst::class,
],
Run the subscriber process:
php artisan daemon8:run
This is a long-running command — drop it into a Horizon process, a Supervisor config, or systemd. Respondents receive observations via SSE with exponential-backoff reconnect and Last-Event-ID resume.
Configuration
# Master switch
DAEMON8_ENABLED=true
# Transport
DAEMON8_URL=http://127.0.0.1:9077/ingest
DAEMON8_TRANSPORT= # 'sync' | 'queue' (default dispatches by scheme)
DAEMON8_TIMEOUT_MS=50
DAEMON8_BATCH_SIZE=100
# App identity (defaults to config/app.php name)
DAEMON8_APP=checkout-api
# Debug — surfaces transport errors to STDERR
DAEMON8_DEBUG=false
See config/daemon8.php for per-watcher options (slow-query threshold, request body size limit, exception argument capture, etc.).
Watcher inventory
Each watcher is individually toggled via environment variable or config/daemon8.php. Example toggles:
DAEMON8_WATCH_REQUEST=true
DAEMON8_WATCH_QUERY=true
DAEMON8_WATCH_JOBS=true
DAEMON8_WATCH_CACHE=false # opt-in watchers default to false
DAEMON8_WATCH_MODEL=false
DAEMON8_WATCH_MAIL=false
DAEMON8_WATCH_REDIS=false
The full list of captured events and what gets sent for each:
| Watcher | Event trigger | Fields sent on each observation |
|---|---|---|
RequestWatcher |
Every HTTP request/response cycle | method, uri, status, duration_ms, input (redacted), route, controller |
QueryWatcher |
Queries exceeding the slow-query threshold (100ms by default) | sql, bindings (interpolated + redacted), duration_ms, connection |
HttpClientWatcher |
Outbound Http:: client requests |
method, url, status, duration_ms, request_headers (redacted), response_headers (redacted) |
ExceptionWatcher |
Every reported \Throwable |
class, message, file, line, trace (structured, with arguments redacted) |
LogWatcher |
Log:: entries at warning level or higher |
level, message, context (redacted), channel |
JobWatcher |
Queue jobs — dispatched, processing, processed, failed | job, queue, connection, attempts, duration_ms, exception (on failure) |
AuthWatcher |
Login, logout, failed authentication | guard, user_id, event |
NotificationWatcher |
Notifications sent / failed | notifiable_type, notifiable_id, channel, notification, status |
ScheduleWatcher |
Scheduled task lifecycle | command, expression, duration_ms, output (truncated) |
MigrationWatcher |
php artisan migrate up / down |
migration, direction, batch, duration_ms |
TransactionWatcher |
DB transaction begin / commit / rollback | connection, event, level |
CacheWatcher (opt-in) |
Cache hit, miss, write, forget | store, key, event, ttl (on write) |
ModelWatcher (opt-in) |
Eloquent model events | model, event (created/updated/deleted/...), attributes (redacted) |
MailWatcher (opt-in) |
Outbound mail | mailer, from, to, subject, queued |
RedisWatcher (opt-in) |
Redis command execution | connection, command, parameters (truncated) |
Octane and queue workers
The package binds its observation buffer via scoped() so state resets per request under Octane, and no static state survives across workers. Full Octane test coverage (worker lifecycle, concurrent requests, memory growth over time) is tracked on the roadmap — the implementation is Octane-aware but formal validation across Octane server versions is not yet complete.
Set DAEMON8_TRANSPORT=queue to push observations through Laravel's queue. A SendObservationsJob runs on the default queue and flushes via the configured HTTP transport. Useful when request paths must never block on the daemon being reachable.
Development
composer install
composer check # phpstan + rector dry-run + phpcs + phpunit
composer test
Requirements
- PHP 8.4+
- Laravel 11 / 12 / 13
daemon8/php(pulled in automatically)- Daemon8 daemon running locally
License
MIT. See LICENSE.