Laravel: Generate a unique request ID for each request and put it into logs

Laravel: Generate a unique request ID for each request and put it into logs

ยท

4 min read

Hey guys,

Today I'll share with you a tip that will skyrocket your production debugging workflow ๐Ÿ˜Ž

We're going to generate a unique ID for each request, which we can use the request ID to search for every related log of the given request ID.

Thus, if any issue happens, we can try to find the request ID (or customers give us), and then jump immediately to the logs and do the surgery ๐Ÿ‘€.

Pretty handy, isn't it? ๐Ÿ’ช

Let's jump to the topic & the implementation.

Requirement

You'll need Laravel 11 because we are going to use the Context feature.

If you haven't learned about Context, visit this page of mine:

Why Context?

Laravel's "context" capabilities enable you to capture, retrieve, and share information throughout requests, jobs, and commands executing within your application. This captured information is also included in logs written by your application, giving you deeper insight into the surrounding code execution history that occurred before a log entry was written and allowing you to trace execution flows throughout a distributed system.

Read more at: Laravel Context

In short terms:

  • So if a context presents in the request's lifecycle, Laravel will automatically log it every time we write a log using Log facade

  • Context is only available for each request, and queue job (Octane is covered too). There won't be any conflict or race condition.

Context::add('hello', 'world');
Log::info('Hello'); // you will see the [datetime] Hello { "contexts": { "hello": "world" } } in the log record

Implementation

Create a middleware to generate/accept ID

We'll have 2 scenarios:

  • If there is a X-REQUEST-ID header with a valid ID, we'll continue to use that ID

    • This will help trace ALL REQUESTS of a particular user's lifecycle.

    • I'll use X-REQUEST-ID, you can use any key that you like ๐Ÿฅน.

  • If not, we'll generate a new unique ID

Let's hit the command to create a new middleware:

php artisan make:middleware UseRequestId

And the implementation:

<?php

namespace App\Http\Middleware;

use Illuminate\Support\Str;

class UseRequestId
{
    public function handle(Request $request, Closure $next): Response
    {
        // get existing id or generate new one
        $requestId = $request->header('X-REQUEST-ID')
            ?? (string) Str::ulid();

        // add to context
        Context::add('requestId', $requestId);

        return $next($request);
    }
}

Register the middleware

Since my application is an API service, I'll apply the middleware to the api group.

Open bootstrap/app.php and register it:

use App\Http\Middleware\UseRequestId;


->withMiddleware(function (Middleware $middleware) {
    $middleware->prependToGroup(UseRequestId::class);
})

Using prependToGroup to ensure this middleware will be resolved at the top (global => this middleware => other middleware of api group)

Add a route and try it out ๐Ÿฅฐ

Add a sample API

// api.php

Route::get('/ping', function () {
    Log::info('pong');

    return response()->json([]);
});

Open the log file to check (laravel.log for the basic driver or laravel-{date}.log for daily driver).

We should see something like:

[2024-04-28 13:22:59] local.INFO: Pong  {"requestId":"01HWJDZ12P3CE4JDY3912YSDEA"}

To ensure it will print the same request ID, you can try to log multiple times/multiple files in the same lifecycle. For example:

Log::info('ping');
// do something
Log::info('pong');
// do something more
Log::info('end');

Result:

[2024-04-28 13:26:35] local.INFO: ping  {"requestId":"01HWJE5MD8DQ24DBDN5R8TXK7K"}
[2024-04-28 13:26:35] local.INFO: pong  {"requestId":"01HWJE5MD8DQ24DBDN5R8TXK7K"}
[2024-04-28 13:26:35] local.INFO: end  {"requestId":"01HWJE5MD8DQ24DBDN5R8TXK7K"}

Context already covered & battle-tested from Laravel too ๐Ÿ‘€

Add the ID to the response

Last part, we need to send the ID to the API consumers, so they can send the same ID or for error reporting purposes.

We'll transform a bit of the middleware handle method:

/** @var \Illuminate\Http\Response $response */
$response = $next($request);

return $response->header('X-REQUEST-ID', $requestId);

When hitting the $next, Laravel will process everything and give us the FINAL RESPONSE (after Error/Exception too).

So we can add the ID into the header (or body if you want) ๐Ÿ˜‰

Result

Now our requests are bound with request IDs. With means:

  • Easier for us to search for logs of a given ID ๐Ÿ˜Ž Debugging is much faster & better now.

    • If you use any logging platform, surely it can search wayyy faster than normal text search โค๏ธ.
  • Customers/API consumers can know their "request ID", if it has any issue, they can extract the request ID and report the problem to us ๐Ÿ‘€

  • Additionally, you can record more data in the Context, e.g.:

    • Logged In User ID

    • URL

    • Timezone

    • etc

Conclusion

Well, happy coding & happy debugging guys.

Until the next blog post ๐Ÿ˜Ž. Thanks for reading!

ย