Compare commits

..

130 commits
1.0 ... main

Author SHA1 Message Date
28f1bcfce9 Changing frequency 2022-01-10 20:34:23 +01:00
9fe028e1cc Removing old version of Monitolite 2022-01-06 16:14:19 +01:00
ec4fb68630 Revert "Removing old version of Monitolite"
This reverts commit 42fe9281f7.
2022-01-06 16:12:51 +01:00
42fe9281f7 Removing old version of Monitolite 2022-01-06 16:10:22 +01:00
3daa19f2de Building as production 2021-12-29 11:32:44 +01:00
25f5a8800d Fixing API request 2021-12-29 11:32:01 +01:00
1414c83cc9 Improving query 2021-12-28 20:22:22 +01:00
78dc7baa73 Adding task type 2021-12-28 18:37:37 +01:00
83d401daa2 Adding missing icon 2021-12-28 18:31:56 +01:00
1996b4fb32 Updating README for DNS 2021-12-28 18:25:57 +01:00
5986875818 Adding DNS check 2021-12-28 18:24:40 +01:00
80ba45b83b README 2021-12-28 17:44:16 +01:00
f2139a7b01 Modifying the README 2021-12-28 17:43:32 +01:00
42f1187776 Adding FTP (anonymous) check 2021-12-28 17:41:52 +01:00
dde679411a Fixing relative URL 2021-12-28 16:37:48 +01:00
f1e1cbfdd9 Adding message when no data 2021-12-28 15:19:47 +01:00
d9c2c6f5f6 Fixing case when task not found 2021-12-28 14:04:44 +01:00
24ed50d9d2 Moving to migrations 2021-12-28 13:34:50 +01:00
fc6f36ce3a Rounding values 2021-12-28 12:52:02 +01:00
fccf7bf165 Merge branch 'main' of github.com:axeloz/monitolite into main 2021-12-28 12:50:34 +01:00
e89e4ca451 Fixing graphs values 2021-12-28 12:50:26 +01:00
6ba0d76dda
Update README.md 2021-12-28 10:52:33 +01:00
230e1c5668
Update README.md 2021-12-28 10:51:59 +01:00
5b73ad72e4 Merge branch 'main' of github.com:axeloz/monitolite into main 2021-12-28 10:48:03 +01:00
d292b0342e Latest db schema 2021-12-28 10:47:54 +01:00
d3f05d6ee1
Update README.md 2021-12-28 10:33:08 +01:00
b0e28950bc Fixing example .env 2021-12-28 10:26:10 +01:00
6fe20c0102 New screenshot 2021-12-28 10:04:37 +01:00
ea63595795 Do not display response time 2021-12-28 09:29:24 +01:00
a704c5bea3 Do not display response times for ping checks 2021-12-28 09:27:57 +01:00
d57cee575b Removing deletion of missing customers 2021-12-28 09:13:01 +01:00
a71505def5 Fixing average 2021-12-28 00:09:56 +01:00
a9ee94b8fd Restoring loader 2021-12-28 00:07:29 +01:00
234547a903 Changing color 2021-12-28 00:06:23 +01:00
84ec4847d9 Adding gradiant 2021-12-27 23:59:25 +01:00
f20adb7f28 Fixing history 2021-12-27 23:37:15 +01:00
3852ef2945 Fixing charts 2021-12-27 23:26:18 +01:00
bae507328b Adding response time graph 2021-12-26 19:03:13 +01:00
2c415cd08b Message when no duration 2021-12-24 10:34:30 +01:00
441353fcb0 Forgot to add output 2021-12-24 10:32:56 +01:00
9ead771d6b Keeping track of duration 2021-12-24 10:23:04 +01:00
1e0d78f67b Changing README 2021-12-24 10:03:31 +01:00
73dd738970 Fixing loader 2021-12-24 09:53:53 +01:00
6ca118f717 Adding loader 2021-12-24 09:45:18 +01:00
e344d5ca38 New db schema 2021-12-24 09:45:07 +01:00
0e7221da07 Improved request 2021-12-24 09:41:47 +01:00
c1817425cd Adding loader 2021-12-23 22:42:26 +01:00
8d7bd04424 Adding notifications log 2021-12-23 19:55:11 +01:00
afd273081b Fix dates 2021-12-23 19:07:59 +01:00
9f4de0dee6 Fixing graph 2021-12-23 18:43:52 +01:00
bbc1afbddc Fixing details 2021-12-23 17:46:02 +01:00
387d910e44 Better styling 2021-12-23 16:55:10 +01:00
3981724c86 Adding date selector 2021-12-23 16:42:48 +01:00
4067a8448b More advanced task details with graph ! 2021-12-23 16:19:48 +01:00
a78320344f Fix 2021-12-23 12:21:53 +01:00
646e7de68f Starting task details 2021-12-23 12:20:55 +01:00
c832ca94fa Removing debug 2021-12-22 17:14:02 +01:00
a997dec6a5 Cleaning unused imports 2021-12-22 16:19:00 +01:00
6a9b886796 Renaming commands 2021-12-22 16:18:19 +01:00
327b2d8f63 Adding email notifications 2021-12-22 16:17:44 +01:00
30dba8447f Temporary fix 2021-12-21 21:58:43 +01:00
22b6b9de57 Adding message when there is no task 2021-12-21 21:48:23 +01:00
e48daa0008 Fixing customers 2021-12-21 20:33:42 +01:00
630ac562ee Fixing customers 2021-12-21 20:33:16 +01:00
c2e2532103 Fixing at last 2021-12-21 19:55:14 +01:00
a518468434 Fixing bug 2021-12-21 19:54:42 +01:00
a1ee1d0335 Fixing bug 2021-12-21 19:54:04 +01:00
9c170504cd FIxing bug 2021-12-21 19:53:30 +01:00
416f5294e6 Full rewrite Laravel style 2021-12-21 19:44:56 +01:00
cc03a0cf03 Starting migrations 2021-12-21 10:18:58 +01:00
b163ce6c5c Removing dd 2021-12-21 09:21:27 +01:00
4381db999c Fixing weird LUMEN timezone for DB 2021-12-20 23:54:03 +01:00
81bcaed76d Increasing refresh time 2021-12-20 23:17:16 +01:00
217428296a Fixing text search 2021-12-20 23:15:51 +01:00
7b999e994b Fixing time 2021-12-20 22:59:08 +01:00
4a36d03729 Fixing time 2021-12-20 22:58:37 +01:00
68d37f6db8 Fixing time 2021-12-20 22:57:45 +01:00
82f543a587 Fixing MySQL error 2021-12-20 22:30:05 +01:00
1cc4ac9a95 ?? 2021-12-20 22:19:44 +01:00
696d7fd299 Trying to bugfix 2021-12-20 22:17:07 +01:00
746e5afa8f Using query builder 2021-12-20 22:14:49 +01:00
caa8ca8ce5 Fixing typo 2021-12-20 21:54:06 +01:00
3a288ff15f Removing CleanHistory 2021-12-20 21:43:46 +01:00
3cdeded7a2 Adapting README 2021-12-20 21:40:34 +01:00
797dade52b Complete rewrite of the monitoring daemon in PHP 2021-12-20 21:35:53 +01:00
6e5f4e9736 Merge remote-tracking branch 'origin/main' into develop 2021-12-20 15:31:44 +01:00
8972e44e6b Screenshot 2021-12-20 15:31:06 +01:00
6b5b7455b5 Starting multipage with router 2021-12-20 15:25:53 +01:00
a5b047887d This version is finished 2021-12-20 15:24:37 +01:00
972914691b Fixing router 2021-12-20 15:22:16 +01:00
ca765a1328 Adding a little bit of style 2021-12-20 15:08:44 +01:00
bece9b724e Fixing styles 2021-12-20 14:28:35 +01:00
fb1f63d4bc Finished sync script 2021-12-20 14:07:58 +01:00
dfe833d670 changing frequency 2021-12-20 13:02:06 +01:00
74a7353b85 Adding a command for my own needs 2021-12-20 12:56:02 +01:00
10ef2dd4e8 Fixing group and images 2021-12-20 11:27:19 +01:00
ee16a6f8f7 Setting color 2021-12-20 11:20:47 +01:00
a6f26e0da1 Fixing group name 2021-12-20 11:17:15 +01:00
7ed25704e1 Checks can now be disabled 2021-12-20 11:10:54 +01:00
6483c516e3 Email subjects 2021-12-20 10:16:20 +01:00
37065e3a3e Using Lumen settings 2021-12-20 10:13:12 +01:00
209bf87c0a Restoring storage folder 2021-12-20 10:09:16 +01:00
374a6d34ea Restoring storage folder 2021-12-20 10:08:47 +01:00
cccaa10c23 Fixing .env.example 2021-12-20 10:03:25 +01:00
df690a63be Fixing README 2021-12-20 09:58:23 +01:00
b32847e827 Moving files 2021-12-20 09:57:06 +01:00
5079e836ca Increasing timeout 2021-12-20 09:51:03 +01:00
6595d09bf2 Migrating everything to Lumen 2021-12-20 07:42:12 +01:00
192aeefc9b Replacing PNG with SVG 2021-12-19 22:47:14 +01:00
0eb5269b9d Fixing setting 2021-12-19 22:24:50 +01:00
45d728410f Fixing frequency display 2021-12-19 22:14:27 +01:00
d4e25c1033 Fixing frequency display 2021-12-19 22:12:55 +01:00
d2e302dcc7 Fixing README 2021-12-19 22:10:38 +01:00
fda3a74a29 Small design improvements 2021-12-19 22:03:34 +01:00
92e6921cf9 Complete rewrite full VueJS 2021-12-19 19:58:07 +01:00
6074009936 Adding distribution folder 2021-12-19 15:53:28 +01:00
a3fabb1609 Smooth scrolling 2021-12-19 14:55:45 +01:00
ea673e0083 Improving quickview 2021-12-19 14:46:08 +01:00
d34a4ff21f Adding task output into db 2021-12-19 10:21:24 +01:00
d3320821c0 Merge branch 'main' of github.com:axeloz/monitolite into main 2021-12-19 10:12:43 +01:00
3bba0930f3 Adding output 2021-12-19 10:12:40 +01:00
72fecab8a6
Update README.md 2021-12-19 10:07:22 +01:00
3569ccabb2
Update README.md 2021-12-19 09:52:15 +01:00
9a98c8076b New database schema 2021-12-19 09:33:39 +01:00
187eae16e1 New version with VueJS 2021-12-18 20:42:21 +01:00
a0fd4aea60
Update DB.php 2021-12-17 20:11:39 +01:00
a49f577dae
Update monitolite.pl 2021-12-17 19:32:25 +01:00
5b87f3c580
Update DB.php 2021-12-17 19:04:50 +01:00
c9bf661626
Update index.php 2021-12-17 19:03:50 +01:00
68c88c7fcf
Update DB.php 2021-12-17 18:32:36 +01:00
123 changed files with 28412 additions and 1671 deletions

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2

38
.env.example Executable file → Normal file
View file

@ -1,14 +1,32 @@
DB_TYPE=mysql APP_NAME=Monitolite
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
APP_TIMEZONE=UTC
DB_TIMEZONE="+1:00"
LOG_CHANNEL=stack
LOG_SLACK_WEBHOOK_URL=
DB_CONNECTION=mysql
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
DB_USER=vagrant
DB_PASSWORD=vagrant
DB_NAME=monitoring
DB_PORT=3306 DB_PORT=3306
SMTP_HOST=localhost DB_DATABASE=homestead
SMTP_USER= DB_USERNAME=homestead
SMTP_PASSWORD= DB_PASSWORD=secret
SMTP_PORT=80
SMTP_SSL=1 CACHE_DRIVER=file
MAIL_FROM=axel@monitolite.fr QUEUE_CONNECTION=sync
NB_TRIES=3 NB_TRIES=3
ARCHIVE_DAYS=10 ARCHIVE_DAYS=10
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=noreply@monitolite.fr
MAIL_FROM_NAME="Monitolite"

7
.gitignore vendored
View file

@ -1,2 +1,7 @@
web/vendor/**/* /vendor
/.idea
Homestead.json
Homestead.yaml
.env .env
.phpunit.result.cache
/node_modules

6
.styleci.yml Normal file
View file

@ -0,0 +1,6 @@
php:
preset: laravel
disabled:
- unused_use
js: true
css: true

184
README.md
View file

