Tech AI Insights

Laravel Reverb: Master Real-Time Log Monitoring With Laravel Reverb, Laravel 11, and Vue.js

In this blog post, we’ll learn how to build a real-time log monitoring app using Laravel 11, Laravel Reverb, and Vue.js. Monitoring application logs in real-time helps developers debug easily and analyze logs efficiently. We will also cover how to simulate log entries dynamically for testing purposes.

Right after this introduction, you’ll find a video demonstrating what we can achieve. So, stay patient and read till the end!

Features

  • Real-time log monitoring
  • Color-coded log levels
  • Custom notifications for new logs
  • Responsive design with Tailwind CSS

Prerequisites

Before we get started, ensure you have the following installed:

  • PHP >= 8.1
  • Node.js >= 16
  • Composer
  • Laravel 11
  • MySQL >= 8.0 or Sqlite3
  • Laravel Reverb Server
  • Vue.js 3.x
  • Inertia.js
  • Tailwind CSS

Step 1: Install Laravel Reverb

Laravel Reverb is built into Laravel, but you need to install and configure it.For detailed installation steps, check out my other blog post, where I explain:

  • How to install Laravel Reverb
  • How to configure it
  • What changes are needed in the .env file

Read that post before proceeding further! ✅

Step 2: Configure the Database

After Configure Reverb and Before running migrations, update your .env file with your database credentials:

DB_CONNECTION=mysql  
DB_HOST=127.0.0.1  
DB_PORT=3306  
DB_DATABASE=laravel_reverb  
DB_USERNAME=your_username  
DB_PASSWORD=your_password 

Clean config cache

php artisan config:clear

Step 3: Set Up the Logging System

Create LogHistory Model & Migration

php artisan make:model LogHistory -m  

Open app/Models/LogHistory.php and modify it.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class LogHistory extends Model
{
    protected $fillable = ['level', 'message', 'context', 'timestamp'];

    protected $casts = [
        'context' => 'array',
    ];
}


Open database/migrations/xxxx_xx_xx_create_log_histories_table.php and modify the up function it.


    public function up(): void
    {
        Schema::create('log_histories', function (Blueprint $table) {
            $table->id();
            $table->string('level')->nullable();
            $table->text('message')->nullable();
            $table->text('context')->nullable();
            $table->timestamp('timestamp')->nullable();
            $table->timestamps();
        });
    }

Run the Migration

php artisan migrate  

Step 4: Create a Custom Log Tap Class

Laravel needs a way to intercept logs and send them to the frontend in real time.

Create RealtimeLogTap.phpFile

mkdir -p app/Logging
touch app/Logging/RealtimeLogTap.php

Open app/Logging/RealtimeLogTap.php and modify it.

<?php

/**
 * Class RealtimeLogTap
 *
 * This class is responsible for tapping into the Laravel logging system and processing log records in real-time.
 * It pushes a custom processor to the Monolog instance used by Laravel's logger.
 *
 * @package App\Logging
 */

namespace App\Logging;

use App\Models\LogHistory;
use Illuminate\Container\Attributes\Log;

class RealtimeLogTap
{
    /**
     * Invoke method to attach the custom log processor.
     *
     * @param \Illuminate\Log\Logger|null $logger The Laravel logger instance.
     * @return void
     */

    public function __invoke($logger = null)
    {
        $monolog = $logger->getLogger();
        $monolog->pushProcessor(function ($record) {
            $data = [
                'level' =>  $record->level->name,
                'message' => $record->message,
                'context' => json_encode($record->context),
                'timestamp' => $record->datetime->format('Y-m-d H:i:s'),
            ];
            try {
                LogHistory::create($data);
                return $record;
            } catch (\Exception $e) {
                \Log::error($e->getMessage());
                return $record;
            }
        });
    }
}

Update config/logging.php to Use the Tap. Modify the default log channel (stack) to use RealtimeLogTap.

'stack' => [
   'driver' => 'stack',
   'channels' => explode(',', env('LOG_STACK', 'single')),
   'ignore_exceptions' => false,
   'tap' => [App\Logging\RealtimeLogTap::class],
],

