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
+171
View File
@@ -0,0 +1,171 @@
<?php
namespace App\Services;
use App\Models\ApiLog;
use App\Models\JobSource;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class ApiClientService
{
/**
* Make an API request with logging and retry logic
*/
public function makeRequest(
JobSource $source,
string $endpoint,
string $method = 'GET',
array $params = [],
int $maxRetries = 3
): array {
$startTime = microtime(true);
$attempt = 0;
$lastException = null;
while ($attempt < $maxRetries) {
$attempt++;
try {
$response = $this->executeRequest($method, $endpoint, $params);
$responseTime = (int)((microtime(true) - $startTime) * 1000);
// Log successful request
$this->logApiRequest($source, $endpoint, $method, $params, $response, $responseTime, true);
return [
'success' => true,
'data' => $response->json(),
'status_code' => $response->status(),
'response_time' => $responseTime,
];
} catch (\Exception $e) {
$lastException = $e;
// Log failed attempt
Log::warning("API request failed (attempt {$attempt}/{$maxRetries})", [
'source' => $source->name,
'endpoint' => $endpoint,
'error' => $e->getMessage(),
]);
if ($attempt < $maxRetries) {
// Exponential backoff: wait 2^attempt seconds
sleep(pow(2, $attempt));
}
}
}
$responseTime = (int)((microtime(true) - $startTime) * 1000);
// Log final failure
$this->logApiRequest(
$source,
$endpoint,
$method,
$params,
null,
$responseTime,
false,
$lastException->getMessage() ?? 'Unknown error'
);
return [
'success' => false,
'error' => $lastException->getMessage() ?? 'Unknown error',
'response_time' => $responseTime,
];
}
/**
* Execute the HTTP request
*/
private function executeRequest(string $method, string $endpoint, array $params): \Illuminate\Http\Client\Response
{
$http = Http::timeout(30)
->retry(1, 100)
->withHeaders([
'User-Agent' => 'JobifyGarden/1.0',
'Accept' => 'application/json',
]);
return match (strtoupper($method)) {
'GET' => $http->get($endpoint, $params),
'POST' => $http->post($endpoint, $params),
'PUT' => $http->put($endpoint, $params),
'DELETE' => $http->delete($endpoint, $params),
default => throw new \InvalidArgumentException("Unsupported HTTP method: {$method}"),
};
}
/**
* Log API request to database
*/
private function logApiRequest(
JobSource $source,
string $endpoint,
string $method,
array $params,
$response,
int $responseTime,
bool $success,
?string $errorMessage = null
): void {
try {
ApiLog::create([
'job_source_id' => $source->id,
'endpoint' => $endpoint,
'method' => $method,
'status_code' => $response?->status(),
'response_time_ms' => $responseTime,
'success' => $success,
'error_message' => $errorMessage,
'request_params' => !empty($params) ? $params : null,
'response_data' => $success && $response ? [
'status' => $response->status(),
'size' => strlen($response->body()),
] : null,
]);
} catch (\Exception $e) {
Log::error('Failed to log API request', [
'error' => $e->getMessage(),
'endpoint' => $endpoint,
]);
}
}
/**
* Check if source is rate limited
*/
public function isRateLimited(JobSource $source): bool
{
if (!$source->rate_limit) {
return false;
}
// Check requests in the last hour
$recentRequests = ApiLog::where('job_source_id', $source->id)
->where('created_at', '>=', now()->subHour())
->count();
return $recentRequests >= $source->rate_limit;
}
/**
* Get rate limit info for a source
*/
public function getRateLimitInfo(JobSource $source): array
{
$recentRequests = ApiLog::where('job_source_id', $source->id)
->where('created_at', '>=', now()->subHour())
->count();
return [
'limit' => $source->rate_limit,
'used' => $recentRequests,
'remaining' => max(0, $source->rate_limit - $recentRequests),
'reset_at' => now()->addHour()->startOfHour(),
];
}
}
+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();
}
}