NEW FEATURE: added mailing for failed runs

This commit is contained in:
Jeroen De Meerleer 2021-11-19 15:13:20 +01:00
parent 65eec4ac55
commit 7f66e559db
Signed by: JeroenED
GPG Key ID: 28CCCB8F62BFADD6
9 changed files with 617 additions and 3 deletions

View File

@ -46,4 +46,15 @@ TZ=Europe/Brussels
## Set it to the IP address of your proxy. You can set to multiple proxies by comma-separating them ## Set it to the IP address of your proxy. You can set to multiple proxies by comma-separating them
TRUSTED_PROXIES=127.0.0.1 TRUSTED_PROXIES=127.0.0.1
##############
### MAILER ###
##############
## Webcron management is sending you mails when cronjob are failing. The MAILER_DSN is providing usefull information on
## the how mails are being sent. Need info? https://symfony.com/doc/current/mailer.html#transport-setup
MAILER_DSN=native://default
## Anonymous is still someone. So even if this someone is unknown you need someone who is sending your mails.
MAILER_FROM=www-data@example.com
## Now that everything is set up: go to your friends and get wasted! ## Now that everything is set up: go to your friends and get wasted!

View File

@ -16,6 +16,7 @@
"symfony/console": "^5.3", "symfony/console": "^5.3",
"symfony/dotenv": "^5.3", "symfony/dotenv": "^5.3",
"symfony/http-foundation": "^5.3", "symfony/http-foundation": "^5.3",
"symfony/mailer": "^5.3",
"symfony/routing": "^5.3", "symfony/routing": "^5.3",
"symfony/yaml": "^5.3", "symfony/yaml": "^5.3",
"twig/intl-extra": "^3.3", "twig/intl-extra": "^3.3",

