Files
portal/app/app/Mail/IncidentDetectedMail.php
T
Дмитрий 527f628a21 feat(ops): incidents:watch-failures расширен на failed_jobs + 3 правила (holes #3+#5)
Закрывает дыры #3 (доп. пороги) и #5 (доп. job-классы) аудита журналирования.

Что добавлено:
* СКАН failed_jobs (Laravel-standard) дополнительно к failed_webhook_jobs:
  покрывает 7 ShouldQueue классов которые раньше не алертились
  (SyncSupplierProject, ImportLeads, GenerateReport, CsvReconcile,
  CleanupInactiveSupplierProjects, RefreshSupplierSession, DeleteSupplierProject)
* 3 правила детекции для failed_jobs:
  - spike: ≥10 failures одного job-класса за окно 10 мин → severity=high
  - daily-total: ≥50 failures одного job-класса за 24ч → severity=medium
  - persistent: exception повторяется >3ч → severity=medium
* Группировка по (job_class, LEFT(exception, 80)) через JSON-экстракт
  `payload::json->>'displayName'`
* Дедуп переведён с LIKE %summary% на точное совпадение root_cause —
  надёжно и без false-positive
* Mailable IncidentDetectedMail (отдельный от SchedulerHeartbeatMissingMail),
  отправка ТОЛЬКО при severity=high (medium = тихий signal в incidents_log)
* warn-only при отсутствии saas_admin_users (паттерн VerifyAuditChains)

Параметры команды (новые):
  --threshold-spike=10 --threshold-daily=50 --persistent-hours=3
  (старые --window=10 --threshold=200 --dedup-window=60 сохранены)

Тесты: 11/11 passed (4 старых + 7 новых, 37 assertions, 3.6s).

Plan: docs/superpowers/plans/2026-05-23-7-holes-overview.md (#3+#5).
2026-05-23 12:01:20 +03:00

45 lines
1.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Mail;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
/**
* Уведомление об автоматически обнаруженном инциденте.
*
* Отправляется только для severity=high командой incidents:watch-failures.
* Subject: [Лидерра HIGH] Incident: {summary first 100}.
*/
final class IncidentDetectedMail extends Mailable
{
public function __construct(
public readonly string $summary,
public readonly string $severity,
) {}
public function envelope(): Envelope
{
$subjectSnippet = mb_substr($this->summary, 0, 100);
return new Envelope(
subject: "[Лидерра HIGH] Incident: {$subjectSnippet}",
);
}
public function content(): Content
{
return new Content(
text: 'emails.incident_detected_text',
with: [
'summary' => $this->summary,
'severity' => $this->severity,
'now' => now()->timezone('Europe/Moscow')->toIso8601String(),
],
);
}
}