@ -1,86 +1,98 @@
# MONITOLITE # MONITOLITE
**MonitoLite** is an old project I recently dug up from my archives. I developed this script years ago for my personal needs. **MonitoLite** is an old project I recently dug up from my archives. I developed this script years ago for my personal needs.
I figured it could be useful for others so here we are. I figured it could be useful for others so I **rewrote** and **updated** it from scratch in a modern way.
## What it does ## What it does
**MonitoLite** is a very simple monitoring tool developed in Perl. It supports : **MonitoLite** is a very simple monitoring tool developed in PHP powered by Lumen (by Laravel). It supports :
* **ping monitoring**: sends a `ping` command to the specified host. Raises an alert if the host is down * **PING monitoring**: sends a `ping` command to the specified host. Raises an alert if the host is down
* **http monitoring**: requests the provided URL and raises an alert if the URL returns an error. Optionally you may specify a string to search on the page using the `param` database field. It raises an alert if the specified text could not be found on the page. * **HTTP monitoring**: requests the provided URL and raises an alert if the URL returns an error. Optionally you may specify a string to search on the page using the `param` database field. It raises an alert if the specified text could not be found on the page.
* **FTP monitoring**: connects to the provided FTP server as anonymous (authentication not supported yet).
In case of an alert, the script sends an email notifications to the specified contacts (one or many). * **DNS monitoring**: runs a DNS lookup on a given DNS server for the hostname specified in the params
The script also sends a recovery email notification when the alert is over.
In case of an alert, the script sends an email notifications to the specified contacts (one or many).
It uses a SQL backend for handling the tasks and the status of the tasks. The script also sends a recovery email notification when the alert is over.
Tested on MySQL only but should support other SQL-based DBMS.
It uses a SQL backend for handling the tasks and the status of the tasks.
It comes with a very straightforward dashboard written in PHP. This is **optional**, the `monitolite.pl` script runs as standalone. Tested on MySQL only but should support other SQL-based DBMS.
**Caution**: the backend is not password-protected. You should make sure you add your own security layer via IP filtering or basic authentication.
It comes with a very straightforward dashboard written in PHP. This is **optional**, the monitoring script runs as standalone.
**Caution**: the backend is not password-protected. You should make sure you add your own security layer via IP filtering or basic authentication.
I rewrote a couple of things today to make sure the script still works.
## Demo
## Screenshot
[DEMO](https://monitolite.mabox.eu)
![screenshot](https://github.com/axeloz/monitolite/raw/main/screenshot.png "Logo")
## Screenshot
## Requirements ### Tasks list with quick preview
* Perl : with DBI, Dotenv, Net::Ping, Email::MIME, Email::Sender::Simple, Email::Sender::Transport::SMTP, LWP::Simple, LWP::UserAgent, LWP::Protocol::https ![screenshot](https://github.com/axeloz/monitolite/raw/main/screenshot.png "Logo")
* a MTA: Postfix, ...
* PHP 7+ (optional): with PDO ### Task details with graph and history
* a webserver (optional): Apache, Nginx, ...
* a Database server: MySQL, other? (untested) ![screenshot](https://github.com/axeloz/monitolite/raw/main/screenshot2.png "Logo")
* access to CRON tasks
* possibly `root` access for the `ping` command to run (needs confirmation)
## Requirements
## Installation * PHP 7+ with cURL, `exec` command allowed, MySQL extension via PDO
* a MTA: Postfix, or an external SMTP ...
* clone this repo * a webserver (optional): Apache, Nginx, ...
* install Perl dependencies * a Database server: MySQL, other? (untested)
* install PHP composer dependencies: `cd ./web && composer install` * access to CRON tasks
* create a Database and import the schema from `sql/create.sql`
* create your own `.env` file: `cp .env.example .env` and adapt it to your needs ## Installation
* create a webserver vhost with document root to the `web` directory
* add tasks and contacts into the database (no backend yet) * clone this repo
* run the script: `perl monitolite.pl` * install PHP composer dependencies: `cd ./web && composer install`
* check the web dashboard for results. * create a Database and import the initial schema using `php artisan migrate`
* when everything works, you may create a CRON `* * * * * cd <change/this/to/the/correct/path> && /usr/bin/perl monitolite.pl > /dev/null` * create your own `.env` file: `cp .env.example .env` and adapt it to your needs
* create a webserver vhost with document root to the `public` directory
* add tasks and contacts into the database (no GUI for CRUD yet)
## Settings * run the script: `cd /var/www/<your-path> && php artisan monitolite:run`
* check the output of the command for results.
* DB_TYPE=mysql * if everything works, you may create a CRON `* * * * * cd /var/www/<your-path> && php artisan monitolite:run > /dev/null`
* DB_HOST=127.0.0.1
* DB_USER=vagrant
* DB_PASSWORD=vagrant ## Settings
* DB_NAME=monitoring
* DB_PORT=3306 * APP_NAME=Monitolite
* SMTP_HOST=localhost * APP_ENV=production
* SMTP_USER= * APP_KEY=<GENERATE KEY HERE>
* SMTP_PASSWORD= * APP_DEBUG=false
* SMTP_PORT=80 * APP_URL=http://localhost
* SMTP_SSL=1 * APP_TIMEZONE=UTC
* MAIL_FROM=axel@monitolite.fr * DB_TIMEZONE="+1:00"
* NB_TRIES=3 * DB_CONNECTION=mysql
* ARCHIVE_DAYS=10 * DB_HOST=127.0.0.1
* DB_PORT=3306
## MORE INFORMATION COMING SOON. * DB_DATABASE=homestead
* DB_USERNAME=homestead
## TODO * DB_PASSWORD=secret
* MAIL_MAILER=smtp
* Make CRUD possible from the backend for adding tasks and contacts * MAIL_HOST=localhost
* Multithreading * MAIL_PORT=25
* SMS Notifications * MAIL_USERNAME=
* Better dashboard * MAIL_PASSWORD=
* Protected backend with authentication * MAIL_ENCRYPTION=
* Create an installation script * MAIL_FROM_ADDRESS=noreply@monitolite.fr
* Raise alert when tasks are not run at the correct frequency (CRON down or other reason) * MAIL_FROM_NAME="Monitolite"
* Set a notification capping limit to prevent many notifications to be sent in case of an up-and-down host * NB_TRIES=3
* Add a notification history log * ARCHIVE_DAYS=10
* Keep track of tasks response time
* Daemonize the script (instead of CRONs)
## TODO
[ ] Make CRUD possible from the backend for adding tasks and contacts
[ ] Multithreading
[ ] SMS Notifications
[ ] Protected backend with authentication
[ ] Create an installation script
[ ] Raise alert when tasks are not run at the correct frequency (CRON down or other reason)
[x] Set a notification capping limit to prevent many notifications to be sent in case of an up-and-down host
[x] Add a notification history log
[x] Keep track of tasks response time
[ ] Daemonize the script (instead of CRONs)

View file

View file

@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class CleanHistory extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monitolite:purge';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Aggregates and cleans tasks history';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$lastweek = \Carbon\Carbon::now()->subWeek();
$history = app('db')->select('
SELECT * FROM task_history as h
WHERE created_at < :lastweek
', [
'lastweek' => $lastweek
]);
}
}

View file

@ -0,0 +1,341 @@
<?php
namespace App\Console\Commands;
use \Exception;
use App\Models\Task;
use App\Models\TaskHistory;
use App\Models\Notification;
use Illuminate\Console\Command;
class RunMonitoring extends Command
{
private $limit = 50;
private $max_tries = 3;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monitolite:run
{--limit=50 : the number of tasks to handle in one run}
{--task= : the ID of an individual task to handle}
{--force : handles tasks even if they are pending}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Executes all the monitoring tasks';
/**
* Storing all the results for output
*/
private $results;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$count = 0;
$limit = $this->option('limit') ?? $this->limit;
$this->max_tries = env('NB_TRIES', $this->max_tries);
// If a force has been asked via command line
$force = false;
if (! empty($this->option('force'))) {
if (empty($this->option('task'))) {
if ($this->confirm('You asked me to force the execution (--force) but you did not specify a particular task ID (--task). I might have to handle a large amount of tasks. Are you sure?')) {
$force = true;
}
}
else {
$force = true;
}
}
// Getting pending tasks
$tasks = Task::where(function($query) use ($force) {
$query->whereRaw('DATE_SUB(NOW(), INTERVAL frequency SECOND) > executed_at');
$query->orWhereBetween('attempts', [1, ($this->max_tries - 1)]);
$query->orWhereNull('executed_at');
if ($force === true) {
$query->orWhere('id', '>', 0);
}
})
->where('active', 1)
->orderBy('attempts', 'DESC')
->orderBy('executed_at', 'ASC')
->take($limit)
;
// If a particular task has been set via the command line
if (! empty($this->option('task'))) {
$tasks = $tasks->where('id', '=', $this->option('task'));
}
// Now getting tasks
$tasks = $tasks->get();
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).' tasks to process. Better get started ...');
$this->newLine();
$bar = $this->output->createProgressBar(count($tasks));
$bar->start();
foreach ($tasks as $task) {
$bar->advance();
// Getting current task last status
$previous_status = $task->status;
try {
switch ($task->type) {
case 'ping':
$result = $this->checkPing($task);
break;
case 'http':
$result = $this->checkRequest($task, CURLPROTO_HTTP | CURLPROTO_HTTPS);
break;
case 'ftp':
$result = $this->checkRequest($task, CURLPROTO_FTP | CURLPROTO_FTPS);
break;
case 'dns':
$result = $this->checkDns($task);
break;
default:
// Nothing to do here
throw new Exception('Unknown type "'.$task->type.'"');
}
$new_status = 1;
$history = $this->saveHistory($task, true, 'success', $result['duration'] ?? null);
}
catch(MonitoringException $e) {
$history = $this->saveHistory($task, false, $e->getMessage());
}
catch(Exception $e) {
//TODO: handle system exception differently
//$history = $this->saveHistory($task, false, $e->getMessage());
$this->error($e->getMessage());
}
finally {
// Changing task timestamps and status
$task->executed_at = $history->created_at; # Using the same timestamp as the task history
$task->attempts = $history->status == 1 ? 0 : $task->attempts + 1; # when success, resetting counter
/**
* We don't want to change the primary status in the task table
* as long as failed tasks have reached the max tries limit
* In the cast of a success, we can change the status straight away
*/
if ($history->status == 0 && $task->attempts >= $this->max_tries) {
$task->status = 0;
}
else if ($history->status === 1) {
$task->status = 1;
}
if (! $task->save()) {
throw new Exception('Cannot save task details');
}
// Task status has changed
// But not from null (new task)
if (! is_null($previous_status) && $task->status != $previous_status) {
// If host is up, no double-check
if ($task->status == 1 || ($task->status == 0 && $task->attempts == $this->max_tries)) {
Notification::addNotificationTask($history);
}
}
}
}
$bar->finish();
$this->newLine(2);
if (!empty($this->results)) {
$this->table(
['ID', 'Host', 'Type', 'Result', 'Attempts', 'Message'],
$this->results
);
}
}
final private function saveHistory(Task $task, $status, $output = null, $duration = null) {
$date = date('Y-m-d H:i:s');
// Inserting new history
$insert = new TaskHistory;
$insert->status = $status === true ? 1 : 0;
$insert->created_at = $date;
$insert->output = $output ?? '';
$insert->duration = $duration;
$insert->task_id = $task->id;
if (! $insert->save()) {
throw new Exception('Cannot insert history for task #'.$task->id);
}
$this->results[] = [
'id' => $task->id,
'host' => $task->host,
'type' => $task->type,
'result' => $status === true ? 'OK' : 'FAILED',
'attempts' => $task->attempts,
'message' => $output
];
return $insert;
}
final private function checkPing(Task $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 checkDns(Task $task) {
if (! function_exists('exec') || ! is_callable('exec')) {
throw new MonitoringException('The "exec" command is required');
}
if (is_null($task->params) || empty($task->params)) {
throw new Exception('Params are required');
}
$cmd = 'nslookup '.trim($task->params).' '.$task->host;
// If command failed
if (false === $exec = exec($cmd.' '.$task->host, $output, $code)) {
throw new MonitoringException('Unable to execute DNS lookup');
}
// If command returned a non-zero code
if ($code > 0) {
throw new MonitoringException('DNS lookup task failed ('.$exec.')');
}
return true;
}
final private function checkRequest(Task $task, $protocol = CURLPROTO_HTTP | CURLPROTO_HTTPS) {
if (app()->environment() == 'local') {
//throw new MonitoringException('Forcing error for testing');
}
// Preparing cURL
$opts = [
CURLOPT_HEADER => true,
CURLOPT_HTTPGET => true,
CURLOPT_FRESH_CONNECT => true,
CURLOPT_PROTOCOLS => $protocol,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 3,
CURLOPT_FAILONERROR => true,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_URL => trim($task->host)
];
$ch = curl_init();
curl_setopt_array($ch, $opts);
if ($result = curl_exec($ch)) {
$duration = curl_getinfo($ch, CURLINFO_TOTAL_TIME);
// We have nothing to check into the page
// So for me, this is a big YES
if (empty($task->params)) {
return [
'result' => true,
'duration' => $duration
];
}
// We are looking for a string in the page
else {
if (strpos($result, $task->params) !== false) {
return [
'result' => true,
'output' => 'String was found in the page',
'duration' => $duration
];
}
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 {}

View file

@ -0,0 +1,94 @@
<?php
namespace App\Console\Commands;
use \Exception;
use App\Models\Notification;
use App\Mail\TaskNotification;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Mail;
class SendNotifications extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monitolite:notify
{--limit=1000 : maximum notifications to process at once }';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends the notifications alerts';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$notifications = Notification::with(['contact', 'task_history', 'task_history.task'])
->where('status', '=', 'pending')
->orderBy('created_at', 'ASC')
->limit($this->option('limit'), 1000)
->get()
;
$results = [];
if (! empty($notifications)) {
foreach ($notifications as $n) {
if (! isset($results[$n->contact_id])) {
$results[$n->contact_id] = [
'contact' => $n->contact->toArray(),
'tasks' => []
];
}
//else {
$history = $n->task_history;
$task = $history->task;
if (! isset($results[$n->contact_id]['tasks'][$task->id])) {
$results[$n->contact_id]['tasks'][$task->id] = [
'history' => []
];
}
array_push($results[$n->contact_id]['tasks'][$task->id]['history'], $history->toArray());
//}
}
}
if (count($results) > 0) {
foreach ($results as $r) {
$this->info('Sending notifications to '.$r['contact']['email']);
try {
Mail::to($r['contact']['email'])->send(new TaskNotification($r));
Notification::where('contact_id', '=', $r['contact']['id'])->update(
['status' => 'sent']
);
}
catch (Exception $e) {
Notification::where('contact_id', '=', $r['contact']['id'])->update(
['status' => 'error']
);
}
}
}
}
}

View file

@ -0,0 +1,156 @@
<?php
namespace App\Console\Commands;
/**
* R E A D T H I S :
* THIS COMMAND IS FOR MY OWN NEEDS ONLY
* IT SYNCS ALL THE TASKS FROM A DISTANT API
* IT IS PROBABLY WORTHLESS FOR YOU
*/
use Illuminate\Console\Command;
class SyncCustomers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'monitolite:sync';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Synchronizes all customers\' websites with Monitolite';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (env('CMS_ENABLE_SYNC') != true) {
$this->error('Customers synchronisation is globally disabled.');
return null;
}
$this->line('Starting synchronisation');
$customers = $tasks = $contacts = [];
// Getting active customers
$opts = [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_FAILONERROR => true,
CURLOPT_POSTFIELDS => [
'access' => env('CMS_API_ACCESS'),
'token' => env('CMS_API_TOKEN')
],
CURLOPT_URL => env('CMS_API_URL')
];
$ch = curl_init();
curl_setopt_array($ch, $opts);
if ($result = curl_exec($ch)) {
$hosts = [];
$customers = json_decode($result);
$bar = $this->output->createProgressBar(count($customers));
$bar->start();
// Getting existing tasks
$tasks_flat = [];
$tasks = app('db')->select('SELECT * FROM tasks');
foreach ($tasks as $t) {
$tasks_flat[$t->id] = preg_replace('~^https?://~', '', trim($t->host));
}
// Getting existing contacts
$contacts = app('db')->select('SELECT * FROM contacts');
// Getting existing groups
$groups_flat = [];
$groups = app('db')->select('SELECT * FROM `groups`');
foreach ($groups as $g) {
$groups_flat[$g->id] = $g->name;
}
// First we insert new customers
foreach($customers as $c) {
$bar->advance();
$hosts[] = 'https://'.trim($c->domain);
// Checking group existence
if (empty($groups_flat[$c->id])) {
app('db')->insert('INSERT INTO `groups` (`id`, `name`) VALUE (?, ?)', [ $c->id, $c->name ]);
$groups_flat[$c->id] = $c->name;
}
if (false === array_search(trim($c->domain), $tasks_flat)) {
$ret = app('db')->insert('
INSERT INTO tasks (`host`, `type`, `params`, `created_at`, `frequency`, `active`, `group_id`)
VALUES(:host, :type, :params, :creation_date, :frequency, :active, :group_id)
', [
'host' => 'https://'.trim($c->domain),
'type' => 'http',
'params' => 'restovisio.com',
'creation_date' => date('Y-m-d H:i:s'),
'frequency' => 3600,
'active' => 1,
'group_id' => $c->id
]);
if ($ret === true) {
$task_id = app('db')->getPdo()->lastInsertId();
// Inserting contacts
foreach ($contacts as $c) {
app('db')->insert('INSERT INTO contact_task (`task_id`, `contact_id`) VALUES (:task_id, :contact_id)', [
'task_id' => $task_id,
'contact_id' => $c->id
]);
}
}
}
}
$bar->finish();
$this->newLine(2);
$this->line('Checking tasks to delete');
$bar = $this->output->createProgressBar(count($tasks));
$bar->start();
// Then we delete old customers
foreach ($tasks as $t) {
$bar->advance();
if (false === array_search($t->host, $hosts)) {
// Must delete task
//$this->line('must delete '.$t->host);
//app('db')->delete('DELETE FROM `tasks` WHERE host = ?', [$t->host]);
}
}
$bar->finish();
}
}
}