✅ This modifies the default logging behavior without needing a custom channel!

Step 5: Create a Custom Artisan Command to Simulate Logs

We’ll create a custom command to generate different log levels dynamically for testing.

Create the Command

php artisan make:command SimulateLogCommand

Open app/Console/Commands/SimulateLogCommand.php and modify it.

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Faker\Factory as Faker;

class SimulateLogCommand extends Command
{
    /**
     * The name and signature of the console command.
     * 
     */
    protected $signature = 'log:simulate {level? : The log level (debug, info, warning, error, critical, etc.)}';

    /**
     * The console command description.
     */
    protected $description = 'Simulate realistic, randomized Laravel logs for testing';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $faker = Faker::create();
        $logLevels = $this->getLogLevels();
        $level = $this->argument('level') ?? array_rand($logLevels); // Use provided level or random

        if (!isset($logLevels[$level])) {
            $this->error("Invalid log level '{$level}'. Available levels: " . implode(', ', array_keys($logLevels)));
            return;
        }

        // Generate a truly random log
        $logData = $this->generateRandomLogData($level, $faker);
        $logLevels[$level]($logData['message'], $logData['context']);

        $this->info("✅ [{$level}] Log generated: ");
    }

    /**
     * Generate a fully randomized log message and context.
     */
    private function generateRandomLogData($level, $faker)
    {
        $commonData = [
            'request_id' => Str::uuid(),
            'timestamp' => now()->format('Y-m-d H:i:s'),
            'user_id' => $faker->randomNumber(3),
            'ip_address' => $faker->ipv4(),
        ];

        $scenarios = [
            'info' => [
                "User {$faker->name} successfully logged in.",
                "New order placed (Order ID: {$faker->randomNumber(5)}).",
                "Profile updated for user {$faker->name}.",
            ],
            'warning' => [
                "User attempted to access a restricted page: {$faker->url}.",
                "Slow response detected on API call to {$faker->domainName}.",
                "Suspicious login attempt from IP {$faker->ipv4}.",
            ],
            'error' => [
                "Database query failed: {$faker->sentence}.",
                "Payment failed for Order #{$faker->randomNumber(5)}: {$faker->sentence}.",
                "API response error from {$faker->domainName}: {$faker->word}.",
            ],
            'critical' => [
                "Server crash detected! Service unavailable at {$faker->time}.",
                "Data corruption detected in table `{$faker->word}`.",
                "Memory limit exceeded while processing {$faker->randomElement(['invoices', 'transactions', 'files'])}.",
            ],
            'alert' => [
                "High CPU usage detected on server {$faker->ipv4}.",
                "Unauthorized admin access attempt detected from {$faker->ipv4}.",
                "Email sending failed to {$faker->email}.",
            ],
            'emergency' => [
                "System is out of disk space! Available: {$faker->randomDigitNotNull()}MB.",
                "Critical security breach detected at " . $faker->dateTimeThisYear->format('Y-m-d H:i:s') . ".",
                "Application is shutting down unexpectedly!",
            ],
        ];

        return array_merge($commonData, [
            'message' => $faker->randomElement($scenarios[$level] ?? ["Unhandled log event occurred."]),
            'context' => $this->generateRandomContext($level, $faker),
        ]);
    }

    /**
     * Generate a dynamic and unpredictable context for logs.
     */
    private function generateRandomContext($level, $faker)
    {
        $contexts = [
            'user' => [
                'id' => $faker->randomNumber(3),
                'name' => $faker->name,
                'email' => $faker->email,
            ],
            'request' => [
                'url' => $faker->url,
                'method' => $faker->randomElement(['GET', 'POST', 'PUT', 'DELETE']),
                'status' => $faker->randomElement([200, 201, 400, 403, 500]),
            ],
            'database' => [
                'query' => "SELECT * FROM {$faker->word} WHERE id = {$faker->randomNumber(2)}",
                'error' => $faker->sentence,
            ],
            'server' => [
                'cpu_usage' => $faker->randomFloat(2, 10, 95) . '%',
                'memory' => $faker->randomFloat(2, 1, 32) . 'GB used',
            ],
        ];

        return $faker->randomElement($contexts);
    }

    /**
     * Get available log levels and their respective logging functions.
     */
    private function getLogLevels(): array
    {
        return [
            'debug' => fn($msg, $data) => Log::debug($msg, $data),
            'info' => fn($msg, $data) => Log::info($msg, $data),
            'notice' => fn($msg, $data) => Log::notice($msg, $data),
            'warning' => fn($msg, $data) => Log::warning($msg, $data),
            'error' => fn($msg, $data) => Log::error($msg, $data),
            'critical' => fn($msg, $data) => Log::critical($msg, $data),
            'alert' => fn($msg, $data) => Log::alert($msg, $data),
            'emergency' => fn($msg, $data) => Log::emergency($msg, $data),
        ];
    }
}

