Skip to content

Latest commit

 

History

History
419 lines (314 loc) · 13.9 KB

File metadata and controls

419 lines (314 loc) · 13.9 KB

Audit Transports Documentation

AuditTrailBundle supports multiple transports to dispatch audit logs. This allows you to store logs locally, publish them to external services, or fan them out to several destinations at once.

In v3, the transport pipeline is strongly typed. Custom transports receive a read-only AuditTransportContext instead of the older stringly array $context payload.

Note

Messenger Confusion Warning: The bundle utilizes Symfony Messenger for two different features.

  1. database: { async: true }: Designed to save logs to your local database asynchronously via an internal worker.
  2. queue: { enabled: true }: Designed to publish logs to an external system (so other microservices or tools like ELK can consume them).

They can be used simultaneously because they target different Messenger message types and transport names. [!NOTE] symfony/messenger and symfony/http-client are optional integrations. Install only what your chosen transport needs:

  • symfony/messenger for queue delivery or async database persistence
  • symfony/http-client for HTTP delivery

If you enable a transport without its package installed, the bundle fails fast with a clear LogicException during container build. You still need the corresponding runtime configuration: Messenger routing/buses for queue or async database, and an endpoint for HTTP transport.

Install Matrix

Use the base bundle by itself if you only need the synchronous database transport.

Feature Extra package Extra Symfony config
Synchronous database transport none none
Async database transport symfony/messenger audit_trail_database Messenger transport
Queue transport symfony/messenger audit_trail Messenger transport/routing
HTTP transport symfony/http-client HTTP endpoint config

Transport Contract

All transport implementations now use the same typed contract:

<?php

declare(strict_types=1);

use Rcsofttech\AuditTrailBundle\Contract\AuditTransportInterface;
use Rcsofttech\AuditTrailBundle\Transport\AuditTransportContext;

final class AppAuditTransport implements AuditTransportInterface
{
    public function send(AuditTransportContext $context): void
    {
        // $context->phase
        // $context->entityManager
        // $context->unitOfWork
        // $context->entity
        // $context->audit
    }

    public function supports(AuditTransportContext $context): bool
    {
        return true;
    }
}

AuditTransportContext contains:

  • phase: the current AuditPhase
  • entityManager: the active Doctrine entity manager for this audit flow
  • audit: the current AuditLog
  • unitOfWork: the active UnitOfWork when the phase has one
  • entity: the source entity when it is available

The AuditTransportContext instance passed to a transport is read-only and is not mutated in place. Internally the dispatcher may create a new context with withAudit() after listeners mutate or swap the AuditLog, but transport implementations should treat the context they receive as read-only input.

1. Database Transport (Async)

By default, the database transport persists logs synchronously using phase-appropriate strategies:

  • during onFlush, ORM-safe audit rows are attached to the current UnitOfWork
  • during deferred phases such as postFlush, database audit rows are written without re-entering Doctrine flush()

For high-traffic applications, you can offload this database write to a background worker.

Configuration

Enable async mode in config/packages/audit_trail.yaml:

audit_trail:
    transports:
        database:
            enabled: true
            async: true # Offloads inserts to Messenger

You must explicitly define a transport named audit_trail_database in config/packages/messenger.yaml:

framework:
    messenger:
        transports:
            # Internal bundle worker will consume from this transport
            audit_trail_database: '%env(MESSENGER_TRANSPORT_DSN)%'

If symfony/messenger is not installed and you enable database.async: true, the bundle throws:

To use async database transport, you must install the symfony/messenger package.

(The bundle auto-registers PersistAuditLogHandler to consume from this transport and insert the records into the database).

Important

Async database mode uses a per-message delivery identifier so worker retries can be handled idempotently. If you enable or upgrade to this feature, generate and run a Doctrine migration so the audit_log.delivery_id column and unique constraint exist before workers process messages.


2. Queue Transport (External Delivery)

