Files
Gnuboard7/app/Seo/SeoCacheStatsService.php
T
2026-04-01 10:30:52 +09:00

210 lines
6.9 KiB
PHP

<?php
namespace App\Seo;
use App\Models\SeoCacheStat;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* SEO 캐시 통계 서비스
*
* 캐시 히트/미스를 기록하고 통계 데이터를 제공합니다.
*/
class SeoCacheStatsService
{
/**
* 캐시 히트를 기록합니다.
*
* @param string $url 요청 URL
* @param string $locale 로케일
* @param string|null $layoutName 레이아웃명
* @param string|null $moduleIdentifier 모듈 식별자
* @return void
*/
public function recordHit(
string $url,
string $locale,
?string $layoutName = null,
?string $moduleIdentifier = null
): void {
try {
SeoCacheStat::create([
'url' => $url,
'locale' => $locale,
'layout_name' => $layoutName,
'module_identifier' => $moduleIdentifier,
'type' => 'hit',
]);
} catch (\Exception $e) {
Log::warning('[SEO] 캐시 히트 기록 실패', [
'url' => $url,
'error' => $e->getMessage(),
]);
}
}
/**
* 캐시 미스를 기록합니다.
*
* @param string $url 요청 URL
* @param string $locale 로케일
* @param string|null $layoutName 레이아웃명
* @param string|null $moduleIdentifier 모듈 식별자
* @param int|null $responseTimeMs 렌더링 소요 시간 (ms)
* @return void
*/
public function recordMiss(
string $url,
string $locale,
?string $layoutName = null,
?string $moduleIdentifier = null,
?int $responseTimeMs = null
): void {
try {
SeoCacheStat::create([
'url' => $url,
'locale' => $locale,
'layout_name' => $layoutName,
'module_identifier' => $moduleIdentifier,
'type' => 'miss',
'response_time_ms' => $responseTimeMs,
]);
} catch (\Exception $e) {
Log::warning('[SEO] 캐시 미스 기록 실패', [
'url' => $url,
'error' => $e->getMessage(),
]);
}
}
/**
* 캐시 통계를 조회합니다.
*
* @param Carbon|null $since 조회 시작 시점 (null이면 전체 기간)
* @return array{total_entries: int, hits: int, misses: int, hit_rate: float, avg_response_time_ms: float|null}
*/
public function getStats(?Carbon $since = null): array
{
$query = SeoCacheStat::query();
if ($since) {
$query->since($since);
}
$total = $query->count();
$hits = (clone $query)->hits()->count();
$misses = (clone $query)->misses()->count();
$avgResponseTime = (clone $query)->misses()
->whereNotNull('response_time_ms')
->avg('response_time_ms');
return [
'total_entries' => $total,
'hits' => $hits,
'misses' => $misses,
'hit_rate' => $total > 0 ? round(($hits / $total) * 100, 2) : 0.0,
'avg_response_time_ms' => $avgResponseTime !== null ? round((float) $avgResponseTime, 2) : null,
];
}
/**
* 레이아웃별 캐시 통계를 조회합니다.
*
* @param Carbon|null $since 조회 시작 시점 (null이면 전체 기간)
* @return array<int, array{layout_name: string|null, total: int, hits: int, misses: int, hit_rate: float, avg_response_time_ms: float|null}>
*/
public function getStatsByLayout(?Carbon $since = null): array
{
$query = SeoCacheStat::query()
->select(
'layout_name',
DB::raw('COUNT(*) as total'),
DB::raw("SUM(CASE WHEN type = 'hit' THEN 1 ELSE 0 END) as hits"),
DB::raw("SUM(CASE WHEN type = 'miss' THEN 1 ELSE 0 END) as misses"),
DB::raw("AVG(CASE WHEN type = 'miss' AND response_time_ms IS NOT NULL THEN response_time_ms ELSE NULL END) as avg_response_time_ms")
)
->groupBy('layout_name');
if ($since) {
$query->where('created_at', '>=', $since);
}
return $query->get()->map(function ($row) {
$total = (int) $row->total;
$hits = (int) $row->hits;
return [
'layout_name' => $row->layout_name,
'total' => $total,
'hits' => $hits,
'misses' => (int) $row->misses,
'hit_rate' => $total > 0 ? round(($hits / $total) * 100, 2) : 0.0,
'avg_response_time_ms' => $row->avg_response_time_ms !== null
? round((float) $row->avg_response_time_ms, 2)
: null,
];
})->toArray();
}
/**
* 모듈별 캐시 통계를 조회합니다.
*
* @param Carbon|null $since 조회 시작 시점 (null이면 전체 기간)
* @return array<int, array{module_identifier: string|null, total: int, hits: int, misses: int, hit_rate: float, avg_response_time_ms: float|null}>
*/
public function getStatsByModule(?Carbon $since = null): array
{
$query = SeoCacheStat::query()
->select(
'module_identifier',
DB::raw('COUNT(*) as total'),
DB::raw("SUM(CASE WHEN type = 'hit' THEN 1 ELSE 0 END) as hits"),
DB::raw("SUM(CASE WHEN type = 'miss' THEN 1 ELSE 0 END) as misses"),
DB::raw("AVG(CASE WHEN type = 'miss' AND response_time_ms IS NOT NULL THEN response_time_ms ELSE NULL END) as avg_response_time_ms")
)
->groupBy('module_identifier');
if ($since) {
$query->where('created_at', '>=', $since);
}
return $query->get()->map(function ($row) {
$total = (int) $row->total;
$hits = (int) $row->hits;
return [
'module_identifier' => $row->module_identifier,
'total' => $total,
'hits' => $hits,
'misses' => (int) $row->misses,
'hit_rate' => $total > 0 ? round(($hits / $total) * 100, 2) : 0.0,
'avg_response_time_ms' => $row->avg_response_time_ms !== null
? round((float) $row->avg_response_time_ms, 2)
: null,
];
})->toArray();
}
/**
* 오래된 통계 레코드를 삭제합니다.
*
* @param int $daysToKeep 보존 기간 (일, 기본값: 30)
* @return int 삭제된 레코드 수
*/
public function cleanup(int $daysToKeep = 30): int
{
$cutoff = Carbon::now()->subDays($daysToKeep);
$deleted = SeoCacheStat::where('created_at', '<', $cutoff)->delete();
Log::info('[SEO] 캐시 통계 정리 완료', [
'deleted' => $deleted,
'days_kept' => $daysToKeep,
]);
return $deleted;
}
}