Skip to content
Advertisement

Laravel 8 – Listen to eloquent events made during a specific request

Currently, I’m trying to wire a telemetry library (OpenTelemetry php lib) to the Laravel event model. The idea is to forward traces to a third party service. A single trace consistents of a request and all database calls that have been made during the request. I’ve created three middleware components:

  • BeforeRequest: starts a trace
  • OnRequest: call event listener which listened to DB queries (see snippet below) and end trace so that it is send to a third party service

My OnRequest middleware component looks like this:

<?php

namespace AppHttpMiddleware;

use AppHelpersOtelTraceProviderFactory;
use Closure;
use IlluminateDatabaseEventsQueryExecuted;
use IlluminateHttpRequest;
use IlluminateSupportFacadesDB;

class OnRequest
{
     public function handle(Request $request, Closure $next)
     {
         $provider = TraceProviderFactory::getTraceProvider($request->get('context_uuid'));
         DB::listen(function (QueryExecuted $query) use ($provider){
              $provider->startDbSpan($query);
         });
         $provider->endSpans();

         return $next($request);
    }
}

In this case, the DB listen call is skipped over because it is async. How can I make sure that each database query is registered? The execution order is important. Everything needs to be in the right order otherwise the traces aren’t logged properly.

Advertisement

Answer

I would do the finalizing of this in a terminable middleware (after the response has been sent to the client) to allow you to log all the queries made from the time you start listening until the Response is sent. This also won’t hold up returning the response to the client.

...

class OnRequest
{
     protected $provider;

     public function handle(Request $request, Closure $next)
     {
         $this->provider = TraceProviderFactory::getTraceProvider($request->input('context_uuid'));

         DB::listen(function (QueryExecuted $query) {
              $this->provider->startDbSpan($query);
         });

         return $next($request);
    }

    public function terminate($request, $response)
    {
         $this->provider->endSpans();
    }
}

You should make this middleware a singleton so that way it doesn’t use a new instance for the terminable part. (We want that same $provider instance assigned). Register the singleton in a Service Provider:

public function register()
{
    $this->app->singleton(OnRequest::class);
}

You only need one middleware to do everything.

Laravel 8.x Docs – Middleware – Terminable Middleware

User contributions licensed under: CC BY-SA
10 People found this is helpful
Advertisement