tibisoft/wide-events-bundle

Wide events (canonical log lines) for Symfony — one structured JSON event per request

Maintainers

Package info

github.com/tibisoft/wide-events-symfony

Type:symfony-bundle

pkg:composer/tibisoft/wide-events-bundle

Statistics

Installs: 9

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.0.4 2026-04-16 20:47 UTC

This package is auto-updated.

Last update: 2026-04-16 20:47:34 UTC


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 4xx or 5xx status 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