daemon8 / symfony
Symfony integration for Daemon8 -- automatic runtime observation via event subscribers
Requires
- php: ^8.4
- daemon8/php: ^0.1
- symfony/config: ^6.4|^7.0
- symfony/dependency-injection: ^6.4|^7.0
- symfony/event-dispatcher: ^6.4|^7.0
- symfony/http-kernel: ^6.4|^7.0
Requires (Dev)
- doctrine/dbal: ^4.2
- doctrine/doctrine-bundle: ^2.13
- doctrine/migrations: ^3.8
- doctrine/orm: ^3.3
- matthiasnoback/symfony-dependency-injection-test: ^6.0
- monolog/monolog: ^3.0
- phpstan/phpstan: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^11.0
- rector/rector: ^2.0
- slevomat/coding-standard: ^8.15
- squizlabs/php_codesniffer: ^3.10
- symfony/browser-kit: ^6.4|^7.0
- symfony/console: ^6.4|^7.0
- symfony/framework-bundle: ^6.4|^7.0
- symfony/http-client: ^6.4|^7.0
- symfony/mailer: ^6.4|^7.0
- symfony/messenger: ^6.4|^7.0
- symfony/monolog-bundle: ^3.10
- symfony/security-bundle: ^6.4|^7.0
Suggests
- doctrine/doctrine-bundle: Enables DoctrineQueryMiddleware, transaction observation, and ORM model listeners.
- doctrine/migrations: Enables migration event observation via DoctrineMigrationSubscriber.
- doctrine/orm: Enables Doctrine ORM model listeners (postPersist / postUpdate / postRemove).
- symfony/http-client: Enables the Daemon8 HttpClient decorator for outbound request tracing.
- symfony/mailer: Enables MailerSubscriber (MessageEvent / SentMessageEvent / FailedMessageEvent).
- symfony/messenger: Enables MessengerSubscriber (worker + transport lifecycle events).
- symfony/monolog-bundle: Enables the Daemon8 Monolog handler for log observation.
- symfony/security-bundle: Enables SecuritySubscriber (login success / failure / logout / switch user).
This package is auto-updated.
Last update: 2026-04-22 13:45:51 UTC
README
Drop-in runtime observation for Symfony applications.
Requests, exceptions, console commands, Messenger, Doctrine queries, HTTP client calls, security events, and Monolog records stream into Daemon8 — with a dedicated profiler panel in dev.
Website · Docs · Daemon · SDKs · Demo · Contact
Free and open source. No tiers, no license keys, no phone-home.
Active development. This package is in public alpha. Tracked work for this bundle lives as GitHub Issues; the broader roadmap is maintained on the primary daemon8 repo.
Daemon8 for Symfony
Symfony 6.4 LTS + 7.x. PHP 8.4+. Works with zero config through the Flex recipe.
Getting Started
Install the bundle:
composer require daemon8/symfony
Symfony Flex publishes config/packages/daemon8.yaml, registers the bundle across every environment, and appends DAEMON8_* entries to .env.
Start the local daemon (if you haven't already):
daemon8 install # one-time setup + start as a system service
No Flex? Register the bundle manually in config/bundles.php:
return [ // ... Daemon8\Symfony\Daemon8Bundle::class => ['all' => true], ];
Every HTTP request, Doctrine query, thrown exception, Messenger dispatch, and Monolog record at warning or above now streams into Daemon8 automatically. Emit an observation manually from anywhere by injecting Daemon8\Daemon8Client:
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); $this->daemon8->send( ['event' => 'checkout', 'order_id' => $order->id], severity: 'info', channel: 'orders', ); } }
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'
Open a page in dev mode and the WebProfiler toolbar shows a dedicated daemon8 panel — a severity-colored count of every observation emitted during the request. Click any observation and its profiler_token data attribute links back to /_profiler/{token} for full request context.
Profiler panel
The correlation is handled by ProfilerCorrelatingBuffer, a #[When('dev')] decorator over the base SDK's Buffer. It wires automatically in dev and is entirely absent from prod. Every observation emitted during a request picks up _profiler_token from the current Request attributes and stamps it onto the observation's data payload.
Disable the panel in dev by removing the profiler bundle, or by registering a local compiler pass that strips the data_collector-tagged service by id. Most operators keep it on — the cost is negligible.
Manual observations
Daemon8Client is registered public: true so the container.get(Daemon8Client::class) escape hatch works in tests and one-off scripts, but autowiring is the intended path. The service is constructed once per request — no scoped resets required because the SDK keeps no per-request state outside the Buffer, which flushes on kernel.terminate / console.terminate via Support\TerminateFlushSubscriber.
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/packages/daemon8.yaml:
daemon8: sensitive: mask: '[masked]' fields: - password - token - api_key - custom_secret headers: - authorization - cookie - x-api-key patterns: - '/sk_(?:live|test)_[A-Za-z0-9]{24,}/' - '/eyJ[\w-]+\.[\w-]+\.[\w-]+/'
On top of that:
#[\SensitiveParameter]— any argument marked sensitive is redacted in captured backtraces.Daemon8\Sanitization\Sensitive::wrap()— force redaction at the call site regardless of key name or value shape.
use function Daemon8\sensitive; $this->daemon8->send(['token' => sensitive($token)], severity: 'debug');
Precedence is wrap > field > pattern. sanitizer: is the FQCN of the Daemon8\Contracts\Sanitizer implementation — swap the class to customize behavior without forking the bundle.
Respondents — the reactive runtime
Subscribers push observations into Daemon8. Respondents react to them. A respondent is an autoconfigured service with full container access.
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 { // Full container is available — inject an Entity manager, logger, // mailer, anything a normal service could depend on. } }
Any service implementing Respondent is auto-tagged daemon8.respondent via registerForAutoconfiguration() — no manual YAML tag required. Run the subscriber loop:
bin/console daemon8:run
Flags mirror Messenger's Worker conventions:
| Flag | Default | Semantics |
|---|---|---|
--time-limit=<seconds> |
unbounded | Clean exit after wall-clock deadline |
--memory-limit=<MB> |
128 |
Clean exit when memory_get_usage(true) crosses the ceiling |
--limit=<count> |
unbounded | Clean exit after dispatching N observations |
--failure-limit=<count> |
3 |
Clean exit after N consecutive dispatch failures |
Signals handled: SIGTERM/SIGINT drain and exit 0; SIGUSR1 is reserved for reload (returns false, keeps the loop running). LockableTrait guarantees one worker per host when symfony/lock is installed; absent that, the guard degrades to a no-op and supervisord/systemd handles singleton discipline.
systemd stub
[Unit] Description=Daemon8 respondent worker After=network.target [Service] Type=simple User=www-data WorkingDirectory=/var/www/app ExecStart=/usr/bin/php bin/console daemon8:run --time-limit=3600 --memory-limit=256 Restart=always RestartSec=5 [Install] WantedBy=multi-user.target
Configuration
# Master switch DAEMON8_ENABLED=true # Transport DAEMON8_URL=http://127.0.0.1:9077/ingest DAEMON8_TIMEOUT_MS=50 DAEMON8_BATCH_SIZE=100 # App identity (defaults to Symfony's app identity) DAEMON8_APP=checkout-api # Debug — surfaces transport errors to STDERR, wires the stopwatch subscriber DAEMON8_DEBUG=false # Opt-in surfaces DAEMON8_CACHE_ENABLED=false # cache.app decorator DAEMON8_CASTERS_ENABLED=false # global VarCloner caster DAEMON8_MONOLOG_ENABLED=false # prepend Daemon8 Monolog handler
Full YAML schema (what the recipe writes, minus env resolvers for readability):
daemon8: enabled: true app: ~ debug: false transport: url: 'http://127.0.0.1:9077/ingest' mode: ~ # null (default) or 'sync' timeout_ms: 50 batch_size: 100 sensitive: sanitizer: Daemon8\Sanitization\Sanitizer mask: '[masked]' fields: [password, token, api_key, authorization, ...] headers: [authorization, cookie, x-api-key, x-auth-token] patterns: ['/sk_(?:live|test)_[A-Za-z0-9]{24,}/', ...] exception: include_arguments: false limit: 50 monolog: enabled: false cache: enabled: false casters: enabled: false watchers: Daemon8\Symfony\EventSubscriber\RequestSubscriber: { enabled: true, size_limit: 65536 } Daemon8\Symfony\EventSubscriber\ExceptionSubscriber: { enabled: true } Daemon8\Symfony\EventSubscriber\CommandSubscriber: { enabled: true } Daemon8\Symfony\Messenger\MessengerSubscriber: { enabled: true } Daemon8\Symfony\Security\SecuritySubscriber: { enabled: true } Daemon8\Symfony\HttpClient\Daemon8HttpClient: { enabled: true, observe_retries: per_logical } Daemon8\Symfony\Monolog\Daemon8Handler: { enabled: true, level: warning } Daemon8\Symfony\Doctrine\Daemon8QueryMiddleware: { enabled: true, slow: 100, all: false } Daemon8\Symfony\Doctrine\MigrationListener: { enabled: true } Daemon8\Symfony\Doctrine\ModelListener: { enabled: false, events: [postPersist, postUpdate, postRemove] } Daemon8\Symfony\Mailer\MailerSubscriber: { enabled: false } Daemon8\Symfony\EventSubscriber\CacheSubscriber: { enabled: false } respondents: []
Subscriber inventory
Each subscriber, decorator, listener, and handler is individually toggled under the watchers: map in config/packages/daemon8.yaml (shown above). Soft deps are guarded in three places (composer, extension loader, prepend). Removing symfony/messenger never crashes the bundle — the subscriber simply does not register.
| Class | Default | Event trigger | Fields sent on each observation |
|---|---|---|---|
EventSubscriber\RequestSubscriber |
enabled | kernel.request / kernel.response for the main request |
method, uri, status, duration_ms, headers (redacted), input (redacted, truncated to size_limit), route |
EventSubscriber\ExceptionSubscriber |
enabled | kernel.exception |
class, message, file, line, trace (structured, arguments redacted unless exception.include_arguments: true) |
EventSubscriber\CommandSubscriber |
enabled | console.command / console.error / console.terminate |
command, arguments, options, exit_code, duration_ms |
Messenger\MessengerSubscriber |
enabled (if symfony/messenger) |
SendMessageToTransportsEvent, WorkerMessageReceived/Handled/Failed/Retried |
message_class, transport, bus, event, duration_ms, exception (on failure) |
Security\SecuritySubscriber |
enabled (if symfony/security-bundle) |
LoginSuccessEvent, LoginFailureEvent, LogoutEvent, SwitchUserEvent |
event, firewall, user_identifier, reason (on failure) |
HttpClient\Daemon8HttpClient |
enabled (if symfony/http-client) |
Outbound requests via #[AsDecorator('http_client')] |
method, url, status, duration_ms, request_headers (redacted), response_headers (redacted), retry_count |
Monolog\Daemon8Handler |
enabled (if symfony/monolog-bundle + daemon8.monolog.enabled: true) |
Log records at level threshold (reentry-guarded) |
channel, level, message, context (redacted), extra |
Doctrine\Daemon8QueryMiddleware |
enabled (if doctrine/doctrine-bundle) |
DBAL Statement::execute() — slow (slow: 100 ms) or all (all: true) |
sql, bindings (interpolated + redacted), duration_ms, connection |
Doctrine\MigrationListener |
enabled (if doctrine/migrations) |
Migration start / version start / version finished / complete | version, direction, duration_ms, description |
Mailer\MailerSubscriber |
opt-in | MessageEvent, SentMessageEvent, FailedMessageEvent |
from, to, subject, attachments (metadata only — names and sizes), status, error (on failure) |
Doctrine\ModelListener |
opt-in | postPersist, postUpdate, postRemove — configurable via events: [...] |
entity_class, entity_id, event, changes (redacted) |
EventSubscriber\CacheSubscriber |
opt-in | Symfony Cache hits / misses / writes | pool, key, event, ttl (on write) |
Cache\Daemon8CacheAdapter |
opt-in (daemon8.cache.enabled: true) |
Decorates cache.app for per-call timing |
key, event, duration_ms, hit |
Caster\Daemon8Caster |
opt-in (daemon8.casters.enabled: true) |
VarCloner dump() invocations |
Caster output (sensitive fields redacted) |
Debug\StopwatchSubscriber |
dev + daemon8.debug: true |
Emits a daemon8 stopwatch category into the Symfony timeline |
Stopwatch markers — no wire emission |
Per-watcher option reference:
| Watcher | Option | Type | Notes |
|---|---|---|---|
RequestSubscriber |
size_limit |
int bytes | Request body truncation ceiling |
Daemon8QueryMiddleware |
slow |
int ms | Threshold below which statements are dropped |
Daemon8QueryMiddleware |
all |
bool | Capture every statement regardless of duration |
Daemon8Handler |
level |
string | Monolog threshold (debug, info, notice, warning, error, ...) |
Daemon8HttpClient |
observe_retries |
per_logical / per_attempt |
One observation per logical call vs per physical attempt |
ModelListener |
events |
string[] | Subset of postPersist, postUpdate, postRemove |
Requirements
- PHP 8.4+
- Symfony 6.4 LTS or 7.x
daemon8/php: ^0.1(pulled in automatically)- Daemon8 daemon running (
daemon8 install)
Soft deps (any combination; the bundle works without all of them):
doctrine/dbal: ^4.2,doctrine/doctrine-bundle: ^2.13,doctrine/orm: ^3.3,doctrine/migrations: ^3.8— DBAL 3 is unsupportedsymfony/messenger,symfony/mailer,symfony/security-bundle,symfony/http-client,symfony/monolog-bundle
Development
composer install composer check # phpstan + rector dry-run + phpcs + phpunit composer test composer analyse composer cs:fix
phpstan level 8 with phpstan-symfony. Rector with php84 + phpunit sets. Slevomat coding standard.
License
MIT. See LICENSE.