Files
sam-api/app/Services/Stats/StatMonitorService.php
권혁성 ca51867cc2 feat: sam_stat 최적화 및 안정화 (Phase 5)
- StatBackfillCommand: 과거 데이터 일괄 백필 (일간+월간, 프로그레스바, 에러 리포트)
- StatVerifyCommand: 원본 DB vs sam_stat 정합성 교차 검증 (--fix 자동 재집계)
- 파티셔닝 준비: 7개 일간 테이블 RANGE COLUMNS(stat_date) 마이그레이션
- Redis 캐싱: StatQueryService Cache::remember TTL 5분 + invalidateCache()
- StatMonitorService: 집계 실패/누락/불일치 시 stat_alerts 알림 기록
- StatAggregatorService: 모니터링 알림 + 캐시 무효화 연동

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 22:17:11 +09:00

121 lines
4.0 KiB
PHP

<?php
namespace App\Services\Stats;
use App\Models\Stats\StatAlert;
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(),
]);
} 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(),
]);
} catch (\Throwable $e) {
Log::error('stat_alert 기록 실패 (mismatch)', [
'tenant_id' => $tenantId,
'domain' => $domain,
'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(),
]);
}
}