Step 6: Create a Log Event for Broadcasting

We need to broadcast logs whenever a new log is created.

Create an Event

php artisan make:event LogDispatched

Open app/Events/LogDispatched.php and modify it.

<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class LogDispatched implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public $logHistroy;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct($data)
    {
        $this->logHistroy = $data;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn(): array
    {
        return [
            new Channel('logs'),
        ];
    }

    public function broadcastAs()
    {
        return 'logEvent';
    }
}

Step 7: Trigger the Event When a Log is Created

Modify the LogHistory model to dispatch the event when a log is created.

Update app/Models/LogHistory.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class LogHistory extends Model
{
    protected $fillable = ['level', 'message', 'context', 'timestamp'];

    protected $casts = [
        'context' => 'array',
    ];

    /**
     * Boot function for the LogHistory model.
     * This method is called when the model is booted and sets up event listeners.
     * 
     * When a new log history record is created, it dispatches a LogDispatched event
     * with the newly created log history instance.
     *
     * @return void
     */
    protected static function boot()
    {
        parent::boot();

        static::created(function ($logHistory) {
            event(new \App\Events\LogDispatched($logHistory));
        });
    }
}

Step 8: Create a Log Controller to Render the Vue Page

We need a controller to retrieve logs and send them to the frontend using Inertia.js.

Create the Controller

php artisan make:controller LogHistoryController

Open app/Http/Controllers/LogHistoryController.php and modify it.

   <?php

namespace App\Http\Controllers;

use App\Models\LogHistory;
use Inertia\Inertia;

class LogHistoryController extends Controller
{
    /**
     * Display a listing of log history entries.
     * 
     * This method retrieves all log history records from the database
     * in descending order by ID and renders them using Inertia.js
     * on the 'Logs' page.
     *
     * @return \Inertia\Response Returns an Inertia response containing the logs data
     */
    public function index()
    {
        $logs = LogHistory::orderBy('id', 'desc')->get();
        return Inertia::render('Logs', [
            'logs' => $logs
        ]);
    }
}

Step 9: Set Up the Routes

Modify routes/web.php to send logs to Inertia:

Route::get('/logs', 'App\Http\Controllers\LogController@index')->name('logs');

Step 10: Set Up the Vue.js Frontend

Create a New File Logs.vue in resources/js/Pages/ Directory

Open resources/js/Pages/Logs.vue and modify it.