50
app/Console/Kernel.php Normal file
View file

@ -0,0 +1,50 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Laravel\Lumen\Console\Kernel as ConsoleKernel;
use App\Console\Commands\SyncCustomers;
use App\Console\Commands\RunMonitoring;
use App\Console\Commands\SendNotifications;
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
SyncCustomers::class,
RunMonitoring::class,
SendNotifications::class
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
/**
* This is for my own needs
* You may safely remove this scheduled task
*/
if (env('CMS_ENABLE_SYNC') == true) {
$schedule->command('monitolite:sync')->hourly();
}
/**
* This is the main monitoring task
*/
$schedule->command('monitolite:run')->everyMinute();
/**
* Send all the notifications
*/
$schedule->command('monitolite:notify')->everyMinute();
}
}

10
app/Events/Event.php Normal file
View file

@ -0,0 +1,10 @@
<?php
namespace App\Events;
use Illuminate\Queue\SerializesModels;
abstract class Event
{
use SerializesModels;
}

View file

@ -0,0 +1,16 @@
<?php
namespace App\Events;
class ExampleEvent extends Event
{
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace App\Exceptions;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Validation\ValidationException;
use Laravel\Lumen\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
*
* @var array
*/
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Throwable $exception
* @return void
*
* @throws \Exception
*/
public function report(Throwable $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse
*
* @throws \Throwable
*/
public function render($request, Throwable $exception)
{
return parent::render($request, $exception);
}
}

View file

@ -0,0 +1,172 @@
<?php
namespace App\Http\Controllers;
use Exception;
use \Carbon\Carbon;
use App\Models\Task;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class ApiController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
//
}
public function getTasks() {
$tasks = [];
$query = Task
::leftJoin('groups', 'groups.id', 'tasks.group_id')
->select(
'tasks.id', 'tasks.host', 'tasks.status', 'tasks.type', 'tasks.params', 'tasks.frequency', 'tasks.created_at', 'tasks.executed_at', 'tasks.active', 'tasks.group_id',
'groups.name as group_name')
->get()
;
//dd($query->toSql());
foreach ($query as $t) {
if (is_null($t->group_id)) {
$group_id = $t->id;
$group_name = 'ungrouped';
}
else {
$group_id = $t->group_id;
$group_name = $t->group_name;
}
if (empty($tasks[$group_id])) {
$tasks[$group_id] = [
'id' => $group_id,
'name' => $group_name,
'tasks' => null
];
}
$tasks[$group_id]['tasks'][$t->id] = $t;
}
return response()->json($tasks);
}
public function getTaskDetails(Request $request, $id) {
$days = ($request->input('days', 15) - 1);
$task = Task::with(['group'])
->findOrFail($id)
;
if (! is_null($task)) {
// First, we get the first date of the stats
// In this case, one month ago
$first_day = Carbon::now()->startOfDay()->subDays($days);
// Then we get all history for the past month
$history = $task
->history()
->orderBy('created_at', 'desc')
->where('created_at', '>', $first_day->toDateString())
->selectRaw('id, date(created_at) as date, created_at, status, duration, output')
->get()
;
// Then we start building an array for the entire month
$stats = $times = [];
$tmpdate = Carbon::now()->subDays($days);
do {
$stats['uptime'][$tmpdate->toDateString()] = [
'up' => 0,
'down' => 0
];
$stats['times'][$tmpdate->toDateString()] = [
'duration' => 0,
'count' => 0
];
$tmpdate = $tmpdate->addDay();
}
while ($tmpdate->lt(Carbon::now()));
// Then we populate the stats data
$prev = null;
if (! is_null($history)) {
$history = $history->reverse();
foreach ($history as $k => $r) {
if (empty($stats['uptime'][$r->date])) {
$stats['uptime'][$r->date] = [
'up' => 0,
'down' => 0
];
}
// Populating the stats
if ($r->status == 1) {
++$stats['uptime'][$r->date]['up'];
}
else {
++$stats['uptime'][$r->date]['down'];
}
// Populating the response times
if ($r->status == 1 && $r->duration > 0) {
$stats['times'][$r->date]['duration'] += $r->duration;
$stats['times'][$r->date]['count'] ++;
}
// We only take tasks when status has changed between them
if (! is_null($prev) && $r->status == $prev) {
unset($history[$k]);
}
$prev = $r->status;
}
}
// Getting the notifications sent
$notifications = $task
->notifications()
->with(['contact', 'task_history'])
->where('notifications.created_at', '>', $first_day->toDateString())
->orderBy('notifications.created_at', 'desc')
->get()
;
return response()->json([
'task' => $task,
'stats' => $stats,
'history' => $history,
'notifications' => $notifications,
'first_day' => $first_day->toDateTimeString()
]);
}
}
public function toggleTaskStatus(Request $request, $id) {
$active = $request->input('active', null);
if (is_null($active)) {
throw new ApiException('Invalid parameters');
}
$active = intval($active);
$task = Task::findOrFail($id);
$task->active = $active;
if ($task->save()) {
return response()->json($task);
}
else {
throw new ApiException('Cannot disable this task');
}
}
}
class ApiException extends Exception {}

View file

@ -0,0 +1,10 @@
<?php
namespace App\Http\Controllers;
use Laravel\Lumen\Routing\Controller as BaseController;
class Controller extends BaseController
{
//
}

View file

@ -0,0 +1,44 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Factory as Auth;
class Authenticate
{
/**
* The authentication guard factory instance.
*
* @var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* Create a new middleware instance.
*
* @param \Illuminate\Contracts\Auth\Factory $auth
* @return void
*/
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if ($this->auth->guard($guard)->guest()) {
return response('Unauthorized.', 401);
}
return $next($request);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Closure;
class ExampleMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
return $next($request);
}
}

24
app/Jobs/Job.php Normal file
View file

@ -0,0 +1,24 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
abstract class Job implements ShouldQueue
{
/*
|--------------------------------------------------------------------------
| Queueable Jobs
|--------------------------------------------------------------------------
|
| This job base class provides a central location to place any logic that
| is shared across all of your jobs. The trait included with the class
| provides access to the "queueOn" and "delay" queue helper methods.
|
*/
use InteractsWithQueue, Queueable, SerializesModels;
}

View file

@ -0,0 +1,31 @@
<?php
namespace App\Listeners;
use App\Events\ExampleEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class ExampleListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param \App\Events\ExampleEvent $event
* @return void
*/
public function handle(ExampleEvent $event)
{
//
}
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Mail;
use App\Models\Notification;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class TaskNotification extends Mailable
{
use SerializesModels;
/**
* The order instance.
*
* @var \App\Models\Order
*/
protected $report;
/**
* Create a new message instance.
*
* @param \App\Models\Order $order
* @return void
*/
public function __construct($report)
{
$this->report = $report;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this
->subject('Monitolite Alert Report')
->from(env('MAIL_FROM_ADDRESS', 'noreply@monitolite.fr'), env('MAIL_FROM_NAME', 'Monitolite'))
->markdown('emails.notification')
->with([
'report' => $this->report,
'url' => env('APP_URL')
])
;
}
}

22
app/Models/Contact.php Normal file
View file

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Contact extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
}

