laravel-event-log

Laravel Event Logger is an event log, not an error log.

It focuses on recording facts about what happened in your system – “user enrolled”, “mandate created”, “payment failed”, “organisation created” – in a way that is:

  • Structured and relational
  • Privacy‑aware
  • Queue‑friendly
  • Ready for audit trails and distributed tracing

Instead of spraying JSON blobs or free‑text messages into a log file, you get a clean event model with:

  • A primary subject (the main model the event is about)
  • One or more related models (for cross‑entity traceability)
  • causer (user, system, worker, webhook, cron, etc.)
  • Lightweight metadata (key‑value context, no sensitive payloads)
  • Correlation IDs and transaction IDs for tracing flows across processes and services

Core design goals

This is the intent behind the package, in your own words:

  • Log facts, not state
    The log records events (“user.enrolled”, “payment.failed”), not full snapshots of your models. That keeps storage lean and the audit trail focused on what actually happened.
  • Privacy by design
    No dumping of full JSON blobs, PII, or sensitive payloads. Events link back to models via relationships so you can always resolve the subject/related records without duplicating their data.
  • Async‑first
    All persistence goes through Laravel’s queue system. Logging events does not slow down your HTTP requests or block users. You can even send event logging to a low‑priority queue.
  • Exactly‑once delivery
    Built‑in idempotency means retried queue jobs will not create duplicate records. If a job is re‑run, the database uniqueness constraint will quietly drop the second insert.
  • Relational graph of events
    Each event can link to a primary subject and multiple related models. That gives you a graph of interactions across entities instead of siloed activity on a single model.
  • Audit‑ready
    Events can capture causer type and ID, correlation IDs, transaction IDs, and metadata. That’s enough to build an audit trail or regulatory report without re‑inventing your own event schema.
  • OpenTelemetry‑ready
    If you’re using OpenTelemetry, the package can act as a bridge, using correlation IDs to plug your event log into distributed tracing.

Installation via Composer

Bash
composer require ayup-creative/event-log

The service provider is auto‑discovered.

Next, you’ll need to publish the migrations, and run them.

Bash
php artisan vendor:publish --tag="event-log-migrations"
php artisan migrate

You can also optionally publish the config if you wish to specify your own Event or EventMetaData models:

Bash
php artisan vendor:publish --tag="event-log-config"

Usage in a project

1. Manual domain events (event_log helper)

This is the primary API: a small helper that records meaningful domain events asynchronously.

PHP
// Basic event
event_log('organisation.created', $organisation);

// Event with related models
event_log('user.enrolled', $user, [$organisation, $course]);

// Event with metadata (e.g. reasons, provider results)
event_log('payment.failed', $payment, metadata: [
    'error_reason' => 'Insufficient funds',
    'provider'     => 'Stripe',
]);

Each call lets you specify:

  • Event name – dot‑notation, human readable (organisation.createdpayment.failed).
  • Subject – the main Eloquent model this event is about.
  • Related – optional array of additional models involved in the event.
  • Causer type – optional (usersystemworkercronwebhook, etc.).
  • Metadata – optional key‑value context, kept small and deliberate.

Because logging runs through Laravel’s queue, these calls are cheap to sprinkle through domain services, listeners, or controllers without cost to the request lifecycle.

 Automatic lifecycle logging (LogsEvents trait)

For models where you want a timeline of CRUD operations, you add a trait:

PHP
use AyupCreative\EventLog\Features\LogsEvents;
use Illuminate\Database\Eloquent\Model;

class Mandate extends Model
{
  use LogsEvents;
}

Out of the box this logs:

  • created
  • updated
  • deleted
  • restored

You can customise how each model logs its lifecycle:

PHP
class Mandate extends Model
{
    use LogsEvents;

    // Use a specific prefix for events (default: snake_case class name)
    public function eventNamespace(): string
    {
        return 'billing.mandate';
    }

    // Filter which events to log
    public function shouldLogEvent(string $event): bool
    {
        return $event !== 'mandate.updated';
    }

    // Attach related models automatically</em>
    public function eventRelations(string $event): array
    {
        return [$this->organisation];
    }

    // Add automatic metadata per event
    public function eventMetadata(string $event): array
    {
        return ['type' => $this->type];
    }
}

This lets you turn any Eloquent model into something with a first‑class event trail, without writing listeners by hand.

