This commit is contained in:
Doar Mihai
2026-04-03 20:41:35 +03:00
parent 19d5e23dfd
commit a8a48f3f6f
13 changed files with 1185 additions and 0 deletions
+274
View File
@@ -0,0 +1,274 @@
<?php
namespace App\Services;
use App\Models\ApiLog;
use App\Models\AnalyticsEvent;
use App\Models\JobFetchLog;
use App\Models\JobSource;
use Illuminate\Support\Facades\DB;
class MetricsService
{
/**
* Get API performance metrics
*/
public function getApiMetrics(int $days = 7): array
{
$startDate = now()->subDays($days);
return [
'total_requests' => ApiLog::where('created_at', '>=', $startDate)->count(),
'successful_requests' => ApiLog::successful()->where('created_at', '>=', $startDate)->count(),
'failed_requests' => ApiLog::failed()->where('created_at', '>=', $startDate)->count(),
'average_response_time' => ApiLog::where('created_at', '>=', $startDate)
->whereNotNull('response_time_ms')
->avg('response_time_ms'),
'success_rate' => $this->calculateSuccessRate($days),
'by_source' => $this->getApiMetricsBySource($days),
];
}
/**
* Get job fetch metrics
*/
public function getJobFetchMetrics(int $days = 7): array
{
$startDate = now()->subDays($days);
return [
'total_fetches' => JobFetchLog::where('created_at', '>=', $startDate)->count(),
'successful_fetches' => JobFetchLog::successful()->where('created_at', '>=', $startDate)->count(),
'failed_fetches' => JobFetchLog::failed()->where('created_at', '>=', $startDate)->count(),
'total_jobs_fetched' => JobFetchLog::where('created_at', '>=', $startDate)->sum('jobs_fetched'),
'total_jobs_created' => JobFetchLog::where('created_at', '>=', $startDate)->sum('jobs_created'),
'total_jobs_updated' => JobFetchLog::where('created_at', '>=', $startDate)->sum('jobs_updated'),
'total_jobs_skipped' => JobFetchLog::where('created_at', '>=', $startDate)->sum('jobs_skipped'),
'average_duration' => JobFetchLog::where('created_at', '>=', $startDate)
->whereNotNull('duration_seconds')
->avg('duration_seconds'),
'by_source' => $this->getJobFetchMetricsBySource($days),
];
}
/**
* Get analytics metrics
*/
public function getAnalyticsMetrics(int $days = 7): array
{
$startDate = now()->subDays($days);
return [
'total_events' => AnalyticsEvent::where('created_at', '>=', $startDate)->count(),
'job_views' => AnalyticsEvent::ofType(AnalyticsEvent::EVENT_JOB_VIEW)
->where('created_at', '>=', $startDate)->count(),
'job_apply_clicks' => AnalyticsEvent::ofType(AnalyticsEvent::EVENT_JOB_APPLY_CLICK)
->where('created_at', '>=', $startDate)->count(),
'searches' => AnalyticsEvent::ofType(AnalyticsEvent::EVENT_SEARCH)
->where('created_at', '>=', $startDate)->count(),
'unique_sessions' => AnalyticsEvent::where('created_at', '>=', $startDate)
->distinct('session_id')->count('session_id'),
'click_through_rate' => $this->calculateClickThroughRate($days),
'popular_jobs' => $this->getPopularJobs($days),
'popular_searches' => $this->getPopularSearches($days),
];
}
/**
* Get system health metrics
*/
public function getSystemHealth(): array
{
$recentFailures = ApiLog::failed()
->where('created_at', '>=', now()->subHours(1))
->count();
$recentFetchFailures = JobFetchLog::failed()
->where('created_at', '>=', now()->subHours(24))
->count();
return [
'status' => $this->determineSystemStatus($recentFailures, $recentFetchFailures),
'api_failures_last_hour' => $recentFailures,
'fetch_failures_last_24h' => $recentFetchFailures,
'last_successful_fetch' => JobFetchLog::successful()
->latest()
->first()?->created_at,
'sources_status' => $this->getSourcesStatus(),
];
}
/**
* Calculate API success rate
*/
private function calculateSuccessRate(int $days): float
{
$total = ApiLog::withinDays($days)->count();
if ($total === 0) {
return 0;
}
$successful = ApiLog::successful()->withinDays($days)->count();
return round(($successful / $total) * 100, 2);
}
/**
* Get API metrics by source
*/
private function getApiMetricsBySource(int $days): array
{
return ApiLog::select('job_source_id')
->selectRaw('COUNT(*) as total')
->selectRaw('SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful')
->selectRaw('AVG(response_time_ms) as avg_response_time')
->withinDays($days)
->with('source:id,name')
->groupBy('job_source_id')
->get()
->map(function ($item) {
return [
'source' => $item->source->name ?? 'Unknown',
'total_requests' => $item->total,
'successful' => $item->successful,
'success_rate' => $item->total > 0 ? round(($item->successful / $item->total) * 100, 2) : 0,
'avg_response_time' => round($item->avg_response_time, 2),
];
})
->toArray();
}
/**
* Get job fetch metrics by source
*/
private function getJobFetchMetricsBySource(int $days): array
{
return JobFetchLog::select('job_source_id')
->selectRaw('COUNT(*) as total_fetches')
->selectRaw('SUM(jobs_fetched) as jobs_fetched')
->selectRaw('SUM(jobs_created) as jobs_created')
->selectRaw('SUM(jobs_updated) as jobs_updated')
->selectRaw('AVG(duration_seconds) as avg_duration')
->withinDays($days)
->with('source:id,name')
->groupBy('job_source_id')
->get()
->map(function ($item) {
return [
'source' => $item->source->name ?? 'Unknown',
'total_fetches' => $item->total_fetches,
'jobs_fetched' => $item->jobs_fetched,
'jobs_created' => $item->jobs_created,
'jobs_updated' => $item->jobs_updated,
'avg_duration' => round($item->avg_duration, 2),
];
})
->toArray();
}
/**
* Calculate click-through rate
*/
private function calculateClickThroughRate(int $days): float
{
$views = AnalyticsEvent::ofType(AnalyticsEvent::EVENT_JOB_VIEW)
->withinDays($days)
->count();
if ($views === 0) {
return 0;
}
$clicks = AnalyticsEvent::ofType(AnalyticsEvent::EVENT_JOB_APPLY_CLICK)
->withinDays($days)
->count();
return round(($clicks / $views) * 100, 2);
}
/**
* Get popular jobs
*/
private function getPopularJobs(int $days, int $limit = 10): array
{
return AnalyticsEvent::select('job_id')
->selectRaw('COUNT(*) as view_count')
->selectRaw('SUM(CASE WHEN event_type = ? THEN 1 ELSE 0 END) as apply_clicks', [AnalyticsEvent::EVENT_JOB_APPLY_CLICK])
->whereNotNull('job_id')
->withinDays($days)
->with('job:id,title,company')
->groupBy('job_id')
->orderByDesc('view_count')
->limit($limit)
->get()
->map(function ($item) {
return [
'job_id' => $item->job_id,
'title' => $item->job->title ?? 'Unknown',
'company' => $item->job->company ?? 'Unknown',
'views' => $item->view_count,
'apply_clicks' => $item->apply_clicks,
];
})
->toArray();
}
/**
* Get popular search terms
*/
private function getPopularSearches(int $days, int $limit = 10): array
{
return AnalyticsEvent::select(DB::raw('JSON_EXTRACT(event_data, "$.keywords") as search_term'))
->selectRaw('COUNT(*) as search_count')
->ofType(AnalyticsEvent::EVENT_SEARCH)
->withinDays($days)
->whereNotNull(DB::raw('JSON_EXTRACT(event_data, "$.keywords")'))
->groupBy('search_term')
->orderByDesc('search_count')
->limit($limit)
->get()
->map(function ($item) {
return [
'term' => trim($item->search_term, '"'),
'count' => $item->search_count,
];
})
->toArray();
}
/**
* Determine system status
*/
private function determineSystemStatus(int $apiFailures, int $fetchFailures): string
{
if ($apiFailures > 10 || $fetchFailures > 5) {
return 'critical';
}
if ($apiFailures > 5 || $fetchFailures > 2) {
return 'warning';
}
return 'healthy';
}
/**
* Get status of all sources
*/
private function getSourcesStatus(): array
{
return JobSource::get()->map(function ($source) {
$lastFetch = JobFetchLog::where('job_source_id', $source->id)
->latest()
->first();
return [
'id' => $source->id,
'name' => $source->name,
'is_active' => $source->is_active,
'last_fetch_success' => $lastFetch?->success ?? null,
'last_fetch_at' => $lastFetch?->created_at ?? null,
'jobs_in_last_fetch' => $lastFetch?->jobs_created ?? 0,
];
})->toArray();
}
}