24
app/Models/Group.php Normal file
View file

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Group extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
public function tasks() {
return $this->hasMany('App\Models\Task');
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Notification extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
public function contact() {
return $this->belongsTo('App\Models\Contact');
}
public function task_history() {
return $this->belongsTo('App\Models\TaskHistory');
}
public static function addNotificationTask(TaskHistory $history) {
$contacts = $history->task->contacts()->get();
if (! is_null($contacts)) {
foreach ($contacts as $c) {
$notification = new Notification;
$notification->contact_id = $c->id;
$notification->task_history_id = $history->id;
$notification->status = 'pending';
$notification->save();
}
}
}
}

40
app/Models/Task.php Normal file
View file

@ -0,0 +1,40 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
public $timestamps = [
'created_at',
'updated_at',
'executed_at'
];
public function group() {
return $this->belongsTo('App\Models\Group');
}
public function contacts() {
return $this->belongsToMany('App\Models\Contact');
}
public function history() {
return $this->hasMany('App\Models\TaskHistory');
}
public function notifications() {
return $this->hasManyThrough('App\Models\Notification', 'App\Models\TaskHistory');
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TaskContact extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TaskHistory extends Model
{
use HasFactory;
protected $table = 'task_history';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [];
public function notifications() {
return $this->hasMany('App\Models\Notification');
}
public function task() {
return $this->belongsTo('App\Models\Task');
}
}

33
app/Models/User.php Normal file
View file

@ -0,0 +1,33 @@
<?php
namespace App\Models;
use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Lumen\Auth\Authorizable;
class User extends Model implements AuthenticatableContract, AuthorizableContract
{
use Authenticatable, Authorizable, HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email',
];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'password',
];
}

View file

@ -0,0 +1,18 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Boot the authentication services for the application.
*
* @return void
*/
public function boot()
{
// Here you may define how you wish users to be authenticated for your Lumen
// application. The callback which receives the incoming request instance
// should return either a User instance or null. You're free to obtain
// the User instance via an API token or any other method necessary.
$this->app['auth']->viaRequest('api', function ($request) {
if ($request->input('api_token')) {
return User::where('api_token', $request->input('api_token'))->first();
}
});
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Providers;
use Laravel\Lumen\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* @var array
*/
protected $listen = [
\App\Events\ExampleEvent::class => [
\App\Listeners\ExampleListener::class,
],
];
}

35
artisan Executable file
View file

@ -0,0 +1,35 @@
#!/usr/bin/env php
<?php
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| First we need to get an application instance. This creates an instance
| of the application / container and bootstraps the application so it
| is ready to receive HTTP / Console requests from the environment.
|
*/
$app = require __DIR__.'/bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/
$kernel = $app->make(
'Illuminate\Contracts\Console\Kernel'
);
exit($kernel->handle(new ArgvInput, new ConsoleOutput));

134
bootstrap/app.php Normal file
View file

@ -0,0 +1,134 @@
<?php
require_once __DIR__.'/../vendor/autoload.php';
(new Laravel\Lumen\Bootstrap\LoadEnvironmentVariables(
dirname(__DIR__)
))->bootstrap();
date_default_timezone_set(env('APP_TIMEZONE', 'UTC'));
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| Here we will load the environment and create the application instance
| that serves as the central piece of this framework. We'll use this
| application as an "IoC" container and router for this framework.
|
*/
$app = new Laravel\Lumen\Application(
dirname(__DIR__)
);
$app->withFacades();
$app->withEloquent();
/*
|--------------------------------------------------------------------------
| Register Container Bindings
|--------------------------------------------------------------------------
|
| Now we will register a few bindings in the service container. We will
| register the exception handler and the console kernel. You may add
| your own bindings here if you like or you can make another file.
|
*/
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
/*
|--------------------------------------------------------------------------
| Register Config Files
|--------------------------------------------------------------------------
|
| Now we will register the "app" configuration file. If the file exists in
| your configuration directory it will be loaded; otherwise, we'll load
| the default version. You may register other files below as needed.
|
*/
/**
* This is a required HACK for Lumen
*
* @return string
*/
function setDbTimezone() {
$offset = timezone_offset_get(new \DateTimeZone(date_default_timezone_get()), new \DateTime());
return sprintf("%s%02d:%02d", ($offset >= 0) ? '+' : '-', abs($offset / 3600), abs($offset % 3600));
}
$app->configure('app');
$app->configure('database');
$app->configure('mail');
$app->alias('mail.manager', Illuminate\Mail\MailManager::class);
$app->alias('mail.manager', Illuminate\Contracts\Mail\Factory::class);
$app->alias('mailer', Illuminate\Mail\Mailer::class);
$app->alias('mailer', Illuminate\Contracts\Mail\Mailer::class);
$app->alias('mailer', Illuminate\Contracts\Mail\MailQueue::class);
/*
|--------------------------------------------------------------------------
| Register Middleware
|--------------------------------------------------------------------------
|
| Next, we will register the middleware with the application. These can
| be global middleware that run before and after each request into a
| route or middleware that'll be assigned to some specific routes.
|
*/
// $app->middleware([
// App\Http\Middleware\ExampleMiddleware::class
// ]);
// $app->routeMiddleware([
// 'auth' => App\Http\Middleware\Authenticate::class,
// ]);
/*
|--------------------------------------------------------------------------
| Register Service Providers
|--------------------------------------------------------------------------
|
| Here we will register all of the application's service providers which
| are used to bind services into the container. Service providers are
| totally optional, so you are not required to uncomment this line.
|
*/
// $app->register(App\Providers\AppServiceProvider::class);
// $app->register(App\Providers\AuthServiceProvider::class);
// $app->register(App\Providers\EventServiceProvider::class);
$app->register(Illuminate\Mail\MailServiceProvider::class);
/*
|--------------------------------------------------------------------------
| Load The Application Routes
|--------------------------------------------------------------------------
|
| Next we will include the routes file so that they can all be added to
| the application. This will provide all of the URLs the application
| can respond to, as well as the controllers that may handle them.
|
*/
$app->router->group([
'namespace' => 'App\Http\Controllers',
], function ($router) {
require __DIR__.'/../routes/web.php';
});
return $app;

41
composer.json Normal file
View file

@ -0,0 +1,41 @@
{
"name": "laravel/lumen",
"description": "The Laravel Lumen Framework.",
"keywords": ["framework", "laravel", "lumen"],
"license": "MIT",
"type": "project",
"require": {
"php": "^7.3|^8.0",
"illuminate/mail": "^8.77",
"laravel/lumen-framework": "^8.3.1"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
"mockery/mockery": "^1.3.1",
"phpunit/phpunit": "^9.5.10"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"classmap": [
"tests/"
]
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true
},
"minimum-stability": "dev",
"prefer-stable": true,
"scripts": {
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
]
}
}

7847
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

137
config/database.php Normal file
View file

@ -0,0 +1,137 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for all database work. Of course
| you may use many connections at once using the Database library.
|
*/
'default' => env('DB_CONNECTION', 'mysql'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Here are each of the database connections setup for your application.
| Of course, examples of configuring each database platform that is
| supported by Laravel is shown below to make development simple.
|
|
| All database work in Laravel is done through the PHP PDO facilities
| so make sure you have the driver for your particular database of
| choice installed on your machine before you begin development.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => env('DB_PREFIX', ''),
],
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => env('DB_PREFIX', ''),
'strict' => env('DB_STRICT_MODE', true),
'engine' => env('DB_ENGINE', null),
'timezone' => setDbTimezone(),
],
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 5432),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => env('DB_PREFIX', ''),
'schema' => env('DB_SCHEMA', 'public'),
'sslmode' => env('DB_SSL_MODE', 'prefer'),
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', 1433),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => env('DB_PREFIX', ''),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run in the database.
|
*/
'migrations' => 'migrations',
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer set of commands than a typical key-value systems
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'lumen'), '_').'_database_'),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],
];

117
config/mail.php Normal file
View file

@ -0,0 +1,117 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Mailer
|--------------------------------------------------------------------------
|
| This option controls the default mailer that is used to send any email
| messages sent by your application. Alternative mailers may be setup
| and used as needed; however, this mailer will be used by default.
|
*/
'default' => env('MAIL_MAILER', 'smtp'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations
|--------------------------------------------------------------------------
|
| Here you may configure all of the mailers used by your application plus
| their respective settings. Several examples have been configured for
| you and you are free to add your own as your application requires.
|
| Laravel supports a variety of mail "transport" drivers to be used while
| sending an e-mail. You will specify which one you are using for your
| mailers below. You are free to add additional mailers as required.
|
| Supported: "smtp", "sendmail", "mailgun", "ses",
| "postmark", "log", "array", "failover"
|
*/
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', 587),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
],
'ses' => [
'transport' => 'ses',
],
'mailgun' => [
'transport' => 'mailgun',
],
'postmark' => [
'transport' => 'postmark',
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -t -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
],
/*
|--------------------------------------------------------------------------
| Global "From" Address
|--------------------------------------------------------------------------
|
| You may wish for all e-mails sent by your application to be sent from
| the same address. Here, you may specify a name and address that is
| used globally for all e-mails that are sent by your application.
|
*/
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'noreply@monitolite.fr'),
'name' => env('MAIL_FROM_NAME', 'Monitolite'),
],
/*
|--------------------------------------------------------------------------
| Markdown Mail Settings
|--------------------------------------------------------------------------
|
| If you are using Markdown based email rendering, you may configure your
| theme and component paths here, allowing you to customize the design
| of the emails. Or, you may simply stick with the Laravel defaults!
|
*/
'markdown' => [
'theme' => 'default',
'paths' => [
resource_path('views/vendor/mail'),
],
],
];

View file

@ -0,0 +1,29 @@
<?php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class UserFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = User::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->name,
'email' => $this->faker->unique()->safeEmail,
];
}
}

View file

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateContactTaskTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('contact_task', function (Blueprint $table) {
$table->bigInteger('task_id')->unsigned();
$table->bigInteger('contact_id')->unsigned();
$table->primary(['task_id', 'contact_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('contact_task');
}
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateContactsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('contacts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('surname', 200);
$table->string('firstname', 200);
$table->string('email', 250);
$table->string('phone', 20);
$table->timestamps();
$table->tinyInteger('active')->default(1);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('contacts');
}
}

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateGroupsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('groups', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name', 128)->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('groups');
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateNotificationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('notifications', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('contact_id');
$table->unsignedBigInteger('task_history_id');
$table->enum('status', ['pending', 'sent', 'error'])->default('pending');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('notifications');
}
}

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTaskHistoryTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('task_history', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedTinyInteger('status');
$table->text('output')->nullable();
$table->float('duration', 5, 3)->unsigned()->nullable();
$table->unsignedBigInteger('task_id');
$table->timestamps();
$table->index(['status', 'created_at']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('task_history');
}
}

View file

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTasksArchivesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tasks_archives', function (Blueprint $table) {
$table->bigIncrements('id');
$table->date('day');
$table->unsignedInteger('uptime')->default('0');
$table->unsignedBigInteger('task_id')->default('0');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('tasks_archives');
}
}

