From 797dade52b728e71b93b8da7ceee123190b15b47 Mon Sep 17 00:00:00 2001 From: Axel <1597611+axeloz@users.noreply.github.com> Date: Mon, 20 Dec 2021 21:35:53 +0100 Subject: [PATCH] Complete rewrite of the monitoring daemon in PHP --- app/Console/Commands/RunMonitoring.php | 238 +++++++++++++++++++++++++ app/Console/Kernel.php | 13 +- 2 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 app/Console/Commands/RunMonitoring.php diff --git a/app/Console/Commands/RunMonitoring.php b/app/Console/Commands/RunMonitoring.php new file mode 100644 index 0000000..61b83bf --- /dev/null +++ b/app/Console/Commands/RunMonitoring.php @@ -0,0 +1,238 @@ +argument('rounds') ?? $this->rounds; + + // Getting pending tasks + $tasks = app('db')->select(' + SELECT id, host, type, params + FROM tasks + WHERE ( DATE_SUB(now(), INTERVAL frequency SECOND) > last_execution OR last_execution IS NULL ) + AND active = 1 + ORDER BY last_execution ASC + LIMIT :limit + ', [ + 'limit' => $rounds + ]); + + if (is_null($tasks) || count($tasks) == 0) { + $this->info('No task to process, going back to sleep'); + return true; + } + + $this->info('I have '.count($tasks).' to process. Better get started ...'); + + $this->newLine(); + $bar = $this->output->createProgressBar(count($tasks)); + $bar->start(); + + foreach ($tasks as $task) { + $last_status = $new_status = $output = null; + $bar->advance(); + + // Getting current task last status + $query = DB::table('tasks_history') + ->select('status') + ->where('task_id', $task->id) + ->orderBy('datetime', 'DESC') + ->first() + ; + if ($query !== false && ! is_null($query)) { + $last_status = $query->status; + } + + try { + switch ($task->type) { + case 'ping': + $new_status = $this->checkPing($task); + break; + + case 'http': + $new_status = $this->checkHttp($task); + break; + + default: + // Nothing to do here + continue 2; + } + + $this->saveHistory($task, true); + } + catch(MonitoringException $e) { + $this->saveHistory($task, false, $e->getMessage()); + } + catch(Exception $e) { + $this->saveHistory($task, false, $e->getMessage()); + } + } + $bar->finish(); + $this->newLine(2); + + if (!empty($this->results)) { + $this->table( + ['Host', 'Result', 'Message'], + $this->results + ); + } + } + + final private function saveHistory($task, $status, $output = null) { + $date = date('Y-m-d H:i:s'); + + $this->results[] = [ + 'host' => $task->host, + 'result' => $status === true ? 'OK' : 'FAILED', + 'message' => $output + ]; + + $insert = DB::table('tasks_history') + ->insert([ + 'status' => $status === true ? 1 : 0, + 'datetime' => $date, + 'output' => $output ?? '', + 'task_id' => $task->id + ] + ); + + if (false !== $insert) { + DB::table('tasks') + ->where('id', $task->id) + ->update([ + 'last_execution' => $date + ]) + ; + return true; + } + } + + final private function checkPing($task) { + if (! function_exists('exec') || ! is_callable('exec')) { + throw new MonitoringException('The "exec" command is required'); + } + + // Different command line for different OS + switch (strtolower(php_uname('s'))) { + case 'darmin': + $cmd = 'ping -n 1 -t 5'; + break; + case 'windows': + $cmd = 'ping /n 1 /w 5'; + break; + case 'linux': + case 'freebsd': + default: + $cmd = 'ping -c 1 -W 5'; + break; + } + + // If command failed + if (false === $exec = exec($cmd.' '.$task->host, $output, $code)) { + throw new MonitoringException('Unable to execute ping command'); + } + + // If command returned a non-zero code + if ($code > 0) { + throw new MonitoringException('Ping task failed ('.$exec.')'); + } + + // Double check + $output = implode(' ', $output); + // Looking for the 100% package loss output + if (preg_match('~([0-9]{1,3})\.[0-9]{0,2}% +(packet)? +loss~', $output, $matches)) { + if (! empty($matches[1])) { + if (floatval($matches[1]) == 100) { + throw new MonitoringException('Packet loss detected ('.($matches[0] ?? 'n/a').')'); + } + } + } + // Else everything is fine + return true; + } + + final private function checkHttp($task) { + // Preparing cURL + $opts = [ + CURLOPT_HTTPGET => true, + CURLOPT_FRESH_CONNECT => true, + CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 3, + CURLOPT_FAILONERROR => true, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_URL => trim($task->host) + ]; + + $ch = curl_init(); + curl_setopt_array($ch, $opts); + if ($result = curl_exec($ch)) { + + // We have nothing to check into the page + // So for me, this is a big YES + if (empty($task->params)) { + return true; + } + // We are looking for a string in the page + else { + if (strpos($result, $task->params) !== false) { + return true; + } + else { + throw new MonitoringException('Cannot find the required string into the page'); + } + } + } + throw new MonitoringException(curl_error($ch), curl_errno($ch)); + } +} + +class MonitoringException extends Exception {} + diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 481cda8..aa7e989 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -5,6 +5,8 @@ namespace App\Console; use Illuminate\Console\Scheduling\Schedule; use Laravel\Lumen\Console\Kernel as ConsoleKernel; use App\Console\Commands\SyncCustomers; +use App\Console\Commands\CleanHistory; +use App\Console\Commands\RunMonitoring; class Kernel extends ConsoleKernel { @@ -14,7 +16,9 @@ class Kernel extends ConsoleKernel * @var array */ protected $commands = [ - SyncCustomers::class + SyncCustomers::class, + CleanHistory::class, + RunMonitoring::class ]; /** @@ -30,7 +34,12 @@ class Kernel extends ConsoleKernel * You may safely remove this scheduled task */ if (env('CMS_ENABLE_SYNC') == true) { - $schedule->command('customers:sync')->hourly(); + $schedule->command('monitolite:customers:sync')->hourly(); } + + /** + * This is the main monitoring task + */ + $schedule->command('monitolite:monitoring:run')->everyMinute(); } }