$event */ public function dispatchEvent(Workspace $workspace, array $event): int { $workspaceId = (int) $workspace->getKey(); $tenantId = (int) ($event['tenant_id'] ?? 0); $eventType = trim((string) ($event['event_type'] ?? '')); if ($workspaceId <= 0 || $tenantId <= 0 || $eventType === '') { return 0; } $tenant = Tenant::query() ->whereKey($tenantId) ->where('workspace_id', $workspaceId) ->first(); if (! $tenant instanceof Tenant) { return 0; } $now = CarbonImmutable::now('UTC'); $eventSeverity = $this->normalizeSeverity((string) ($event['severity'] ?? '')); $rules = AlertRule::query() ->with(['destinations' => fn ($query) => $query->where('is_enabled', true)]) ->where('workspace_id', $workspaceId) ->where('is_enabled', true) ->where('event_type', $eventType) ->orderBy('id') ->get(); $createdDeliveries = 0; foreach ($rules as $rule) { if (! $rule->appliesToTenant($tenantId)) { continue; } if (! $this->meetsMinimumSeverity($eventSeverity, (string) $rule->minimum_severity)) { continue; } foreach ($rule->destinations as $destination) { $fingerprintHash = $this->fingerprintService->hash($rule, $destination, $tenantId, $event); $isSuppressed = $this->shouldSuppress( workspaceId: $workspaceId, ruleId: (int) $rule->getKey(), destinationId: (int) $destination->getKey(), fingerprintHash: $fingerprintHash, cooldownSeconds: (int) ($rule->cooldown_seconds ?? 0), now: $now, ); $sendAfter = null; $status = AlertDelivery::STATUS_QUEUED; if ($isSuppressed) { $status = AlertDelivery::STATUS_SUPPRESSED; } else { $deferUntil = $this->quietHoursService->deferUntil($rule, $workspace, $now); if ($deferUntil instanceof CarbonImmutable) { $status = AlertDelivery::STATUS_DEFERRED; $sendAfter = $deferUntil; } } AlertDelivery::query()->create([ 'workspace_id' => $workspaceId, 'tenant_id' => $tenantId, 'alert_rule_id' => (int) $rule->getKey(), 'alert_destination_id' => (int) $destination->getKey(), 'event_type' => $eventType, 'severity' => $eventSeverity, 'status' => $status, 'fingerprint_hash' => $fingerprintHash, 'send_after' => $sendAfter, 'attempt_count' => 0, 'payload' => $this->buildPayload($event), ]); $createdDeliveries++; } } return $createdDeliveries; } private function normalizeSeverity(string $severity): string { $severity = strtolower(trim($severity)); return in_array($severity, ['low', 'medium', 'high', 'critical'], true) ? $severity : 'high'; } private function meetsMinimumSeverity(string $eventSeverity, string $minimumSeverity): bool { $rank = [ 'low' => 1, 'medium' => 2, 'high' => 3, 'critical' => 4, ]; $eventRank = $rank[$eventSeverity] ?? 0; $minimumRank = $rank[strtolower(trim($minimumSeverity))] ?? 0; return $eventRank >= $minimumRank; } private function shouldSuppress( int $workspaceId, int $ruleId, int $destinationId, string $fingerprintHash, int $cooldownSeconds, CarbonImmutable $now, ): bool { if ($cooldownSeconds <= 0) { return false; } $cutoff = $now->subSeconds($cooldownSeconds); return AlertDelivery::query() ->where('workspace_id', $workspaceId) ->where('alert_rule_id', $ruleId) ->where('alert_destination_id', $destinationId) ->where('fingerprint_hash', $fingerprintHash) ->whereNotIn('status', [ AlertDelivery::STATUS_SUPPRESSED, AlertDelivery::STATUS_CANCELED, ]) ->where('created_at', '>=', $cutoff) ->exists(); } /** * @param array $event * @return array */ private function buildPayload(array $event): array { $title = trim((string) ($event['title'] ?? 'Alert')); $body = trim((string) ($event['body'] ?? 'A matching alert event was detected.')); if ($title === '') { $title = 'Alert'; } if ($body === '') { $body = 'A matching alert event was detected.'; } $metadata = Arr::get($event, 'metadata', []); if (! is_array($metadata)) { $metadata = []; } return [ 'title' => $title, 'body' => $body, 'metadata' => $metadata, ]; } }