diff --git a/frontend/admin/src/components/LogsViewer.vue b/frontend/admin/src/components/LogsViewer.vue index b120524..81f3593 100644 --- a/frontend/admin/src/components/LogsViewer.vue +++ b/frontend/admin/src/components/LogsViewer.vue @@ -1,15 +1,190 @@ - + + + + + + + + + + + + + + + + + {{ data.datetime }} + — + + + + + + + — + + + + + + {{ data.channel }} + — + + + + + + {{ data.message }} + + + + + + + + Дата и время: + + {{ selectedLog.datetime }} + + ({{ selectedLog.datetime_raw }}) + + — + + + + + Уровень: + + {{ selectedLog.level }} + + — + + + + Канал: + {{ selectedLog.channel }} + — + + + + Сообщение: + {{ selectedLog.message || '—' }} + + + + Контекст: + {{ JSON.stringify(selectedLog.context, null, 2) }} + + + + Исходная строка: + {{ selectedLog.raw }} + + + + + + + + diff --git a/frontend/admin/src/stores/logs.js b/frontend/admin/src/stores/logs.js index bcaaedb..4b0abbe 100644 --- a/frontend/admin/src/stores/logs.js +++ b/frontend/admin/src/stores/logs.js @@ -3,14 +3,19 @@ import {apiGet} from "@/utils/http.js"; export const useLogsStore = defineStore('logs', { state: () => ({ - lines: '', + logs: [], + loading: false, }), actions: { async fetchLogsFromServer() { - if (this.lines) return; - const response = await apiGet('getLogs'); - this.lines = response.data; + this.loading = true; + try { + const response = await apiGet('getLogs'); + this.logs = response.data || []; + } finally { + this.loading = false; + } }, }, }); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/LogsHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/LogsHandler.php index 1609a8b..823ac15 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/LogsHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/bastion/Handlers/LogsHandler.php @@ -19,17 +19,154 @@ class LogsHandler public function getLogs(): JsonResponse { - $data = []; + $parsedLogs = []; $logsPath = $this->findLastLogsFileInDir( $this->settings->get('logs.path') ); if ($logsPath) { - $data = implode(PHP_EOL, $this->readLastLogsRows($logsPath, 100)); + $lines = $this->readLastLogsRows($logsPath, 100); + $parsedLogs = $this->parseLogLines($lines); } - return new JsonResponse(compact('data')); + return new JsonResponse(['data' => $parsedLogs]); + } + + private function parseLogLines(array $lines): array + { + $parsed = []; + + // Регулярка для формата Monolog с ISO 8601 датой: [YYYY-MM-DDTHH:MM:SS.microseconds+timezone] channel.LEVEL: message {context} [extra] + // Пример: [2025-11-23T14:28:21.772518+00:00] TeleCart.ERROR: Invalid Telegram Signature. {"exception":"..."} [] + // Поддерживает также формат без контекста и extra: [2025-11-23T14:28:21.772518+00:00] TeleCart.INFO: Message text + $pattern = '/^\[([^\]]+)\]\s+([^.]+)\.(\w+):\s+(.+)$/s'; + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) { + continue; + } + + if (preg_match($pattern, $line, $matches)) { + $datetime = $matches[1] ?? ''; + $channel = $matches[2] ?? ''; + $level = $matches[3] ?? ''; + $rest = $matches[4] ?? ''; + + // Извлекаем сообщение и контекст + // Контекст начинается с { и заканчивается соответствующим } + $message = $rest; + $context = null; + + // Ищем JSON контекст (начинается с {, может быть после пробела или сразу) + $jsonStart = strpos($rest, ' {'); + if ($jsonStart === false) { + $jsonStart = strpos($rest, '{'); + } else { + $jsonStart++; // Пропускаем пробел перед { + } + + if ($jsonStart !== false) { + $message = trim(substr($rest, 0, $jsonStart)); + $jsonPart = substr($rest, $jsonStart); + + // Находим конец JSON объекта, учитывая вложенность + $jsonEnd = $this->findJsonEnd($jsonPart); + if ($jsonEnd !== false) { + $contextJson = substr($jsonPart, 0, $jsonEnd + 1); + $decoded = json_decode($contextJson, true); + if (json_last_error() === JSON_ERROR_NONE) { + $context = $decoded; + } + } + } + + // Форматируем дату для отображения (убираем микросекунды и временную зону для читаемости) + $formattedDatetime = $this->formatDateTime($datetime); + + $parsed[] = [ + 'datetime' => $formattedDatetime, + 'datetime_raw' => $datetime, + 'channel' => $channel, + 'level' => $level, + 'message' => $message, + 'context' => $context, + 'raw' => $line, + ]; + } else { + // Если строка не соответствует формату, сохраняем как есть + $parsed[] = [ + 'datetime' => '', + 'datetime_raw' => '', + 'channel' => '', + 'level' => '', + 'message' => $line, + 'context' => null, + 'raw' => $line, + ]; + } + } + + return $parsed; + } + + /** + * Находит позицию конца JSON объекта, учитывая вложенность + * @param string $json JSON строка, начинающаяся с { + * @return int|false Позиция закрывающей скобки или false, если не найдено + */ + private function findJsonEnd(string $json) + { + $depth = 0; + $inString = false; + $escape = false; + $len = strlen($json); + + for ($i = 0; $i < $len; $i++) { + $char = $json[$i]; + + if ($escape) { + $escape = false; + continue; + } + + if ($char === '\\') { + $escape = true; + continue; + } + + if ($char === '"') { + $inString = !$inString; + continue; + } + + if ($inString) { + continue; + } + + if ($char === '{') { + $depth++; + } elseif ($char === '}') { + $depth--; + if ($depth === 0) { + return $i; + } + } + } + + return false; + } + + private function formatDateTime(string $datetime): string + { + // Парсим ISO 8601 формат: 2025-11-23T14:28:21.772518+00:00 + // Преобразуем в более читаемый формат: 2025-11-23 14:28:21 + if (preg_match('/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/', $datetime, $dateMatches)) { + return $dateMatches[1] . ' ' . $dateMatches[2]; + } + + return $datetime; } private function readLastLogsRows(string $path, int $lines = 1000, int $buffer = 4096): array
{{ JSON.stringify(selectedLog.context, null, 2) }}
{{ selectedLog.raw }}