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.php
File
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="<script>" title="<script>" />
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!