daemon8/symfony

Symfony integration for Daemon8 -- automatic runtime observation via event subscribers

Maintainers

Package info

github.com/daemon8ai/daemon8-symfony

Homepage

Issues

Type:symfony-bundle

pkg:composer/daemon8/symfony

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

dev-main / 0.1.x-dev 2026-04-21 20:04 UTC

This package is auto-updated.

Last update: 2026-04-22 13:45:51 UTC


README

Daemon8

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 unsupported
  • symfony/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.