The bundle includes a full notification system for performance alerts, supporting multiple channels.
- Available Channels
- Full Configuration
- How It Works
- Creating Custom Channels
- Email Template Customization
- Alert Formats
- Disabling Notifications
- Dynamic Configuration from Database
- Troubleshooting
Sends alerts by email using Symfony Mailer.
Requirements:
composer require symfony/mailerConfiguration:
nowo_performance:
notifications:
enabled: true
email:
enabled: true
from: 'noreply@example.com'
to:
- 'admin@example.com'
- 'devops@example.com'Sends alerts to Slack via webhooks.
Requirements:
composer require symfony/http-clientConfiguration:
nowo_performance:
notifications:
enabled: true
slack:
enabled: true
webhook_url: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'Getting the Webhook URL:
- Go to https://api.slack.com/apps
- Create a new app or select an existing one
- Go to "Incoming Webhooks"
- Enable "Incoming Webhooks"
- Create a new webhook and copy the URL
Sends alerts to Microsoft Teams via webhooks.
Requirements:
composer require symfony/http-clientConfiguration:
nowo_performance:
notifications:
enabled: true
teams:
enabled: true
webhook_url: 'https://outlook.office.com/webhook/YOUR/WEBHOOK/URL'Getting the Webhook URL:
- In Teams, go to the channel where you want to receive notifications
- Click "..." → "Connectors"
- Search for "Incoming Webhook"
- Configure and copy the URL
Sends alerts to any custom webhook.
Requirements:
composer require symfony/http-clientConfiguration:
nowo_performance:
notifications:
enabled: true
webhook:
enabled: true
url: 'https://your-custom-service.com/webhook'
format: 'json' # json, slack, or teams
headers:
'X-API-Key': 'your-api-key'
'Authorization': 'Bearer your-token'nowo_performance:
# ... other configuration ...
notifications:
enabled: true # Enable/disable all notifications
email:
enabled: true
from: 'noreply@example.com'
to:
- 'admin@example.com'
slack:
enabled: true
webhook_url: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'
teams:
enabled: true
webhook_url: 'https://outlook.office.com/webhook/YOUR/WEBHOOK/URL'
webhook:
enabled: false
url: ''
format: 'json'
headers: []Notifications are sent automatically when:
- Request Time exceeds the configured thresholds
- Query Count exceeds the configured thresholds
- Memory Usage exceeds the configured thresholds
Thresholds are configured in:
nowo_performance:
thresholds:
request_time:
warning: 0.5 # seconds
critical: 1.0 # seconds
query_count:
warning: 20
critical: 50
memory_usage:
warning: 20.0 # MB
critical: 50.0 # MBYou can create your own notification channels by implementing the NotificationChannelInterface:
<?php
use Nowo\PerformanceBundle\Notification\NotificationChannelInterface;
use Nowo\PerformanceBundle\Notification\PerformanceAlert;
use Nowo\PerformanceBundle\Entity\RouteData;
class CustomNotificationChannel implements NotificationChannelInterface
{
public function send(PerformanceAlert $alert, RouteData $routeData): bool
{
// Your sending logic here
return true;
}
public function isEnabled(): bool
{
return true;
}
public function getName(): string
{
return 'custom';
}
}Register your channel in services.yaml:
services:
App\Notification\CustomNotificationChannel:
tags:
- { name: 'nowo_performance.notification_channel', alias: 'custom' }Emails are rendered using Twig templates that you can customize.
The bundle includes two templates:
@NowoPerformanceBundle/Notification/email_alert.html.twig— HTML version@NowoPerformanceBundle/Notification/email_alert.txt.twig— Plain text version
You can override the templates by creating your own versions:
1. Create the template in your project:
templates/
bundles/
NowoPerformanceBundle/
Notification/
email_alert.html.twig
email_alert.txt.twig
2. Variables available in the templates:
alert—PerformanceAlertobject with:alert.message— Alert messagealert.type— Alert type (request_time, query_count, etc.)alert.severity— Severity (warning, critical)alert.context— Array with additional context
routeData—RouteDataentity (identity and review metadata in v2; aggregate metrics are not stored as scalar fields on the entity)severityColor— HTML color for severity (#dc3545 for critical, #ffc107 for warning)severityLabel— Severity label (Critical, Warning)
3. Example custom template:
{# templates/bundles/NowoPerformanceBundle/Notification/email_alert.html.twig #}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* Your custom styles */
</style>
</head>
<body>
<h1>{{ severityLabel }} Alert</h1>
<p>{{ alert.message }}</p>
<h2>Route: {{ routeData.name }}</h2>
<p>Request Time: {{ routeData.requestTime|number_format(4) }}s</p>
{# ... more custom content ... #}
</body>
</html>Note: If Twig is not available, the bundle uses a simple fallback with basic HTML.
Emails include:
- Title with severity (Warning/Critical)
- Full route information
- Table with all metrics
- Alert context
- Customizable templates using Twig
Slack message format with:
- Color by severity (yellow for warning, red for critical)
- Fields with route information
- Timestamp
Teams MessageCard format with:
- Color by severity
- Section with facts about the route
- Teams-compatible format
Generic JSON format:
{
"alert": {
"type": "request_time",
"severity": "critical",
"message": "Critical: Route 'app_home' has request time of 1.2345s",
"context": {
"value": 1.2345,
"threshold": 1.0
}
},
"route": {
"name": "app_home",
"env": "prod",
"http_method": "GET",
"request_time": 1.2345,
"query_count": 15,
"query_time": 0.5,
"memory_usage": 1048576,
"access_count": 100,
"last_accessed_at": "2026-01-26T10:30:00+00:00"
},
"timestamp": "2026-01-26T10:30:00+00:00"
}To disable all notifications:
nowo_performance:
notifications:
enabled: falseOr disable individual channels:
nowo_performance:
notifications:
enabled: true
email:
enabled: false
slack:
enabled: trueIf you need to store notification credentials in the database instead of the YAML file, you can use a service to configure channels dynamically.
This approach registers channels during container compilation. Useful when configuration does not change frequently.
1. Create the dynamic configuration service:
<?php
// src/Service/DynamicNotificationConfiguration.php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use Nowo\PerformanceBundle\Notification\Channel\EmailNotificationChannel;
use Nowo\PerformanceBundle\Notification\Channel\WebhookNotificationChannel;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class DynamicNotificationConfiguration
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly ?MailerInterface $mailer = null,
private readonly ?HttpClientInterface $httpClient = null
) {
}
private function getNotificationConfigFromDatabase(): array
{
// Get configuration from your entity/table
$settings = $this->entityManager
->getRepository('App\Entity\NotificationSettings')
->findOneBy(['key' => 'performance_notifications']);
if ($settings && $settings->getValue()) {
return json_decode($settings->getValue(), true) ?? [];
}
return ['enabled' => false];
}
public function createChannelsFromDatabase(): array
{
$config = $this->getNotificationConfigFromDatabase();
$channels = [];
// Email
if ($config['email']['enabled'] ?? false) {
$channels[] = new EmailNotificationChannel(
$this->mailer,
$config['email']['from'] ?? 'noreply@example.com',
$config['email']['to'] ?? [],
true
);
}
// Slack
if ($config['slack']['enabled'] ?? false && !empty($config['slack']['webhook_url'] ?? '')) {
$channels[] = new WebhookNotificationChannel(
$this->httpClient,
$config['slack']['webhook_url'],
'slack',
[],
true
);
}
// Teams
if ($config['teams']['enabled'] ?? false && !empty($config['teams']['webhook_url'] ?? '')) {
$channels[] = new WebhookNotificationChannel(
$this->httpClient,
$config['teams']['webhook_url'],
'teams',
[],
true
);
}
// Generic webhook
if ($config['webhook']['enabled'] ?? false && !empty($config['webhook']['url'] ?? '')) {
$channels[] = new WebhookNotificationChannel(
$this->httpClient,
$config['webhook']['url'],
$config['webhook']['format'] ?? 'json',
$config['webhook']['headers'] ?? [],
true
);
}
return $channels;
}
public function areNotificationsEnabled(): bool
{
$config = $this->getNotificationConfigFromDatabase();
return $config['enabled'] ?? false;
}
}2. Create the Compiler Pass:
<?php
// src/DependencyInjection/Compiler/NotificationCompilerPass.php
namespace App\DependencyInjection\Compiler;
use App\Service\DynamicNotificationConfiguration;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
class NotificationCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasParameter('nowo_performance.notifications.enabled')) {
return;
}
try {
$configService = new DynamicNotificationConfiguration(
$container->get('doctrine.orm.entity_manager'),
$container->has('mailer.mailer') ? $container->get('mailer.mailer') : null,
$container->has('http_client') ? $container->get('http_client') : null
);
$channels = $configService->createChannelsFromDatabase();
foreach ($channels as $channel) {
$serviceId = sprintf('nowo_performance.notification.channel.dynamic.%s', $channel->getName());
$definition = new Definition(get_class($channel));
// Configure arguments according to channel type...
// (see full example in docs/examples/NotificationCompilerPass.php)
$definition->setPublic(true);
$definition->addTag('nowo_performance.notification_channel', [
'alias' => $channel->getName()
]);
$container->setDefinition($serviceId, $definition);
}
} catch (\Exception $e) {
// Fall back to default YAML configuration on failure
error_log('Error loading notification config from database: ' . $e->getMessage());
}
}
}3. Register the Compiler Pass in your Bundle:
<?php
// src/Kernel.php or src/YourBundle.php
use App\DependencyInjection\Compiler\NotificationCompilerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class Kernel extends BaseKernel
{
protected function build(ContainerBuilder $container): void
{
$container->addCompilerPass(new NotificationCompilerPass());
}
}This approach creates channels on demand. Useful when credentials change frequently.
1. Create the Factory Service:
<?php
// src/Service/NotificationFactoryService.php
namespace App\Service;
use Nowo\PerformanceBundle\Service\NotificationService;
class NotificationFactoryService
{
public function __construct(
private readonly DynamicNotificationConfiguration $configService
) {
}
public function createNotificationService(): NotificationService
{
$channels = $this->configService->createChannelsFromDatabase();
$enabled = $this->configService->areNotificationsEnabled();
return new NotificationService($channels, $enabled);
}
}2. Register the Factory Service:
# config/services.yaml
services:
App\Service\DynamicNotificationConfiguration:
public: true
App\Service\NotificationFactoryService:
arguments:
$configService: '@App\Service\DynamicNotificationConfiguration'
public: true3. Use the Factory Service in your code:
<?php
// In an EventListener or Service
use App\Service\NotificationFactoryService;
use Nowo\PerformanceBundle\Event\AfterMetricsRecordedEvent;
class PerformanceAlertListener
{
public function __construct(
private readonly NotificationFactoryService $notificationFactory
) {
}
public function onAfterMetricsRecorded(AfterMetricsRecordedEvent $event): void
{
// Create NotificationService with up-to-date config from DB
$notificationService = $this->notificationFactory->createNotificationService();
// Use the service as usual (metrics are on the event in v2, not on RouteData)
$requestTime = $event->getRequestTime();
if ($requestTime !== null && $requestTime > 1.0) {
$alert = new PerformanceAlert(/* ... */);
$notificationService->sendAlert($alert, $event->getRouteData());
}
}
}Example entity for storing configuration:
<?php
// src/Entity/NotificationSettings.php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'notification_settings')]
class NotificationSettings
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', unique: true)]
private string $key;
#[ORM\Column(type: 'text')]
private string $value;
// Getters and setters...
}Example stored JSON value:
{
"enabled": true,
"email": {
"enabled": true,
"from": "noreply@example.com",
"to": ["admin@example.com", "devops@example.com"]
},
"slack": {
"enabled": true,
"webhook_url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
},
"teams": {
"enabled": false,
"webhook_url": ""
},
"webhook": {
"enabled": true,
"url": "https://api.example.com/webhook",
"format": "json",
"headers": {
"X-API-Key": "secret-key"
}
}
}See full, documented examples in:
docs/examples/DynamicNotificationConfiguration.php— Configuration servicedocs/examples/NotificationCompilerPass.php— Full Compiler Passdocs/examples/NotificationFactoryService.php— Full Factory Service
- Check that
notifications.enabled: true - Check that the specific channel is enabled
- Check that dependencies are installed (mailer/http-client)
- Check logs for errors
- Check Symfony Mailer configuration
- Check that
fromandtoare set - Review SMTP configuration
- Check that the webhook URL is correct
- Check that
symfony/http-clientis installed - Check logs for HTTP errors
- Test the webhook URL manually
- Compiler Pass: Ensure the DB is available during
cache:clear - Factory Service: Ensure the service is correctly injected
- Check logs for DB connection errors
- Verify the JSON structure in the DB is correct