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_USER=vagrant
DB_PASSWORD=vagrant
DB_NAME=monitoring
DB_PORT=3306
SMTP_HOST=localhost
SMTP_USER=
SMTP_PASSWORD=
SMTP_PORT=80
SMTP_SSL=1
MAIL_FROM=axel@monitolite.fr
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
NB_TRIES=3
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
.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** 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.
## What it does
**MonitoLite** is a very simple monitoring tool developed in Perl. It supports :
* **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.
In case of an alert, the script sends an email notifications to the specified contacts (one or many).
The script also sends a recovery email notification when the alert is over.
It uses a SQL backend for handling the tasks and the status of the tasks.
Tested on MySQL only but should support other SQL-based DBMS.
It comes with a very straightforward dashboard written in PHP. This is **optional**, the `monitolite.pl` 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.
## Screenshot
![screenshot](https://github.com/axeloz/monitolite/raw/main/screenshot.png "Logo")
## Requirements
* Perl : with DBI, Dotenv, Net::Ping, Email::MIME, Email::Sender::Simple, Email::Sender::Transport::SMTP, LWP::Simple, LWP::UserAgent, LWP::Protocol::https
* a MTA: Postfix, ...
* PHP 7+ (optional): with PDO
* a webserver (optional): Apache, Nginx, ...
* a Database server: MySQL, other? (untested)
* access to CRON tasks
* possibly `root` access for the `ping` command to run (needs confirmation)
## Installation
* clone this repo
* install Perl dependencies
* install PHP composer dependencies: `cd ./web && composer install`
* 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
* create a webserver vhost with document root to the `web` directory
* add tasks and contacts into the database (no backend yet)
* run the script: `perl monitolite.pl`
* check the web dashboard for results.
* when everything works, you may create a CRON `* * * * * cd <change/this/to/the/correct/path> && /usr/bin/perl monitolite.pl > /dev/null`
## Settings
* DB_TYPE=mysql
* DB_HOST=127.0.0.1
* DB_USER=vagrant
* DB_PASSWORD=vagrant
* DB_NAME=monitoring
* DB_PORT=3306
* SMTP_HOST=localhost
* SMTP_USER=
* SMTP_PASSWORD=
* SMTP_PORT=80
* SMTP_SSL=1
* MAIL_FROM=axel@monitolite.fr
* NB_TRIES=3
* ARCHIVE_DAYS=10
## MORE INFORMATION COMING SOON.
## TODO
* Make CRUD possible from the backend for adding tasks and contacts
* Multithreading
* SMS Notifications
* Better dashboard
* Protected backend with authentication
* Create an installation script
* Raise alert when tasks are not run at the correct frequency (CRON down or other reason)
* Set a notification capping limit to prevent many notifications to be sent in case of an up-and-down host
* Add a notification history log
* Keep track of tasks response time
* Daemonize the script (instead of CRONs)
# MONITOLITE
**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 I **rewrote** and **updated** it from scratch in a modern way.
## What it does
**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
* **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).
* **DNS monitoring**: runs a DNS lookup on a given DNS server for the hostname specified in the params
In case of an alert, the script sends an email notifications to the specified contacts (one or many).
The script also sends a recovery email notification when the alert is over.
It uses a SQL backend for handling the tasks and the status of the tasks.
Tested on MySQL only but should support other SQL-based DBMS.
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.
## Demo
[DEMO](https://monitolite.mabox.eu)
## Screenshot
### Tasks list with quick preview
![screenshot](https://github.com/axeloz/monitolite/raw/main/screenshot.png "Logo")
### Task details with graph and history
![screenshot](https://github.com/axeloz/monitolite/raw/main/screenshot2.png "Logo")
## Requirements
* PHP 7+ with cURL, `exec` command allowed, MySQL extension via PDO
* a MTA: Postfix, or an external SMTP ...
* a webserver (optional): Apache, Nginx, ...
* a Database server: MySQL, other? (untested)
* access to CRON tasks
## Installation
* clone this repo
* install PHP composer dependencies: `cd ./web && composer install`
* create a Database and import the initial schema using `php artisan migrate`
* 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)
* run the script: `cd /var/www/<your-path> && php artisan monitolite:run`
* check the output of the command for results.
* if everything works, you may create a CRON `* * * * * cd /var/www/<your-path> && php artisan monitolite:run > /dev/null`
## Settings
* APP_NAME=Monitolite
* APP_ENV=production
* APP_KEY=<GENERATE KEY HERE>
* APP_DEBUG=false
* APP_URL=http://localhost
* APP_TIMEZONE=UTC
* DB_TIMEZONE="+1:00"
* DB_CONNECTION=mysql
* DB_HOST=127.0.0.1
* DB_PORT=3306
* DB_DATABASE=homestead
* DB_USERNAME=homestead
* DB_PASSWORD=secret
* MAIL_MAILER=smtp
* MAIL_HOST=localhost
* MAIL_PORT=25
* MAIL_USERNAME=
* MAIL_PASSWORD=
* MAIL_ENCRYPTION=
* MAIL_FROM_ADDRESS=noreply@monitolite.fr
* MAIL_FROM_NAME="Monitolite"
* NB_TRIES=3
* ARCHIVE_DAYS=10
## 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