The queue transport dispatches a strictly-typed DTO (AuditLogMessage) to a Symfony Messenger bus. You must define the Messenger transport routing yourself and provide the downstream consumer that ingests those messages.

If symfony/messenger is not installed and you enable queue, the bundle throws:

To use the Queue transport, you must install the symfony/messenger package.

Configuration for Queue transport

Enable the queue transport in config/packages/audit_trail.yaml:

audit_trail:
    transports:
        queue:
            enabled: true
            bus: 'messenger.bus.default' # Optional: specify a custom bus

You must define a transport named audit_trail in config/packages/messenger.yaml:

framework:
    messenger:
        transports:
            # Your external service/worker will consume from this transport
            audit_trail: '%env(MESSENGER_TRANSPORT_DSN)%'

Advanced Usage: Messenger Stamps

Use the AuditMessageStampEvent to add stamps to the message right before it is dispatched to the bus. This is the supported way to add transport-specific stamps such as DelayStamp or DispatchAfterCurrentBusStamp.

<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use Rcsofttech\AuditTrailBundle\Event\AuditMessageStampEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Stamp\DelayStamp;

class AuditMessengerSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            AuditMessageStampEvent::class => 'onAuditMessageStamp',
        ];
    }

    public function onAuditMessageStamp(AuditMessageStampEvent $event): void
    {
        // Add a 5-second delay to all audit logs sent via Queue
        $event->addStamp(new DelayStamp(5000));
    }
}

Queue Payload Signing

When audit_trail.integrity.enabled is true, the bundle automatically signs the payload to ensure authenticity. The signature is added as a SignatureStamp to the Messenger envelope.

<?php

declare(strict_types=1);

use Rcsofttech\AuditTrailBundle\Message\Stamp\SignatureStamp;

// In your message handler
$signature = $envelope->last(SignatureStamp::class)?->signature;

3. HTTP Transport

The HTTP transport streams audit logs to an external API endpoint (e.g., a logging service, ELK, or Splunk).

HTTP delivery is synchronous when it runs, but it is phase-limited: the built-in HTTP transport only supports deferred phases such as postFlush and postLoad. Setting defer_transport_until_commit: false does not move HTTP delivery into Doctrine's onFlush transaction window.

If symfony/http-client is not installed and you enable http, the bundle throws:

To use the HTTP transport, you must install the symfony/http-client package.

Configuration for http transport

audit_trail:
    transports:
        http:
            enabled: true
            endpoint: 'https://audit-api.example.com/v1/logs'
            headers:
                'Authorization': 'Bearer your-api-token'
                'X-App-Name': 'MySymfonyApp'
            timeout: 10 # seconds

HTTP Payload Signing

When audit_trail.integrity.enabled is true, the bundle adds an X-Signature header to the HTTP request. This header contains the HMAC signature of the JSON body.

POST /api/logs HTTP/1.1
X-Signature: a1b2c3d4...
Content-Type: application/json

{ ... }

Payload Structure

Different transports send slightly different JSON payloads based on their delivery target.

HTTP Transport

The HTTP transport sends a flat JSON object including the entity signature and the persisted audit context captured on the AuditLog.

{
    "entity_class": "App\\Entity\\Product",
    "entity_id": "123",
    "action": "update",
    "old_values": {"price": 100},
    "new_values": {"price": 120},
    "changed_fields": ["price"],
    "user_id": "1",
    "username": "admin",
    "ip_address": "127.0.0.1",
    "user_agent": "Mozilla/5.0...",
    "transaction_hash": "a1b2c3d4...",
    "signature": "hmac-signature-here",
    "context": {
        "impersonation": {
            "impersonator_id": "99",
            "impersonator_username": "admin"
        },
        "runtime_meta": "value"
    },
    "created_at": "2024-01-01T12:00:00+00:00"
}

Queue Transport (AuditLogMessage)

The Queue transport dispatches a strictly-typed DTO. It omits the entity signature from the body and carries transport signing separately through Messenger metadata (SignatureStamp).

