tibisoft / wide-events-bundle
Wide events (canonical log lines) for Symfony — one structured JSON event per request
Package info
github.com/tibisoft/wide-events-symfony
Type:symfony-bundle
pkg:composer/tibisoft/wide-events-bundle
Requires
- php: >=8.2
- psr/log: ^3.0
- symfony/config: ^7.2 || ^8.0
- symfony/dependency-injection: ^7.2 || ^8.0
- symfony/http-kernel: ^7.2 || ^8.0
- symfony/uid: ^8.0
Requires (Dev)
- monolog/monolog: ^3.0
- phpunit/phpunit: ^11.0
- symfony/framework-bundle: ^7.2 || ^8.0
Suggests
- symfony/security-core: Required to use the built-in SecurityEnricher (wide_events.enrichers.security: true)
README
A Symfony bundle for wide events — also known as canonical log lines.
Instead of scattering dozens of log statements across your codebase, you emit one rich, structured JSON event per request. It gets built up progressively as the request moves through your application, then emitted in a single shot after the response is sent.
"Instead of logging what your code is doing, log what happened to this request." — loggingsucks.com
Installation
composer require tibisoft/wide-events-bundle
Register the bundle in config/bundles.php:
return [ // ... Tibisoft\WideEventsBundle\WideEventsBundle::class => ['all' => true], ];
Configuration
Create config/packages/wide_events.yaml:
wide_events: service_name: '%env(APP_NAME)%' # added to every event service_version: '1.0.0' # optional emitter: stdout # stdout | monolog | null sampler: tail # tail | always tail_sampler: slow_threshold_ms: 1000 # requests slower than this are always emitted default_rate: 0.05 # 5% of fast, successful requests are emitted
Emitters
| Value | Behaviour |
|---|---|
stdout |
Writes one JSON line to stdout — ideal for containerised environments |
monolog |
Passes the event to Monolog as a structured context array |
null |
Discards everything — useful in test environments |
Samplers
| Value | Behaviour |
|---|---|
tail |
Retains 100% of errors (4xx/5xx), 100% of slow requests, and a random sample of everything else |
always |
Retains every single event — useful during development |
What you get for free
Every event is automatically seeded with the following fields on each request:
| Field | Source |
|---|---|
timestamp |
RFC 3339 with milliseconds |
request_id |
X-Request-Id / X-Correlation-Id header, or a generated hex ID |
service.name |
From config |
service.version |
From config (if set) |
http.method |
Request method |
http.path |
Request path |
http.user_agent |
User-Agent header |
http.client_ip |
Client IP |
http.status_code |
Response status code |
duration_ms |
Total request duration in milliseconds |
error.class |
Exception class (only when an exception occurs) |
error.message |
Exception message (only when an exception occurs) |
error.code |
Exception code (only when an exception occurs) |
Adding your own context
Inject WideEventStore anywhere in your application and call set():
use Tibisoft\WideEventsBundle\WideEventStore; final class CheckoutController { public function __construct(private readonly WideEventStore $wideEvents) {} public function checkout(Order $order, User $user): Response { $this->wideEvents->current() ?->set('user.id', $user->getId()) ->set('user.tier', $user->getSubscriptionTier()) ->set('order.id', $order->getId()) ->set('order.total', $order->getTotal()) ->set('order.item_count', $order->getItemCount()); // ... handle the request } }
You can call set() from any service, at any point during the request. Everything lands in the same event.
Enrichers
For context that should be added to every event (e.g. the authenticated user), implement EnricherInterface. It is called automatically just before the event is emitted.
use Tibisoft\WideEventsBundle\Contract\EnricherInterface; use Tibisoft\WideEventsBundle\WideEvent; final class AuthenticatedUserEnricher implements EnricherInterface { public function __construct( private readonly Security $security, ) {} public function enrich(WideEvent $event): void { $user = $this->security->getUser(); if ($user === null) { return; } $event ->set('user.id', $user->getId()) ->set('user.email', $user->getUserIdentifier()); } }
With Symfony's autoconfigure enabled (the default), this is all you need — the bundle automatically picks up any service implementing EnricherInterface via the wide_events.enricher tag.
Example output
A single request to POST /checkout might produce this event:
{
"timestamp": "2026-04-16T14:23:01.847+00:00",
"request_id": "a3f8c2d19e4b",
"service.name": "shop-api",
"service.version": "3.12.0",
"http.method": "POST",
"http.path": "/checkout",
"http.user_agent": "Mozilla/5.0",
"http.client_ip": "93.184.216.34",
"http.status_code": 200,
"duration_ms": 142.5,
"user.id": 8821,
"user.email": "alice@example.com",
"user.tier": "premium",
"order.id": "ord_9f3a",
"order.total": 129.99,
"order.item_count": 3,
"payment.method": "card",
"payment.last4": "4242",
"feature.new_checkout": true
}
One event. Everything you need to debug, audit, or analyse — no log archaeology required.
Tail sampling
Wide events can generate a lot of data. The built-in tail sampler helps keep costs under control by making retention decisions after the request completes, when you have full context:
- Always retain requests with
4xxor5xxstatus codes - Always retain requests that exceed
slow_threshold_ms - Randomly sample everything else at
default_rate(default 5%)
This means you never miss an error or a slow request, while routine successful traffic is sampled down significantly.
Extending
You can swap out any part of the bundle by implementing the relevant interface and binding it in your container:
| Interface | Purpose |
|---|---|
EmitterInterface |
Controls how/where the event is written |
SamplerInterface |
Controls which events are retained |
EnricherInterface |
Adds fields to every event before emit |