Every two weeks I send out a newsletter containing lots of interesting stuff for the modern PHP developer. You can expect quick tips, links to interesting tutorials, opinions and packages. Want to learn the cool stuff? Then sign up now!

An uptime and ssl certificate monitor written in PHP

Today we released our newest package: spatie/laravel-uptime-monitor. It’s a powerful, easy to configure uptime monitor. It’s written in PHP and distributed as a Laravel package. It will notify you when your site is down (and when it comes back up). You can also be notified a few days before an SSL certificate on one of your sites expires. Under the hood, the package leverages Laravel 5.3’s notifications, so it’s easy to use Slack, Telegram or one of the many available notification drivers.

To make sure you can easily work with the package, we’ve written extensive documentation. Topics range from the basic installation setup to some more advanced settings. In this post I’d like to show you how to start using the package and discuss some of the problems we faced (and solved) while creating the code.

The basics

After you’ve set up the package you can use the monitor:create command to monitor a url. Here’s how to add a monitor for https://example.com:

php artisan monitor:create https://example.com

You will be asked if the uptime check should look for a specific string on the response. This is handy if you know a few words that appear on the url you want to monitor. This is optional, but if you do choose to specify a string and the string does not appear in the response when checking the url, the package will consider that uptime check failed.

Instead of using the monitor:create command you may also manually create a row in the monitors table. The documentations contains a description of all the fields in that table.

Now, if all goes well the package will check the uptime of https://example.com every five minutes. That number can be changed in the configuration. If the site becomes unreachable for any reason, the package will send you a notification of that event. Here’s how that looks like in Slack:

monitor-failed

When an uptime check fails the package goes into crunch mode and starts checking that site every single minute. As soon as the connectivity to the site is restored you’ll be notified.

monitor-recovered

Out of the box the uptime check will be performed in a sane way. You can tweak some settings of the uptime check to your liking.

Like mentioned above the package will also check the validity of the ssl certificate. By default this check is being performed daily. If a certificate is expiring soon or is invalid, you’ll get notified. Here’s how such a notification looks like:

ssl-expiring-soon

You can view all configured monitors with the monitor:list command. You’ll see some output not unlike this:

monitor-list

Making the uptime check fast

Like many other agencies, at our company we have to check the uptime of a lot of sites. That’s why this process happens as fast as possible. Under the hood the uptime check uses Guzzle Promises. So instead of making a request to a site and just keep waiting until there is a response, the package will perform multiple http request at the same time. If you want to know more about Guzzle promises, read this blogpost by Hannes Van de Vreken. Take a look at the MonitorCollection-class in the package to learn how promises are being leveraged in this package.

Testing the code

To make sure the package works correctly we’ve built an extensive test suite. It was a challenge to keep the test suite fast. Reaching out to real sites to test reachability is much too slow. A typical response from a site takes 200ms (and that’s a very optimistic number), multiply that by the ±50 tests we you’ll get a suite that takes over 10 seconds to run. That’s much too slow. Another problem we needed to solve is the timing of the tests. By default the package will check a reachable site only once in 5 minutes. Of course our test suite can’t wait five minutes.

To solve the problem of slow responses we built a little node server that mimics a site and included that in the testsuite. The Express framework makes it really easy to do so. Here’s the entire source code of the server:

"use strict";

let app = require('express')();

let bodyParser = require('body-parser');
app.use(bodyParser.json()); // support json encoded bodies
app.use(bodyParser.urlencoded({ extended: true })); // support encoded bodies

let serverResponse = {};

app.get('/', function (request, response) {
    const statusCode = serverResponse.statusCode;

    response.writeHead(statusCode || 200, { 'Content-Type': 'text/html' });
    response.end(serverResponse.body || "This is the testserver");
});

app.post('/setServerResponse', function(request, response) {
    serverResponse.statusCode = request.body.statusCode
    serverResponse.body = request.body.body;

    response.send("Response set");
});

let server = app.listen(8080, function () {
    const host = 'localhost';
    const port = server.address().port;

    console.log('Testing server listening at http://%s:%s', host, port);
});

By default visiting http://localhost:8080/ returns an http response with content This is the testserver and status code 200. To change that response a post request to setServerResponse can be submitted with the text and status code that a visit to / should return. Unlike making a request to a site on the internet, this server, because it runs locally, is blazing fast.

This is the PHP class in the testsuite that communicates with the node server:

namespace Spatie\UptimeMonitor\Test;

use GuzzleHttp\Client;

class Server
{
    /** @var \GuzzleHttp\Client */
    protected $client;

    public function __construct()
    {
        $this->client = new Client();

        $this->up();
    }

    public function setResponseBody(string $text, int $statusCode = 200)
    {
        $this->client->post('http://localhost:8080/setServerResponse', [
            'form_params' => [
                'statusCode' => $statusCode,
                'body' => $text,
            ],
        ]);
    }

    public function up()
    {
        $this->setResponseBody('Site is up', 200);
    }

    public function down()
    {
        $this->setResponseBody('Site is down', 503);
    }
}

The second problem – testing time based functionality – can be solved by just controlling time. Yeah, you’ve read that right. The whole package makes use of Carbon instances to work with time. Carbon has this method to just set the current time.

use Carbon\Carbon;

Carbon::setTestNow(Carbon::create(2016, 1, 1, 00, 00, 00));

// will return a Carbon instance with a datetime value 
// of 1st January 2016 no matter what the real
// current date or time is
Carbon::now();

To make time progress a couple of minutes we made this function :

public function progressMinutes(int $minutes)
{
   $newNow = Carbon::now()->addMinutes($minutes);

   Carbon::setTestNow($newNow);
}

Now let’s take a look at a real test that uses both the testserver and the time control. Our uptime check will only fire of a UptimeCheckRecovered event after a UptimeCheckFailed was sent first. The UptimeCheckRecovered contains a datetime indicating when the UptimeCheckFailed event failed for the first time.

Here’s the test to make sure the the UptimeCheckRecovered gets fired at the right time and it contains the right info:

/** @test */
public function the_recovered_event_will_be_fired_when_an_uptime_check_succeeds_after_it_has_failed()
{
    /**
     * Get all monitors which uptime we should check
     *
     * In this test there is only one monitor configured.
     */
    $monitors = MonitorRepository::getForUptimeCheck();

    /**
     * Bring the testserver down.
     */
    $this->server->down();

    /**
     * To avoid false positives the package will only raise an `UptimeCheckFailed`
     * event after 3 consecutive failures.
     */
    foreach (range(1, 3) as $index) {
        /** Perform the uptime check */
        Event::assertNotFired(UptimeCheckFailed::class);

        $monitors->checkUptime();
    }

    /**
     * The `UptimeCHeckFailed`-event should have fired by now.
     */
    Event::assertFired(UptimeCheckFailed::class);

    /**
     * Let's simulate a downtime of 10 minutes
     */
    $downTimeLengthInMinutes = 10;
    $this->progressMinutes($downTimeLengthInMinutes);

    /**
     * Bring the testserver up
     */
    $this->server->up();

    /**
     * To be 100% certain let's assert the the `UptimeCheckRecovered`-event
     * hasn't been fired yet.
     */
    Event::assertNotFired(UptimeCheckRecovered::class);

    /**
     * Let's go ahead a check the uptime again.
     */
    $monitors->checkUptime();

    /**
     * And now the `UptimeCheckEventRecovered` event should have fired
     *
     * We'll also assert that `downtimePeriod` is correct. It should have a
     * a startDateTime of 10 minutes ago.
     */
    Event::assertFired(UptimeCheckRecovered::class, function ($event) use ($downTimeLengthInMinutes) {


        if ($event->downtimePeriod->startDateTime->toDayDateTimeString() !== Carbon::now()->subMinutes($downTimeLengthInMinutes)->toDayDateTimeString()) {
            return false;
        }

        if ($event->monitor->id !== $this->monitor->id) {
            return false;
        }

        return true;
    });
}

Sure, there’s a lot going on, but it all should be very readable.

Y U provide no GUI?

Because everybody’s needs are a bit different, the package does not come with any views. If you need a some screens to manage and view your configured monitors you should handle this in your own project. There’s only one table to manage – monitors – that should not be to overly difficult. We use semver, so we guarantee we’ll make no breaking changes within a major version. The screens you build around the package should just keep working after upgrading the package. If you’re in a generous mood you could make your fellow developers happy by make a package out of your screens.

A word on recording uptime history

Some notifications, for example UptimeCheckRecovered, have a little bit of knowledge on how long a period of downtime was. Take a look at the notification:

monitor-recovered

But other than that the package records no history. If you want to for example calculate an uptime percentage or to draw a graph of the uptime you can leverage the various events that are fired. The documentation specifies which events get send by the uptime check and the certificate check. A possible strategy would be to just write all the those events to an big table, a kind of event log. You could use that event log to generate your desired reports. This strategy has got a name: event sourcing. If you’re interested in knowing more about event sourcing watch this talk by Mitchell van Wijngaarden given at this year’s Laracon.

Closing notes

Sure, there already are a lot of excellent services out there to check uptime, both free and paid. We created this package to be in full control of how the uptime and ssl check work and how notifications get send. You can also choose from which location the checks should be performed. Just install the package onto a server in that location.

Although it certainly took some time get it right, the core functionalities was fairly easy to implement. Guzzle already had much of the functionality we needed to perform uptime checks quickly. Laravel itself make it a breeze to schedule uptime checks and comes with an excellent notification system out of the box.

We put a lot of effort in polishing the code and making sure everything just works. At Spatie, we are already using our own monitor to check to uptime of all our sites. If you choose to use our package, we very much hope that you’ll like it. Check out the extensive documentation we’ve written. If you have any remarks or questions, just open up an issue on the repo on GitHub. And don’t forget to send us a postcard! 🙂

We’ve made a lot of other packages in the past, check out this list on our company site. Maybe we’ve made something that could be of use in your projects.

Freek Van der Herten is a partner and developer at Spatie, an Antwerp based company that specializes in creating web apps with Laravel. After hours he writes about modern PHP and Laravel on this blog. When not coding he’s probably rehearsing with his kraut rock band.