~xdavidwu/uptime-monitor

0f744134734fbbdec6d5d834b5eb4a274e5f2ff4 — xdavidwu 2 years ago 2c6a1cf
initial working versions for probes
A app/Console/Commands/MonitorList.php => app/Console/Commands/MonitorList.php +49 -0
@@ 0,0 1,49 @@
<?php

namespace App\Console\Commands;

use App\ProbeInstance;
use Illuminate\Console\Command;

class MonitorList extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'monitor:list';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'List registerd probes and monitored objects';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $headers = ['ID', 'Description'];
        $rows = [];
        foreach (ProbeInstance::all() as $probe_instance) {
            $probe = unserialize($probe_instance->probe);
            $rows[] = [$probe_instance->id, $probe->describe()];
        }
        $this->table($headers, $rows);
    }
}

A app/Console/Commands/MonitorProbe.php => app/Console/Commands/MonitorProbe.php +60 -0
@@ 0,0 1,60 @@
<?php

namespace App\Console\Commands;

use App\ProbeInstance;
use App\ProbeLog;
use Exception;
use Illuminate\Console\Command;

class MonitorProbe extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'monitor:probe';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Perform probes';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        foreach (ProbeInstance::all() as $probe_instance) {
            $probe = unserialize($probe_instance->probe);
            try {
                $probe->execute();
                $log = new ProbeLog();
                $log->success = true;
                $probe_instance->logs()->save($log);
            } catch (Exception $e) {
                $this->error($probe->describe() . ' failed');
                $this->info($e->getMessage());
                $log = new ProbeLog();
                $log->success = false;
                $log->outputs = $e->getMessage();
                $probe_instance->logs()->save($log);
            }
        }
    }
}

A app/Console/Commands/MonitorRegister.php => app/Console/Commands/MonitorRegister.php +53 -0
@@ 0,0 1,53 @@
<?php

namespace App\Console\Commands;

use App\ProbeInstance;
use Illuminate\Console\Command;

class MonitorRegister extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'monitor:register
                            {class : Class of the probe}
                            {args?* : Extra arguments of <class> constructor}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Register a probe';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        $class = $this->argument('class');
        $args = $this->argument('args');
        $probe = new $class(...$args);
        // test round
        $probe->execute();

        $record = new ProbeInstance();
        $record->probe = serialize($probe);
        $record->save();
    }
}

A app/Exceptions/StreamSocketException.php => app/Exceptions/StreamSocketException.php +21 -0
@@ 0,0 1,21 @@
<?php

namespace App\Exceptions;

use Exception;

class StreamSocketException extends Exception
{
    public function __construct($url, $errno, $errstr)
    {
        if (!$errstr) {
            if (!$errno) {
                $this->message = "Connection to $url failed: Unknown error";
            } else {
                $this->message = "Connection to $url failed: $errno";
            }
        } else {
            $this->message = "Connection to $url failed: $errstr";
        }
    }
}

A app/Exceptions/TcpException.php => app/Exceptions/TcpException.php +13 -0
@@ 0,0 1,13 @@
<?php

namespace App\Exceptions;

use Exception;

class TcpException extends Exception
{
    public function __construct($address, $port, $socket_errno)
    {
        $this->message = "TCP connection to $address:$port failed: " . socket_strerror($socket_errno);
    }
}

A app/Exceptions/UnexpectedHttpStatusCodeException.php => app/Exceptions/UnexpectedHttpStatusCodeException.php +13 -0
@@ 0,0 1,13 @@
<?php

namespace App\Exceptions;

use Exception;

class UnexpectedHttpStatusCodeException extends Exception
{
    public function __construct($url, $expected_code, $result_code)
    {
        $this->message = "Unexpected status code at $url, expected: $expected_code, got: $result_code";
    }
}

A app/Http/Controllers/UptimeController.php => app/Http/Controllers/UptimeController.php +58 -0
@@ 0,0 1,58 @@
<?php

