benjamin-rqt/data-watcher-bundle
Symfony bundle for database anomaly detection with email notifications and execution history.
AbstractDoctrineCheck)composer.json{
"repositories": [
{
"type": "vcs",
"url": "https://github.com/BenjaminRqt/data-watcher-bundle"
}
]
}
composer require benjamin-rqt/data-watcher-bundle
// config/bundles.php
return [
// ...
BenjaminRqt\DataWatcherBundle\DataWatcherBundle::class => ['all' => true],
];
# config/packages/data_watcher.yaml
data_watcher:
from_email: 'datawatcher@myapp.com'
recipients:
- 'admin@myapp.com'
app_name: 'My Application' # Optional, used in email subjects and templates
# config/packages/messenger.yaml
framework:
messenger:
transports:
scheduler_data_watcher:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
BenjaminRqt\DataWatcherBundle\Scheduler\Message\RunCheckMessage: scheduler_data_watcher
.env.local)MAILER_DSN=smtp://user:password@smtp.myserver.com:587
MESSENGER_TRANSPORT_DSN=doctrine://default
This bundle uses a custom table data_watcher_run to store the history of checks. Run the following command to create it:
php bin/console data-watcher:setup
Create a class in your app (not in the bundle):
<?php
namespace App\DataWatcher;
use BenjaminRqt\DataWatcherBundle\Check\AbstractSqlCheck;
final class MyCheck extends AbstractSqlCheck
{
public function getName(): string { return 'my.sql.check'; }
public function getDescription(): string { return 'Detects X'; }
public function getSchedule(): string { return '0 9 * * 1-5'; } // Mon-Fri at 9:00 AM
protected function getSql(): string
{
return "SELECT id, name FROM my_table WHERE problem = 1";
}
}
use BenjaminRqt\DataWatcherBundle\Check\AbstractDoctrineCheck;
use Doctrine\ORM\Query;
final class MyDoctrineCheck extends AbstractDoctrineCheck
{
public function getName(): string { return 'my.doctrine.check'; }
public function getDescription(): string { return 'Detects Y via ORM'; }
public function getSchedule(): string { return '@daily'; }
protected function buildQuery(): Query
{
return $this->em->getRepository(User::class)
->createQueryBuilder('u')
->select('u.id')
->where('u.email LIKE :s')
->setParameter('s', '%admin%')
->getQuery();
}
}
use BenjaminRqt\DataWatcherBundle\Check\AbstractCallableCheck;
final class MyCallableCheck extends AbstractCallableCheck
{
public function __construct(private readonly MyCustomCallableHandler $callableHandler) {}
public function getName(): string { return 'my.callable.check'; }
public function getDescription(): string { return 'Detects Z via callable'; }
public function getSchedule(): string { return '@daily'; }
protected function getCallable(): ArrayCallableInterface
{
return $this->callableHandler;
}
}
Then declare the handler containing the callable and the detection logic:
use BenjaminRqt\DataWatcherBundle\Check\ArrayCallableInterface;
final class CallableHandler implements ArrayCallableInterface
{
public function __invoke(): array
{
return [
['id' => 42],
];
}
}
public function getMaxAnomalies(): int
{
return 5; // 1 by default, here at least 5 results are needed to send an alert
}
public function getRecipients(): array
{
return ['manager@myapp.com']; // overrides global config
}
# Setup the database table for history
php bin/console data-watcher:setup
# List all registered checks
php bin/console data-watcher:run --list
# Test without email or history
php bin/console data-watcher:run my.check --dry-run
# Run a check (email + history)
php bin/console data-watcher:run my.check
# Run all checks
php bin/console data-watcher:run
composer require symfony/scheduler
# Development
php bin/console messenger:consume scheduler_data_watcher -vv
# Production (with Supervisor or systemd)
php bin/console messenger:consume scheduler_data_watcher --time-limit=3600
No configuration is required for the scheduler, it automatically detects CRON expressions defined in checks and executes them at the right time.
| Expression | Meaning |
|---|---|
0 8 * * * |
Every day at 8:00 AM |
*/30 * * * * |
Every 30 minutes |
0 9 * * 1-5 |
Mon–Fri at 9:00 AM |
@daily |
Once a day |
@hourly |
Once an hour |
How can I help you explore Laravel packages today?