3. Custom actor & causer resolution

By default, the package uses auth()->id() and checks whether the app is running in the console to decide:

  • Who caused the event
  • What type of causer it is (userworkersystemcron, etc.)

You can override that in your AppServiceProvider:

PHP
use AyupCreative\EventLog\Facades\EventLog;

public function boot()
{
    // Custom actor resolution (e.g. API guard)
    EventLog::resolveActorWith(function ($app) {
        return auth('api')->id();
    });

    // Custom causer type logic
    EventLog::determineCauserTypeWith(function ($app) {
        if ($app->runningInConsole()) {
            return 'cron';
        }

        return 'user';
    });
}

This is useful in multi‑tenant, API‑only, or multi‑guard applications.

4. Event transactions (grouping events)

Sometimes multiple events belong to a single, atomic business operation. WithEventTransaction lets you group them and assign a shared transaction_id:

PHP
use AyupCreative\EventLog\Support\WithEventTransaction;

WithEventTransaction::run(function () use ($user, $org) {
    $org->save();
    $user->organisations()->attach($org);

    event_log('organisation.created', $org);
    event_log('user.enrolled', $user, [$org]);
});

Every event logged inside the closure shares the same transaction identifier. That makes it easy to reconstruct what happened within a particular operation, even across multiple models.

5. Correlation IDs & cross‑service tracing

The package ships with middleware and HTTP client helpers to keep a correlation ID flowing through your system.

Global middleware

PHP
use \AyupCreative\EventLog\Http\Middleware\EventCorrelationMiddleware;

// bootstrap/app.php or app/Http/Kernel.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->append(EventCorrelationMiddleware::class);
})

This middleware:

  • Reads an incoming correlation ID header (if present)
  • Or generates a new one
  • Attaches it to the request and response
  • Shares it with the event logger

Propagating to other services:

PHP
use Illuminate\Support\Facades\Http;

Http::withEventContext()->post('https://api.other-service.com/data');

withEventContext() makes sure your correlation ID travels with outbound HTTP calls, so you can follow a single logical action through multiple services and their event logs.

6. Human‑readable descriptions for timelines

Event names are intentionally machine‑friendly (user.createdpayment.failed), but you often want something more readable for admins or end‑users.

You can register a formatter via the facade:

PHP
use AyupCreative\EventLog\Facades\EventLog;

public function boot()
{
    EventLog::formatEventsWith(function ($eventLog) {
        return match ($eventLog->event) {
            'user.created'    => "User {$eventLog->subject->name} joined the platform",
            'payment.failed'  => "Payment failed: {$eventLog->meta->error_reason}",
            default           => $eventLog->event,
        };
    });
}

For config caching and cleaner boot code you can move that into a dedicated class:

PHP
namespace App\Support;

class MyEventFormatter
{
    public function __invoke($eventLog)
    {
        return "Action: " . $eventLog->event;
    }
}

And register it in config/event-log.php:

PHP
'event_formatter' => \App\Support\MyEventFormatter::class,

Once a formatter is registered, you can use:

PHP
$eventLog = EventLog::getFor($user)->first();
echo $eventLog->description;

Perfect for building timelines or audit views.

Querying the event log

The package exposes helpers for building timelines around any model.

PHP
use AyupCreative\EventLog\Facades\EventLog;

// Unified timeline where $organisation is either subject or related
$events = EventLog::getFor($organisation);

foreach ($events as $log) {
    echo "{$log->description} caused by {$log->causerLabel()}";
    echo $log->meta->error_reason ?? '';
}

// Paginated version
$paginated = EventLog::getForPaginated($organisation);

The EventLog model provides:

  • causerLabel() – a friendly label based on causer type
  • meta – a convenient accessor for metadata as a small object/collection:
PHP
echo $log->meta->error_reason;

You still have access to the raw metadata relationship if you want full control.

Advanced behaviours

  • Idempotency
    A deterministic idempotency_key per event, plus a database uniqueness constraint, ensures queue retries don’t produce duplicate log records.
  • OpenTelemetry bridge
    If you have open-telemetry/opentelemetry installed, the logger can automatically create spans for events and tie them into your existing traces using the correlation ID.
  • Test suite
Bash
composer test
# or
vendor/bin/phpunit

The package ships with a full test suite verifying async persistence, idempotency, and relational behaviours.


Posted

Tags: