Create a Request ID handler for Symfony with Messenger support
In this post, we'll look at how to add a unique request ID to a Symfony application, using Monolog and Symfony Messenger.
It can be useful if you want to be able to follow logs in a requests and async messages generated by a request with an unique request ID.
We're going to do it step by step. This tutorial assumes that you're familiar with Symfony and Monolog.
Step 1: Install necessary dependencies
If you want to use an UUID, you can require the Symfony UID library for generating unique request IDs. To install it, run:
composer require symfony/uid
Step 2: Create the RequestId Generator service
First, you'll create a service that generates unique request IDs. Create a new interface and a service class:
<?php
// src/Request/RequestIdGeneratorInterface.php
namespace App\Request;
interface RequestIdGeneratorInterface
{
public function generate(): string;
}
<?php
// src/Request/RequestUuidGenerator.php
namespace App\Request;
use Symfony\Component\Uid\Uuid;
class RequestUuidGenerator implements RequestIdGeneratorInterface
{
public function generate(): string
{
return Uuid::v4()->toRfc4122(); // Or bin2hex(random_bytes(16)) or something else.
}
}
This service will generate a unique request ID using the UUID v4 standard.
Step 3: Create a RequestId middleware
Now you'll create a middleware that sets the request ID on the request and response. This middleware will call the RequestUuidGenerator service to generate an ID if one is not already set.
<?php
// src/Request/Listener/RequestIdSubscriber.php
namespace App\Request\Listener;
use App\Request\RequestIdGeneratorInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class RequestIdSubscriber implements EventSubscriberInterface
{
public function __construct(
private RequestIdGeneratorInterface $requestIdGenerator
) {
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['onKernelRequest'],
KernelEvents::RESPONSE => ['onKernelResponse'],
];
}
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
if (! $request->headers->has('X-Request-ID')) {
$request->headers->set('X-Request-ID', $this->requestIdGenerator->generate());
}
}
public function onKernelResponse(ResponseEvent $event)
{
$response = $event->getResponse();
$request = $event->getRequest();
if (! $response->headers->has('X-Request-ID') && $request->headers->has('X-Request-ID')) {
$response->headers->set('X-Request-ID', $request->headers->get('X-Request-ID'));
}
}
}
This middleware will listen to the kernel.request and kernel.response events, and set a X-Request-ID
header in both the request and response if one is not already present.
Step 5: Create Monolog Processor
You'll need to create a Monolog Processor that will add the request ID to every log record:
<?php
// src/Monolog/RequestIdProcessor.php
namespace App\Monolog;
use Monolog\Attribute\AsMonologProcessor;
use Monolog\LogRecord;
use Symfony\Component\HttpFoundation\RequestStack;
#[AsMonologProcessor]
class RequestIdProcessor implements \Monolog\Processor\ProcessorInterface
{
public function __construct(
private RequestStack $requestStack
) {
}
public function __invoke(LogRecord $record): LogRecord
{
$request = $this->requestStack->getCurrentRequest();
if ($request && $request->headers->has('X-Request-ID')) {
$record->extra['request_id'] = $request->headers->get('X-Request-ID');
}
return $record;
}
}
Step 6: Handle Symfony Messenger
For Symfony Messenger, you'll need to pass the request ID through the Envelope and read it in a middleware. This involves updating your Messenger configuration and creating a new middleware.
<?php
// src/Messenger/RequestIdMiddleware.php
namespace App\Messenger;
use App\Messenger\Stamp\RequestIdStamp;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp;
class RequestIdMiddleware implements MiddlewareInterface
{
public function __construct(
private RequestStack $requestStack
) {
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$request = $this->requestStack->getCurrentRequest();
if (! $envelope->last(ConsumedByWorkerStamp::class) && $request && $request->headers->has('X-Request-ID')) {
$envelope = $envelope->with(new RequestIdStamp($request->headers->get('X-Request-ID')));
}
return $stack->next()->handle($envelope, $stack);
}
}
Also, you need to create RequestIdStamp:
<?php
// src/Messenger/RequestIdStamp.php
namespace App\Messenger\Stamp;
use Symfony\Component\Messenger\Stamp\StampInterface;
class RequestIdStamp implements StampInterface
{
public function __construct(
private string $requestId
) {
}
public function getRequestId(): string
{
return $this->requestId;
}
}
Register the middleware in messenger.yaml
:
# config/packages/messenger.yaml
framework:
messenger:
buses:
command.bus:
middleware:
# service ids that implement Symfony\Component\Messenger\Middleware\MiddlewareInterface
- 'App\Messenger\RequestIdMiddleware'
Now, all your requests and Symfony Messenger messages will have a unique request ID that will be logged with Monolog. You can use this request ID to follow logs for specific requests and messages.
Let's continue with handling the RequestIdStamp in the messenger's consumer and keeping the request-id through to the logs.
First, we need to create a new processor for Monolog to pull the request-id from the envelope's stamps.
Step 7: Create a Monolog Processor for the Messenger's Consumer
This processor will get the request-id from the RequestIdStamp when a message is being consumed.
<?php
// src/Monolog/MessengerRequestIdProcessor.php
namespace App\Monolog;
use App\Messenger\Stamp\RequestIdStamp;
use Monolog\Attribute\AsMonologProcessor;
use Monolog\LogRecord;
#[AsMonologProcessor]
class MessengerRequestIdProcessor
{
private ?string $requestId = null;
public function setStamp(?RequestIdStamp $stamp): void
{
$this->requestId = $stamp?->getRequestId();
}
public function __invoke(LogRecord $record): LogRecord
{
if ($this->requestId !== null) {
$record->extra['request_id'] = $this->requestId;
}
return $record;
}
}
Step 8: Update the Messenger Middleware
Update the RequestIdMiddleware to use MessengerRequestIdProcessor and set the request-id from the envelope's stamps.
<?php
// src/Messenger/RequestIdMiddleware.php
namespace App\Messenger;
use App\Messenger\Stamp\RequestIdStamp;
use App\Monolog\MessengerRequestIdProcessor;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp;
class RequestIdMiddleware implements MiddlewareInterface
{
private ?RequestIdStamp $currentRequestIdStamp = null;
public function __construct(
private RequestStack $requestStack,
private MessengerRequestIdProcessor $messengerRequestIdProcessor
) {
}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
if ($stamp = $envelope->last(RequestIdStamp::class)) {
$this->messengerRequestIdProcessor->setStamp($stamp);
$this->currentRequestIdStamp = $stamp;
try {
return $stack->next()->handle($envelope, $stack);
} finally {
$this->messengerRequestIdProcessor->setStamp(null);
$this->currentRequestIdStamp = null;
}
}
$request = $this->requestStack->getCurrentRequest();
if (! $envelope->last(ConsumedByWorkerStamp::class) && $request && $request->headers->has('X-Request-ID')) {
$envelope = $envelope->with(new RequestIdStamp($request->headers->get('X-Request-ID')));
} elseif (! $envelope->last(ConsumedByWorkerStamp::class) && $this->currentRequestIdStamp !== null) {
$envelope = $envelope->with($this->currentRequestIdStamp);
}
return $stack->next()->handle($envelope, $stack);
}
}
With these updates, your logs should now include the request-id for messages consumed by the messenger's worker. This request-id will persist from the original HTTP request that dispatched the message, through the messenger's dispatch process, and finally into the worker that consumes the message.
You can find a Symfony 6.3 app with this code and tests available here: https://github.com/kayneth/symfony-messenger-request-id-demo.