|
|
@@ -0,0 +1,469 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Module\ThirdParty\Listeners;
|
|
|
+
|
|
|
+use App\Module\ThirdParty\Events\QuotaAlertEvent;
|
|
|
+use App\Module\ThirdParty\Events\MonitorAlertEvent;
|
|
|
+use App\Module\ThirdParty\Events\ServiceStatusChangedEvent;
|
|
|
+use Illuminate\Contracts\Queue\ShouldQueue;
|
|
|
+use Illuminate\Queue\InteractsWithQueue;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+use Illuminate\Support\Facades\Mail;
|
|
|
+use Illuminate\Support\Facades\Http;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 告警通知监听器
|
|
|
+ *
|
|
|
+ * 监听各种告警事件并发送通知
|
|
|
+ */
|
|
|
+class AlertNotificationListener implements ShouldQueue
|
|
|
+{
|
|
|
+ use InteractsWithQueue;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 队列名称
|
|
|
+ *
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ public $queue = 'notifications';
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理配额告警事件
|
|
|
+ *
|
|
|
+ * @param QuotaAlertEvent $event
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ public function handleQuotaAlert(QuotaAlertEvent $event): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $this->sendQuotaAlertNotification($event);
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('配额告警通知发送失败', [
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'event_data' => $event->toArray(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理监控告警事件
|
|
|
+ *
|
|
|
+ * @param MonitorAlertEvent $event
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ public function handleMonitorAlert(MonitorAlertEvent $event): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $this->sendMonitorAlertNotification($event);
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('监控告警通知发送失败', [
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'event_data' => $event->toArray(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理服务状态变更事件
|
|
|
+ *
|
|
|
+ * @param ServiceStatusChangedEvent $event
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ public function handleServiceStatusChanged(ServiceStatusChangedEvent $event): void
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ if ($event->shouldNotify()) {
|
|
|
+ $this->sendServiceStatusNotification($event);
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('服务状态变更通知发送失败', [
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'event_data' => $event->toArray(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送配额告警通知
|
|
|
+ *
|
|
|
+ * @param QuotaAlertEvent $event
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ protected function sendQuotaAlertNotification(QuotaAlertEvent $event): void
|
|
|
+ {
|
|
|
+ $channels = $this->getNotificationChannels($event->getAlertLevel());
|
|
|
+
|
|
|
+ foreach ($channels as $channel) {
|
|
|
+ switch ($channel) {
|
|
|
+ case 'email':
|
|
|
+ $this->sendEmailNotification($event);
|
|
|
+ break;
|
|
|
+ case 'sms':
|
|
|
+ $this->sendSmsNotification($event);
|
|
|
+ break;
|
|
|
+ case 'webhook':
|
|
|
+ $this->sendWebhookNotification($event);
|
|
|
+ break;
|
|
|
+ case 'log':
|
|
|
+ $this->sendLogNotification($event);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送监控告警通知
|
|
|
+ *
|
|
|
+ * @param MonitorAlertEvent $event
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ protected function sendMonitorAlertNotification(MonitorAlertEvent $event): void
|
|
|
+ {
|
|
|
+ $channels = $this->getNotificationChannels($event->alertLevel);
|
|
|
+
|
|
|
+ foreach ($channels as $channel) {
|
|
|
+ switch ($channel) {
|
|
|
+ case 'email':
|
|
|
+ $this->sendEmailNotification($event);
|
|
|
+ break;
|
|
|
+ case 'sms':
|
|
|
+ $this->sendSmsNotification($event);
|
|
|
+ break;
|
|
|
+ case 'webhook':
|
|
|
+ $this->sendWebhookNotification($event);
|
|
|
+ break;
|
|
|
+ case 'log':
|
|
|
+ $this->sendLogNotification($event);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送服务状态变更通知
|
|
|
+ *
|
|
|
+ * @param ServiceStatusChangedEvent $event
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ protected function sendServiceStatusNotification(ServiceStatusChangedEvent $event): void
|
|
|
+ {
|
|
|
+ $channels = $this->getNotificationChannels($event->getAlertLevel());
|
|
|
+
|
|
|
+ foreach ($channels as $channel) {
|
|
|
+ switch ($channel) {
|
|
|
+ case 'email':
|
|
|
+ $this->sendEmailNotification($event);
|
|
|
+ break;
|
|
|
+ case 'sms':
|
|
|
+ $this->sendSmsNotification($event);
|
|
|
+ break;
|
|
|
+ case 'webhook':
|
|
|
+ $this->sendWebhookNotification($event);
|
|
|
+ break;
|
|
|
+ case 'log':
|
|
|
+ $this->sendLogNotification($event);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取通知渠道
|
|
|
+ *
|
|
|
+ * @param string $alertLevel
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ protected function getNotificationChannels(string $alertLevel): array
|
|
|
+ {
|
|
|
+ $config = config('thirdparty.alerts.channels', []);
|
|
|
+
|
|
|
+ return match ($alertLevel) {
|
|
|
+ 'CRITICAL' => $config['critical'] ?? ['email', 'sms', 'webhook', 'log'],
|
|
|
+ 'WARNING' => $config['warning'] ?? ['email', 'log'],
|
|
|
+ 'INFO' => $config['info'] ?? ['log'],
|
|
|
+ default => ['log'],
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送邮件通知
|
|
|
+ *
|
|
|
+ * @param mixed $event
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ protected function sendEmailNotification($event): void
|
|
|
+ {
|
|
|
+ $recipients = config('thirdparty.alerts.email.recipients', []);
|
|
|
+
|
|
|
+ if (empty($recipients)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $subject = $this->getEmailSubject($event);
|
|
|
+ $content = $this->getEmailContent($event);
|
|
|
+
|
|
|
+ foreach ($recipients as $recipient) {
|
|
|
+ try {
|
|
|
+ Mail::raw($content, function ($message) use ($recipient, $subject) {
|
|
|
+ $message->to($recipient)
|
|
|
+ ->subject($subject);
|
|
|
+ });
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error("邮件通知发送失败: {$recipient}", [
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送短信通知
|
|
|
+ *
|
|
|
+ * @param mixed $event
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ protected function sendSmsNotification($event): void
|
|
|
+ {
|
|
|
+ $phones = config('thirdparty.alerts.sms.phones', []);
|
|
|
+
|
|
|
+ if (empty($phones)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $message = $this->getSmsMessage($event);
|
|
|
+
|
|
|
+ foreach ($phones as $phone) {
|
|
|
+ try {
|
|
|
+ // 这里可以集成短信服务
|
|
|
+ Log::info("短信通知: {$phone}", ['message' => $message]);
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error("短信通知发送失败: {$phone}", [
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送Webhook通知
|
|
|
+ *
|
|
|
+ * @param mixed $event
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ protected function sendWebhookNotification($event): void
|
|
|
+ {
|
|
|
+ $webhooks = config('thirdparty.alerts.webhooks', []);
|
|
|
+
|
|
|
+ if (empty($webhooks)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $payload = $this->getWebhookPayload($event);
|
|
|
+
|
|
|
+ foreach ($webhooks as $webhook) {
|
|
|
+ try {
|
|
|
+ $response = Http::timeout(10)->post($webhook['url'], $payload);
|
|
|
+
|
|
|
+ if (!$response->successful()) {
|
|
|
+ Log::warning("Webhook通知失败: {$webhook['url']}", [
|
|
|
+ 'status' => $response->status(),
|
|
|
+ 'response' => $response->body(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error("Webhook通知发送失败: {$webhook['url']}", [
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送日志通知
|
|
|
+ *
|
|
|
+ * @param mixed $event
|
|
|
+ * @return void
|
|
|
+ */
|
|
|
+ protected function sendLogNotification($event): void
|
|
|
+ {
|
|
|
+ $level = $this->getLogLevel($event);
|
|
|
+ $message = $this->getLogMessage($event);
|
|
|
+ $context = method_exists($event, 'toArray') ? $event->toArray() : [];
|
|
|
+
|
|
|
+ Log::log($level, $message, $context);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取邮件主题
|
|
|
+ *
|
|
|
+ * @param mixed $event
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function getEmailSubject($event): string
|
|
|
+ {
|
|
|
+ return match (get_class($event)) {
|
|
|
+ QuotaAlertEvent::class => "第三方服务配额告警 - {$event->service->name}",
|
|
|
+ MonitorAlertEvent::class => "第三方服务监控告警 - {$event->service->name}",
|
|
|
+ ServiceStatusChangedEvent::class => "第三方服务状态变更 - {$event->service->name}",
|
|
|
+ default => '第三方服务告警通知',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取邮件内容
|
|
|
+ *
|
|
|
+ * @param mixed $event
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function getEmailContent($event): string
|
|
|
+ {
|
|
|
+ return match (get_class($event)) {
|
|
|
+ QuotaAlertEvent::class => $this->getQuotaAlertEmailContent($event),
|
|
|
+ MonitorAlertEvent::class => $this->getMonitorAlertEmailContent($event),
|
|
|
+ ServiceStatusChangedEvent::class => $this->getServiceStatusEmailContent($event),
|
|
|
+ default => '第三方服务告警通知',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取配额告警邮件内容
|
|
|
+ *
|
|
|
+ * @param QuotaAlertEvent $event
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function getQuotaAlertEmailContent(QuotaAlertEvent $event): string
|
|
|
+ {
|
|
|
+ $content = "第三方服务配额告警\n\n";
|
|
|
+ $content .= "服务名称: {$event->service->name}\n";
|
|
|
+ $content .= "服务代码: {$event->service->code}\n";
|
|
|
+ $content .= "告警类型: {$event->getAlertMessage()}\n";
|
|
|
+ $content .= "当前使用: {$event->currentUsage}/{$event->quotaLimit}\n";
|
|
|
+ $content .= "使用率: " . number_format($event->usagePercentage, 1) . "%\n";
|
|
|
+ $content .= "剩余配额: {$event->getRemainingQuota()}\n";
|
|
|
+ $content .= "告警时间: {$event->alertTime->format('Y-m-d H:i:s')}\n\n";
|
|
|
+
|
|
|
+ $actions = $event->getSuggestedActions();
|
|
|
+ if (!empty($actions)) {
|
|
|
+ $content .= "建议操作:\n";
|
|
|
+ foreach ($actions as $action) {
|
|
|
+ $content .= "- {$action}\n";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $content;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取监控告警邮件内容
|
|
|
+ *
|
|
|
+ * @param MonitorAlertEvent $event
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function getMonitorAlertEmailContent(MonitorAlertEvent $event): string
|
|
|
+ {
|
|
|
+ $content = "第三方服务监控告警\n\n";
|
|
|
+ $content .= "服务名称: {$event->service->name}\n";
|
|
|
+ $content .= "服务代码: {$event->service->code}\n";
|
|
|
+ $content .= "告警类型: {$event->getAlertTypeLabel()}\n";
|
|
|
+ $content .= "告警级别: {$event->alertLevel}\n";
|
|
|
+ $content .= "告警消息: {$event->alertMessage}\n";
|
|
|
+ $content .= "告警时间: {$event->alertTime->format('Y-m-d H:i:s')}\n\n";
|
|
|
+
|
|
|
+ $actions = $event->getSuggestedActions();
|
|
|
+ if (!empty($actions)) {
|
|
|
+ $content .= "建议操作:\n";
|
|
|
+ foreach ($actions as $action) {
|
|
|
+ $content .= "- {$action}\n";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $content;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取服务状态变更邮件内容
|
|
|
+ *
|
|
|
+ * @param ServiceStatusChangedEvent $event
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function getServiceStatusEmailContent(ServiceStatusChangedEvent $event): string
|
|
|
+ {
|
|
|
+ $content = "第三方服务状态变更通知\n\n";
|
|
|
+ $content .= "服务名称: {$event->service->name}\n";
|
|
|
+ $content .= "服务代码: {$event->service->code}\n";
|
|
|
+ $content .= "状态变更: {$event->oldStatus->getLabel()} -> {$event->newStatus->getLabel()}\n";
|
|
|
+ $content .= "变更原因: " . ($event->reason ?? '未知') . "\n";
|
|
|
+ $content .= "变更时间: {$event->changedAt->format('Y-m-d H:i:s')}\n";
|
|
|
+
|
|
|
+ return $content;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取短信消息
|
|
|
+ *
|
|
|
+ * @param mixed $event
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function getSmsMessage($event): string
|
|
|
+ {
|
|
|
+ return match (get_class($event)) {
|
|
|
+ QuotaAlertEvent::class => "【告警】{$event->service->name} 配额使用率 " . number_format($event->usagePercentage, 1) . "%",
|
|
|
+ MonitorAlertEvent::class => "【告警】{$event->service->name} {$event->getAlertTypeLabel()}异常",
|
|
|
+ ServiceStatusChangedEvent::class => "【通知】{$event->service->name} 状态变更为 {$event->newStatus->getLabel()}",
|
|
|
+ default => '第三方服务告警',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取Webhook载荷
|
|
|
+ *
|
|
|
+ * @param mixed $event
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ protected function getWebhookPayload($event): array
|
|
|
+ {
|
|
|
+ $basePayload = [
|
|
|
+ 'timestamp' => now()->toISOString(),
|
|
|
+ 'source' => 'thirdparty-module',
|
|
|
+ 'event_type' => get_class($event),
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (method_exists($event, 'toArray')) {
|
|
|
+ $basePayload['data'] = $event->toArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ return $basePayload;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取日志级别
|
|
|
+ *
|
|
|
+ * @param mixed $event
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function getLogLevel($event): string
|
|
|
+ {
|
|
|
+ return match (get_class($event)) {
|
|
|
+ QuotaAlertEvent::class => $event->isCritical() ? 'critical' : 'warning',
|
|
|
+ MonitorAlertEvent::class => strtolower($event->alertLevel),
|
|
|
+ ServiceStatusChangedEvent::class => $event->isCriticalChange() ? 'critical' : 'info',
|
|
|
+ default => 'info',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取日志消息
|
|
|
+ *
|
|
|
+ * @param mixed $event
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function getLogMessage($event): string
|
|
|
+ {
|
|
|
+ return match (get_class($event)) {
|
|
|
+ QuotaAlertEvent::class => $event->getAlertMessage(),
|
|
|
+ MonitorAlertEvent::class => $event->alertMessage,
|
|
|
+ ServiceStatusChangedEvent::class => $event->getNotificationMessage(),
|
|
|
+ default => '第三方服务事件通知',
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|