471
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "a9ab04a9aabfad43a84bb6181c37717f", "content-hash": "fe2fc744691aef0b1c463d6dc857a69c",
"packages": [ "packages": [
{ {
"name": "composer/package-versions-deprecated", "name": "composer/package-versions-deprecated",
@ -424,6 +424,154 @@
], ],
"time": "2020-05-29T18:28:51+00:00" "time": "2020-05-29T18:28:51+00:00"
}, },
{
"name": "doctrine/lexer",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/lexer.git",
"reference": "e864bbf5904cb8f5bb334f99209b48018522f042"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042",
"reference": "e864bbf5904cb8f5bb334f99209b48018522f042",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^6.0",
"phpstan/phpstan": "^0.11.8",
"phpunit/phpunit": "^8.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
"homepage": "https://www.doctrine-project.org/projects/lexer.html",
"keywords": [
"annotations",
"docblock",
"lexer",
"parser",
"php"
],
"support": {
"issues": "https://github.com/doctrine/lexer/issues",
"source": "https://github.com/doctrine/lexer/tree/1.2.1"
},
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer",
"type": "tidelift"
}
],
"time": "2020-05-25T17:44:05+00:00"
},
{
"name": "egulias/email-validator",
"version": "3.1.2",
"source": {
"type": "git",
"url": "https://github.com/egulias/EmailValidator.git",
"reference": "ee0db30118f661fb166bcffbf5d82032df484697"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ee0db30118f661fb166bcffbf5d82032df484697",
"reference": "ee0db30118f661fb166bcffbf5d82032df484697",
"shasum": ""
},
"require": {
"doctrine/lexer": "^1.2",
"php": ">=7.2",
"symfony/polyfill-intl-idn": "^1.15"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^8.5.8|^9.3.3",
"vimeo/psalm": "^4"
},
"suggest": {
"ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Egulias\\EmailValidator\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Eduardo Gulias Davis"
}
],
"description": "A library for validating emails against several RFCs",
"homepage": "https://github.com/egulias/EmailValidator",
"keywords": [
"email",
"emailvalidation",
"emailvalidator",
"validation",
"validator"
],
"support": {
"issues": "https://github.com/egulias/EmailValidator/issues",
"source": "https://github.com/egulias/EmailValidator/tree/3.1.2"
},
"funding": [
{
"url": "https://github.com/egulias",
"type": "github"
}
],
"time": "2021-10-11T09:18:27+00:00"
},
{ {
"name": "guzzlehttp/guzzle", "name": "guzzlehttp/guzzle",
"version": "7.4.0", "version": "7.4.0",
@ -2500,6 +2648,164 @@
], ],
"time": "2021-09-14T15:57:41+00:00" "time": "2021-09-14T15:57:41+00:00"
}, },
{
"name": "symfony/mailer",
"version": "v5.3.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
"reference": "c1f83da2296741110be35dd779f2a9e412cec466"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mailer/zipball/c1f83da2296741110be35dd779f2a9e412cec466",
"reference": "c1f83da2296741110be35dd779f2a9e412cec466",
"shasum": ""
},
"require": {
"egulias/email-validator": "^2.1.10|^3",
"php": ">=7.2.5",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.1",
"symfony/event-dispatcher": "^4.4|^5.0",
"symfony/mime": "^5.2.6",
"symfony/polyfill-php80": "^1.16",
"symfony/service-contracts": "^1.1|^2"
},
"conflict": {
"symfony/http-kernel": "<4.4"
},
"require-dev": {
"symfony/http-client-contracts": "^1.1|^2",
"symfony/messenger": "^4.4|^5.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Mailer\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/mailer/tree/v5.3.9"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-07-23T15:55:36+00:00"
},
{
"name": "symfony/mime",
"version": "v5.3.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/mime.git",
"reference": "a756033d0a7e53db389618653ae991eba5a19a11"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/a756033d0a7e53db389618653ae991eba5a19a11",
"reference": "a756033d0a7e53db389618653ae991eba5a19a11",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1",
"symfony/polyfill-intl-idn": "^1.10",
"symfony/polyfill-mbstring": "^1.0",
"symfony/polyfill-php80": "^1.16"
},
"conflict": {
"egulias/email-validator": "~3.0.0",
"phpdocumentor/reflection-docblock": "<3.2.2",
"phpdocumentor/type-resolver": "<1.4.0",
"symfony/mailer": "<4.4"
},
"require-dev": {
"egulias/email-validator": "^2.1.10|^3.1",
"phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/property-access": "^4.4|^5.1",
"symfony/property-info": "^4.4|^5.1",
"symfony/serializer": "^5.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Mime\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows manipulating MIME messages",
"homepage": "https://symfony.com",
"keywords": [
"mime",
"mime-type"
],
"support": {
"source": "https://github.com/symfony/mime/tree/v5.3.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-09-10T12:30:38+00:00"
},
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.23.0", "version": "v1.23.0",
@ -2660,6 +2966,93 @@
], ],
"time": "2021-05-27T12:26:48+00:00" "time": "2021-05-27T12:26:48+00:00"
}, },
{
"name": "symfony/polyfill-intl-idn",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-idn.git",
"reference": "65bd267525e82759e7d8c4e8ceea44f398838e65"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/65bd267525e82759e7d8c4e8ceea44f398838e65",
"reference": "65bd267525e82759e7d8c4e8ceea44f398838e65",
"shasum": ""
},
"require": {
"php": ">=7.1",
"symfony/polyfill-intl-normalizer": "^1.10",
"symfony/polyfill-php72": "^1.10"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Intl\\Idn\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Laurent Bassin",
"email": "laurent@bassin.info"
},
{
"name": "Trevor Rowbotham",
"email": "trevor.rowbotham@pm.me"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"idn",
"intl",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-05-27T09:27:20+00:00"
},
{ {
"name": "symfony/polyfill-intl-normalizer", "name": "symfony/polyfill-intl-normalizer",
"version": "v1.23.0", "version": "v1.23.0",
@ -2824,6 +3217,82 @@
], ],
"time": "2021-05-27T12:26:48+00:00" "time": "2021-05-27T12:26:48+00:00"
}, },
{
"name": "symfony/polyfill-php72",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php72.git",
"reference": "9a142215a36a3888e30d0a9eeea9766764e96976"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976",
"reference": "9a142215a36a3888e30d0a9eeea9766764e96976",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php72\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php72/tree/v1.23.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-05-27T09:17:38+00:00"
},
{ {
"name": "symfony/polyfill-php73", "name": "symfony/polyfill-php73",
"version": "v1.23.0", "version": "v1.23.0",

View File

@ -0,0 +1,68 @@
<?php
namespace JeroenED\Webcron\Command;
use JeroenED\Framework\Kernel;
use JeroenED\Framework\Twig;
use JeroenED\Webcron\Repository\Job;
use JeroenED\Webcron\Repository\User;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
class MailFailedRunsCommand extends Command
{
protected static $defaultName = 'mail-failed-runs';
protected $kernel;
public function __construct(Kernel $kernel)
{
$this->kernel = $kernel;
parent::__construct();
}
protected function configure()
{
$this
->setDescription('Sends email about failed runs')
->setHelp('This command will send emails to the users when jobs are failing');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$userRepo = new User($this->kernel->getDbCon());
$jobRepo = new Job($this->kernel->getDbCon());
$failedJobs = $jobRepo->getFailingJobs();
if(!empty($failedJobs)) {
$twig = new Twig($this->kernel);
$html = $twig->render('mail-failed-runs.html.twig', ['jobs' => $failedJobs]);
$transport = Transport::fromDsn($_ENV['MAILER_DSN']);
$mailer = new Mailer($transport);
$email = (new Email())
->from($_ENV['MAILER_FROM'])
->subject('Some cronjobs are failing')
->html($html);
$recipients = $userRepo->getMailAddresses();
foreach ($recipients as $recipient) {
$email->addTo($recipient);
}
$mailer->send($email);
$output->writeln('Message sent');
}
return Command::SUCCESS;
}
}

View File

@ -13,6 +13,39 @@ use phpseclib3\Net\SSH2;
class Job extends Repository class Job extends Repository
{ {
public function getFailingJobs()
{
$runRepo = new Run($this->dbcon);
$jobsSql = "SELECT * FROM job";
$jobsStmt = $this->dbcon->prepare($jobsSql);
$jobsRslt = $jobsStmt->executeQuery();
$jobs = $jobsRslt->fetchAllAssociative();
foreach ($jobs as $key=>&$job) {
$job['data'] = json_decode($job['data'], true);
$job['host-displayname'] = $job['data']['host'];
$job['host'] = $job['data']['host'];
$job['service'] = $job['data']['service'] ?? '';
$failedruns = $runRepo->getRunsForJob($job['id'], true, $job['data']['fail-days']);
$failed = count($failedruns);
$all = count($runRepo->getRunsForJob($job['id'], false, $job['data']['fail-days']));
$job['lastfail'] = $failedruns[0] ?? NULL;
$job['needschecking'] = $all > 0 && (($failed / $all) * 100) > $job['data']['fail-pct'];
if(!empty($job['data']['containertype']) && $job['data']['containertype'] != 'none') {
$job['host-displayname'] = $job['data']['service'] . ' on ' . $job['data']['host'];
}
if($job['needschecking']) $failingjobs[] = $job;
}
if(empty($failingjobs)) return [];
array_multisort(
array_column($failingjobs, 'name'), SORT_ASC,
array_column($failingjobs, 'host'), SORT_ASC,
array_column($failingjobs, 'service'), SORT_ASC,
$failingjobs);
return $failingjobs;
}
public function getAllJobs(bool $idiskey = false) public function getAllJobs(bool $idiskey = false)
{ {
$runRepo = new Run($this->dbcon); $runRepo = new Run($this->dbcon);

View File

@ -22,7 +22,7 @@ class User extends Repository
{ {
$userSql = "SELECT * from user WHERE email = :user"; $userSql = "SELECT * from user WHERE email = :user";
$userStmt = $this->dbcon->prepare($userSql); $userStmt = $this->dbcon->prepare($userSql);
$userRslt = $userStmt->execute([':user' => $user]); $userRslt = $userStmt->executeQuery([':user' => $user]);
if($user = $userRslt->fetchAssociative()) { if($user = $userRslt->fetchAssociative()) {
if($autologin) $password = $this->getPassFromAutologinToken($password); if($autologin) $password = $this->getPassFromAutologinToken($password);
@ -55,4 +55,16 @@ class User extends Repository
) )
? substr($decrypted, 0, -7) : null; ? substr($decrypted, 0, -7) : null;
} }
public function getMailAddresses() {
$emailSql = "SELECT email FROM user WHERE sendmail = 1";
$emailStmt = $this->dbcon->prepare($emailSql);
$emailRslt = $emailStmt->executeQuery();
$return = [];
foreach($emailRslt->fetchAllAssociative() as $email) {
$return[] = $email['email'];
}
return $return;
}
} }

View File

@ -14,7 +14,8 @@ CREATE TABLE job (
CREATE TABLE "user" ( CREATE TABLE "user" (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
email TEXT(50) NOT NULL, email TEXT(50) NOT NULL,
password TEXT(72) NOT NULL password TEXT(72) NOT NULL,
sendmail INTEGER NOT NULL
); );
-- run definition -- run definition

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Some cronjobs are Failing</title>
</head>
<body>
<p>Some cronjobs are failing</p>
{% for job in jobs %}
<h2>{{ job.name }} ({{ attribute(job, 'host-displayname') }})</h2>
<p>Last failed on {{ job.lastfail.timestamp | date("d/m/Y H:i:s") }}</p>
{% if job.lastfail.output is not empty %}
<pre>{{ job.lastfail.output }}</pre>
{% endif %}
{% endfor %}
</body>
</html>

View File

@ -5,6 +5,7 @@ require_once 'bootstrap.php';
use JeroenED\Framework\Kernel; use JeroenED\Framework\Kernel;
use JeroenED\Webcron\Command\CleanupCommand; use JeroenED\Webcron\Command\CleanupCommand;
use JeroenED\Webcron\Command\DaemonCommand; use JeroenED\Webcron\Command\DaemonCommand;
use JeroenED\Webcron\Command\MailFailedRunsCommand;
use JeroenED\Webcron\Command\RunCommand; use JeroenED\Webcron\Command\RunCommand;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
@ -20,6 +21,7 @@ $kernel->parseDotEnv($kernel->getProjectDir() . '/.env');
$application->add(new RunCommand($kernel)); $application->add(new RunCommand($kernel));
$application->add(new DaemonCommand($kernel)); $application->add(new DaemonCommand($kernel));
$application->add(new CleanupCommand($kernel)); $application->add(new CleanupCommand($kernel));
$application->add(new MailFailedRunsCommand($kernel));
$application->run(); $application->run();