namespace App\Http\Controllers;

use App\ProbeInstance;
use Carbon\Carbon;
use Carbon\CarbonInterval;
use Illuminate\Http\Request;

class UptimeController extends Controller
{
    public function show()
    {
        $to = Carbon::now();
        $from = Carbon::now()->subDays(7);
        $timeslot = CarbonInterval::minutes(90);
        $instances = ProbeInstance::all();
        $data = [];
        foreach ($instances as $instance) {
            $probe = unserialize($instance->probe);

            $raw_logs = $instance->logs()->where('created_at', '>=', $from)->orderBy('created_at')->get();
            $raw_index = 0;
            $raw_length = count($raw_logs);
            $logs = [];
            for ($i = $from->copy(); $i < $to; $i->add($timeslot)) {
                $localto = $i->copy()->add($timeslot);

                $known = false;
                $up = true;
                $info = 'Succeeded';
                while ($raw_index < $raw_length &&
                        $raw_logs[$raw_index]->created_at < $localto) {
                    $known = true;
                    if ($raw_logs[$raw_index]->success == false) {
                        $up = false;
                        $info = 'Failed';
                    }
                    $raw_index++;
                }

                $logs[] = [
                    'from' => $i->copy(),
                    'to' => $localto,
                    'known' => $known,
                    'up' => $up,
                    'info' =>'',
                ];
            }

            $data[] = [
                'description' => $probe->describe(),
                'logs' => $logs,
            ];
        }
        return view('uptime', [ 'data' => $data, 'from' => $from, 'to' => $to ]);
    }
}

