- Phase 1: backup.conf.example + sam-db-backup.sh 백업 스크립트 - Phase 2: BackupCheckCommand + StatMonitorService.recordBackupFailure() - Phase 2: routes/console.php에 db:backup-check 05:00 스케줄 등록 - Phase 4: SlackNotificationService 생성 (웹훅 알림) - Phase 4: BackupCheckCommand/StatMonitorService에 Slack 알림 연동 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
163 lines
5.4 KiB
PHP
163 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Stats;
|
|
|
|
use App\Models\Stats\StatAlert;
|
|
use App\Services\SlackNotificationService;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class StatMonitorService
|
|
{
|
|
/**
|
|
* 집계 실패 알림 기록
|
|
*/
|
|
public function recordAggregationFailure(int $tenantId, string $domain, string $jobType, string $errorMessage): void
|
|
{
|
|
try {
|
|
StatAlert::create([
|
|
'tenant_id' => $tenantId,
|
|
'alert_type' => 'aggregation_failure',
|
|
'domain' => $domain,
|
|
'severity' => 'critical',
|
|
'title' => "[{$jobType}] 집계 실패",
|
|
'message' => mb_substr($errorMessage, 0, 500),
|
|
'current_value' => 0,
|
|
'threshold_value' => 0,
|
|
'is_read' => false,
|
|
'is_resolved' => false,
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
// critical 알림 Slack 전송
|
|
app(SlackNotificationService::class)->sendStatAlert(
|
|
"[{$jobType}] 집계 실패",
|
|
mb_substr($errorMessage, 0, 300),
|
|
$domain
|
|
);
|
|
} catch (\Throwable $e) {
|
|
Log::error('stat_alert 기록 실패', [
|
|
'tenant_id' => $tenantId,
|
|
'domain' => $domain,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 데이터 누락 알림 (특정 날짜에 통계 데이터 없음)
|
|
*/
|
|
public function recordMissingData(int $tenantId, string $domain, string $date): void
|
|
{
|
|
try {
|
|
// 동일 알림 중복 방지
|
|
$exists = StatAlert::where('tenant_id', $tenantId)
|
|
->where('alert_type', 'missing_data')
|
|
->where('domain', $domain)
|
|
->where('title', 'LIKE', "%{$date}%")
|
|
->where('is_resolved', false)
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
return;
|
|
}
|
|
|
|
StatAlert::create([
|
|
'tenant_id' => $tenantId,
|
|
'alert_type' => 'missing_data',
|
|
'domain' => $domain,
|
|
'severity' => 'warning',
|
|
'title' => "[{$domain}] {$date} 데이터 누락",
|
|
'message' => "{$date} 날짜의 {$domain} 통계 데이터가 없습니다. stat:backfill 실행을 권장합니다.",
|
|
'current_value' => 0,
|
|
'threshold_value' => 1,
|
|
'is_read' => false,
|
|
'is_resolved' => false,
|
|
'created_at' => now(),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('stat_alert 기록 실패 (missing_data)', [
|
|
'tenant_id' => $tenantId,
|
|
'domain' => $domain,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 정합성 불일치 알림
|
|
*/
|
|
public function recordMismatch(int $tenantId, string $domain, string $label, float|int $expected, float|int $actual): void
|
|
{
|
|
try {
|
|
StatAlert::create([
|
|
'tenant_id' => $tenantId,
|
|
'alert_type' => 'data_mismatch',
|
|
'domain' => $domain,
|
|
'severity' => 'critical',
|
|
'title' => "[{$domain}] {$label} 정합성 불일치",
|
|
'message' => "원본={$expected}, 통계={$actual}, 차이=".($actual - $expected),
|
|
'current_value' => $actual,
|
|
'threshold_value' => $expected,
|
|
'is_read' => false,
|
|
'is_resolved' => false,
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
// critical 알림 Slack 전송
|
|
app(SlackNotificationService::class)->sendStatAlert(
|
|
"[{$label}] 정합성 불일치",
|
|
"원본={$expected}, 통계={$actual}, 차이=" . ($actual - $expected),
|
|
$domain
|
|
);
|
|
} catch (\Throwable $e) {
|
|
Log::error('stat_alert 기록 실패 (mismatch)', [
|
|
'tenant_id' => $tenantId,
|
|
'domain' => $domain,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 백업 실패 알림 기록 (시스템 레벨, tenantId=0)
|
|
*/
|
|
public function recordBackupFailure(string $title, string $message): void
|
|
{
|
|
try {
|
|
StatAlert::create([
|
|
'tenant_id' => 0,
|
|
'alert_type' => 'backup_failure',
|
|
'domain' => 'backup',
|
|
'severity' => 'critical',
|
|
'title' => $title,
|
|
'message' => mb_substr($message, 0, 500),
|
|
'current_value' => 0,
|
|
'threshold_value' => 0,
|
|
'is_read' => false,
|
|
'is_resolved' => false,
|
|
'created_at' => now(),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('stat_alert 기록 실패 (backup_failure)', [
|
|
'title' => $title,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 알림 해결 처리
|
|
*/
|
|
public function resolveAlerts(int $tenantId, string $domain, string $alertType): int
|
|
{
|
|
return StatAlert::where('tenant_id', $tenantId)
|
|
->where('domain', $domain)
|
|
->where('alert_type', $alertType)
|
|
->where('is_resolved', false)
|
|
->update([
|
|
'is_resolved' => true,
|
|
'resolved_at' => now(),
|
|
]);
|
|
}
|
|
}
|