647 lines
16 KiB
Markdown
647 lines
16 KiB
Markdown
# Multi-Channel Notifications Implementation Guide
|
||
|
||
This guide provides a complete implementation for multi-channel monitoring alerts in your Laravel application.
|
||
|
||
## Overview
|
||
|
||
The notification system supports the following channels:
|
||
- **Email** (built-in Laravel)
|
||
- **Database** (built-in Laravel)
|
||
- **Slack** (via webhook)
|
||
- **Telegram** (via Bot API)
|
||
- **Discord** (via webhook)
|
||
- **Microsoft Teams** (via webhook)
|
||
- **Google Chat** (via webhook)
|
||
- **Custom Webhook** (generic HTTP POST)
|
||
- **SMS** (via Twilio/Vonage - requires additional setup)
|
||
- **Opsgenie** (via API)
|
||
- **PagerDuty** (via Events API)
|
||
|
||
## Installation Complete
|
||
|
||
The following packages have been installed:
|
||
```bash
|
||
composer require laravel/slack-notification-channel
|
||
composer require laravel-notification-channels/telegram
|
||
composer require laravel-notification-channels/discord
|
||
```
|
||
|
||
## Database Schema
|
||
|
||
Migration created: `2026_01_18_123539_add_notification_channels_to_users_table.php`
|
||
|
||
Fields added to `users` table:
|
||
- `notification_channels` (JSON) - Stores enabled channels per alert type
|
||
- `slack_webhook_url` - Slack incoming webhook URL
|
||
- `telegram_chat_id` - Telegram bot chat ID
|
||
- `discord_webhook_url` - Discord webhook URL
|
||
- `teams_webhook_url` - Microsoft Teams webhook URL
|
||
- `google_chat_webhook_url` - Google Chat webhook URL
|
||
- `webhook_url` - Custom webhook endpoint
|
||
- `sms_phone_number` - Phone number for SMS alerts
|
||
- `opsgenie_api_key` - Opsgenie API key
|
||
- `pagerduty_integration_key` - PagerDuty integration key
|
||
|
||
## Alert Types
|
||
|
||
The system supports these alert types:
|
||
- `uptime` - Site down/up alerts
|
||
- `performance` - Performance threshold alerts
|
||
- `ssl` - SSL certificate expiration
|
||
- `dns` - DNS record changes
|
||
- `broken_links` - Broken link detection
|
||
- `domain_expiry` - Domain expiration warnings
|
||
- `sitemap` - Sitemap validation issues
|
||
- `health_check` - Health endpoint failures
|
||
- `tcp_port` - TCP port monitoring
|
||
|
||
## Base Notification Class
|
||
|
||
Create `app/Notifications/MonitorAlert.php`:
|
||
|
||
```php
|
||
<?php
|
||
|
||
namespace App\Notifications;
|
||
|
||
use Illuminate\Bus\Queueable;
|
||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||
use Illuminate\Notifications\Messages\MailMessage;
|
||
use Illuminate\Notifications\Messages\SlackMessage;
|
||
use Illuminate\Notifications\Notification;
|
||
use NotificationChannels\Discord\DiscordChannel;
|
||
use NotificationChannels\Discord\DiscordMessage;
|
||
use NotificationChannels\Telegram\TelegramChannel;
|
||
use NotificationChannels\Telegram\TelegramMessage;
|
||
|
||
abstract class MonitorAlert extends Notification implements ShouldQueue
|
||
{
|
||
use Queueable;
|
||
|
||
public string $alertType;
|
||
public string $siteName;
|
||
public string $message;
|
||
public array $data;
|
||
public string $severity; // info, warning, critical
|
||
|
||
public function __construct(string $alertType, string $siteName, string $message, array $data = [], string $severity = 'warning')
|
||
{
|
||
$this->alertType = $alertType;
|
||
$this->siteName = $siteName;
|
||
$this->message = $message;
|
||
$this->data = $data;
|
||
$this->severity = $severity;
|
||
}
|
||
|
||
/**
|
||
* Get the notification's delivery channels.
|
||
*/
|
||
public function via($notifiable): array
|
||
{
|
||
$enabledChannels = $notifiable->getNotificationChannelsFor($this->alertType);
|
||
|
||
$channelMap = [
|
||
'email' => 'mail',
|
||
'database' => 'database',
|
||
'slack' => 'slack',
|
||
'telegram' => TelegramChannel::class,
|
||
'discord' => DiscordChannel::class,
|
||
];
|
||
|
||
$channels = [];
|
||
foreach ($enabledChannels as $channel) {
|
||
if (isset($channelMap[$channel])) {
|
||
$channels[] = $channelMap[$channel];
|
||
}
|
||
}
|
||
|
||
return $channels ?: ['database'];
|
||
}
|
||
|
||
/**
|
||
* Get the mail representation of the notification.
|
||
*/
|
||
public function toMail($notifiable): MailMessage
|
||
{
|
||
$color = $this->getColorForSeverity();
|
||
|
||
return (new MailMessage)
|
||
->subject("[{$this->getSeverityLabel()}] {$this->siteName} - {$this->getAlertTypeLabel()}")
|
||
->greeting("Alert: {$this->siteName}")
|
||
->line($this->message)
|
||
->when(!empty($this->data), function ($mail) {
|
||
foreach ($this->data as $key => $value) {
|
||
$mail->line("**{$key}:** {$value}");
|
||
}
|
||
})
|
||
->action('View Dashboard', url('/dashboard'))
|
||
->line('Timestamp: ' . now()->format('Y-m-d H:i:s T'));
|
||
}
|
||
|
||
/**
|
||
* Get the Slack representation of the notification.
|
||
*/
|
||
public function toSlack($notifiable): SlackMessage
|
||
{
|
||
$color = $this->getColorForSeverity();
|
||
|
||
$message = (new SlackMessage)
|
||
->from('Site Monitor', ':robot_face:')
|
||
->to('#monitoring')
|
||
->attachment(function ($attachment) use ($color) {
|
||
$attachment->title($this->siteName, url('/dashboard'))
|
||
->content($this->message)
|
||
->color($color)
|
||
->timestamp(now());
|
||
|
||
foreach ($this->data as $key => $value) {
|
||
$attachment->field($key, $value, true);
|
||
}
|
||
});
|
||
|
||
return $message;
|
||
}
|
||
|
||
/**
|
||
* Get the Telegram representation of the notification.
|
||
*/
|
||
public function toTelegram($notifiable): TelegramMessage
|
||
{
|
||
$emoji = $this->getEmojiForSeverity();
|
||
|
||
$text = "{$emoji} **{$this->getSeverityLabel()} Alert**\n\n";
|
||
$text .= "**Site:** {$this->siteName}\n";
|
||
$text .= "**Type:** {$this->getAlertTypeLabel()}\n\n";
|
||
$text .= "{$this->message}\n\n";
|
||
|
||
if (!empty($this->data)) {
|
||
$text .= "**Details:**\n";
|
||
foreach ($this->data as $key => $value) {
|
||
$text .= "• {$key}: {$value}\n";
|
||
}
|
||
}
|
||
|
||
$text .= "\n_" . now()->format('Y-m-d H:i:s T') . "_";
|
||
|
||
return TelegramMessage::create()
|
||
->to($notifiable->telegram_chat_id)
|
||
->content($text)
|
||
->button('View Dashboard', url('/dashboard'));
|
||
}
|
||
|
||
/**
|
||
* Get the Discord representation of the notification.
|
||
*/
|
||
public function toDiscord($notifiable): DiscordMessage
|
||
{
|
||
$color = $this->getDiscordColorForSeverity();
|
||
|
||
return DiscordMessage::create()
|
||
->embed(function ($embed) use ($color) {
|
||
$embed->title("{$this->siteName} - {$this->getAlertTypeLabel()}")
|
||
->description($this->message)
|
||
->color($color)
|
||
->timestamp(now());
|
||
|
||
foreach ($this->data as $key => $value) {
|
||
$embed->field($key, $value, true);
|
||
}
|
||
|
||
$embed->footer('Site Monitoring Alert');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Get the array representation of the notification (database).
|
||
*/
|
||
public function toArray($notifiable): array
|
||
{
|
||
return [
|
||
'alert_type' => $this->alertType,
|
||
'site_name' => $this->siteName,
|
||
'message' => $this->message,
|
||
'severity' => $this->severity,
|
||
'data' => $this->data,
|
||
'timestamp' => now()->toDateTimeString(),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Get color based on severity
|
||
*/
|
||
protected function getColorForSeverity(): string
|
||
{
|
||
return match ($this->severity) {
|
||
'critical' => 'danger',
|
||
'warning' => 'warning',
|
||
default => 'info',
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Get Discord color based on severity
|
||
*/
|
||
protected function getDiscordColorForSeverity(): int
|
||
{
|
||
return match ($this->severity) {
|
||
'critical' => 0xFF0000, // Red
|
||
'warning' => 0xFFA500, // Orange
|
||
default => 0x0099FF, // Blue
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Get emoji based on severity
|
||
*/
|
||
protected function getEmojiForSeverity(): string
|
||
{
|
||
return match ($this->severity) {
|
||
'critical' => '🚨',
|
||
'warning' => '⚠️',
|
||
default => 'ℹ️',
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Get severity label
|
||
*/
|
||
protected function getSeverityLabel(): string
|
||
{
|
||
return strtoupper($this->severity);
|
||
}
|
||
|
||
/**
|
||
* Get alert type label
|
||
*/
|
||
protected function getAlertTypeLabel(): string
|
||
{
|
||
return str_replace('_', ' ', ucwords($this->alertType));
|
||
}
|
||
}
|
||
```
|
||
|
||
## Specific Notification Classes
|
||
|
||
### 1. UptimeAlert
|
||
|
||
Create `app/Notifications/UptimeAlert.php`:
|
||
|
||
```php
|
||
<?php
|
||
|
||
namespace App\Notifications;
|
||
|
||
class UptimeAlert extends MonitorAlert
|
||
{
|
||
public function __construct(string $siteName, bool $isDown, int $statusCode, string $url, float $responseTime = null)
|
||
{
|
||
$message = $isDown
|
||
? "Your site is DOWN (HTTP {$statusCode})"
|
||
: "Your site is back ONLINE";
|
||
|
||
$data = [
|
||
'Status Code' => $statusCode,
|
||
'URL' => $url,
|
||
];
|
||
|
||
if ($responseTime) {
|
||
$data['Response Time'] = round($responseTime, 2) . 'ms';
|
||
}
|
||
|
||
parent::__construct(
|
||
'uptime',
|
||
$siteName,
|
||
$message,
|
||
$data,
|
||
$isDown ? 'critical' : 'info'
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. SslExpiryAlert
|
||
|
||
Create `app/Notifications/SslExpiryAlert.php`:
|
||
|
||
```php
|
||
<?php
|
||
|
||
namespace App\Notifications;
|
||
|
||
use Carbon\Carbon;
|
||
|
||
class SslExpiryAlert extends MonitorAlert
|
||
{
|
||
public function __construct(string $siteName, Carbon $expiresAt, int $daysRemaining)
|
||
{
|
||
$message = $daysRemaining <= 0
|
||
? "SSL certificate has EXPIRED"
|
||
: "SSL certificate expires in {$daysRemaining} days";
|
||
|
||
parent::__construct(
|
||
'ssl',
|
||
$siteName,
|
||
$message,
|
||
[
|
||
'Expires On' => $expiresAt->format('Y-m-d H:i:s'),
|
||
'Days Remaining' => $daysRemaining,
|
||
],
|
||
$daysRemaining <= 7 ? 'critical' : 'warning'
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. DomainExpiryAlert
|
||
|
||
Create `app/Notifications/DomainExpiryAlert.php`:
|
||
|
||
```php
|
||
<?php
|
||
|
||
namespace App\Notifications;
|
||
|
||
use Carbon\Carbon;
|
||
|
||
class DomainExpiryAlert extends MonitorAlert
|
||
{
|
||
public function __construct(string $siteName, string $domain, Carbon $expiresAt, int $daysRemaining)
|
||
{
|
||
$message = $daysRemaining <= 0
|
||
? "Domain '{$domain}' has EXPIRED"
|
||
: "Domain '{$domain}' expires in {$daysRemaining} days";
|
||
|
||
parent::__construct(
|
||
'domain_expiry',
|
||
$siteName,
|
||
$message,
|
||
[
|
||
'Domain' => $domain,
|
||
'Expires On' => $expiresAt->format('Y-m-d'),
|
||
'Days Remaining' => $daysRemaining,
|
||
],
|
||
$daysRemaining <= 7 ? 'critical' : 'warning'
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. SitemapAlert
|
||
|
||
Create `app/Notifications/SitemapAlert.php`:
|
||
|
||
```php
|
||
<?php
|
||
|
||
namespace App\Notifications;
|
||
|
||
class SitemapAlert extends MonitorAlert
|
||
{
|
||
public function __construct(string $siteName, string $sitemapUrl, string $error)
|
||
{
|
||
parent::__construct(
|
||
'sitemap',
|
||
$siteName,
|
||
'Sitemap validation failed',
|
||
[
|
||
'Sitemap URL' => $sitemapUrl,
|
||
'Error' => $error,
|
||
],
|
||
'warning'
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5. PerformanceAlert
|
||
|
||
Create `app/Notifications/PerformanceAlert.php`:
|
||
|
||
```php
|
||
<?php
|
||
|
||
namespace App\Notifications;
|
||
|
||
class PerformanceAlert extends MonitorAlert
|
||
{
|
||
public function __construct(string $siteName, float $responseTime, float $threshold)
|
||
{
|
||
parent::__construct(
|
||
'performance',
|
||
$siteName,
|
||
"Response time exceeded threshold",
|
||
[
|
||
'Response Time' => round($responseTime, 2) . 'ms',
|
||
'Threshold' => round($threshold, 2) . 'ms',
|
||
'Difference' => '+' . round($responseTime - $threshold, 2) . 'ms',
|
||
],
|
||
'warning'
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
## Integration with Existing Jobs
|
||
|
||
Update your monitoring jobs to send notifications. Example for `CheckSitemap`:
|
||
|
||
```php
|
||
// In app/Jobs/CheckSitemap.php
|
||
use App\Notifications\SitemapAlert;
|
||
|
||
protected function sendAlert(string $message): void
|
||
{
|
||
// Send notification using the new system
|
||
$this->sitemap->site->user->notify(
|
||
new SitemapAlert(
|
||
$this->sitemap->site->name,
|
||
$this->sitemap->url,
|
||
$this->sitemap->last_error
|
||
)
|
||
);
|
||
|
||
// Keep existing Notification model creation for backwards compatibility
|
||
Notification::create([
|
||
'user_id' => $this->sitemap->site->user_id,
|
||
'type' => 'sitemap_invalid',
|
||
'message' => "{$message}: {$this->sitemap->url}",
|
||
'data' => json_encode([
|
||
'sitemap_id' => $this->sitemap->id,
|
||
'site_id' => $this->sitemap->site_id,
|
||
'url' => $this->sitemap->url,
|
||
'error' => $this->sitemap->last_error,
|
||
]),
|
||
'read_at' => null,
|
||
]);
|
||
}
|
||
```
|
||
|
||
## Configuration Files
|
||
|
||
### Services Configuration
|
||
|
||
Add to `config/services.php`:
|
||
|
||
```php
|
||
'telegram-bot-api' => [
|
||
'token' => env('TELEGRAM_BOT_TOKEN'),
|
||
],
|
||
|
||
'twilio' => [
|
||
'sid' => env('TWILIO_SID'),
|
||
'token' => env('TWILIO_TOKEN'),
|
||
'from' => env('TWILIO_FROM'),
|
||
],
|
||
|
||
'opsgenie' => [
|
||
'api_key' => env('OPSGENIE_API_KEY'),
|
||
'api_url' => 'https://api.opsgenie.com',
|
||
],
|
||
|
||
'pagerduty' => [
|
||
'api_url' => 'https://events.pagerduty.com/v2/enqueue',
|
||
],
|
||
```
|
||
|
||
### Environment Variables
|
||
|
||
Add to `.env`:
|
||
|
||
```env
|
||
# Telegram Bot
|
||
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
||
|
||
# Twilio SMS
|
||
TWILIO_SID=your_account_sid
|
||
TWILIO_TOKEN=your_auth_token
|
||
TWILIO_FROM=+1234567890
|
||
|
||
# Opsgenie
|
||
OPSGENIE_API_KEY=your_api_key
|
||
|
||
# Global defaults (optional)
|
||
DEFAULT_SLACK_CHANNEL=#monitoring
|
||
DEFAULT_NOTIFICATION_QUEUE=notifications
|
||
```
|
||
|
||
## Notification Settings Controller
|
||
|
||
The controller and views for managing notification settings will be created separately to allow users to:
|
||
- Configure which channels to use for each alert type
|
||
- Enter webhook URLs and API keys
|
||
- Test notification delivery
|
||
- View notification history
|
||
|
||
## Channel Setup Guides
|
||
|
||
### Slack
|
||
1. Create an Incoming Webhook in your Slack workspace
|
||
2. Copy the webhook URL
|
||
3. Paste in notification settings
|
||
|
||
### Telegram
|
||
1. Create a bot via @BotFather
|
||
2. Get your bot token
|
||
3. Start a chat with your bot
|
||
4. Get your chat ID (use @userinfobot)
|
||
5. Enter chat ID in settings
|
||
|
||
### Discord
|
||
1. Edit channel settings
|
||
2. Go to Integrations > Webhooks
|
||
3. Create webhook
|
||
4. Copy webhook URL
|
||
|
||
### Microsoft Teams
|
||
1. Add "Incoming Webhook" connector to channel
|
||
2. Configure and create
|
||
3. Copy webhook URL
|
||
|
||
### Google Chat
|
||
1. Create a webhook in Google Chat space
|
||
2. Copy webhook URL
|
||
|
||
## Testing Notifications
|
||
|
||
Create a test command:
|
||
|
||
```bash
|
||
php artisan make:command TestNotifications
|
||
```
|
||
|
||
```php
|
||
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Notifications\UptimeAlert;
|
||
use App\Models\User;
|
||
use Illuminate\Console\Command;
|
||
|
||
class TestNotifications extends Command
|
||
{
|
||
protected $signature = 'notifications:test {user}';
|
||
protected $description = 'Test notification delivery for a user';
|
||
|
||
public function handle()
|
||
{
|
||
$user = User::findOrFail($this->argument('user'));
|
||
|
||
$user->notify(new UptimeAlert(
|
||
'Test Site',
|
||
false, // Not down
|
||
200,
|
||
'https://example.com',
|
||
150.5
|
||
));
|
||
|
||
$this->info('Test notification sent!');
|
||
}
|
||
}
|
||
```
|
||
|
||
## Best Practices
|
||
|
||
1. **Queue Notifications**: All notifications implement `ShouldQueue`
|
||
2. **Rate Limiting**: Implement rate limiting to prevent notification spam
|
||
3. **Retry Logic**: Configure queue retry attempts
|
||
4. **Monitoring**: Log notification failures
|
||
5. **Testing**: Test each channel before production use
|
||
6. **Security**: Encrypt sensitive webhook URLs and API keys
|
||
7. **User Preferences**: Respect user notification preferences
|
||
8. **Batching**: Consider batching similar alerts
|
||
|
||
## Next Steps
|
||
|
||
1. Run migration: `php artisan migrate`
|
||
2. Create notification settings UI
|
||
3. Integrate with existing monitoring jobs
|
||
4. Test each notification channel
|
||
5. Configure queue worker for notifications
|
||
6. Set up monitoring for the notification system itself
|
||
|
||
## Troubleshooting
|
||
|
||
### Notifications not sending
|
||
- Check queue worker is running
|
||
- Verify webhook URLs are correct
|
||
- Check logs: `storage/logs/laravel.log`
|
||
- Test with `notifications:test` command
|
||
|
||
### Channel-specific issues
|
||
- **Slack**: Verify webhook URL format
|
||
- **Telegram**: Ensure bot token is valid and chat ID is correct
|
||
- **Discord**: Check webhook hasn't been deleted
|
||
- **Email**: Verify mail configuration
|
||
|
||
## Future Enhancements
|
||
|
||
- Notification grouping (digest emails)
|
||
- Quiet hours configuration
|
||
- Escalation policies
|
||
- Notification templates
|
||
- Webhook retry logic
|
||
- Notification analytics
|
||
- Mobile push notifications
|
||
- In-app notification center
|