#!/usr/bin/env php > /var/www/geosector/api/logs/aggregate_stats.log 2>&1 */ declare(strict_types=1); // Configuration define('LOCK_FILE', '/tmp/aggregate_event_stats.lock'); define('EVENT_LOG_DIR', __DIR__ . '/../../logs/events'); // Empêcher l'exécution multiple simultanée if (file_exists(LOCK_FILE)) { $lockTime = filemtime(LOCK_FILE); if (time() - $lockTime > 3600) { unlink(LOCK_FILE); } else { die("[" . date('Y-m-d H:i:s') . "] Le processus est déjà en cours d'exécution\n"); } } file_put_contents(LOCK_FILE, (string) getmypid()); register_shutdown_function(function () { if (file_exists(LOCK_FILE)) { unlink(LOCK_FILE); } }); // Simuler l'environnement web pour AppConfig en CLI if (php_sapi_name() === 'cli') { $hostname = gethostname(); if (strpos($hostname, 'pra') !== false) { $_SERVER['SERVER_NAME'] = 'app3.geosector.fr'; } elseif (strpos($hostname, 'rca') !== false) { $_SERVER['SERVER_NAME'] = 'rapp.geosector.fr'; } else { $_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; } $_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME']; $_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; if (!function_exists('getallheaders')) { function getallheaders() { return []; } } } // Chargement de l'environnement require_once __DIR__ . '/../../vendor/autoload.php'; require_once __DIR__ . '/../../src/Config/AppConfig.php'; require_once __DIR__ . '/../../src/Core/Database.php'; require_once __DIR__ . '/../../src/Services/LogService.php'; use App\Services\LogService; /** * Parse les arguments CLI */ function parseArgs(array $argv): array { $args = [ 'date' => null, 'from' => null, 'to' => null, ]; foreach ($argv as $arg) { if (strpos($arg, '--date=') === 0) { $args['date'] = substr($arg, 7); } elseif (strpos($arg, '--from=') === 0) { $args['from'] = substr($arg, 7); } elseif (strpos($arg, '--to=') === 0) { $args['to'] = substr($arg, 5); } } return $args; } /** * Génère la liste des dates à traiter */ function getDatesToProcess(array $args): array { $dates = []; if ($args['date']) { $dates[] = $args['date']; } elseif ($args['from'] && $args['to']) { $current = new DateTime($args['from']); $end = new DateTime($args['to']); while ($current <= $end) { $dates[] = $current->format('Y-m-d'); $current->modify('+1 day'); } } else { // Par défaut : J-1 $dates[] = date('Y-m-d', strtotime('-1 day')); } return $dates; } /** * Parse un fichier JSONL et retourne les événements */ function parseJsonlFile(string $filePath): array { $events = []; if (!file_exists($filePath)) { return $events; } $handle = fopen($filePath, 'r'); if (!$handle) { return $events; } while (($line = fgets($handle)) !== false) { $line = trim($line); if (empty($line)) { continue; } $event = json_decode($line, true); if ($event && isset($event['event'])) { $events[] = $event; } } fclose($handle); return $events; } /** * Agrège les événements par entity_id et event_type */ function aggregateEvents(array $events): array { $stats = []; foreach ($events as $event) { $entityId = $event['entity_id'] ?? null; $eventType = $event['event'] ?? 'unknown'; $userId = $event['user_id'] ?? null; // Clé d'agrégation : entity_id peut être NULL (stats globales) $key = ($entityId ?? 'NULL') . '|' . $eventType; if (!isset($stats[$key])) { $stats[$key] = [ 'entity_id' => $entityId, 'event_type' => $eventType, 'count' => 0, 'sum_amount' => 0.0, 'user_ids' => [], 'metadata' => [], ]; } $stats[$key]['count']++; // Collecter les user_ids pour unique_users if ($userId !== null) { $stats[$key]['user_ids'][$userId] = true; } // Somme des montants pour les passages et paiements Stripe if (in_array($eventType, ['passage_created', 'passage_updated'])) { $amount = $event['amount'] ?? 0; $stats[$key]['sum_amount'] += (float) $amount; } elseif (in_array($eventType, ['stripe_payment_success', 'stripe_payment_created'])) { // Montant en centimes -> euros $amount = ($event['amount'] ?? 0) / 100; $stats[$key]['sum_amount'] += (float) $amount; } // Collecter metadata spécifiques collectMetadata($stats[$key], $event); } // Convertir user_ids en count foreach ($stats as &$stat) { $stat['unique_users'] = count($stat['user_ids']); unset($stat['user_ids']); // Finaliser les metadata $stat['metadata'] = finalizeMetadata($stat['metadata'], $stat['event_type']); } return $stats; } /** * Collecte les métadonnées spécifiques par type d'événement */ function collectMetadata(array &$stat, array $event): void { $eventType = $event['event'] ?? ''; switch ($eventType) { case 'login_failed': $reason = $event['reason'] ?? 'unknown'; $stat['metadata']['reasons'][$reason] = ($stat['metadata']['reasons'][$reason] ?? 0) + 1; break; case 'passage_created': $sectorId = $event['sector_id'] ?? null; if ($sectorId) { $stat['metadata']['sectors'][$sectorId] = ($stat['metadata']['sectors'][$sectorId] ?? 0) + 1; } $paymentType = $event['payment_type'] ?? 'unknown'; $stat['metadata']['payment_types'][$paymentType] = ($stat['metadata']['payment_types'][$paymentType] ?? 0) + 1; break; case 'stripe_payment_failed': $errorCode = $event['error_code'] ?? 'unknown'; $stat['metadata']['error_codes'][$errorCode] = ($stat['metadata']['error_codes'][$errorCode] ?? 0) + 1; break; case 'stripe_terminal_error': $errorCode = $event['error_code'] ?? 'unknown'; $stat['metadata']['error_codes'][$errorCode] = ($stat['metadata']['error_codes'][$errorCode] ?? 0) + 1; break; } } /** * Finalise les métadonnées (top 5, tri, etc.) */ function finalizeMetadata(array $metadata, string $eventType): ?array { if (empty($metadata)) { return null; } $result = []; // Top 5 secteurs if (isset($metadata['sectors'])) { arsort($metadata['sectors']); $result['top_sectors'] = array_slice($metadata['sectors'], 0, 5, true); } // Raisons d'échec login if (isset($metadata['reasons'])) { arsort($metadata['reasons']); $result['failure_reasons'] = $metadata['reasons']; } // Types de paiement if (isset($metadata['payment_types'])) { arsort($metadata['payment_types']); $result['payment_types'] = $metadata['payment_types']; } // Codes d'erreur if (isset($metadata['error_codes'])) { arsort($metadata['error_codes']); $result['error_codes'] = $metadata['error_codes']; } return empty($result) ? null : $result; } /** * Insère ou met à jour les stats dans la base de données */ function upsertStats(PDO $db, string $date, array $stats): int { $upsertedCount = 0; $sql = " INSERT INTO event_stats_daily (stat_date, entity_id, event_type, count, sum_amount, unique_users, metadata) VALUES (:stat_date, :entity_id, :event_type, :count, :sum_amount, :unique_users, :metadata) ON DUPLICATE KEY UPDATE count = VALUES(count), sum_amount = VALUES(sum_amount), unique_users = VALUES(unique_users), metadata = VALUES(metadata), updated_at = CURRENT_TIMESTAMP "; $stmt = $db->prepare($sql); foreach ($stats as $stat) { try { $stmt->execute([ 'stat_date' => $date, 'entity_id' => $stat['entity_id'], 'event_type' => $stat['event_type'], 'count' => $stat['count'], 'sum_amount' => $stat['sum_amount'], 'unique_users' => $stat['unique_users'], 'metadata' => $stat['metadata'] ? json_encode($stat['metadata'], JSON_UNESCAPED_UNICODE) : null, ]); $upsertedCount++; } catch (PDOException $e) { echo " ERREUR insertion {$stat['event_type']}: " . $e->getMessage() . "\n"; } } return $upsertedCount; } /** * Génère également les stats globales (entity_id = NULL) */ function generateGlobalStats(array $statsByEntity): array { $globalStats = []; foreach ($statsByEntity as $stat) { $eventType = $stat['event_type']; if (!isset($globalStats[$eventType])) { $globalStats[$eventType] = [ 'entity_id' => null, 'event_type' => $eventType, 'count' => 0, 'sum_amount' => 0.0, 'unique_users' => 0, 'metadata' => null, ]; } $globalStats[$eventType]['count'] += $stat['count']; $globalStats[$eventType]['sum_amount'] += $stat['sum_amount']; $globalStats[$eventType]['unique_users'] += $stat['unique_users']; } return array_values($globalStats); } // ============================================================ // MAIN // ============================================================ try { echo "[" . date('Y-m-d H:i:s') . "] Démarrage de l'agrégation des statistiques\n"; // Initialisation $appConfig = AppConfig::getInstance(); $config = $appConfig->getFullConfig(); $environment = $appConfig->getEnvironment(); echo "Environnement: {$environment}\n"; Database::init($config['database']); $db = Database::getInstance(); // Parser les arguments $args = parseArgs($argv); $dates = getDatesToProcess($args); echo "Dates à traiter: " . implode(', ', $dates) . "\n\n"; $totalStats = 0; $totalEvents = 0; foreach ($dates as $date) { $jsonlFile = EVENT_LOG_DIR . '/' . $date . '.jsonl'; echo "--- Traitement de {$date} ---\n"; if (!file_exists($jsonlFile)) { echo " Fichier non trouvé: {$jsonlFile}\n"; continue; } $fileSize = filesize($jsonlFile); echo " Fichier: " . basename($jsonlFile) . " (" . number_format($fileSize / 1024, 2) . " KB)\n"; // Parser le fichier $events = parseJsonlFile($jsonlFile); $eventCount = count($events); echo " Événements parsés: {$eventCount}\n"; if ($eventCount === 0) { echo " Aucun événement à agréger\n"; continue; } $totalEvents += $eventCount; // Agréger par entity/event_type $stats = aggregateEvents($events); echo " Agrégations par entité: " . count($stats) . "\n"; // Générer les stats globales $globalStats = generateGlobalStats($stats); echo " Agrégations globales: " . count($globalStats) . "\n"; // Fusionner stats entités + globales $allStats = array_merge(array_values($stats), $globalStats); // Insérer en base $upserted = upsertStats($db, $date, $allStats); echo " Stats insérées/mises à jour: {$upserted}\n"; $totalStats += $upserted; } // Résumé echo "\n=== RÉSUMÉ ===\n"; echo "Dates traitées: " . count($dates) . "\n"; echo "Événements traités: {$totalEvents}\n"; echo "Stats agrégées: {$totalStats}\n"; // Log LogService::log('Agrégation des statistiques terminée', [ 'level' => 'info', 'script' => 'aggregate_event_stats.php', 'environment' => $environment, 'dates_count' => count($dates), 'events_count' => $totalEvents, 'stats_count' => $totalStats, ]); echo "\n[" . date('Y-m-d H:i:s') . "] Agrégation terminée avec succès\n"; } catch (Exception $e) { $errorMsg = 'Erreur lors de l\'agrégation: ' . $e->getMessage(); LogService::log($errorMsg, [ 'level' => 'error', 'script' => 'aggregate_event_stats.php', 'trace' => $e->getTraceAsString(), ]); echo "\n❌ ERREUR: {$errorMsg}\n"; echo "Stack trace:\n" . $e->getTraceAsString() . "\n"; exit(1); } exit(0);