View file

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTasksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('tasks', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('host');
$table->enum('type', ['ping', 'http', 'dns', 'ftp']);
$table->string('params')->nullable();
$table->unsignedInteger('frequency');
$table->unsignedTinyInteger('attempts')->default('0');
$table->unsignedTinyInteger('active')->default(1);
$table->unsignedTinyInteger('status')->nullable();
$table->unsignedBigInteger('group_id')->nullable();
$table->timestamps();
$table->timestamp('executed_at')->nullable();
$table->unique(['host', 'type']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('tasks');
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddForeignKeysToContactTaskTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('contact_task', function (Blueprint $table) {
$table->foreign(['task_id'], 'contact_task_ibfk_1')->references(['id'])->on('tasks')->onUpdate('NO ACTION')->onDelete('CASCADE');
$table->foreign(['contact_id'], 'contact_task_ibfk_2')->references(['id'])->on('contacts')->onUpdate('NO ACTION')->onDelete('CASCADE');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('contact_task', function (Blueprint $table) {
$table->dropForeign('contact_task_ibfk_1');
$table->dropForeign('contact_task_ibfk_2');
});
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddForeignKeysToNotificationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('notifications', function (Blueprint $table) {
$table->foreign(['contact_id'], 'contact_id_frgn')->references(['id'])->on('contacts')->onUpdate('NO ACTION')->onDelete('CASCADE');
$table->foreign(['task_history_id'], 'task_history_id_frgn')->references(['id'])->on('task_history')->onUpdate('NO ACTION')->onDelete('CASCADE');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('notifications', function (Blueprint $table) {
$table->dropForeign('contact_id_frgn');
$table->dropForeign('task_history_id_frgn');
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddForeignKeysToTaskHistoryTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('task_history', function (Blueprint $table) {
$table->foreign(['task_id'], 'task_history_ibfk_1')->references(['id'])->on('tasks')->onUpdate('NO ACTION')->onDelete('CASCADE');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('task_history', function (Blueprint $table) {
$table->dropForeign('task_history_ibfk_1');
});
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddForeignKeysToTasksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tasks', function (Blueprint $table) {
$table->foreign(['group_id'], 'group_id_frgn')->references(['id'])->on('groups')->onUpdate('NO ACTION')->onDelete('CASCADE');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropForeign('group_id_frgn');
});
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
// $this->call('UsersTableSeeder');
}
}

View file

@ -1,350 +0,0 @@
#!/usr/bin/perl
################################
# #
# M O N I T O L I T E #
# #
# Lightweight Monitoring Tool #
# #
# @author: Axel de Vignon #
# @copyright: www.vidax.net #
# @license: Mozilla Public 1.1 #
# #
################################
use warnings;
use strict;
use DBI;
use Dotenv;
use Net::Ping;
use Email::MIME;
use Email::Sender::Simple qw(sendmail);
use Email::Sender::Transport::SMTP qw();
use LWP::Simple;
use LWP::UserAgent;
use LWP::Protocol::https;
my $query;
my $result;
my $tasks;
my $update_query;
my $emails;
my $email;
my $message;
my $response;
my $html;
my $numtasks;
my $previous_status;
my $subject;
my $datas;
############################
# #
# S E T T I N G S #
# #
############################
Dotenv->load;
my $dbtype = $ENV{'DB_TYPE'};
my $hostname = $ENV{'DB_HOST'};
my $database = $ENV{'DB_NAME'};
my $login = $ENV{'DB_USER'};
my $port = $ENV{'DB_PORT'};
my $password = $ENV{'DB_PASSWORD'};
my $email_from = $ENV{'MAIL_FROM'};
my $number_tries = $ENV{'NB_TRIES'};
my $days_history_archive = $ENV{'ARCHIVE_DAYS'};
my $smtp_host = $ENV{'SMTP_HOST'};
my $smtp_user = $ENV{'SMTP_USER'};
my $smtp_password = $ENV{'SMTP_PASSWORD'};
my $smtp_port = $ENV{'SMTP_PORT'};
my $smtp_ssl = $ENV{'SMTP_SSL'};
############################
######
# Testing database connection
######
my $dsn = "DBI:$dbtype:database=$database;host=$hostname;port=$port";
my $dbh = DBI->connect($dsn, $login, $password) or output('cannot connect to database', 'ERROR', 1);
######
# Getting tasks
######
my $execution_time = server_time();
my $query1 = $dbh->prepare('SELECT id, host, type, params FROM tasks WHERE ( DATE_SUB(now(), INTERVAL frequency SECOND) > last_execution OR last_execution IS NULL ) AND active = 1');
$query1->execute() or output('Cannot execute query fetching all pending tasks', 'ERROR', 1);
$numtasks = $query1->rows;
#####
# Processing all tasks
#####
if ($numtasks > 0) {
while ($tasks = $query1->fetchrow_hashref()) {
print "\n";
my $status = -1;
$previous_status = -1;
$message = 'Host is back up';
####
# Getting last history for this host
####
my $query2 = $dbh->prepare('SELECT status FROM tasks_history WHERE task_id = ' . $tasks->{'id'} . ' ORDER BY datetime DESC LIMIT 1');
$query2->execute() or output('Cannot get history for this task', 'ERROR', 0);
if ($query2->rows > 0) {
my $history = $query2->fetchrow_hashref();
$previous_status = $history->{'status'};
}
if ($tasks->{'type'} =~ 'ping') {
# Ping check returned an error
if (! check_ping($tasks->{'host'})) {
$status = 0;
output('Host "'. $tasks->{'host'} .'" [' . $tasks->{'type'} . '] is down', 'ALERT');
$message = 'Host does not reply to ping. Timed out after 5s. Giving up...';
}
# Ping check went fine
else {
$status = 1;
output('Host "'. $tasks->{'host'} .'" [' . $tasks->{'type'} . '] is up', 'SUCCESS');
}
}
elsif ($tasks->{'type'} =~ 'http') {
$response = check_http($tasks->{'host'}, $tasks->{'params'});
# HTTP check went fine
if ($response =~ 'OK') {
$status = 1;
output('Host "'. $tasks->{'host'} .'" [' . $tasks->{'type'} . '] is up', 'SUCCESS');
}
# HTTP check returned an error
else {
$status = 0;
output('Host "'. $tasks->{'host'} .'" [' . $tasks->{'type'} . '] is down', 'ALERT');
$message = 'HTTP response was: ' . $response;
}
}
else {
output('dunno how to process this task', 'DEBUG');
next;
}
# Notify on status changes only
if ($previous_status != -1 && $status != $previous_status) {
output('Should send notification', 'DEBUG');
&send_notifications($tasks->{'id'}, $tasks->{'host'}, $tasks->{'type'}, $message, $status);
}
# Saving Status into DB
if ($status >= 0) {
save_history($tasks->{'id'}, $status, $execution_time);
}
}
}
else {
output('nothing to monitor, sleeping back', 'DEBUG');
}
#####
# Function used for the PING test
#####
sub check_ping {
my ($host, $round) = @_;
$round = 1 if (! $round);
my $ping = Net::Ping->new('icmp');
output('ping check n°' . $round . ' on ' . $host, 'DEBUG');
if (! $ping->ping($host)) {
$ping->close();
if ($number_tries && $round <= $number_tries) {
sleep (2);
return check_ping($host, $round + 1)
}
else {
return undef;
}
} else {
$ping->close();
return 'OK';
}
}
#####
# Function used to check HTTP service
#####
sub check_http {
my ($host, $find, $round) = @_;
$round = 1 if (! $round);
$host = 'http://'.$host if ($host !~ m/^http/i);
my $check = LWP::UserAgent->new(
ssl_opts => { verify_hostname => 0 },
protocols_allowed => ['http', 'https']
);
$check->timeout(5);
$check->env_proxy;
my $response = $check->get($host, ':content_cb' => \&process_data);
output('http check n°' . $round . ' on ' . $host, 'DEBUG');
if ($response->is_success) {
if ($find && length($find) > 0) {
output('searching "' . $find . '" into html content on ' . $host, 'DEBUG');
if ($html =~ m/$find/i) {
output('html content found, looks fine', 'SUCCESS');
return 'OK';
}
else {
output('html content not found', 'ERROR');
return 'Could not find "' . $find . '" into the page';
}
}
else {
return 'OK';
}
}
else {
output('HTTP response error was: '.$response->status_line, 'DEBUG');
if ($number_tries && $round < $number_tries) {
sleep (2);
return check_http($host, $find, $round + 1);
}
else {
return $response->status_line;
}
}
}
#####
# Save the page HTML content
#####
sub process_data {
my ($content, $handler1, $handler2) = @_;
$html .= $content;
}
#####
# Function managing DEBUG and OUTPUT
#####
sub output {
my ($output, $level, $fatal) = @_;
$output = server_time().' - '.$level.' - '.$output."\n";
if ($fatal && $fatal == 1) {
die ('FATAL '.$output);
}
else {
print ($output);
}
return 1;
}
#####
# Function that keeps an history
#####
sub save_history {
my ($task_id, $status, $datetime) = @_;
my $query = $dbh->prepare('INSERT INTO tasks_history (status, datetime, task_id) VALUES(' . $status . ', "'.$datetime.'", ' . $task_id . ')');
if ($query->execute()) {
output('saving status to history', 'DEBUG');
}
else {
output('cannot save status to history', 'ERROR');
}
$update_query = $dbh->prepare('UPDATE tasks SET last_execution = "'.$datetime.'" WHERE id = ' . $task_id);
if ($update_query->execute()) {
output('saving last execution time for this task', 'DEBUG');
}
else {
output('cannot save last execution time for this task', 'ERROR');
}
return 1;
}
#####
# Function sending notifications
#####
sub send_notifications {
my ($task_id, $host, $type, $message, $status) = @_;
if ($status == 0) {
$subject = 'ALERT: host "' . $host . '" [' . $type . '] is down';
$datas = "------ ALERT DETECTED BY MONITORING SERVICE ------ \n\n\nDATETIME: " . server_time() . "(server time)\nHOST: " . $host . "\nSERVICE: " . $type . "\nMESSAGE: " . $message;
}
else {
$subject = 'RECOVERY: host "' . $host . '" [' . $type . '] is up';
$datas = "------ RECOVERY DETECTED BY MONITORING SERVICE ------ \n\n\nDATETIME: " . server_time() . "(server time)\nHOST: " . $host . "\nSERVICE: " . $type . "\nMESSAGE: " . $message;
}
my $query = $dbh->prepare('SELECT c.email FROM contacts as c JOIN notifications as n ON (n.contact_id = c.id) WHERE c.active = 1 AND n.task_id = '.$task_id);
if ($query->execute()) {
while ($emails = $query->fetchrow_hashref()) {
my $email = Email::MIME->create(
header_str => [
From => $email_from,
To => $emails->{'email'},
Subject => $subject
],
parts => [
$datas
],
);
eval {
sendmail(
$email,
{
from => $email_from,
transport => Email::Sender::Transport::SMTP->new({
host => $smtp_host,
port => $smtp_port,
sasl_username => $smtp_user,
sasl_password => $smtp_password,
ssl => $smtp_ssl,
timeout => 10
})
}
);
output('Notification email was sent to '.$emails->{'email'}, 'DEBUG');
};
warn $@ if $@;
}
return 1
}
output('failed to send notifications', 'ERROR');
return undef;
}
#####
# Function getting datetime
#####
sub server_time {
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
my $now = (1900 + $year).'-'.($mon + 1).'-'.$mday.' '.$hour.':'.$min.':00';
return $now;
}

16852
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

20
package.json Normal file
View file

@ -0,0 +1,20 @@
{
"devDependencies": {
"laravel-mix": "^6.0.39",
"resolve-url-loader": "^4.0.0",
"sass": "^1.45.0",
"sass-loader": "^12.4.0",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14"
},
"dependencies": {
"apexcharts": "^3.32.0",
"axios": "^0.24.0",
"moment": "^2.29.1",
"vue": "^2.6.14",
"vue-apexcharts": "^1.6.2",
"vue-loading-overlay": "^3.4.2",
"vue-router": "^3.5.3",
"vuex": "^3.6.2"
}
}

17
phpunit.xml Normal file
View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
</php>
</phpunit>

21
public/.htaccess Normal file
View file

@ -0,0 +1,21 @@
<IfModule mod_rewrite.c>
<IfModule mod_negotiation.c>
Options -MultiViews -Indexes
</IfModule>
RewriteEngine On
# Handle Authorization Header
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
# Redirect Trailing Slashes If Not A Folder...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Handle Front Controller...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [L]
</IfModule>

4
public/css/app.css Normal file
View file

@ -0,0 +1,4 @@
@import url(https://fonts.googleapis.com/css2?family=Hind:wght@300;400;500;600;700&display=swap);
@font-face{font-family:Digital7;font-style:normal;font-weight:400;src:url(/fonts/digital.ttf) format("truetype")}*{margin:0;padding:0}html{font-size:100%;scroll-behavior:smooth}html body{background-attachment:fixed;background-image:url(../img/bush.png);color:#3d3d3d;font-family:Hind,sans-serif;font-size:1rem;padding:10px}html body a,html body a:visited{color:inherit;text-decoration:inherit}html body a:hover,html body a:visited:hover{text-decoration:underline}html body .container{margin:0 auto;max-width:1000px;padding:0}html body h1,html body h2,html body h3,html body h4{margin-bottom:.8rem;margin-top:.8rem}html body h1{font-size:2.4rem;margin:3rem 0;text-align:center}html body h2{font-size:1.4rem;margin-top:0;padding-top:0}html body h3{background-color:#0a9f9a;color:#f0f0f0;font-size:1.4rem;margin:0;padding:.8rem;position:relative}html body h3 small{font-size:.9rem}html body h3 .context-menu{cursor:pointer;font-size:1rem;position:absolute;right:.7rem;top:.7rem}html body div.round{background-color:#fff;border-radius:5px;box-shadow:3px 3px 6px 0 rgba(0,0,0,.3);margin-bottom:3rem;overflow:hidden;position:relative}html body div.round h3{margin-bottom:1rem}html body img{vertical-align:sub}html body table{border:1px solid #abc;border-collapse:collapse;border-spacing:0;font-size:14px}html body table th{background-color:#e6eeee}html body table td,html body table th{border:1px solid #9ccece;padding:.3rem}html body table td{background-color:#fff;color:#3d3d3d;text-align:center}html body table td,html body table td img{vertical-align:middle}html body table td.right{text-align:right}html body table#contacts_tbl,html body table#tasks_tbl{width:100%}html body .no-data{color:#727272;font-size:.9rem;font-style:italic;margin-bottom:1.3rem;text-align:center}html body .quick-view .new-group{border-radius:.4rem;cursor:pointer;display:inline-block;margin:.2rem;overflow:hidden}html body .quick-view .new-group .square{float:left;height:100%;line-height:1.2rem;margin:0;min-width:1.4rem;padding:.2rem .6rem;text-align:center;vertical-align:middle}html body .quick-view .new-group .square:not(:first-of-type){border-left:1px solid #fff}html body .tasks .task{background-color:#fff;border-radius:5px;box-shadow:3px 3px 6px 0 rgba(0,0,0,.3);margin-top:2rem;overflow:hidden;padding:0;position:relative}html body .spacer{clear:both;line-height:0;margin:0;padding:0}html body .block-content{padding:.8rem}html body .highlight{background-color:#166260;border-radius:.5rem;color:#fff;display:inline-block;font-size:1rem;padding:0 1rem;vertical-align:middle}html body .small{font-size:.8rem}html body .hidden{display:none}html body .up{background-color:#8adf8a}html body .down{background-color:#f79292}html body .unknown{background-color:#f5d69e}html body .inactive{background-color:#dfdfdf!important;opacity:.5}html body .refreshed-time{font-size:.8rem;margin-bottom:2rem;text-align:right}html body .refreshed-time .clock{background-color:#000;border-radius:4px;color:#fff;font-family:Digital7;font-size:1.2rem;padding:.3rem .5rem}@-webkit-keyframes shake{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}@keyframes shake{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}
/*# sourceMappingURL=app.css.map*/

1
public/css/app.css.map Normal file

File diff suppressed because one or more lines are too long

BIN
public/fonts/digital.ttf Normal file

Binary file not shown.

BIN
public/img/bush.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

1
public/img/disable.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><title/><g data-name="Layer 51" id="Layer_51"><path d="M16,2A14,14,0,1,0,30,16,14,14,0,0,0,16,2ZM4,16A11.89,11.89,0,0,1,6.85,8.26L23.74,25.15A12,12,0,0,1,4,16Zm21.15,7.74L8.26,6.85A12,12,0,0,1,25.15,23.74Z"/></g></svg>

After

Width:  |  Height:  |  Size: 300 B

1
public/img/dns.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg data-name="Layer 1" id="Layer_1" viewBox="0 0 272 272" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:#424242;}.cls-2{fill:#f3f4f2;}</style></defs><title/><rect class="cls-1" height="8" width="136" x="68" y="68"/><rect class="cls-1" height="8" width="136" x="68" y="92"/><rect class="cls-1" height="8" width="136" x="68" y="116"/><rect class="cls-1" height="8" width="136" x="68" y="140"/><rect class="cls-1" height="8" width="136" x="68" y="164"/><rect class="cls-1" height="8" width="136" x="68" y="188"/><rect class="cls-1" height="8" width="136" x="68" y="212"/><path class="cls-1" d="M216,40H192V16l-8-8H48V264H224V48Zm0,216H56V16H184V48h32Z"/><rect class="cls-2" height="8" width="136" x="68" y="68"/><rect class="cls-2" height="8" width="136" x="68" y="92"/><rect class="cls-2" height="8" width="136" x="68" y="116"/><rect class="cls-2" height="8" width="136" x="68" y="140"/><rect class="cls-2" height="8" width="136" x="68" y="164"/><rect class="cls-2" height="8" width="136" x="68" y="188"/><rect class="cls-2" height="8" width="136" x="68" y="212"/><path class="cls-1" d="M184,8V48h40Zm8,32V27.31L204.69,40Z"/><rect class="cls-1" height="8" width="136" x="68" y="68"/><rect class="cls-1" height="8" width="136" x="68" y="92"/><rect class="cls-1" height="8" width="136" x="68" y="116"/><rect class="cls-1" height="8" width="136" x="68" y="140"/><rect class="cls-1" height="8" width="136" x="68" y="164"/><rect class="cls-1" height="8" width="136" x="68" y="188"/><rect class="cls-1" height="8" width="136" x="68" y="212"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
public/img/down.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M22 15h-3V3h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zm-5.293 1.293l-6.4 6.4a.5.5 0 0 1-.654.047L8.8 22.1a1.5 1.5 0 0 1-.553-1.57L9.4 16H3a2 2 0 0 1-2-2v-2.104a2 2 0 0 1 .15-.762L4.246 3.62A1 1 0 0 1 5.17 3H16a1 1 0 0 1 1 1v11.586a1 1 0 0 1-.293.707z"/></g></svg>

After

Width:  |  Height:  |  Size: 385 B

1
public/img/error.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M4 20v-6a8 8 0 1 1 16 0v6h1v2H3v-2h1zm2-6h2a4 4 0 0 1 4-4V8a6 6 0 0 0-6 6zm5-12h2v3h-2V2zm8.778 2.808l1.414 1.414-2.12 2.121-1.415-1.414 2.121-2.121zM2.808 6.222l1.414-1.414 2.121 2.12L4.93 8.344 2.808 6.222z"/></g></svg>

After

Width:  |  Height:  |  Size: 352 B

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

1
public/img/external.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M10 6v2H5v11h11v-5h2v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h6zm11-3v8h-2V6.413l-7.793 7.794-1.414-1.414L17.585 5H13V3h8z"/></g></svg>

After

Width:  |  Height:  |  Size: 273 B

1
public/img/ftp.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg data-name="Layer 1" id="Layer_1" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M7,25A7,7,0,0,1,7,11a1,1,0,0,1,0,2A5,5,0,0,0,7,23a1,1,0,0,1,0,2Z"/><path d="M25,25a1,1,0,0,1,0-2,5,5,0,0,0,0-10,1,1,0,0,1,0-2,7,7,0,0,1,0,14Z"/><path d="M25,13a1,1,0,0,1-1-1A8,8,0,0,0,8,12a1,1,0,0,1-2,0,10,10,0,0,1,20,0A1,1,0,0,1,25,13Z"/><path d="M21,22H11a1,1,0,0,1,0-2H21a1,1,0,0,1,0,2Z"/><path d="M21,28H11a1,1,0,0,1,0-2H21a1,1,0,0,1,0,2Z"/><path d="M21,28a1,1,0,0,1-.83-.45l-2-3a1,1,0,1,1,1.66-1.1l2,3a1,1,0,0,1-.28,1.38A.94.94,0,0,1,21,28Z"/><path d="M19,31a.94.94,0,0,1-.55-.17,1,1,0,0,1-.28-1.38l2-3a1,1,0,0,1,1.66,1.1l-2,3A1,1,0,0,1,19,31Z"/><path d="M13,25a1,1,0,0,1-.83-.45l-2-3a1,1,0,0,1,1.66-1.1l2,3a1,1,0,0,1-.28,1.38A.94.94,0,0,1,13,25Z"/><path d="M11,22a.94.94,0,0,1-.55-.17,1,1,0,0,1-.28-1.38l2-3a1,1,0,0,1,1.66,1.1l-2,3A1,1,0,0,1,11,22Z"/><path d="M9,25H7a1,1,0,0,1,0-2H9a1,1,0,0,1,0,2Z"/><path d="M25,25H23a1,1,0,0,1,0-2h2a1,1,0,0,1,0,2Z"/></svg>

After

Width:  |  Height:  |  Size: 989 B

1
public/img/http.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg id="Layer_1" style="enable-background:new 0 0 128 128;" version="1.1" viewBox="0 0 128 128" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><path d="M64,126c34.2,0,62-27.8,62-62S98.2,2,64,2S2,29.8,2,64S29.8,126,64,126z M16,88.7l25.2-0.2c2.8,10.1,7.5,19.9,13.9,28.7 C38,114.4,23.7,103.5,16,88.7z M47.6,47H79c2.3,11,2.3,22.3,0.2,33.3l-31.6,0.2C45.3,69.4,45.3,58,47.6,47z M63.3,114.9 c-6.3-8.1-10.9-17-13.7-26.4l27.5-0.2C74.2,97.7,69.6,106.7,63.3,114.9z M71.3,117.5c6.6-9,11.3-18.9,14.1-29.3l26.9-0.2 C104.5,103.7,89.3,115,71.3,117.5z M118,64c0,5.6-0.9,11-2.4,16l-28.3,0.2c2-11,1.9-22.2-0.2-33.2h28.1C117,52.3,118,58.1,118,64z M111.8,39H85.2c-2.9-10-7.5-19.7-13.9-28.5C89,12.9,103.9,23.8,111.8,39z M76.9,39H49.7c2.9-9.2,7.4-17.9,13.6-25.9 C69.5,21.1,74,29.8,76.9,39z M55.1,10.8C48.8,19.5,44.2,29,41.4,39H16.2C23.9,24.3,38.1,13.6,55.1,10.8z M39.5,47 c-2.1,11.1-2.1,22.4-0.1,33.5l-26.7,0.2C10.9,75.4,10,69.8,10,64c0-5.9,1-11.7,2.8-17H39.5z"/></g></svg>

After

Width:  |  Height:  |  Size: 1 KiB

1
public/img/info.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z"/></g></svg>

After

Width:  |  Height:  |  Size: 248 B

1
public/img/menu.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M12 3c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 14c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0-7c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></g></svg>

After

Width:  |  Height:  |  Size: 290 B

1
public/img/off.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg fill="none" height="20" viewBox="0 0 20 20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="M6 12C4.89543 12 4 11.1046 4 10C4 8.89543 4.89543 8 6 8C7.10457 8 8 8.89543 8 10C8 11.1046 7.10457 12 6 12Z" fill="#212121"/><path d="M18 10C18 7.79086 16.2091 6 14 6H6C3.79086 6 2 7.79086 2 10C2 12.2091 3.79086 14 6 14H14C16.2091 14 18 12.2091 18 10ZM14 7C15.6569 7 17 8.34315 17 10C17 11.6569 15.6569 13 14 13H6C4.34315 13 3 11.6569 3 10C3 8.34315 4.34315 7 6 7H14Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 517 B

1
public/img/on.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 7C4.23858 7 2 9.23858 2 12C2 14.7614 4.23858 17 7 17H17C19.7614 17 22 14.7614 22 12C22 9.23858 19.7614 7 17 7H7ZM16.75 14.5C15.3693 14.5 14.25 13.3807 14.25 12C14.25 10.6193 15.3693 9.5 16.75 9.5C18.1307 9.5 19.25 10.6193 19.25 12C19.25 13.3807 18.1307 14.5 16.75 14.5Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 422 B

1
public/img/ping.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg enable-background="new 0 0 32 32" id="Layer_4" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><polygon fill="none" points="12,3 12,8 31,8 31,14 1,14 " stroke="#000000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/><polygon fill="none" points="20,29 20,24 1,24 1,18 31,18 " stroke="#000000" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2"/></g></svg>

After

Width:  |  Height:  |  Size: 508 B

1
public/img/see.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M1.181 12C2.121 6.88 6.608 3 12 3c5.392 0 9.878 3.88 10.819 9-.94 5.12-5.427 9-10.819 9-5.392 0-9.878-3.88-10.819-9zM12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10zm0-2a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></g></svg>

After

Width:  |  Height:  |  Size: 330 B

1
public/img/success.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></g></svg>

After

Width:  |  Height:  |  Size: 210 B

1
public/img/trash.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M4 8h16v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8zm2 2v10h12V10H6zm3 2h2v6H9v-6zm4 0h2v6h-2v-6zM7 5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v2h5v2H2V5h5zm2-1v1h6V4H9z"/></g></svg>

After

Width:  |  Height:  |  Size: 294 B

1
public/img/unknown.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm2-1.645A3.502 3.502 0 0 0 12 6.5a3.501 3.501 0 0 0-3.433 2.813l1.962.393A1.5 1.5 0 1 1 12 11.5a1 1 0 0 0-1 1V14h2v-.645z"/></g></svg>

After

Width:  |  Height:  |  Size: 354 B

1
public/img/up.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M2 9h3v12H2a1 1 0 0 1-1-1V10a1 1 0 0 1 1-1zm5.293-1.293l6.4-6.4a.5.5 0 0 1 .654-.047l.853.64a1.5 1.5 0 0 1 .553 1.57L14.6 8H21a2 2 0 0 1 2 2v2.104a2 2 0 0 1-.15.762l-3.095 7.515a1 1 0 0 1-.925.619H8a1 1 0 0 1-1-1V8.414a1 1 0 0 1 .293-.707z"/></g></svg>

After

Width:  |  Height:  |  Size: 383 B

1
public/img/warning.svg Normal file
View file

@ -0,0 +1 @@
<?xml version="1.0" ?><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g><path d="M0 0h24v24H0z" fill="none"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></g></svg>

After

Width:  |  Height:  |  Size: 247 B

28
public/index.php Normal file
View file

@ -0,0 +1,28 @@
<?php
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| First we need to get an application instance. This creates an instance
| of the application / container and bootstraps the application so it
| is ready to receive HTTP / Console requests from the environment.
|
*/
$app = require __DIR__.'/../bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request
| through the kernel, and send the associated response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have prepared for them.
|
*/
$app->run();

3
public/js/app.js Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,29 @@
/*!
* ApexCharts v3.32.0
* (c) 2018-2021 ApexCharts
* Released under the MIT License.
*/
/*!
* Vue.js v2.6.14
* (c) 2014-2021 Evan You
* Released under the MIT License.
*/
/*!
* vuex v3.6.2
* (c) 2021 Evan You
* @license MIT
*/
/*! svg.draggable.js - v2.2.2 - 2019-01-08
* https://github.com/svgdotjs/svg.draggable.js
* Copyright (c) 2019 Wout Fierens; Licensed MIT */
/*! svg.filter.js - v2.0.2 - 2016-02-24
* https://github.com/wout/svg.filter.js
* Copyright (c) 2016 Wout Fierens; Licensed MIT */
//! moment.js
//! moment.js locale configuration

1
public/js/app.js.map Normal file

File diff suppressed because one or more lines are too long

4
public/mix-manifest.json Normal file
View file

@ -0,0 +1,4 @@
{
"/js/app.js": "/js/app.js",
"/css/app.css": "/css/app.css"
}

86
resources/js/app.js Normal file
View file

@ -0,0 +1,86 @@
//window.Vue = require('vue')
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import axios from 'axios'
Vue.prototype.$http = axios
import moment from 'moment'
Vue.prototype.moment = moment
import VueApexCharts from 'vue-apexcharts'
Vue.use(VueApexCharts)
Vue.component('apexchart', VueApexCharts)
import VueLoading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/vue-loading.css';
Vue.use(VueLoading, {
// Optional parameters
//container: this.fullPage ? null : this.$refs.formContainer,
canCancel: true,
backgroundColor: '#000',
color: '#0a9f9a',
width: 128,
height: 128,
opacity: 0.9,
loader: 'dots'
})
import Home from '../views/app.vue'
import TaskDetails from '../views/taskdetails.vue'
const router = new VueRouter({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/task/:id',
name: 'taskdetails',
component: TaskDetails,
},
],
});
const store = new Vuex.Store({
state: {
tasks: null
},
mutations: {
setTasks(state, tasks) {
state.tasks = tasks
},
updateTask(state, update) {
let tasks = state.tasks
if (
tasks.hasOwnProperty(update.group_id) &&
tasks[update.group_id].hasOwnProperty('tasks') &&
tasks[update.group_id]['tasks'].hasOwnProperty(update.id)
) {
tasks[update.group_id]['tasks'][update.id] = update;
}
}
}
})
var runApp = function() {
new Vue({
router,
components: { Home },
store,
}).$mount('#app')
}
window.addEventListener('load', function () {
runApp();
})

280
resources/sass/app.scss Normal file
View file

@ -0,0 +1,280 @@
@import url('https://fonts.googleapis.com/css2?family=Hind:wght@300;400;500;600;700&display=swap');
@font-face {
font-family: 'Digital7';
src:
url('/fonts/digital.ttf') format('truetype')
;
font-weight: normal;
font-style: normal;
}
/**
* SETTINGS
**/
$bg_color: #0a9f9a;
$up_color: #8adf8a;
$down_color: #f79292;
$unknown_color: rgb(245, 214, 158);
$inactive_color: #dfdfdf;
* {
padding: 0;
margin: 0;
}
html {
font-size: 100%;
scroll-behavior: smooth;
body {
padding: 10px;
font-family: 'Hind', sans-serif;
font-size: 1rem;
color: #3D3D3D;
background-image: url(../img/bush.png);
background-attachment: fixed;
a, a:visited {
color: inherit;
text-decoration: inherit;
&:hover {
text-decoration: underline;
}
}
.container {
padding: 0;
margin: 0 auto;
max-width: 1000px;
}
h1, h2, h3, h4 {
margin-top: .8rem;
margin-bottom: .8rem;
}
h1 {
font-size: 2.4rem;
text-align: center;
margin: 3rem 0;
}
h2 {
font-size: 1.4rem;
margin-top: 0;
padding-top: 0;
}
h3 {
font-size: 1.4rem;
background-color: $bg_color;
margin: 0;
padding: .8rem;
color: rgb(240, 240, 240);
position: relative;
small {
font-size: .9rem;
}
.context-menu {
position: absolute;
right: .7rem;
top: .7rem;
cursor: pointer;
font-size: 1rem;
}
}
div.round {
-webkit-box-shadow: 3px 3px 6px 0px rgba(0,0,0,0.3);
-moz-box-shadow: 3px 3px 6px 0px rgba(0,0,0,0.3);
box-shadow: 3px 3px 6px 0px rgba(0,0,0,0.3);
border-radius: 5px;
position: relative;
background-color: white;
overflow: hidden;
margin-bottom: 3rem;
h3 {
margin-bottom: 1rem;
}
}
img {
vertical-align: sub;
}
table {
border: 1px solid #ABC;
font-size: 14px;
border-spacing : 0;
border-collapse : collapse;
th {
background-color: #e6EEEE;
border: 1px solid #9ccece;
padding: 0.3rem;
}
td {
color: #3D3D3D;
padding: 0.3rem;
background-color: #FFF;
border: 1px solid #9ccece;
text-align: center;
vertical-align: middle;
img {
vertical-align: middle;
}
&.right {
text-align: right;
}
}
&#tasks_tbl, &#contacts_tbl {
width: 100%;
}
}
.no-data {
text-align: center;
font-style: italic;
margin-bottom: 1.3rem;
font-size: .9rem;
color: #727272;
}
.quick-view {
.new-group {
margin: .2rem;
display: inline-block;
border-radius: .4rem;
overflow: hidden;
cursor: pointer;
.square {
height: 100%;
margin: 0;
text-align: center;
vertical-align: middle;
float: left;
line-height: 1.2rem;
min-width: 1.4rem;
padding: .2rem .6rem;
&:not(:first-of-type) {
border-left: 1px solid white;
}
}
}
}
.tasks {
.task {
margin-top: 2rem;
padding: 0;
-webkit-box-shadow: 3px 3px 6px 0px rgba(0,0,0,0.3);
-moz-box-shadow: 3px 3px 6px 0px rgba(0,0,0,0.3);
box-shadow: 3px 3px 6px 0px rgba(0,0,0,0.3);
border-radius: 5px;
position: relative;
background-color: white;
overflow: hidden;
}
}
.spacer {
clear:both;
line-height: 0;
padding: 0;
margin:0;
}
.block-content {
padding: .8rem;
}
.highlight {
background-color: #166260;
padding: 0px 1rem;
display: inline-block;
color: #FFF;
vertical-align: middle;
border-radius: .5rem;
font-size: 1rem;
}
.small {
font-size: .8rem;
}
.hidden {
display: none;
}
.up {
background-color: $up_color;
}
.down {
background-color: $down_color;
}
.unknown {
background-color: $unknown_color;
}
.inactive {
background-color: $inactive_color !important;
opacity: 0.5;
}
.refreshed-time {
text-align: right;
font-size: .8rem;
margin-bottom: 2rem;
.clock {
font-family: Digital7;
font-size: 1.2rem;
background-color: #000;
border-radius: 4px;
color: #FFF;
padding: .3rem .5rem;
}
}
}
}
@keyframes shake {
10%, 90% {
transform: translate3d(-1px, 0, 0);
}
20%, 80% {
transform: translate3d(2px, 0, 0);
}
30%, 50%, 70% {
transform: translate3d(-4px, 0, 0);
}
40%, 60% {
transform: translate3d(4px, 0, 0);
}
}

0
resources/views/.gitkeep Normal file
View file

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>MonitoLite - Network monitoring tool</title>
<script type="text/javascript" src="{{ url('js/app.js') }}"></script>
<link type="text/css" rel="stylesheet" href="{{ url('css/app.css') }}" />
</head>
<body>
<div id="app">
<router-view></router-view>
</div>
</body>
</html>

64
resources/views/app.vue Normal file
View file

@ -0,0 +1,64 @@
<template>
<div class="container">
<h1>MonitoLite Dashboard</h1>
<p class="refreshed-time">Last refresh: <br /><span class="clock">{{ refreshedTime }}</span></p>
<quick-view></quick-view>
<task-list></task-list>
</div>
</template>
<script>
import TaskList from './components/tasklist.vue'
import QuickView from './components/quickview.vue'
export default{
components: {
QuickView,
TaskList,
},
data: function() {
return {
refreshed_time: null,
refresh: null,
loading: true,
color: '#FF0000',
size: '10rem',
}
},
computed: {
refreshedTime: function() {
return this.refreshed_time != null ? this.moment(this.refreshed_time).format('HH:mm:ss') : 'never'
}
},
methods: {
getTasks: function() {
this.$http.get('/api/getTasks')
.then(response => this.$store.commit('setTasks', response.data))
.catch(error => {
this.loading.hide()
clearTimeout(this.refresh)
window.alert('An error occurred when getting tasks. Automatic refresh has been disabled. You should fix and reload this page.')
})
.then(() => {
this.refreshed_time = this.moment();
this.loading.hide()
})
}
},
beforeRouteLeave(to, from, next) {
clearTimeout(this.refresh)
next();
},
mounted: function() {
this.loading = this.$loading.show()
this.getTasks()
this.refresh = window.setInterval(() => {
this.getTasks();
}, 60000)
}
}
</script>
<style scoped>
</style>

View file

@ -0,0 +1,24 @@
<template>
<div>
<form
v-on:submit.prevent="addTask"
>
<button>Add task</button>
</form>
</div>
</template>
<script>
export default {
props: [
'tasks'
],
methods: {
addTask: function() {
this.$http.post('api.php?a=add_task')
}
}
}
</script>

View file

@ -0,0 +1,65 @@
<template>
<div class="quick-view round">
<h3>
Quick overview
</h3>
<div class="block-content">
<div
v-if="tasks && Object.keys(tasks).length > 0"
>
<div
v-for="group in tasks"
v-bind:key="group.id"
class="new-group"
:title="'Group: '+group.name"
>
<a :href="'#group-'+group.id">
<p
v-for="task in group.tasks"
v-bind:key="task.id"
:href="'#task-'+task.id"
:class="statusText(task.status)+(task.active == 0 ? ' inactive' : '')"
class="square"
>
<span class="small">{{task.id }}</span>
</p>
</a>
</div>
<p class="spacer">&nbsp;</p>
</div>
<div
class="no-data"
v-else
>
Sorry, there is no task here.
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
tasks: function() {
return this.$store.state.tasks
}
},
methods: {
statusText: function (status) {
switch (status) {
case 1:
return 'up';
break;
case 0:
return 'down';
break;
default:
return 'unknown';
}
},
}
}
</script>

View file

@ -0,0 +1,116 @@
<template>
<div class="tasks">
<div
v-for="group in tasks"
v-bind:key="group.id"
class="task round"
>
<a :name="'group-'+group.id"></a>
<h3>
Tasks for <span class="highlight">{{ group.name }} <small>(#{{ group.id }})</small></span>
<!-- <p class="context-menu"><img src="/img/menu.svg" width="40" /></p> -->
</h3>
<div class="block-content">
<table id="tasks_tbl">
<thead>
<tr>
<th width="5%">Up?</th>
<th width="*">Host</th>
<th width="10%">Type</th>
<th width="20%">Last checked</th>
<th width="13%">Frequency (min)</th>
<th width="5%">Active</th>
<th width="5%">Actions</th>
</tr>
</thead>
<tbody>
<tr
v-for="task in group.tasks"
v-bind:key="task.id"
:class="task.active == 0 ? 'inactive' : ''"
>
<td :class="statusText(task.status)">
<img :src="'/img/'+statusText(task.status)+'.svg'" width="16" alt="Status" />
</td>
<td>
<img src="/img/external.svg" alt="View host" width="16">
<a :href="task.host" target="_blank">{{ task.host }}</a>
</td>
<td>
<img :src="'/img/'+task.type+'.svg'" width="16" alt="Type of check" :title="'Type: '+task.type" />
{{ task.type.toUpperCase() }}
</td>
<td>
<span
v-if="task.executed_at"
>
{{ moment(task.executed_at).fromNow() }}
</span>
<span
v-else
>
Never
</span>
<td>{{ task.frequency / 60 }}</td>
<td :class="task.active == 0 ? 'inactive' : ''">
<a
v-on:click.prevent="disableTask(task.id, task.active)"
href="#"
:title="task.active == 1 ? 'Disable task' : 'Enable task'"
>
<img :src="task.active == 1 ? '/img/on.svg' : '/img/off.svg'" alt="Disable" width="24" />
</a>
</td>
<td>
<router-link :to="{ name: 'taskdetails', params: { id: task.id }}">
<img src="/img/see.svg" alt="Details" width="20" />
</router-link>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script>
export default {
components: {
},
computed: {
tasks: function() {
return this.$store.state.tasks
},
},
methods: {
statusText: function (status) {
switch (status) {
case 1:
return 'up';
break;
case 0:
return 'down';
break;
default:
return 'unknown';
}
},
disableTask: function(task_id, current_status) {
this.loading = this.$loading.show()
this.$http.patch('/api/toggleTaskStatus/'+task_id, {
active: + !current_status
})
.then(response => {
this.$store.commit('updateTask', response.data)
})
.then(() => {
this.loading.hide()
})
}
}
}
</script>

View file

@ -0,0 +1,24 @@
@component('mail::message')
# Monitolite Notification Report
Hello {{ $report['contact']['firstname'] }},
You will find below the full report digest of the Monitolite monitoring application.
@component('mail::table')
| Host | Status | Datetime |
|:------------:|:--------:|:--------:|
@foreach ($report['tasks'] as $t)
@foreach ($t['history'] as $h)
| [{{ $h['task']['host'] }}]({{ $h['task']['host'] }}) | {{ $h['status'] == 1 ? '**UP**': '**DOWN**' }} | {{ date('Y-m-d H:i:s', strtotime($h['created_at'])) }} |
@endforeach
@endforeach
@endcomponent
@component('mail::button', ['url' => $url])
View the dashboard
@endcomponent
Thanks,<br>
{{ config('app.name') }}
@endcomponent

View file

@ -0,0 +1,363 @@
<template>
<div>
<div class="container"
v-if="task.id != null"
>
<h1>
<span class="highlight">{{ task.type }}</span> for host <span class="highlight">{{ task.host }}</span>
<!-- <p class="context-menu"><img src="/img/menu.svg" width="40" /></p> -->
</h1>
Show:
<select
v-model="days"
@change="refreshTask"
>
<option value="3">3 days</option>
<option value="7">7 days</option>
<option value="15">15 days</option>
<option value="30">30 days</option>
</select>
<!-- Uptime chart block -->
<div id="chart" class="round">
<h3>Last {{ days }} days uptime</h3>
<div class="block-content">
<apexchart class="graph" v-if="charts.uptime.render" type="bar" height="350" :options="charts.uptime.options" :series="charts.uptime.series"></apexchart>
<p class="no-data" v-else>No chart to display here</p>
</div>
</div>
<!-- Response time chart block -->
<div id="chart" class="round" v-if="task.type == 'http'">
<h3>Last {{ days }} days response time</h3>
<div class="block-content">
<apexchart class="graph" v-if="charts.response.render" type="line" height="350" :options="charts.response.options" :series="charts.response.series"></apexchart>
<p class="no-data" v-else>No chart to display here</p>
</div>
</div>
<!-- History backlog -->
<div class="round">
<h3>Last {{ days }} days history log</h3>
<div class="block-content" v-if="history && Object.keys(history).length > 0">
<p><i>Showing only records where status has changed</i></p>
<table id="tasks_tbl">
<thead>
<tr>
<th width="10%">Status</th>
<th width="10%">Date</th>
<th width="10%">Time</th>
<th width="*">Output</th>
<th width="10%">Duration</th>
</tr>
</thead>
<tbody>
<tr
v-for="h in history"
v-bind:key="h.id"
>
<td :class="statusText(h.status)">
<img :src="'/img/'+statusText(h.status)+'.svg'" width="16" alt="Status" />
</td>
<td>{{ moment(h.created_at).format('YYYY-MM-DD') }}</td>
<td>{{ moment(h.created_at).format('HH:mm:ss') }}</td>
<td>
<span v-if="h.output">
{{ h.output }}
</span>
<span v-else>
<i>No output</i>
</span>
</td>
<td>
<span v-if="h.duration != null">{{ h.duration+'s' }}</span>
<span v-else><i>No duration</i></span>
</td>
</tr>
</tbody>
</table>
</div>
<p class="no-data" v-else>No history to display here</p>
</div>
<!-- Notifications block -->
<div class="round">
<h3>Last {{ days }} days notifications log</h3>
<div class="block-content" v-if="notifications && Object.keys(notifications).length > 0">
<table id="tasks_tbl">
<thead>
<tr>
<th width="10%">Date</th>
<th width="10%">Time</th>
<th width="15">Firstname</th>
<th width="15%">Lastname</th>
<th width="30%">Email</th>
<th width="10%">Type</th>
<th width="10%">Status</th>
</tr>
</thead>
<tbody>
<tr
v-for="n in notifications"
v-bind:key="n.id"
>
<td>{{ moment(n.created_at).format('YYYY-MM-DD') }}</td>
<td>{{ moment(n.created_at).format('HH:mm:ss') }}</td>
<td>{{ n.contact.firstname }}</td>
<td>{{ n.contact.surname }}</td>
<td>{{ n.contact.email }}</td>
<td>{{ n.task_history.status == 1 ? 'UP' : 'DOWN' }}</td>
<td>{{ n.status.toUpperCase() }}</td>
</tr>
</tbody>
</table>
</div>
<p class="no-data" v-else>No notification to display here</p>
</div>
</div>
</div>
</template>
<script>
export default{
data: function() {
return {
task: {
id: null
},
history: null,
notifications: null,
refresh: null,
loader: null,
days: 3,
first_day: null,
charts: {
uptime: {
render: false,
},
response: {
render: false,
}
}
}
},
methods: {
statusText: function (status) {
switch (status) {
case 1:
return 'up';
break;
case 0:
return 'down';
break;
default:
return 'unknown';
}
},
refreshTask: function(callback) {
this.$http.post('/api/getTask/'+this.task.id, {
days: this.days
})
.then(response => {
this.task = response.data.task
this.history = response.data.history
this.first_day = new Date(response.data.first_day).getTime();
this.notifications = response.data.notifications
this.refreshUptimeGraph(response.data.stats.uptime)
if (this.task.type == 'http') {
this.refreshResponseTimeGraph(response.data.stats.times)
}
this.loader.hide()
})
.then(() => {
if (this.refresh == null) {
this.refresh = window.setInterval(() => {
this.refreshTask()
}, 10000)
}
})
.catch(error => {
//TODO: do something
})
.then(() => {
this.loader.hide()
})
},
refreshResponseTimeGraph: function(stats) {
let data = [];
let xaxis = [];
for (let date in stats) {
xaxis.push(new Date(date).getTime())
if (stats[date]['count'] > 0) {
data.push(Math.round( (stats[date]['duration'] / stats[date]['count']) * 100) / 100)
}
else {
data.push(0)
}
}
this.charts.response.options = {
xaxis: {
type: 'datetime',
//min: this.first_day,
categories: xaxis,
labels: {
show: true,
rotate: -45,
}
},
yaxis: {
labels: {
formatter: function (value) {
return (Math.round(value * 100) / 100) + "s";
}
}
},
tooltip: {
x: {
format: "dd MMM yyyy"
}
},
chart: {
type: 'line',
height: 350,
stacked: false
},
legend: {
position: 'right',
offsetX: 0,
offsetY: 50
},
dataLabels: {
enabled: true,
},
colors: ['#00955c'],
stroke: {
curve: 'smooth',
},
fill: {
type: 'gradient',
gradient: {
//shade: 'dark',
shadeIntensity: 1,
type: 'vertical',
opacityFrom: 1,
opacityTo: 1,
colorStops: [
{
offset: 20,
color: "#FAD375",
opacity: 1
},
{
offset: 40,
color: "#61DBC3",
opacity: 1
}
]
}
}
}
this.charts.response.series = [{
name: 'Response time',
data: data
}]
this.charts.response.render = true
},
refreshUptimeGraph: function(stats) {
let xaxis = [];
let new_data_a = [];
let new_data_b = [];
for (let date in stats) {
let total = stats[date]['up'] + stats[date]['down']
xaxis.push(new Date(date).getTime())
if (total > 0) {
new_data_a.push( Math.round(stats[date]['up'] / total * 100) )
new_data_b.push( Math.round(stats[date]['down'] / total * 100) )
}
else {
new_data_a.push( 0 )
new_data_b.push( 0 )
}
}
this.charts.uptime.options = {
xaxis: {
type: 'datetime',
min: this.first_day,
categories: xaxis,
tickAmount: 6,
labels: {
show: true,
rotate: -45,
}
},
yaxis: {
labels: {
formatter: function (value) {
return value + "%";
}
}
},
tooltip: {
x: {
format: "yyyy MMM dd"
}
},
chart: {
type: 'bar',
height: 350,
stacked: true,
stackType: '100%'
},
legend: {
position: 'right',
offsetX: 0,
offsetY: 50
},
}
this.charts.uptime.series = [{
name: 'UP',
data: new_data_a,
color: '#00955c'
},
{
name: 'DOWN',
data: new_data_b,
color: '#ef3232'
}]
this.charts.uptime.render = true
},
},
mounted: function() {
this.loader = this.$loading.show()
this.task.id = this.$route.params.id ?? null
if (this.task.id != null) {
this.refreshTask()
}
},
beforeRouteLeave(to, from, next) {
clearTimeout(this.refresh);
next();
},
}
</script>
<style scoped>
</style>

24
routes/web.php Normal file
View file

@ -0,0 +1,24 @@
<?php
/** @var \Laravel\Lumen\Routing\Router $router */
/*
|--------------------------------------------------------------------------
| Application Routes
|--------------------------------------------------------------------------
|
| Here is where you can register all of the routes for an application.
| It is a breeze. Simply tell Lumen the URIs it should respond to
| and give it the Closure to call when that URI is requested.
|
*/
$router->group(['prefix' => '/api'], function () use ($router) {
$router->get('/getTasks/', ['uses' => 'ApiController@getTasks']);
$router->post('/getTask/{id}', ['uses' => 'ApiController@getTaskDetails']);
$router->patch('/toggleTaskStatus/{id}', ['uses' => 'ApiController@toggleTaskStatus']);
});
$router->get('/{route:.*}/', function () {
return View('app');
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 300 KiB

BIN
screenshot2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

View file

@ -1,114 +0,0 @@
-- MySQL dump 10.13 Distrib 5.1.37, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: monitoring
-- ------------------------------------------------------
-- Server version 5.1.37-1ubuntu5
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Current Database: `monitoring`
--
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `monitoring` /*!40100 DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci */;
USE `monitoring`;
--
-- Table structure for table `contacts`
--
DROP TABLE IF EXISTS `contacts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `contacts` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`surname` varchar(200) NOT NULL,
`firstname` varchar(200) NOT NULL,
`email` varchar(250) NOT NULL,
`phone` varchar(20) NOT NULL,
`creation_date` datetime NOT NULL,
`active` int(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `notifications`
--
DROP TABLE IF EXISTS `notifications`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `notifications` (
`task_id` int(11) unsigned NOT NULL,
`contact_id` int(11) unsigned NOT NULL,
PRIMARY KEY (`task_id`,`contact_id`),
KEY `contact_id` (`contact_id`),
CONSTRAINT `notifications_ibfk_2` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE,
CONSTRAINT `notifications_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tasks`
--
DROP TABLE IF EXISTS `tasks`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tasks` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`host` varchar(255) NOT NULL,
`type` enum('ping','http') NOT NULL,
`params` varchar(255) NOT NULL,
`creation_date` datetime NOT NULL,
`frequency` int(10) unsigned NOT NULL,
`last_execution` datetime NULL,
`active` int(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `host` (`host`,`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `tasks_history`
--
DROP TABLE IF EXISTS `tasks_history`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tasks_history` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`status` int(1) unsigned NOT NULL,
`datetime` datetime NOT NULL,
`task_id` int(11) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `task_id` (`task_id`),
CONSTRAINT `tasks_history_ibfk_1` FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`) ON DELETE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2010-03-04 16:41:51

2
storage/app/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

Some files were not shown because too many files have changed in this diff Show more