M app/Http/Kernel.php => app/Http/Kernel.php +0 -1
@@ 14,7 14,6 @@ class Kernel extends HttpKernel
     * @var array
     */
    protected $middleware = [
        \App\Http\Middleware\TrustProxies::class,
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,

D app/Http/Middleware/TrustProxies.php => app/Http/Middleware/TrustProxies.php +0 -23
@@ 1,23 0,0 @@
<?php

namespace App\Http\Middleware;

use Fideloper\Proxy\TrustProxies as Middleware;
use Illuminate\Http\Request;

class TrustProxies extends Middleware
{
    /**
     * The trusted proxies for this application.
     *
     * @var array|string
     */
    protected $proxies;

    /**
     * The headers that should be used to detect proxies.
     *
     * @var int
     */
    protected $headers = Request::HEADER_X_FORWARDED_ALL;
}

A app/Interfaces/Probe.php => app/Interfaces/Probe.php +9 -0
@@ 0,0 1,9 @@
<?php

namespace App\Interfaces;

interface Probe
{
    public function execute();
    public function describe();
}

A app/ProbeInstance.php => app/ProbeInstance.php +13 -0
@@ 0,0 1,13 @@
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class ProbeInstance extends Model
{
    public function logs()
    {
        return $this->hasMany('App\ProbeLog');
    }
}

A app/ProbeLog.php => app/ProbeLog.php +13 -0
@@ 0,0 1,13 @@
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class ProbeLog extends Model
{
    public function instance()
    {
        return $this->belongsTo('App\ProbeInstance');
    }
}

A app/Probes/CommandProbe.php => app/Probes/CommandProbe.php +32 -0
@@ 0,0 1,32 @@
<?php

namespace App\Probes;

use App\Interfaces\Probe;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

class CommandProbe implements Probe
{
    public function __construct($command)
    {
        $this->command = $command;
    }

    public function execute()
    {
        $process = new Process($this->command);
        $process->run();

        if (!$process->isSuccessful()) {
            throw new ProcessFailedException($process);
        } else {
            return true;
        }
    }

    public function describe()
    {
        return "Execute command $this->command";
    }
}

A app/Probes/HttpProbe.php => app/Probes/HttpProbe.php +34 -0
@@ 0,0 1,34 @@
<?php

namespace App\Probes;

use App\Exceptions\UnexpectedHttpStatusCodeException;
use App\Interfaces\Probe;
use GuzzleHttp\Client;

class HttpProbe implements Probe
{
    public function __construct($url, int $code)
    {
        $this->url = $url;
        $this->code = $code;
    }

    public function execute()
    {
        $client = new Client();
        $res = $client->request(
            'GET',
            $this->url,
            ['allow_redirects' => false, 'timeout' => 1]
        );
        if ($res->getStatusCode() !== $this->code) {
            throw new UnexpectedHttpStatusCodeException($this->url, $this->code, $res->getStatusCode());
        }
    }

    public function describe()
    {
        return "Access $this->url";
    }
}

A app/Probes/PingProbe.php => app/Probes/PingProbe.php +22 -0
@@ 0,0 1,22 @@
<?php

namespace App\Probes;

class PingProbe extends CommandProbe
{
    public function __construct($host)
    {
        $this->host = $host;
        parent::__construct([
            'ping',
            '-c1',
            '-W1',
            $host
        ]);
    }

    public function describe()
    {
        return "Ping $this->host";
    }
}

A app/Probes/StreamSocketProbe.php => app/Probes/StreamSocketProbe.php +42 -0
@@ 0,0 1,42 @@
<?php

namespace App\Probes;

use App\Exceptions\StreamSocketException;
use App\Interfaces\Probe;

class StreamSocketProbe implements Probe
{
    public function __construct($url)
    {
        $this->url = $url;
    }

    protected function connect($context = null)
    {
        $fp = false;
        try {
            $fp = stream_socket_client($this->url, $errno, $errstr, 1, STREAM_CLIENT_CONNECT, $context);
        } catch (\ErrorException $e) {
            if (!$errstr && $errno === 0) {
                // Message from exceptions may be more useful
                $errstr = $e->getMessage();
            }
            $fp = false;
        }
        if ($fp === false) {
            throw new StreamSocketException($this->url, $errno, $errstr);
        }
        fclose($fp);
    }

    public function execute()
    {
        $this->connect();
    }

    public function describe()
    {
        return "Connect to $this->url";
    }
}

A app/Probes/TcpProbe.php => app/Probes/TcpProbe.php +43 -0
@@ 0,0 1,43 @@
<?php

namespace App\Probes;

use App\Exceptions\TcpException;
use App\Interfaces\Probe;

class TcpProbe implements Probe
{
    public function __construct($address, $port)
    {
        $this->address = $address;
        $this->port = $port;
    }

    public function execute()
    {
        $socket = socket_create(
            (strpos($this->address, ':') === false) ? AF_INET : AF_INET6,
            SOCK_STREAM,
            SOL_TCP
        );
        socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 1, 'usec' => 0]);
        socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, ['sec' => 1, 'usec' => 0]);
        $res = false;
        try {
            $res = socket_connect($socket, $this->address, $this->port);
        } catch (\ErrorException $e) {
            $res = false;
        }
        if ($res === false) {
            $errno = socket_last_error($socket);
            socket_close($socket);
            throw new TcpException($this->address, $this->port, $errno);
        }
        socket_close($socket);
    }

    public function describe()
    {
        return "Establish TCP connection with $this->address:$this->port";
    }
}

A app/Probes/TlsProbe.php => app/Probes/TlsProbe.php +32 -0
@@ 0,0 1,32 @@
<?php

namespace App\Probes;

class TlsProbe extends StreamSocketProbe
{
    public function __construct($address, $port, $fingerprint = null)
    {
        $this->address = $address;
        $this->port = $port;
        $this->fingerprint = $fingerprint;
    }

    public function execute()
    {
        $this->url = "tls://$this->address:$this->port";
        $context = null;
        if ($this->fingerprint) {
            $context = stream_context_create();
            // When we know the expected fingerprint, ignore self-signed
            // This allows usage like in Gemini protocol
            stream_context_set_option($context, 'ssl', 'allow_self_signed', true);
            stream_context_set_option($context, 'ssl', 'peer_fingerprint', $this->fingerprint);
        }
        $this->connect($context);
    }

