Create a live logs dashboard for Laravel

Introduction

In this tutorial, we’ll make use of these concepts to display our Laravel logs on a dashboard in realtime using Pusher Channels.

In an earlier tutorial, we looked at how to use the new log stacks and channels introduced in Laravel 5.6.

Prerequisites

Setting up

We’ll start off with an existing project so we can observe how the logs work. We’ll use a small stock application I built. You can clone the project from GitHub by running:

    git clone https://github.com/shalvah/stockt.git realtime-laravel-logs

You can also download the source directly from this link.

Then cd into the project folder and install dependencies:

    composer install

Next, copy the .env.example to a new file called .env. Run the following command to generate an application encryption key:

    php artisan key:generate

Lastly, create a file called database.sqlite in the database directory and run the following command to set up and populate the database:

    php artisan migrate --seed

Configuring the Pusher handler

Run the following command to add the monolog-pusher package:

    composer require shalvah/monolog-pusher

This package will be doing the heavy lifting. Laravel’s logging system is powered by Monolog, which uses handlers to define where a log gets sent to. For instance, the FileHandler sends logs to a file, the EmailHandler sends logs to a specified email address, and so forth. You can view a list of available handlers here. This package provides us with a PusherHandler which sends logs to a Pusher channel.

We’re going to configure Laravel’s log component to use Monolog, with the PusherHandler as its handler. To do this, we’ll register a new channel that uses the monolog driver. Open up your config/logging.php and add the following to the channels array:

1// config/logging.php
2    
3    
4    'realtime' => [
5        'driver' => 'monolog',
6        'handler' => \Shalvah\MonologPusher\PusherHandler::class,
7        'with' => [
8            'pusher' => [
9                env('PUSHER_APP_KEY'),
10                env('PUSHER_APP_SECRET'),
11                env('PUSHER_APP_ID'),
12                [
13                    'cluster' => env('PUSHER_APP_CLUSTER')
14                ]
15            ],
16            'level' => \Monolog\Logger::DEBUG
17        ],
18    ],

NOTE: “realtime” is just an arbitrary name we chose for our log channel. We could equally use “chimichanga”.

The configuration is pretty easy to understand. We specify the driver as monolog, and the handler as the PusherHandler. In the with array, we specify the parameters to be passed to the constructor of the handler, which in this case are our Pusher credentials and the minimum log level we want this handler to catch.

You’ll then need to add your Pusher app credentials to the .env file:

1PUSHER_APP_ID=your-app-id
2    PUSHER_APP_KEY=your-app-key
3    PUSHER_APP_SECRET=your-app-secret
4    PUSHER_APP_CLUSTER=your-app-cluster

Viewing logs

We need to add a page to our app where we can view our logs. Let’s add a new route:

1// routes/web.php
2    
3    Route::view('logs', 'logs')->middleware('auth');

This route will render the view logs.blade.php whenever we visit /logs. Create the file resources/views/logs.blade.php with the following content:

1@extends('layouts.app')
2    
3    @section('content')
4        <div class="container">
5            <h2>Logs</h2>
6            <div class="list-group" id="logs">
7            </div>
8        </div>
9    
10        <script src="https://code.jquery.com/jquery-3.3.1.min.js"
11                integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
12                crossorigin="anonymous"></script>
13        <script src="https://js.pusher.com/4.2/pusher.min.js"></script>
14    
15        <script>
16            function displayLog(data) {
17                let $log = $('<div>').addClass('list-group-item')
18                    .html(`<pre><code>${JSON.stringify(data, null, 4)}</code></pre>`);
19                $('#logs').prepend($log);
20            }
21        </script>
22        <script>
23            var socket = new Pusher("your-app-key", {
24                cluster: 'your-app-cluster',
25            });
26            socket.subscribe('{{ config('app.env') }}')
27                .bind('log', displayLog);
28        </script>
29    @endsection

Don’t forget to replace your-app-key and your-app-cluster with their real values.

Here, we subscribe to the Pusher channel and listen for the log event. When there is a new log, we format it and display it on the page.

NOTE: the name of the Laravel log channel (“realtime”) is different from the name of the Monolog log channel. Laravel uses the name of the current app environment (config('app.env')) as the name of the Monolog log channel. This is what also gets used as the Pusher channel.

Now let’s test it out. We’ll add a few logging calls at random places in our app.

Open up the ProductsController and modify the index method so it looks like this:

1// app/Http/Controllers/ProductController.php
2    
3    public function index()
4    {
5        Log::channel('realtime')->info(request()->user()->name.' is viewing all products!');
6        return view('products.index', ['products' => Product::all()]);
7    }
8
9Also, modify the `show` method so it looks like this:
10
11
12    // app/Http/Controllers/ProductController.php
13    
14    public function show()
15    {
16        Log::channel('realtime')->debug(request()->user()->name." is viewing the product with ID {$product->id}!");
17        return view('products.show', ['product' => $product]);
18    }

Don’t forget to import the Log class at the top via use Illuminate\Support\Facades\Log;.

By calling Log::channel('realtime'), we’re explicitly telling Laravel to broadcast this particular log message over the realtime channel we created earlier.

Let’s see our realtime logs in action. Start the app by running php artisan serve.

Log in at http://localhost:8000/login with admin@stockt.test as email and secret as password. Now open up the logs page (http://localhost:8000/logs) in one tab and the products page (http://localhost:8000/products) in another. Try to navigate across a few products. You should see the logs page update similar to this:

laravel-live-logs-demo

Making Pusher our default logger

You’ll notice that every time we wanted to use the realtime logger, we had to call Log::channel('realtime') first. We can get rid of this step by making the realtime logger our default logger. To do this, set the LOG_CHANNEL variable in your .env file to realtime.

    LOG_CHANNEL=realtime

Laravel sometimes caches app config, so you might need to run php artisan config:clear after making a change to your .env file for the change to reflect

Now, in our code snippets above we can simply write:

        Log::info(request()->user()->name." is viewing all products!");

And logs will get sent to Pusher.

Combining loggers using a stack

Supposing we want to see our logs in realtime, but we also want to persist them to a file or database. Laravel 5.6 allows us to do this by using a stack. A stack combines two or more channels into one.

You can create a stack dynamically by using the stack method:

    Log::stack(['single', 'realtime'])->info("Someone is viewing all products!");

NOTE: ‘single’ is the name of the default Laravel channel that writes to a single file, storage/logs/laravel.log

Alternatively, you can configure a stack in your config/logging.php, by adding a new entry in the channels array, this time using the stack driver:

1// config/logging.php
2    
3        'realtime-stack' => [
4            'driver' => 'stack',
5            'channels' => ['single', 'realtime'],
6        ],

Now, when we send log messages to this stack, they will be sent to both Pusher and the regular log file.

Conclusion

In this article, we’ve been able to send our application’s logs in realtime to our frontend using Pusher Channels. This is especially useful in applications where there is a need for realtime monitoring of actions. You can check out the source code of the completed application on GitHub.