<template>
    <div class="container mx-auto p-4">
        <!-- Notification Component -->
        <div
            v-if="notification.show"
            :class="[
                'fixed top-4 right-4 p-4 rounded-lg shadow-lg transition-all duration-500 transform',
                getNotificationClass(notification.level),
            ]"
        >
            <div class="flex items-center">
                <div class="flex-shrink-0">
                    <!-- Icon based on level -->
                    <svg
                        class="h-5 w-5"
                        xmlns="http://www.w3.org/2000/svg"
                        viewBox="0 0 20 20"
                        fill="currentColor"
                    >
                        <path
                            fill-rule="evenodd"
                            d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
                            clip-rule="evenodd"
                        />
                    </svg>
                </div>
                <div class="ml-3">
                    <p class="text-sm font-medium">
                        {{ notification.message }}
                    </p>
                </div>
                <div class="ml-4 flex-shrink-0 flex">
                    <button
                        @click="hideNotification"
                        class="inline-flex text-current"
                    >
                        <span class="sr-only">Close</span>
                        <svg
                            class="h-5 w-5"
                            xmlns="http://www.w3.org/2000/svg"
                            viewBox="0 0 20 20"
                            fill="currentColor"
                        >
                            <path
                                fill-rule="evenodd"
                                d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
                                clip-rule="evenodd"
                            />
                        </svg>
                    </button>
                </div>
            </div>
        </div>
        <h1 class="text-2xl font-bold mb-4">Log Details</h1>
        <!-- Table Component -->
        <div class="overflow-x-auto">
            <table
                class="min-w-full bg-white border border-gray-300 rounded-lg"
            >
                <thead class="bg-gray-100">
                    <tr>
                        <th class="py-2 px-4 border-b border-gray-300">Id</th>
                        <th class="py-2 px-4 border-b border-gray-300">
                            Level
                        </th>
                        <th class="py-2 px-4 border-b border-gray-300">
                            Message
                        </th>
                        <th class="py-2 px-4 border-b border-gray-300">
                            Context
                        </th>
                        <th class="py-2 px-4 border-b border-gray-300">
                            DateTime
                        </th>
                    </tr>
                </thead>
                <tbody>
                    <tr
                        v-for="(logEntry, index) in logHistroy"
                        :key="logEntry.id"
                        class="hover:bg-gray-50"
                    >
                        <td class="py-2 px-4 border-b border-gray-300">
                            {{ index + 1 }}
                        </td>
                        <td
                            class="py-2 px-4 border-b border-gray-300"
                            :class="getLogLevelClass(logEntry.level)"
                        >
                            {{ logEntry.level }}
                        </td>
                        <td
                            class="py-2 px-4 border-b border-gray-300 break-words max-w-xs"
                        >
                            {{ logEntry.message }}
                        </td>
                        <td
                            class="py-2 px-4 border-b border-gray-300 break-words max-w-xs"
                        >
                            {{ logEntry.context }}
                        </td>
                        <td class="py-2 px-4 border-b border-gray-300">
                            {{ logEntry.timestamp }}
                        </td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</template>