{
    "entity_class": "App\\Entity\\Product",
    "entity_id": "123",
    "action": "update",
    "old_values": {"price": 100},
    "new_values": {"price": 120},
    "changed_fields": ["price"],
    "user_id": "1",
    "username": "admin",
    "ip_address": "127.0.0.1",
    "user_agent": "Mozilla/5.0...",
    "transaction_hash": "a1b2c3d4...",
    "created_at": "2024-01-01T12:00:00+00:00",
    "context": {
        "impersonation": {
            "impersonator_id": "99",
            "impersonator_username": "admin"
        }
    }
}

4. Database Transport (Default)

The Database transport stores logs in your local database. It is enabled by default.

Sync Mode (Default)

Logs are persisted directly during the Doctrine lifecycle using the current phase:

audit_trail:
    transports:
        database:
            enabled: true
            async: false

Behavior summary:

  • onFlush: the bundle uses persist() plus UnitOfWork::computeChangeSet() so the audit row joins the current flush safely
  • deferred phases such as postFlush and postLoad: the bundle resolves the final entity identifier if needed and inserts the audit row through a dedicated database writer

This avoids nested flush() calls from Doctrine event listeners while preserving immediate local persistence for database-backed audits.

Note

Deferred database writes are not ORM-managed AuditLog persistence operations. If your application attaches Doctrine lifecycle listeners/subscribers specifically to AuditLog, those hooks are only relevant to the in-UnitOfWork ORM path, not the deferred direct-write path.

Async Mode

Logs are dispatched via Symfony Messenger and persisted by a built-in handler (PersistAuditLogHandler). This is useful for high-traffic applications where you want to offload DB writes to a worker.

audit_trail:
    transports:
        database:
            enabled: true
            async: true

You must define a transport named audit_trail_database in config/packages/messenger.yaml:

framework:
    messenger:
        transports:
            audit_trail_database: '%env(MESSENGER_TRANSPORT_DSN)%'

Worker retries are handled safely: duplicate deliveries for the same internal message are ignored instead of creating duplicate audit rows.


5. Chain Transport (Multiple Transports)

You can enable multiple transports simultaneously. The bundle will automatically use a ChainAuditTransport to dispatch logs to all enabled transports.

audit_trail:
    transports:
        database:
            enabled: true
        queue:
            enabled: true

ChainAuditTransport is fail-fast by design. If one child transport throws, later transports in the chain are not executed and the exception is allowed to bubble back to the dispatcher. This keeps transport failure semantics explicit and lets the bundle apply fail_on_transport_error and fallback rules in one place instead of partially succeeding silently.

Fail-fast does not mean transactional rollback across transports. If an earlier transport has already produced a side effect, the chain does not undo that side effect when a later transport fails.

Audit deliveries now carry an internal deliveryId so the bundle's database-backed writes are idempotent across fallback and retry paths. This prevents duplicate local audit rows when an earlier database-capable transport succeeds and a later transport fails in the same chain. It does not roll back or deduplicate side effects performed by external systems.

Transport order is fixed by the bundle, not by YAML key order. When multiple transports are enabled, they are registered in this order:

  1. database
  2. http
  3. queue

That means:

  • a database transport runs before HTTP or queue when it supports the current phase
  • an HTTP transport failure prevents later queue delivery in the same chain execution
  • swapping YAML key order does not change execution order

6. Signature vs. Payload Signing

It is important to distinguish between the two types of signatures:

  1. Entity Signature (signature field in JSON):

    • Generated at the moment of creation.
    • Signs the business data (Entity Class, ID, Changes).
    • Purpose: Long-term data integrity and non-repudiation. Stored in the database.
  2. Transport Signature (X-Signature header or SignatureStamp):

    • Generated just before sending.
    • Signs the outbound transport payload.
    • Purpose: Transport security. Ensures the message wasn't tampered with during transit.