    public function describe()
    {
        return "Establish TLS connection with $this->address:$this->port";
    }
}

M composer.json => composer.json +1 -0
@@ 9,6 9,7 @@
    "license": "MIT",
    "require": {
        "php": "^7.2.5|^8.0",
        "guzzlehttp/guzzle": "^7.3",
        "laravel/framework": "^6.20"
    },
    "require-dev": {

M composer.lock => composer.lock +361 -1
@@ 4,7 4,7 @@
        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
        "This file is @generated automatically"
    ],
    "content-hash": "c4e7cceb2aa49838798e5767909b7311",
    "content-hash": "cccf05d5f03d2c18794c94e8107895ae",
    "packages": [
        {
            "name": "doctrine/inflector",


@@ 298,6 298,227 @@
            "time": "2020-12-29T14:50:06+00:00"
        },
        {
            "name": "guzzlehttp/guzzle",
            "version": "7.3.0",
            "source": {
                "type": "git",
                "url": "https://github.com/guzzle/guzzle.git",
                "reference": "7008573787b430c1c1f650e3722d9bba59967628"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7008573787b430c1c1f650e3722d9bba59967628",
                "reference": "7008573787b430c1c1f650e3722d9bba59967628",
                "shasum": ""
            },
            "require": {
                "ext-json": "*",
                "guzzlehttp/promises": "^1.4",
                "guzzlehttp/psr7": "^1.7 || ^2.0",
                "php": "^7.2.5 || ^8.0",
                "psr/http-client": "^1.0"
            },
            "provide": {
                "psr/http-client-implementation": "1.0"
            },
            "require-dev": {
                "bamarni/composer-bin-plugin": "^1.4.1",
                "ext-curl": "*",
                "php-http/client-integration-tests": "^3.0",
                "phpunit/phpunit": "^8.5.5 || ^9.3.5",
                "psr/log": "^1.1"
            },
            "suggest": {
                "ext-curl": "Required for CURL handler support",
                "ext-intl": "Required for Internationalized Domain Name (IDN) support",
                "psr/log": "Required for using the Log middleware"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "7.3-dev"
                }
            },
            "autoload": {
                "psr-4": {
                    "GuzzleHttp\\": "src/"
                },
                "files": [
                    "src/functions_include.php"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Michael Dowling",
                    "email": "mtdowling@gmail.com",
                    "homepage": "https://github.com/mtdowling"
                },
                {
                    "name": "Márk Sági-Kazár",
                    "email": "mark.sagikazar@gmail.com",
                    "homepage": "https://sagikazarmark.hu"
                }
            ],
            "description": "Guzzle is a PHP HTTP client library",
            "homepage": "http://guzzlephp.org/",
            "keywords": [
                "client",
                "curl",
                "framework",
                "http",
                "http client",
                "psr-18",
                "psr-7",
                "rest",
                "web service"
            ],
            "funding": [
                {
                    "url": "https://github.com/GrahamCampbell",
                    "type": "github"
                },
                {
                    "url": "https://github.com/Nyholm",
                    "type": "github"
                },
                {
                    "url": "https://github.com/alexeyshockov",
                    "type": "github"
                },
                {
                    "url": "https://github.com/gmponos",
                    "type": "github"
                }
            ],
            "time": "2021-03-23T11:33:13+00:00"
        },
        {
            "name": "guzzlehttp/promises",
            "version": "1.4.1",
            "source": {
                "type": "git",
                "url": "https://github.com/guzzle/promises.git",
                "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d",
                "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d",
                "shasum": ""
            },
            "require": {
                "php": ">=5.5"
            },
            "require-dev": {
                "symfony/phpunit-bridge": "^4.4 || ^5.1"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "1.4-dev"
                }
            },
            "autoload": {
                "psr-4": {
                    "GuzzleHttp\\Promise\\": "src/"
                },
                "files": [
                    "src/functions_include.php"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Michael Dowling",
                    "email": "mtdowling@gmail.com",
                    "homepage": "https://github.com/mtdowling"
                }
            ],
            "description": "Guzzle promises library",
            "keywords": [
                "promise"
            ],
            "time": "2021-03-07T09:25:29+00:00"
        },
        {
            "name": "guzzlehttp/psr7",
            "version": "1.8.2",
            "source": {
                "type": "git",
                "url": "https://github.com/guzzle/psr7.git",
                "reference": "dc960a912984efb74d0a90222870c72c87f10c91"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91",
                "reference": "dc960a912984efb74d0a90222870c72c87f10c91",
                "shasum": ""
            },
            "require": {
                "php": ">=5.4.0",
                "psr/http-message": "~1.0",
                "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
            },
            "provide": {
                "psr/http-message-implementation": "1.0"
            },
            "require-dev": {
                "ext-zlib": "*",
                "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
            },
            "suggest": {
                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "1.7-dev"
                }
            },
            "autoload": {
                "psr-4": {
                    "GuzzleHttp\\Psr7\\": "src/"
                },
                "files": [
                    "src/functions_include.php"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Michael Dowling",
                    "email": "mtdowling@gmail.com",
                    "homepage": "https://github.com/mtdowling"
                },
                {
                    "name": "Tobias Schultze",
                    "homepage": "https://github.com/Tobion"
                }
            ],
            "description": "PSR-7 message implementation that also provides common utility methods",
            "keywords": [
                "http",
                "message",
                "psr-7",
                "request",
                "response",
                "stream",
                "uri",
                "url"
            ],
            "time": "2021-04-26T09:17:50+00:00"
        },
        {
            "name": "laravel/framework",
            "version": "v6.20.26",
            "source": {


@@ 1085,6 1306,105 @@
            "time": "2021-03-05T17:36:06+00:00"
        },
        {
            "name": "psr/http-client",
            "version": "1.0.1",
            "source": {
                "type": "git",
                "url": "https://github.com/php-fig/http-client.git",
                "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
                "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
                "shasum": ""
            },
            "require": {
                "php": "^7.0 || ^8.0",
                "psr/http-message": "^1.0"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "1.0.x-dev"
                }
            },
            "autoload": {
                "psr-4": {
                    "Psr\\Http\\Client\\": "src/"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "PHP-FIG",
                    "homepage": "http://www.php-fig.org/"
                }
            ],
            "description": "Common interface for HTTP clients",
            "homepage": "https://github.com/php-fig/http-client",
            "keywords": [
                "http",
                "http-client",
                "psr",
                "psr-18"
            ],
            "time": "2020-06-29T06:28:15+00:00"
        },
        {
            "name": "psr/http-message",
            "version": "1.0.1",
            "source": {
                "type": "git",
                "url": "https://github.com/php-fig/http-message.git",
                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
                "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
                "shasum": ""
            },
            "require": {
                "php": ">=5.3.0"
            },
            "type": "library",
            "extra": {
                "branch-alias": {
                    "dev-master": "1.0.x-dev"
                }
            },
            "autoload": {
                "psr-4": {
                    "Psr\\Http\\Message\\": "src/"
                }
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "PHP-FIG",
                    "homepage": "http://www.php-fig.org/"
                }
            ],
            "description": "Common interface for HTTP messages",
            "homepage": "https://github.com/php-fig/http-message",
            "keywords": [
                "http",
                "http-message",
                "psr",
                "psr-7",
                "request",
                "response"
            ],
            "time": "2016-08-06T14:39:51+00:00"
        },
        {
            "name": "psr/log",
            "version": "1.1.3",
            "source": {


@@ 1180,6 1500,46 @@
            "time": "2017-10-23T01:57:42+00:00"
        },
        {
            "name": "ralouphie/getallheaders",
            "version": "3.0.3",
            "source": {
                "type": "git",
                "url": "https://github.com/ralouphie/getallheaders.git",
                "reference": "120b605dfeb996808c31b6477290a714d356e822"
            },
            "dist": {
                "type": "zip",
                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
                "reference": "120b605dfeb996808c31b6477290a714d356e822",
                "shasum": ""
            },
            "require": {
                "php": ">=5.6"
            },
            "require-dev": {
                "php-coveralls/php-coveralls": "^2.1",
                "phpunit/phpunit": "^5 || ^6.5"
            },
            "type": "library",
            "autoload": {
                "files": [
                    "src/getallheaders.php"
                ]
            },
            "notification-url": "https://packagist.org/downloads/",
            "license": [
                "MIT"
            ],
            "authors": [
                {
                    "name": "Ralph Khattar",
                    "email": "ralph.khattar@gmail.com"
                }
            ],
            "description": "A polyfill for getallheaders.",
            "time": "2019-03-08T08:55:37+00:00"
        },
        {
            "name": "ramsey/uuid",
            "version": "3.9.3",
            "source": {

A database/migrations/2021_07_24_064634_create_probe_instances.php => database/migrations/2021_07_24_064634_create_probe_instances.php +32 -0
@@ 0,0 1,32 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateProbeInstances extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('probe_instances', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('probe');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('probe_instances');
    }
}

A database/migrations/2021_07_24_073612_create_probe_logs.php => database/migrations/2021_07_24_073612_create_probe_logs.php +36 -0
@@ 0,0 1,36 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateProbeLogs extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('probe_logs', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('probe_instance_id');
            $table->foreign('probe_instance_id')->references('id')
                    ->on('probe_instances')->onDelete('cascade');
            $table->boolean('success');
            $table->string('outputs')->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('probe_logs');
    }
}

A resources/views/uptime.blade.php => resources/views/uptime.blade.php +82 -0
@@ 0,0 1,82 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Laravel</title>
        <style>
            body {
                background-color: #fafafa;
                font-family: sans-serif;
            }

            .card {
                display: inline-block;
                border-radius: 2px;
                background-color: white;
                box-shadow: 0 1px 1px 0 rgba(60,64,67,.08),0 1px 3px 1px rgba(60,64,67,.16);
                padding: 16px;
                margin: 16px;
                //overflow: auto;
            }

            .uptime-view {
                width: 320px;
                margin-top: 8px;
            }

            .uptime-from, .uptime-to {
                font-size: 10px;
                color: #aaa;
            }

            .uptime-to {
                text-align: right;
            }

            .uptime-grid {
                display: grid;
                column-gap: 1%;
                row-gap: 2px;
                grid-template-rows: repeat(4, 1fr);
                grid-auto-flow: column;
                margin: 4px 0;
            }

            .uptime-item {
                width: 8px;
                height: 8px;
            }

            .up {
                background: #0f0;
            }

            .down {
                background: #f00;
            }

            .unknown {
                background: #ccc;
            }
        </style>
    </head>
    <body>
        @foreach ($data as $instance)
            <div class="card">
                {{ $instance['description'] }}
                <div class="uptime-view">
                    <div class="uptime-from">{{ $instance['logs'][0]['from'] }}</div>
                    <div class="uptime-grid">
                        @foreach ($instance['logs'] as $log)
                            <div class="uptime-item {{ $log['known'] ? $log['up'] ? 'up' : 'down' : 'unknown'}}"
                                title="{{ "{$log['from']} to {$log['to']}" }}">
                            </div>
                        @endforeach
                    </div>
                    <div class="uptime-to">{{ $instance['logs'][count($instance['logs']) - 1]['to'] }}</div>
                </div>
            </div>
        @endforeach
    </body>
</html>

M routes/web.php => routes/web.php +1 -3
@@ 11,6 11,4 @@
|
*/

Route::get('/', function () {
    return view('welcome');
});
Route::get('/', 'UptimeController@show');