<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" data-wp-preserve="%3Cscript%20setup%3E%0Aimport%20%7B%20onMounted%2C%20ref%20%7D%20from%20%22vue%22%3B%0Aimport%20%7B%20usePage%20%7D%20from%20%22%40inertiajs%2Fvue3%22%3B%0A%0A%2F%2F%20Fetch%20the%20logs%20data%20from%20the%20page%20props%0Aconst%20%7B%20logs%20%7D%20%3D%20usePage().props%3B%0A%0Aconst%20logHistroy%20%3D%20ref(logs)%3B%0A%0A%2F%2F%20Notification%20state%0Aconst%20notification%20%3D%20ref(%7B%0A%20%20%20%20show%3A%20false%2C%0A%20%20%20%20message%3A%20%22%22%2C%0A%20%20%20%20level%3A%20%22info%22%2C%0A%20%20%20%20timeout%3A%20null%2C%0A%7D)%3B%0A%0AonMounted(async%20()%20%3D%3E%20%7B%0A%20%20%20%20window.Echo.channel(%22logs%22).listen(%22.logEvent%22%2C%20(e)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20logHistroy.value.unshift(e.logHistroy)%3B%20%2F%2F%20Add%20new%20logs%20at%20the%20top%0A%20%20%20%20%20%20%20%20%2F%2F%20Show%20notification%20when%20new%20log%20arrives%0A%20%20%20%20%20%20%20%20showNotification(e.logHistroy.message%2C%20e.logHistroy.level)%3B%0A%20%20%20%20%7D)%3B%0A%7D)%3B%0A%0A%2F%2F%20Show%20notification%0Aconst%20showNotification%20%3D%20(message%2C%20level)%20%3D%3E%20%7B%0A%20%20%20%20%2F%2F%20Clear%20any%20existing%20timeout%0A%20%20%20%20if%20(notification.value.timeout)%20%7B%0A%20%20%20%20%20%20%20%20clearTimeout(notification.value.timeout)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20notification.value%20%3D%20%7B%0A%20%20%20%20%20%20%20%20show%3A%20true%2C%0A%20%20%20%20%20%20%20%20message%2C%0A%20%20%20%20%20%20%20%20level%2C%0A%20%20%20%20%20%20%20%20timeout%3A%20setTimeout(()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20hideNotification()%3B%0A%20%20%20%20%20%20%20%20%7D%2C%203000)%2C%20%2F%2F%20Hide%20after%203%20seconds%0A%20%20%20%20%7D%3B%0A%7D%3B%0A%0A%2F%2F%20Hide%20notification%0Aconst%20hideNotification%20%3D%20()%20%3D%3E%20%7B%0A%20%20%20%20notification.value.show%20%3D%20false%3B%0A%7D%3B%0A%0A%2F%2F%20Get%20notification%20styling%20based%20on%20level%0Aconst%20getNotificationClass%20%3D%20(level)%20%3D%3E%20%7B%0A%20%20%20%20switch%20(level.toLowerCase())%20%7B%0A%20%20%20%20%20%20%20%20case%20%22debug%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-blue-100%20text-blue-800%22%3B%0A%20%20%20%20%20%20%20%20case%20%22info%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-green-100%20text-green-800%22%3B%0A%20%20%20%20%20%20%20%20case%20%22notice%22%3A%0A%20%20%20%20%20%20%20%20case%20%22warning%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-yellow-100%20text-yellow-800%22%3B%0A%20%20%20%20%20%20%20%20case%20%22error%22%3A%0A%20%20%20%20%20%20%20%20case%20%22emergency%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-red-100%20text-red-800%22%3B%0A%20%20%20%20%20%20%20%20default%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-gray-100%20text-gray-800%22%3B%0A%20%20%20%20%7D%0A%7D%3B%0A%0Aconst%20getLogLevelClass%20%3D%20(level)%20%3D%3E%20%7B%0A%20%20%20%20switch%20(level.toLowerCase())%20%7B%0A%20%20%20%20%20%20%20%20case%20%22debug%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-blue-100%20text-blue-800%22%3B%0A%20%20%20%20%20%20%20%20case%20%22info%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-green-100%20text-green-800%22%3B%0A%20%20%20%20%20%20%20%20case%20%22notice%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-yellow-100%20text-yellow-800%22%3B%0A%20%20%20%20%20%20%20%20case%20%22warning%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-orange-100%20text-orange-800%22%3B%0A%20%20%20%20%20%20%20%20case%20%22error%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-red-100%20text-red-800%22%3B%0A%20%20%20%20%20%20%20%20case%20%22critical%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-purple-100%20text-purple-800%22%3B%0A%20%20%20%20%20%20%20%20case%20%22alert%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-pink-100%20text-pink-800%22%3B%0A%20%20%20%20%20%20%20%20case%20%22emergency%22%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22bg-red-600%20text-white%22%3B%0A%20%20%20%20%20%20%20%20default%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%22%22%3B%0A%20%20%20%20%7D%0A%7D%3B%0A%3C%2Fscript%3E" data-mce-resize="false" data-mce-placeholder="1" class="mce-object" width="20" height="20" alt="&lt;script&gt;" title="&lt;script&gt;" />

Step 9: Start Everything

Clear cache configuration files, routes

php artisan optimize:clear

Start Laravel development server

php artisan serve

Start Laravel Reverb server

php artisan reverb:start

Start Vite development server

npm run dev

Run log simulator for testing

# To generate an info-type log
php artisan log:simulate info  

# To generate an error-type log
php artisan log:simulate error

Conclusion

With real-time logging in Laravel using Reverb and Vue.js, you can instantly monitor and debug your application. This setup helps catch issues quickly, keeping your system reliable. You can also customize it further to fit your needs. 🚀

For more insightful tutorials, visit our Tech Blog and explore the latest in Laravel, AI, and Vue.js development!

Scroll to Top