toDateTimeString(); $logData = [ 'timestamp' => $timestamp, 'action' => $action, 'data' => $data, ]; $message = "[Scheduler] {$action}"; if (!empty($data)) { $message .= " | " . json_encode($data, JSON_UNESCAPED_UNICODE); } Log::info($message, $logData); // Write to dedicated notification log file // Use storage_path() to write directly to storage/logs/ directory $logDir = storage_path('logs'); $logFile = $logDir . '/notifications-' . date('Y-m-d') . '.log'; // Determine status for log file $status = 'scheduled'; if ($action === 'command_finished') { $status = 'finished'; } elseif ($action === 'schedule_run_started') { $status = 'started'; } elseif ($action === 'schedule_run_completed') { $status = 'completed'; } $logLine = sprintf( "[%s] %s | SCHEDULER | %s\n", $timestamp, str_pad($status, 10), json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ); // Use file_put_contents with FILE_APPEND flag file_put_contents($logFile, $logLine, FILE_APPEND | LOCK_EX); } catch (\Exception $e) { Log::error('Failed to write scheduler log', [ 'error' => $e->getMessage(), 'action' => $action ]); } } /** * Log notification command execution * * @param string $commandName Command name (e.g., 'notifications:check-deleted-joints') * @param string $status Status: 'started', 'completed', 'skipped', 'error' * @param array $data Additional data to log * @return void */ function logNotificationCommand(string $commandName, string $status, array $data = []) { try { $timestamp = now()->toDateTimeString(); $logData = [ 'timestamp' => $timestamp, 'command' => $commandName, 'status' => $status, 'data' => $data, ]; // Log to Laravel's default log $message = "[Notification Command] {$commandName} - {$status}"; if (!empty($data)) { $message .= " | " . json_encode($data, JSON_UNESCAPED_UNICODE); } switch ($status) { case 'started': Log::info($message, $logData); break; case 'completed': Log::info($message, $logData); break; case 'skipped': Log::info($message, $logData); break; case 'error': Log::error($message, $logData); break; default: Log::info($message, $logData); } // Also write to dedicated notification log file // Use storage_path() to write directly to storage/logs/ directory $logDir = storage_path('logs'); $logFile = $logDir . '/notifications-' . date('Y-m-d') . '.log'; $logLine = sprintf( "[%s] %s | %s | %s\n", $timestamp, str_pad($status, 10), str_pad($commandName, 50), json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ); // Use file_put_contents with FILE_APPEND flag file_put_contents($logFile, $logLine, FILE_APPEND | LOCK_EX); } catch (\Exception $e) { // Fallback to Laravel log if file write fails Log::error('Failed to write notification log', [ 'error' => $e->getMessage(), 'command' => $commandName, 'status' => $status ]); } } /** * Log notification command start * * @param string $commandName * @return void */ function logNotificationStart(string $commandName) { logNotificationCommand($commandName, 'started', [ 'time' => now()->toTimeString(), ]); } /** * Log notification command completion * * @param string $commandName * @param int $sentCount Number of notifications sent * @param int $recordCount Number of records found * @param int $newCount Number of new records (if applicable) * @param float|null $duration Execution duration in seconds * @return void */ function logNotificationComplete(string $commandName, int $sentCount = 0, int $recordCount = 0, int $newCount = 0, ?float $duration = null) { logNotificationCommand($commandName, 'completed', [ 'sent_count' => $sentCount, 'record_count' => $recordCount, 'new_count' => $newCount, 'duration_seconds' => $duration ? round($duration, 2) : null, ]); } /** * Log notification command skip * * @param string $commandName * @param string $reason Reason for skipping * @return void */ function logNotificationSkip(string $commandName, string $reason) { logNotificationCommand($commandName, 'skipped', [ 'reason' => $reason, ]); } /** * Log notification command error * * @param string $commandName * @param string $errorMessage * @param array $context Additional context * @return void */ function logNotificationError(string $commandName, string $errorMessage, array $context = []) { logNotificationCommand($commandName, 'error', array_merge([ 'error' => $errorMessage, ], $context)); } /** * Get notification log summary for a specific date * * @param string|null $date Date in Y-m-d format (default: today) * @return array */ function getNotificationLogSummary(?string $date = null): array { $date = $date ?: date('Y-m-d'); $logFile = storage_path('logs/notifications-' . $date . '.log'); if (!file_exists($logFile)) { return [ 'date' => $date, 'total_executions' => 0, 'completed' => 0, 'skipped' => 0, 'errors' => 0, 'total_notifications_sent' => 0, 'commands' => [], ]; } $content = file_get_contents($logFile); $lines = explode("\n", trim($content)); $summary = [ 'date' => $date, 'total_executions' => 0, 'completed' => 0, 'skipped' => 0, 'errors' => 0, 'total_notifications_sent' => 0, 'commands' => [], ]; foreach ($lines as $line) { if (empty(trim($line))) continue; // Parse log line: [timestamp] status | command | data // Handle both command logs and scheduler logs if (preg_match('/\[([^\]]+)\]\s+(\w+)\s+\|\s+([^\|]+)\s+\|\s+(.+)/', $line, $matches)) { $timestamp = $matches[1]; $status = trim($matches[2]); $command = trim($matches[3]); $dataJson = trim($matches[4]); // Check if it's a scheduler log if ($command === 'SCHEDULER') { $data = json_decode($dataJson, true); if (isset($data['command'])) { // Scheduler log for a specific command $scheduledCommand = $data['command']; if ($status === 'scheduled') { // Command was scheduled to run if (!isset($summary['commands'][$scheduledCommand])) { $summary['commands'][$scheduledCommand] = [ 'executions' => 0, 'completed' => 0, 'skipped' => 0, 'errors' => 0, 'total_sent' => 0, 'scheduled_count' => 0, ]; } $summary['commands'][$scheduledCommand]['scheduled_count']++; } // Note: 'finished' status is just informational, actual completion is logged by command itself } continue; } $summary['total_executions']++; if ($status === 'completed') { $summary['completed']++; $data = json_decode($dataJson, true); if (isset($data['sent_count'])) { $summary['total_notifications_sent'] += $data['sent_count']; } if (!isset($summary['commands'][$command])) { $summary['commands'][$command] = [ 'executions' => 0, 'completed' => 0, 'skipped' => 0, 'errors' => 0, 'total_sent' => 0, 'scheduled_count' => 0, ]; } $summary['commands'][$command]['executions']++; $summary['commands'][$command]['completed']++; if (isset($data['sent_count'])) { $summary['commands'][$command]['total_sent'] += $data['sent_count']; } } elseif ($status === 'skipped') { $summary['skipped']++; if (!isset($summary['commands'][$command])) { $summary['commands'][$command] = [ 'executions' => 0, 'completed' => 0, 'skipped' => 0, 'errors' => 0, 'total_sent' => 0, 'scheduled_count' => 0, ]; } $summary['commands'][$command]['executions']++; $summary['commands'][$command]['skipped']++; } elseif ($status === 'error') { $summary['errors']++; if (!isset($summary['commands'][$command])) { $summary['commands'][$command] = [ 'executions' => 0, 'completed' => 0, 'skipped' => 0, 'errors' => 0, 'total_sent' => 0, 'scheduled_count' => 0, ]; } $summary['commands'][$command]['executions']++; $summary['commands'][$command]['errors']++; } } } return $summary; }