# Laravel Pulse Laravel Pulse is a real-time application performance monitoring (APM) tool and dashboard designed for Laravel applications. It provides insights into slow requests, slow database queries, exceptions, queue performance, cache usage, and server health, all displayed in a beautiful Livewire-powered dashboard. The package automatically records metrics through event listeners and provides aggregation over configurable time periods. Pulse stores performance data in dedicated database tables and supports multiple ingest drivers (direct database storage, Redis buffering) for high-performance applications. It integrates seamlessly with Laravel's authentication system to track user activity and supports Laravel Octane for high-concurrency environments. The dashboard is protected by authorization gates and can be customized through publishable views and configuration. ## Installation and Setup ### Installing via Composer Install Laravel Pulse using Composer and publish the migrations to set up the required database tables. ```bash # Install the package composer require laravel/pulse # Publish and run migrations php artisan vendor:publish --tag=pulse-migrations php artisan migrate # Optionally publish the configuration php artisan vendor:publish --tag=pulse-config # Optionally publish the dashboard view for customization php artisan vendor:publish --tag=pulse-dashboard ``` ## Configuration ### Basic Configuration (config/pulse.php) The main configuration file controls the dashboard path, storage driver, ingest method, and recorder settings. ```php env('PULSE_PATH', 'pulse'), // Subdomain for the dashboard (optional) 'domain' => env('PULSE_DOMAIN'), // Master switch to enable/disable all recording 'enabled' => env('PULSE_ENABLED', true), // Storage configuration 'storage' => [ 'driver' => env('PULSE_STORAGE_DRIVER', 'database'), 'trim' => [ 'keep' => env('PULSE_STORAGE_KEEP', '7 days'), ], 'database' => [ 'connection' => env('PULSE_DB_CONNECTION'), 'chunk' => 1000, ], ], // Ingest configuration (how data is captured) 'ingest' => [ 'driver' => env('PULSE_INGEST_DRIVER', 'storage'), 'buffer' => env('PULSE_INGEST_BUFFER', 5_000), 'trim' => [ 'lottery' => [1, 1_000], 'keep' => env('PULSE_INGEST_KEEP', '7 days'), ], 'redis' => [ 'connection' => env('PULSE_REDIS_CONNECTION'), 'chunk' => 1000, ], ], // Cache driver for Pulse internals 'cache' => env('PULSE_CACHE_DRIVER'), // Middleware for dashboard routes 'middleware' => [ 'web', \Laravel\Pulse\Http\Middleware\Authorize::class, ], ]; ``` ### Recorder Configuration Configure individual recorders to control what metrics are captured and their thresholds. ```php [ // Cache hit/miss tracking Recorders\CacheInteractions::class => [ 'enabled' => env('PULSE_CACHE_INTERACTIONS_ENABLED', true), 'sample_rate' => env('PULSE_CACHE_INTERACTIONS_SAMPLE_RATE', 1), 'ignore' => [ ...Pulse::defaultVendorCacheKeys(), ], 'groups' => [ '/^job-exceptions:.*/' => 'job-exceptions:*', ], ], // Exception tracking Recorders\Exceptions::class => [ 'enabled' => env('PULSE_EXCEPTIONS_ENABLED', true), 'sample_rate' => env('PULSE_EXCEPTIONS_SAMPLE_RATE', 1), 'location' => env('PULSE_EXCEPTIONS_LOCATION', true), 'ignore' => [ '/^App\\\\Exceptions\\\\IgnoredException/', ], ], // Queue monitoring Recorders\Queues::class => [ 'enabled' => env('PULSE_QUEUES_ENABLED', true), 'sample_rate' => env('PULSE_QUEUES_SAMPLE_RATE', 1), 'ignore' => [], ], // Server metrics (CPU, memory, disk) Recorders\Servers::class => [ 'server_name' => env('PULSE_SERVER_NAME', gethostname()), 'directories' => explode(':', env('PULSE_SERVER_DIRECTORIES', '/')), ], // Slow jobs (threshold in milliseconds) Recorders\SlowJobs::class => [ 'enabled' => env('PULSE_SLOW_JOBS_ENABLED', true), 'sample_rate' => env('PULSE_SLOW_JOBS_SAMPLE_RATE', 1), 'threshold' => env('PULSE_SLOW_JOBS_THRESHOLD', 1000), 'ignore' => [], ], // Slow outgoing HTTP requests Recorders\SlowOutgoingRequests::class => [ 'enabled' => env('PULSE_SLOW_OUTGOING_REQUESTS_ENABLED', true), 'sample_rate' => env('PULSE_SLOW_OUTGOING_REQUESTS_SAMPLE_RATE', 1), 'threshold' => env('PULSE_SLOW_OUTGOING_REQUESTS_THRESHOLD', 1000), 'ignore' => [], 'groups' => [ '#^https://api\.github\.com/repos/.*$#' => 'api.github.com/repos/*', ], ], // Slow database queries Recorders\SlowQueries::class => [ 'enabled' => env('PULSE_SLOW_QUERIES_ENABLED', true), 'sample_rate' => env('PULSE_SLOW_QUERIES_SAMPLE_RATE', 1), 'threshold' => env('PULSE_SLOW_QUERIES_THRESHOLD', 1000), 'location' => env('PULSE_SLOW_QUERIES_LOCATION', true), 'max_query_length' => env('PULSE_SLOW_QUERIES_MAX_QUERY_LENGTH'), 'ignore' => [ '/(["`])pulse_[\w]+?\1/', '/(["`])telescope_[\w]+?\1/', ], ], // Slow HTTP requests Recorders\SlowRequests::class => [ 'enabled' => env('PULSE_SLOW_REQUESTS_ENABLED', true), 'sample_rate' => env('PULSE_SLOW_REQUESTS_SAMPLE_RATE', 1), 'threshold' => env('PULSE_SLOW_REQUESTS_THRESHOLD', 1000), 'ignore' => [ '#^/pulse$#', '#^/telescope#', ], ], // User job activity Recorders\UserJobs::class => [ 'enabled' => env('PULSE_USER_JOBS_ENABLED', true), 'sample_rate' => env('PULSE_USER_JOBS_SAMPLE_RATE', 1), 'ignore' => [], ], // User request activity Recorders\UserRequests::class => [ 'enabled' => env('PULSE_USER_REQUESTS_ENABLED', true), 'sample_rate' => env('PULSE_USER_REQUESTS_SAMPLE_RATE', 1), 'ignore' => [ '#^/pulse$#', ], ], ], ``` ## Dashboard Authorization ### Configuring Access via Gate Define authorization for accessing the Pulse dashboard in your `AuthServiceProvider`. ```php email, [ 'admin@example.com', 'developer@example.com', ]); }); // Or allow based on role/permission Gate::define('viewPulse', function (User $user) { return $user->hasRole('admin'); }); } } ``` ## Pulse Facade API ### Recording Custom Entries Use the `Pulse` facade to record custom metrics with aggregation support. ```php count(); // Record with multiple aggregations Pulse::record( type: 'payment_processed', key: 'stripe', value: 9999, // amount in cents )->count()->sum()->avg()->max()->min(); // Record only bucket aggregates (no raw entries saved) Pulse::record( type: 'memory_usage', key: 'web-server-1', value: 2048, // MB )->avg()->onlyBuckets(); // Record using enum types (Laravel Pulse v1.7+) enum MetricType: string { case ApiLatency = 'api_latency'; case CacheSize = 'cache_size'; } Pulse::record( type: MetricType::ApiLatency, key: 'users/show', value: 45, )->count()->avg(); ``` ### Setting Values (Key-Value Storage) Store snapshot values that represent the current state rather than aggregated metrics. ```php 'web-server-1', 'cpu' => 45, 'memory_used' => 8192, 'memory_total' => 16384, 'storage' => [ ['directory' => '/', 'total' => 100000, 'used' => 45000], ], ]), ); // Store deployment information Pulse::set( type: 'deployment', key: 'latest', value: json_encode([ 'version' => 'v2.3.1', 'deployed_at' => now()->toIso8601String(), 'commit' => 'abc123', ]), ); ``` ### Lazy Recording Defer recording until the end of the request to avoid performance impact during execution. ```php count(); }); // Useful for capturing request-scoped data $startTime = microtime(true); // ... your code ... Pulse::lazy(function () use ($startTime) { $duration = (microtime(true) - $startTime) * 1000; Pulse::record( type: 'custom_operation', key: 'data_sync', value: (int) $duration, )->count()->max(); }); ``` ### Controlling Recording Temporarily disable recording or ignore specific operations. ```php where('status', 'pending') ->get(); }); // Manually stop and start recording Pulse::stopRecording(); // ... code that shouldn't be monitored ... Pulse::startRecording(); // Filter entries before storage Pulse::filter(function ($entry) { // Don't record entries for health checks if ($entry->type === 'slow_request' && str_contains($entry->key, 'health')) { return false; } return true; }); ``` ### Reporting Exceptions Manually report exceptions to Pulse. ```php $e]); // Optionally report to error tracking if (app()->bound('sentry')) { app('sentry')->captureException($e); } }); ``` ## Querying Data ### Retrieving Aggregate Data Query aggregated metrics for use in custom cards or API endpoints. ```php '["GET","/api/users","App\\Http\\Controllers\\UserController"]', 'count' => 150, 'max' => 2500], // (object) ['key' => '["POST","/api/orders","App\\Http\\Controllers\\OrderController"]', 'count' => 45, 'max' => 1800], // ] foreach ($slowRequests as $request) { [$method, $path, $controller] = json_decode($request->key); echo "{$method} {$path}: {$request->count} requests, max {$request->max}ms\n"; } ``` ### Retrieving Graph Data Get time-series data for plotting charts. ```php [ // 'cpu' => ['2024-01-01 00:00:00' => 45, '2024-01-01 00:06:00' => 52, ...], // 'memory' => ['2024-01-01 00:00:00' => 8192, '2024-01-01 00:06:00' => 8456, ...], // ], // 'web-server-2' => [...], // ] // Use in a custom Livewire card foreach ($graphData as $serverKey => $metrics) { $cpuData = $metrics['cpu']->values()->all(); $memoryData = $metrics['memory']->values()->all(); } ``` ### Retrieving Values Get key-value stored data like server status. ```php (object) [ // 'timestamp' => 1704067200, // 'key' => 'web-server-1', // 'value' => '{"name":"web-server-1","cpu":45,...}', // ], // ] foreach ($servers as $key => $server) { $data = json_decode($server->value); echo "{$data->name}: CPU {$data->cpu}%, Memory {$data->memory_used}MB/{$data->memory_total}MB\n"; } ``` ### Aggregate Totals Get total aggregated values across all keys. ```php 15000, 'cache_miss' => 500] $hitRate = $cacheTotals['cache_hit'] / ($cacheTotals['cache_hit'] + $cacheTotals['cache_miss']) * 100; echo "Cache hit rate: {$hitRate}%\n"; // Get single type total $totalExceptions = Pulse::aggregateTotal( types: 'exception', aggregate: 'count', interval: CarbonInterval::day(), ); // Returns float: 42.0 ``` ## Custom User Resolution ### Customizing User Display Configure how users are displayed in the dashboard. ```php $user->full_name, 'extra' => $user->department, 'avatar' => $user->profile_photo_url, ]; }); } ``` ### Custom User Resolver Implement a custom user resolver for complex scenarios. ```php getAuthIdentifier(); } public function load(Collection $keys): self { // Load from external service or cache $this->users = User::whereIn('id', $keys) ->with('department') ->get(); return $this; } public function find(int|string|null $key): object { $user = $this->users->firstWhere('id', $key); return (object) [ 'name' => $user?->name ?? "Unknown (ID: {$key})", 'extra' => $user?->department?->name ?? '', 'avatar' => $user?->avatar_url ?? 'https://gravatar.com/avatar?d=mp', ]; } } // Register in AppServiceProvider use App\Pulse\CustomUserResolver; use Laravel\Pulse\Contracts\ResolvesUsers; $this->app->bind(ResolvesUsers::class, CustomUserResolver::class); ``` ## Custom Dashboard Cards ### Creating a Custom Livewire Card Build custom dashboard cards that extend the base Card class. ```php remember(fn () => [ $this->aggregate('api_call', ['count', 'avg', 'max']), now()->toDateTimeString(), ]); return View::make('livewire.pulse.custom-api-metrics', [ 'apiCalls' => $apiCalls, 'time' => $time, 'runAt' => $runAt, ]); } } ``` ```blade {{-- resources/views/livewire/pulse/custom-api-metrics.blade.php --}} @if ($apiCalls->isEmpty()) @else Endpoint Count Avg Max @foreach ($apiCalls as $call) {{ $call->key }} {{ number_format($call->count) }} {{ number_format($call->avg) }}ms {{ number_format($call->max) }}ms @endforeach @endif ``` ### Registering Custom Cards Register your custom card with Livewire. ```php {{-- Custom card --}} ``` ## Artisan Commands ### pulse:work Command Process incoming Pulse data from the ingest stream (used with Redis ingest driver). ```bash # Start the worker process php artisan pulse:work # Stop when the stream is empty (useful for testing) php artisan pulse:work --stop-when-empty # Run with Supervisor (recommended for production) # /etc/supervisor/conf.d/pulse.conf [program:pulse-worker] process_name=%(program_name)s command=php /var/www/html/artisan pulse:work autostart=true autorestart=true user=www-data redirect_stderr=true stdout_logfile=/var/log/pulse-worker.log ``` ### pulse:check Command Capture server metrics (CPU, memory, disk) on a regular interval. ```bash # Take continuous snapshots (every second) php artisan pulse:check # Take a single snapshot php artisan pulse:check --once # Run with Supervisor for continuous monitoring # /etc/supervisor/conf.d/pulse-check.conf [program:pulse-check] process_name=%(program_name)s command=php /var/www/html/artisan pulse:check autostart=true autorestart=true user=www-data redirect_stderr=true stdout_logfile=/var/log/pulse-check.log ``` ### pulse:restart Command Gracefully restart the pulse:work and pulse:check processes. ```bash # Signal workers to restart after next iteration php artisan pulse:restart ``` ### pulse:clear Command Clear all Pulse data from the database. ```bash # Clear all Pulse data php artisan pulse:clear # Clear specific types only php artisan pulse:clear --type=slow_query --type=exception ``` ## Custom Server Metrics ### Custom CPU Detection Override the default CPU detection for specialized environments. ```php (int) ($memLimit / 1024 / 1024), // MB 'used' => (int) ($memUsage / 1024 / 1024), // MB ]; }); } ``` ## Ignoring Routes ### Disabling Automatic Routes Disable Pulse's automatic route registration to define custom routes. ```php group(function () { Route::get('/admin/pulse', function () { return view('pulse::dashboard'); })->name('admin.pulse'); }); ``` ## Adding Custom CSS ### Including Additional Styles Add custom CSS to the Pulse dashboard. ```php .pulse-card { border-radius: 1rem; }'; } }); // Add multiple CSS files Pulse::css([ resource_path('css/pulse-custom.css'), resource_path('css/pulse-dark.css'), ]); } ``` ## Production Deployment ### Using Redis Ingest Driver Configure Redis as the ingest driver for high-traffic applications. ```env # .env PULSE_INGEST_DRIVER=redis PULSE_REDIS_CONNECTION=default PULSE_INGEST_BUFFER=5000 ``` ```php [ 'default' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), ], ], ``` ### Supervisor Configuration Run Pulse workers in production with Supervisor. ```ini ; /etc/supervisor/conf.d/pulse.conf [program:pulse-worker] process_name=%(program_name)s_%(process_num)02d command=php /var/www/html/artisan pulse:work autostart=true autorestart=true stopasgroup=true killasgroup=true user=www-data numprocs=1 redirect_stderr=true stdout_logfile=/var/log/pulse/worker.log stopwaitsecs=3600 [program:pulse-check] process_name=%(program_name)s_%(process_num)02d command=php /var/www/html/artisan pulse:check autostart=true autorestart=true stopasgroup=true killasgroup=true user=www-data numprocs=1 redirect_stderr=true stdout_logfile=/var/log/pulse/check.log ``` Laravel Pulse is designed for developers who need real-time visibility into their Laravel application's performance without the complexity of external APM services. It excels at identifying slow database queries, tracking exception frequency, monitoring queue throughput, and observing server resource utilization. The package's Livewire-powered dashboard provides instant updates and can be customized with additional cards for application-specific metrics. For production deployments, Pulse supports high-traffic scenarios through its Redis ingest driver, which buffers metrics before batch processing. The package integrates seamlessly with Laravel Octane for high-concurrency applications and includes built-in support for Laravel Vapor environments. By combining automatic event-based recording with manual instrumentation APIs, Pulse provides comprehensive observability while maintaining minimal performance overhead.