Creating a custom Laravel Pulse card
December 4, 2023
Laravel Pulse is a lightweight application monitoring tool for Laravel. It was just released today and I took a bit of time to create a custom card to show outdated composer dependencies.
This is what the card looks like right now:
I was surprised at how easy the card was to create. Pulse has all of the infrastructure built out for:
- storing data in response to events or on a schedule
- retrieving your data back, aggregated or not
- rendering your data into a view.
The hooks are all very well thought through.
There is no official documentation for custom cards yet, so much of this is subject to change. Everything I'm telling you here I learned through diving into the source code.
Recording data
The first step is to create a Recorder
that will record the data you're looking to monitor. If you
open config/pulse.php
you'll see a list of recorders:
/*|--------------------------------------------------------------------------| Pulse Recorders|--------------------------------------------------------------------------|| The following array lists the "recorders" that will be registered with| Pulse, along with their configuration. Recorders gather application| event data from requests and tasks to pass to your ingest driver.|*/'recorders' => [ Recorders\Servers::class => [ 'server_name' => env('PULSE_SERVER_NAME', gethostname()), 'directories' => explode(':', env('PULSE_SERVER_DIRECTORIES', '/')), ], // more recorders ...]
The recorders listen for application events. Pulse emits a SharedBeat
event if your recorder needs to run on an
interval instead of in response to an application event.
For example, the Servers
recorder records server stats every 15 seconds in response to the SharedBeat
event:
class Servers{ public string $listen = SharedBeat::class; public function record(SharedBeat $event): void { if ($event->time->second % 15 !== 0) { return; } // Record server stats... }}
But the Queue
recorder listens for specific application events:
class Queues{ public array $listen = [ JobReleasedAfterException::class, JobFailed::class, JobProcessed::class, JobProcessing::class, JobQueued::class, ]; public function record( JobReleasedAfterException|JobFailed|JobProcessed|JobProcessing|JobQueued $event ): void { // Record the job... }}
In our case, we just need to check for outdated packages once a day on a schedule, so we'll use the SharedBeat
event.
Creating the recorder
The recorder is a plain PHP class with a record
method. Inside of that method you're given one of the events to which
you're listening. You also have access to Pulse
in the constructor.
class Outdated{ public string $listen = SharedBeat::class; public function __construct( protected Pulse $pulse, protected Repository $config ) { // } public function record(SharedBeat $event): void { // } }
The SharedBeat
event has a time property on it, which we can use to decide if we want to run or not.
class Outdated{ // ... public function record(SharedBeat $event): void { // Only run once per day if ($event->time !== $event->time->startOfDay()) { return; } }}
Pulse will handle invoking the record
method, we just need to figure out what to do there. In our case we're going to
run composer outdated
.
class Outdated{ // ... public function record(SharedBeat $event): void { // Only run once per day if ($event->time !== $event->time->startOfDay()) { return; } // Run composer to get the outdated dependencies $result = Process::run("composer outdated -D -f json"); if ($result->failed()) { throw new RuntimeException( 'Composer outdated failed: ' . $result->errorOutput() ); } // Just make sure it's valid JSON json_decode($result->output(), JSON_THROW_ON_ERROR); }}
Writing to the Pulse tables
Pulse ships with three separate tables:
- pulse_aggregates
- pulse_entries
- pulse_values
There is currently no documentation, but from what I can tell the pulse_aggregates
table stores pre-computed rollups
of time-series data for better performance. The entries
table stores individual events, like requests or exceptions.
The values
table seems to be a simple "point in time" store.
We're going to use the values
table to stash the output of composer outdated
. To do this, we use the pulse->set()
method.
class Outdated{ // ... public function record(SharedBeat $event): void { // Only run once per day if ($event->time !== $event->time->startOfDay()) { return; } // Run composer to get the outdated dependencies $result = Process::run("composer outdated -D -f json"); if ($result->failed()) { throw new RuntimeException( 'Composer outdated failed: ' . $result->errorOutput() ); } // Just make sure it's valid JSON json_decode($result->output(), JSON_THROW_ON_ERROR); // Store it in one of the Pulse tables $this->pulse->set('composer_outdated', 'result', $result->output()); }}
Now our data is stored and will be updated once per day. Let's move on to displaying that data!
(Note: You don't have to create a recorder. Your card can pull data from anywhere!)
Displaying the data
Pulse is built on top of Laravel Livewire. To add a new Pulse card to your dashboard,
we'll create a new Livewire component called ComposerOutdated
.
php artisan livewire:make ComposerOutdated # COMPONENT CREATED 🤙# CLASS: app/Livewire/ComposerOutdated.php# VIEW: resources/views/livewire/composer-outdated.blade.php
By default, our ComposerOutdated
class extends Livewire's Component
class, but we're going to change that to extend
Pulse's Card
class.
namespace App\Livewire; use Livewire\Component;use Laravel\Pulse\Livewire\Card; class ComposerOutdated extends Componentclass ComposerOutdated extends Card{ public function render() { return view('livewire.composer-outdated'); }}
To get our data back out of the Pulse data store, we can just use the Pulse
facade. This is one of the things I'm
really liking about Pulse. I don't have to add migrations, maintain tables, add new models, etc. I can just use their
data store!
class ComposerOutdated extends Card{ public function render() { // Get the data out of the Pulse data store. $packages = Pulse::values('composer_outdated', ['result'])->first(); $packages = $packages ? json_decode($packages->value, JSON_THROW_ON_ERROR)['installed'] : [] return View::make('composer-outdated', [ 'packages' => $packages, ]); }}
Publishing the Pulse dashboard
To add our card to the Pulse dashboard, we must first publish the vendor view.
php artisan vendor:publish --tag=pulse-dashboard
Now, in our resources/views/vendor/pulse
folder, we have a new dashboard.blade.php
where we can add our custom card. This is what it looks like by default:
<x-pulse> <livewire:pulse.servers cols="full" /> <livewire:pulse.usage cols="4" rows="2" /> <livewire:pulse.queues cols="4" /> <livewire:pulse.cache cols="4" /> <livewire:pulse.slow-queries cols="8" /> <livewire:pulse.exceptions cols="6" /> <livewire:pulse.slow-requests cols="6" /> <livewire:pulse.slow-jobs cols="6" /> <livewire:pulse.slow-outgoing-requests cols="6" /></x-pulse>
Adding our custom card
We can now add our new card wherever we want!
<x-pulse> <livewire:composer-outdated cols="1" rows="3" /> <livewire:pulse.servers cols="full" /> <livewire:pulse.usage cols="4" rows="2" /> <livewire:pulse.queues cols="4" /> <livewire:pulse.cache cols="4" /> <livewire:pulse.slow-queries cols="8" /> <livewire:pulse.exceptions cols="6" /> <livewire:pulse.slow-requests cols="6" /> <livewire:pulse.slow-jobs cols="6" /> <livewire:pulse.slow-outgoing-requests cols="6" /></x-pulse>
Community site
There is a lot to learn about Pulse, and I'll continue to post here as I do. I'm working on builtforpulse.com to showcase Pulse-related packages and articles, so make sure you stay tuned over there!
GitHub Package
You can see this package at github.com/aarondfrancis/pulse-outdated.