Laravel error logging with Sentry

29th June 2019

Laravel error logging with Sentry

Introduction

I'm going to assume you're already familiar with the Laravel PHP framework and the basics of error logging. Whilst the framework does come with plenty of options out the box for how and where to log errors, if you're using one of the simple file or system based channels (as defined via your config/logging.php / .env files), you could be doing something better.

Wouldn't it be great to get an instant alert when there is a problem with your web app? If you work in a team, surely it would be even better for that alert to go straight to the right person to resolve the issue?

Are you fed up reading through long log files to try and trace the cause of an issue? Imagine being able to browse your complete history of errors from a user-friendly web based UI with all the supporting data you need.

This is where Sentry comes in. It's a cloud based error tracking tool (with free and paid plans) - visit the website to learn more about the full set of features. One of which is the fact it's super easy to integrate with Laravel, so let's get started:

Installation

I'm assuming you're using Laravel 5.6 (which supports various log channels) or above and that you are using package discovery. At the time of writing, everything also works in the current latest version (5.8).

First up, you'll of course need to register for an account with Sentry (the free plan will be enough to get you started) and create a project. This is an easy process, and there is even an option for a Laravel project (which tailors their documentation to your project). Once you've done that, come back here!

You'll need to install their Laravel package to your project via Composer:

composer require sentry/sentry-laravel

Next up, we'll need up publish the Sentry Laravel config file by running:

php artisan vendor:publish --provider="Sentry\Laravel\ServiceProvider"

When you created your project in Sentry, you will have been given a DSN string (this is like an API key), if you haven't already copied it, you should be able to find it in the settings area. You'll need to add that to your .env file e.g:

SENTRY_LARAVEL_DSN=https://xxxxx@sentry.io/0000000

Whilst the current Sentry Laravel documentation recommends starting by hooking into Laravel to capture errors via the Exception handler, there is a better way - using log channels. To start, you'll need to add a new channel in your config/logging.php file:

'channels' => [
    // ...
    'sentry' => [
        'driver' => 'sentry',
        'level'  => null,
        'bubble' => true,
    ],
],

Aside from "driver", you'll notice two other options in the channel config.

There first, "level", allows you to control the minimum error level that will be sent to Sentry. Laravel uses Monolog for handling errors, so you can take a look at the available levels in their documentation.

The second, "bubble", is useful when working with the stack driver to allow / prevent errors being reported to other channels after being handled by Sentry.

Configuring channels

You'll now need to configure your application to use the new "sentry" error logging channel. The first option (called "default") in your config/logging.php file defines which logging channel is used, and is probably pointing to the LOG_CHANNEL option in your .env file. You can easily change this to sentry (LOG_CHANNEL=sentry) and all your errors will now be logged with Sentry.

However, I like to use the "stack" channel, which allows errors to be logged with multiple channels. I usually like to use both the "daily" and "sentry" channels in my stack. This provides some redundancy - by logging to a local file first, errors won't be totally lost if there is ever an issue reporting to Sentry. To follow my approach, you should update your .env file to contain:

LOG_CHANNEL=stack

And your config/logging.php should contain:

'default' => env('LOG_CHANNEL', 'stack'),

// ...

'channels' => [
    'stack' => [
    'driver'   => 'stack',
    'channels' => ['daily', 'sentry']
],
'daily' => [
    'driver' => 'daily',
    'path'   => storage_path('logs/laravel.log'),
    'level'  => 'debug',
    'days'   => 28,
],
'sentry' => [
    'driver' => 'sentry',
    'level'  => 'debug',
    'bubble' => true,
],

// ...

Don't forget to take a read over the Laravel logging documentation to fully understand everything that is happening above, and to see the full range of configuration options.

Time for a test

The Sentry Laravel package comes with a handy artisan command for testing your integration:

php artisan sentry:test

After you run this, you should receive an email alert (depending on your settings in Sentry) and be able to see it in your Sentry dashboard.

You can also test it by throwing an exception of your own. For example, you can add a simple route closure to your routes/web.php file, then visit it in your browser:

Route::get('/debug-sentry', function () {
    throw new \Exception("My test Exception");
});

Again, you should receive an email alert and see the exception logged in your Sentry dashboard.

Adding user context

As you've probably seen, Sentry provides a lot of detailed information for errors including a full stack trace and some environment details. But imagine knowing which errors have occurred for which users. You could reach out to them to let the problem has been fixed, even if they didn't report the issue.

To tell Sentry which user was active at the time, you'll need to create some custom middleware:

php artisan make:middleware SentryUserContext

And then add the following to the newly created app/Http/Middleware/SentryUserContext.php file:

namespace App\Http\Middleware;

use Auth;
use Closure;
use Sentry\State\Scope;

class SentryUserContext
{
   /**
    * Handle an incoming request.
    *
    * @param \Illuminate\Http\Request $request
    * @param \Closure                 $next
    *
    * @return mixed
    */
    public function handle($request, Closure $next)
    {
        if(Auth::check() && app()->bound('sentry')) {
            \Sentry\configureScope(function (Scope $scope): void {
                $scope->setUser([
                    'id'    => Auth::user()->id,
                    'email' => Auth::user()->email,
                    'name'  => Auth::user()->name,
                ]);
            });
        }

        return $next($request);
    }
}

You'll then need to register the middleware against all of your routes. The easiest way to do this is to add it to the relevant sections (e.g. "web" and "api") in the $middlewareGroups property in yourapp/Http/Kernel.php file. For example:

protected $middlewareGroups = [
    'web' => [
        // ...
        \App\Http\Middleware\SentryUserContext::class
    ]
]

There are several other ways of registering middleware, so check the Laravel documentation for middleware to see if there is a better approach for your particular application.

Let's look back and explain what's happening in the middleware - it's pretty easy. First off we check that there is a user authenticated (in the default guard) and that Sentry is available from the container (this should have been done automatically through package discovery).

Then, we simply pass the details of the current user to Sentry - giving the ID, email and name. This array could be tailored to include other useful info relevant to your app (e.g. subscription package).

Now when an error is logged with Sentry and a user is authenticated with you app, you should see the user details displayed with the error details in your Sentry dashboard. There are more tools available in the dashboard, such us filtering your error list by user to see which errors they've experienced, but I'll leave that for you to discover.

Further reading

Hopefully that's enough to get you started with a better error logging setup for your Laravel application. There is so much more that Sentry can do for you (release tracking, detailed control over notifications, removing sensitive data captured by Sentry (known as data scrubbing) etc) - check out the documentation for more details.

If you need help with anything I've mentioned - feel free to get in contact.