From c39b8909ad347bfc9f8f3b9cb97194a4cd0598d9 Mon Sep 17 00:00:00 2001 From: fomichev Date: Fri, 1 Nov 2024 12:12:15 +0300 Subject: [PATCH] =?utf8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B8=D1=80=D0=BE=D0=B2?= =?utf8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BB=D0=BE=D0=B3=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- erp24/actions/log/CollectAction.php | 104 +++++++++--- erp24/config/web.php | 2 +- erp24/controllers/TestLogController.php | 84 ++++++++++ erp24/log/Message.php | 202 ++++++++++++++++++++++++ erp24/log/TelegramTarget.php | 146 +++++++++++++++++ erp24/services/InfoLogService.php | 76 +++++++-- erp24/services/LogService.php | 78 +++++++-- erp24/services/TelegramService.php | 23 ++- erp24/services/TelegramTarget.php | 3 + erp24/views/test-log/index.php | 18 +++ 10 files changed, 683 insertions(+), 53 deletions(-) create mode 100644 erp24/controllers/TestLogController.php create mode 100644 erp24/log/Message.php create mode 100644 erp24/log/TelegramTarget.php create mode 100644 erp24/views/test-log/index.php diff --git a/erp24/actions/log/CollectAction.php b/erp24/actions/log/CollectAction.php index b87a6e27..5cd86e99 100755 --- a/erp24/actions/log/CollectAction.php +++ b/erp24/actions/log/CollectAction.php @@ -29,31 +29,93 @@ class CollectAction extends Action ]); // Сохраняем лог в базе данных if ($log->save()) { + + // Проверка на наличие аналогичных записей перед отправкой в Telegram + if (!self::shouldSendToTelegram($log)) { + return; // Пропускаем отправку, если уже есть записи за текущую дату + } + // Формируем сообщение для отправки в Telegram с использованием MarkdownV2 - $errorMessage = sprintf( - "*JavaScript Error Detected*\n\n" . - "*URL:*\n```%s```\n\n" . - "*Created At:*\n```%s```\n\n" . - "*IP:*\n```%s```\n\n" . - "*User Agent:*\n```%s```\n\n" . - "*File:* `%s`\n" . - "*Line:* `%s`, *Column:* `%s`\n" . - "*Message:*\n```%s```\n\n" . - "*Context:*\n```%s```", - $this->controller->request->getReferrer() ?? '', - date('Y-m-d H:i:s', $log->log_time), - $log->ip, - $log->user_agent, - $log->file ?? '', - $log->line ?? '', - $log->col ?? '', - $log->message, - $log->context ?? '' - ); + $errorMessage = "⚠️*Ошибка JavaScript Обнаружена*⚠️\n\n"; + +// Добавляем строки в сообщение только если параметры присутствуют + if ($url = $this->controller->request->getReferrer()) { + $errorMessage .= "*URL:*\n```" . self::escapeMarkdown($url) . "```\n\n"; + } + + if ($createdAt = date('Y-m-d H:i:s', $log->log_time)) { + $errorMessage .= "*Created At:*\n```" . self::escapeMarkdown($createdAt) . "```\n\n"; + } + + if ($ip = $log->ip) { + $errorMessage .= "*IP:*\n```" . self::escapeMarkdown($ip) . "```\n\n"; + } + + if ($userAgent = $log->user_agent) { + $errorMessage .= "*User Agent:*\n```" . self::escapeMarkdown($userAgent) . "```\n\n"; + } + + if ($file = $log->file) { + $errorMessage .= "*File:*\n```" . self::escapeMarkdown($file) . "```\n\n"; + } + + if ($line = $log->line) { + $errorMessage .= "*Line:*\n```" . self::escapeMarkdown($line) . "```\n\n"; + } + + if ($col = $log->col) { + $errorMessage .= "*Column:*\n```" . self::escapeMarkdown($col) . "```\n\n"; + } + + if ($message = $log->message) { + $errorMessage .= "*Message:*\n```" . self::escapeMarkdown($message) . "```\n\n"; + } + + if ($context = $log->context) { + $errorMessage .= "*Context:*\n```" . self::escapeMarkdown($context) . "```\n\n"; + } + + + + $disableNotification = false; // Отправляем сообщение об ошибке в Telegram через TelegramService - TelegramService::sendErrorToTelegramMessage($errorMessage); + TelegramService::sendErrorToTelegramMessage($errorMessage, $disableNotification); + } + } + +// Метод для проверки наличия аналогичных записей в ErrorLog за текущую дату + private static function shouldSendToTelegram($log) + { + $startOfDay = strtotime('today'); // Метка времени для начала текущего дня + + // Запрос в таблицу ErrorLog для поиска аналогичных записей + $count = ErrorLog::find() + ->where([ + 'ip' => $log->ip, + 'category' => $log->category, + 'level' => $log->level, + 'referrer' => $log->referrer, + 'user_agent' => $log->user_agent, + 'file' => $log->file, + 'line' => $log->line, + 'col' => $log->col, + 'message' => $log->message, + 'context' => $log->context, + ]) + ->andWhere(['>=', 'log_time', $startOfDay]) // Фильтр по началу текущего дня + ->count(); + + return $count <= 1; // Отправляем только если нет более одной записи + } + + private static function escapeMarkdown($text) + { + $specialChars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']; + foreach ($specialChars as $char) { + $text = str_replace($char, '\\' . $char, $text); } + return $text; } } diff --git a/erp24/config/web.php b/erp24/config/web.php index 51b09cc9..b4a1b6a8 100644 --- a/erp24/config/web.php +++ b/erp24/config/web.php @@ -62,7 +62,7 @@ $config = [ 'levels' => ['error', 'warning'], ], [ - 'class' => 'yii_app\services\TelegramTarget', + 'class' => 'app\log\TelegramTarget', 'levels' => ['error', 'warning'], // 'categories' => ['api.error', 'js.error', 'command.error'], ], diff --git a/erp24/controllers/TestLogController.php b/erp24/controllers/TestLogController.php new file mode 100644 index 00000000..ec978504 --- /dev/null +++ b/erp24/controllers/TestLogController.php @@ -0,0 +1,84 @@ +generateApiErrorLog(); + break; + + case 'error_log': + $this->generateErrorLog(); + break; + + case 'info_log': + $this->generateInfoLog(); + break; + + default: + throw new \yii\web\BadRequestHttpException("Invalid log type specified."); + } + + return $this->redirect(['index']); + } + + /** + * Создает копию записи в таблице ApiErrorLog с текущей датой. + */ + protected function generateApiErrorLog() + { + $existingLog = ApiErrorLog::find()->orderBy(['created_at' => SORT_DESC])->one(); + if ($existingLog) { + LogService::apiErrorLog($existingLog->payload); // Используем метод из LogService + } + } + + /** + * Создает копию записи в таблице ErrorLog с текущей датой. + */ + protected function generateErrorLog() + { + $existingLog = ErrorLog::find()->orderBy(['log_time' => SORT_DESC])->one(); + if ($existingLog) { + $logController = new \app\controllers\LogController('log-controller', Yii::$app); // экземпляр контроллера + $action = new \yii_app\actions\log\CollectAction('collect', $logController); + $action->run(); // Запускаем действие, чтобы создать копию + } + } + + /** + * Создает копию записи в таблице InfoLog с текущей датой. + */ + protected function generateInfoLog() + { + $existingLog = InfoLog::find()->orderBy(['created_at' => SORT_DESC])->one(); + if ($existingLog) { + InfoLogService::setInfoLog($existingLog->file, $existingLog->line, $existingLog->message, $existingLog->context); + } + } + + /** + * Действие для отображения представления с кнопками. + */ + public function actionIndex() + { + return $this->render('index'); + } +} \ No newline at end of file diff --git a/erp24/log/Message.php b/erp24/log/Message.php new file mode 100644 index 00000000..fbc27b34 --- /dev/null +++ b/erp24/log/Message.php @@ -0,0 +1,202 @@ +message = $message; + parent::__construct($config); + } + + /** + * Returns the message category. + * @return string message category. + */ + public function getCategory() + { + return $this->message[2]; + } + + /** + * Returns the command line. + * @return string|null command line, `null` if not available. + */ + public function getCommandLine() + { + if (\Yii::$app === null || !$this->getIsConsoleRequest()) { + return null; + } + + $params = []; + if (isset($_SERVER['argv'])) { + $params = $_SERVER['argv']; + } + return implode(' ', $params); + } + + /** + * Returns whether the current request is a console request. + * @return bool whether the current request is a console request. + * @throws InvalidConfigException if unable to determine. + */ + public function getIsConsoleRequest() + { + if ($this->_isConsoleRequest === null && \Yii::$app !== null) { + if (\Yii::$app->getRequest() instanceof \yii\console\Request) { + $this->_isConsoleRequest = true; + } elseif (\Yii::$app->getRequest() instanceof \yii\web\Request) { + $this->_isConsoleRequest = false; + } + } + if ($this->_isConsoleRequest === null) { + throw new InvalidConfigException('Unable to determine if the application is a console or web application.'); + } + + return $this->_isConsoleRequest; + } + + /** + * Returns the message level as a string. + * @return string message level as a string. + */ + public function getLevel() + { + return Logger::getLevelName($this->message[0][1]); + } + + /** + * Returns a string to be prefixed to the message. + * @return string messsage prefix string. + */ + public function getPrefix() + { + if ($this->target !== null) { + return $this->target->getMessagePrefix($this->message); + } else { + return ''; + } + } + + /** + * Returns the session ID. + * @return string|null session ID, `null` if not available. + */ + public function getSessionId() + { + if ( + \Yii::$app !== null + && \Yii::$app->has('session', true) + && \Yii::$app->getSession() !== null + && \Yii::$app->getSession()->getIsActive() + ) { + return \Yii::$app->getSession()->getId(); + } else { + return null; + } + } + + /** + * Returns the additional stack trace as a string. + * @return string|null stack trace, `null` if not available. + */ + public function getStackTrace() + { + if (!isset($this->message[4]) || empty($this->message[4])) { + return null; + } + + $traces = array_map(function ($trace) { + return "in {$trace['file']}:{$trace['line']}"; + }, $this->message[4]); + return implode("\n", $traces); + } + + /** + * Returns the message text. + * @return string message text. + */ + public function getText() + { + $text = $this->message[0]; + if (!is_string($text)) { + if ($text instanceof \Throwable) { + $text = (string) $text; + } else { + $text = VarDumper::export($text); + } + } + return $text; + } + + /** + * Returns the message creation timestamp. + * @return float message creation timestamp. + */ + public function getTimestamp() + { + return $this->message[3]; + } + + /** + * Returns the current absolute URL. + * @return null|string absolute URL, `null` if not available. + * @throws InvalidConfigException + */ + public function getUrl() + { + if (\Yii::$app === null || $this->getIsConsoleRequest()) { + return null; + } + + return \Yii::$app->getRequest()->getAbsoluteUrl(); + } + + /** + * Returns the user identity ID. + * @return int|string|null user identity ID, `null` if not available. + */ + public function getUserId() + { + if ( + \Yii::$app !== null + && \Yii::$app->has('user', true) + && \Yii::$app->getUser() !== null + ) { + $user = \Yii::$app->getUser()->getIdentity(false); + if ($user !== null) { + return $user->getId(); + } + } + return null; + } + + /** + * Returns the user IP address. + * @return string|null user IP address, `null` if not available. + */ + public function getUserIp() + { + if (\Yii::$app === null || $this->getIsConsoleRequest()) { + return null; + } + + return \Yii::$app->getRequest()->getUserIP(); + } +} \ No newline at end of file diff --git a/erp24/log/TelegramTarget.php b/erp24/log/TelegramTarget.php new file mode 100644 index 00000000..044247c4 --- /dev/null +++ b/erp24/log/TelegramTarget.php @@ -0,0 +1,146 @@ + "☠ ", + Logger::LEVEL_WARNING => "⚠ ", + Logger::LEVEL_INFO => "ℹ ", + Logger::LEVEL_TRACE => "📝 ", + ]; + + + private $errorTrackingFile = '@runtime/logs/error_tracking.log'; + +// Метод для проверки и записи уникальной ошибки + private function shouldSendMessage($currentError) + { + $filePath = Yii::getAlias($this->errorTrackingFile); + $currentDate = date('Y-m-d'); + $errorExists = false; + $errorLines = []; + + // Загружаем существующие ошибки, если файл существует + if (file_exists($filePath)) { + $errorLines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + } + + // Создаём хеш текущей ошибки после нормализации текста + $normalizedCurrentError = $this->normalizeText($currentError); + $currentErrorHash = md5($normalizedCurrentError); + + foreach ($errorLines as &$line) { + [$date, $count, $hash, $errorText] = explode('|', $line, 4); + + // Сравниваем хеши + if ($hash === $currentErrorHash) { + $errorExists = true; + + // Если ошибка уже была за текущую дату + if ($date === $currentDate && $count >= 1) { + return false; // Не отправляем + } + + // Увеличиваем счётчик и обновляем дату + $count++; + $line = implode('|', [$currentDate, $count, $hash, $errorText]); + break; + } + } + + // Если ошибка новая, добавляем её в список + if (!$errorExists) { + $errorLines[] = implode('|', [$currentDate, 1, $currentErrorHash, $currentError]); + } + + // Сохраняем обновлённый список ошибок + file_put_contents($filePath, implode(PHP_EOL, $errorLines) . PHP_EOL); + + return true; // Ошибка не найдена или обновлена, можно отправлять + } + +// Метод для нормализации текста ошибки + private function normalizeText($text) + { + // Приводим текст к единому формату: нижний регистр, удаление лишних пробелов + $text = strtolower($text); + $text = preg_replace('/\s+/', ' ', $text); + return trim($text); + } + + /** + * @throws InvalidConfigException + */ + public function export() + { + if (!$this->enable) { + return; + } + + + + $message = new Message($this->messages[0]); + $errorText = $message->getText(); + + + // Проверка наличия подобных сообщений + if (!$this->shouldSendMessage($errorText)) { + return; // Пропускаем отправку, если ошибка уже зарегистрирована + } + + // Формирование уровня и основного сообщения + $level = isset($this->levelEmojis[$message->message[1]]) ? $this->levelEmojis[$message->message[1]] . ' ' : '*' . ucfirst($message->getLevel()) . '* @ '; + if ($message->getIsConsoleRequest()) { + $level .= '```' . $message->getCommandLine() . '```'; + } else { + $level .= '[' . $message->getUrl() . '](' . $message->getUrl() . ')'; + } + + // Текст сообщения + $text = [ + $level, + " ", + "```" . mb_substr($message->getText(), 0, 3300) . "```", + " ", + "```" . $message->getStackTrace() . "```", + "🙂 " . $message->getUserIp(), + ]; + + $formattedMessage = implode("\n", $text); + + // Настройка уведомления для сообщений уровня ERROR + $disableNotification = $message->message[1] !== Logger::LEVEL_ERROR; + + // Отправка сообщения через TelegramService + try { + TelegramService::sendErrorToTelegramMessage($formattedMessage, $disableNotification); + } catch (\Exception $e) { + throw new InvalidValueException( + 'Unable to send logs to Telegram: ' . $e->getMessage(), $e->getCode() + ); + } + } +} \ No newline at end of file diff --git a/erp24/services/InfoLogService.php b/erp24/services/InfoLogService.php index 6aaae416..6b9795df 100644 --- a/erp24/services/InfoLogService.php +++ b/erp24/services/InfoLogService.php @@ -2,6 +2,7 @@ namespace yii_app\services; +use yii\db\Expression; use yii_app\records\InfoLog; class InfoLogService @@ -22,24 +23,73 @@ class InfoLogService ->setMessage($messageText) ->setLogTime() ->setCreatedAt(); + + + // Валидация и сохранение лога в базе данных if ($infoLog->validate() && $infoLog->save()) { - // Форматируем сообщение для отправки в Telegram - $telegramMessage = sprintf( - "*Info Log Detected*\n\n" . - "*File:* `%s`\n" . - "*Line:* `%s`\n" . - "*Message:*\n```%s```\n\n" . - "*Context:*\n```%s```", - $file ?? '', - $line ?? '', - $messageText, - $context ?? '' - ); + // Проверяем, нужно ли отправлять сообщение в Telegram + if (!self::shouldSendToTelegram($file, $line, $messageText, $context)) { + return; // Пропускаем отправку, если уже есть записи за текущую дату + } + + // Формируем сообщение для отправки в Telegram + $telegramMessage = "⚠️*Сообщение из InfoLog*⚠️\n\n"; + + // Добавляем строки в сообщение только если параметры присутствуют + if ($file) { + $telegramMessage .= "*File:*\n```" . self::escapeMarkdown($file) . "```\n\n"; + } + + if ($line) { + $telegramMessage .= "*Line:*\n```" . self::escapeMarkdown($line) . "```\n\n"; + } + + if ($messageText) { + $telegramMessage .= "*Сообщение:*\n```log" . self::escapeMarkdown($messageText) . "```\n\n"; + } + + if ($context) { + $telegramMessage .= "*Context:*\n```log" . self::escapeMarkdown($context) . "```\n\n"; + } + + + $disableNotification = false; // Отправляем сообщение в Telegram - TelegramService::sendErrorToTelegramMessage($telegramMessage); + TelegramService::sendErrorToTelegramMessage($telegramMessage, $disableNotification); } } + + // Метод для экранирования символов MarkdownV2 + private static function escapeMarkdown($text) + { + $specialChars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']; + foreach ($specialChars as $char) { + $text = str_replace($char, '\\' . $char, $text); + } + return $text; + } + + + // Метод для проверки, существуют ли подобные записи в InfoLog за текущую дату + private static function shouldSendToTelegram($file, $line, $messageText, $context) + { + $currentDate = date('Y-m-d'); + + // Выполняем запрос, чтобы найти записи с такими же значениями за текущую дату + $count = InfoLog::find() + ->where([ + 'file' => $file, + 'line' => $line, + 'message' => $messageText, + 'context' => $context, + ]) + ->andWhere(['>=', 'created_at', new Expression("DATE('$currentDate')")]) + ->count(); + + return $count <= 1; // Возвращаем true, только если нет более одной записи + } + } \ No newline at end of file diff --git a/erp24/services/LogService.php b/erp24/services/LogService.php index efee9ec7..f4c75019 100644 --- a/erp24/services/LogService.php +++ b/erp24/services/LogService.php @@ -62,21 +62,36 @@ class LogService $apiErrorLog->ip = Yii::$app->request->remoteIP ?? ''; $apiErrorLog->save(); - // Форматируем сообщение об ошибке для Telegram с использованием MarkdownV2 - $errorMessage = sprintf( - "*API Error Detected*\n\n" . - "*URL:*\n```%s```\n\n" . - "*Created At:*\n```%s```\n\n" . - "*IP:*\n```%s```\n\n" . - "*Payload:*\n```json\n%s```", - $apiErrorLog->url, - $apiErrorLog->created_at, - $apiErrorLog->ip, - $jsonString - ); - - // Отправляем сообщение об ошибке в Telegram - TelegramService::sendErrorToTelegramMessage($errorMessage); + // Проверка на наличие аналогичной записи перед созданием новой и отправкой в Telegram + if (!self::shouldSendToTelegram($hash_input, $jsonString)) { + return; // Пропускаем отправку, если уже есть аналогичная запись за текущую дату + } + + + // Форматирование сообщения об ошибке с условным добавлением строк + $errorMessage = "⚠️*Ошибка API Обнаружена*⚠️\n\n"; + + // Добавляем строки только если параметры присутствуют + if ($url = $apiErrorLog->url) { + $errorMessage .= "*URL:*\n```" . self::escapeMarkdown($url) . "```\n\n"; + } + + if ($createdAt = $apiErrorLog->created_at) { + $errorMessage .= "*Created At:*\n```" . $createdAt . "```\n\n"; + } + + if ($ip = $apiErrorLog->ip) { + $errorMessage .= "*IP:*\n```" . self::escapeMarkdown($ip) . "```\n\n"; + } + + if ($jsonString) { + $errorMessage .= "*Payload:*\n```json\n" . $jsonString . "```\n\n"; + } + + + $disableNotification = false; + // Отправляем сообщение об ошибке в Telegram через TelegramService + TelegramService::sendErrorToTelegramMessage($errorMessage, $disableNotification); } else { @@ -85,4 +100,37 @@ class LogService $h->save(); } } + + +// Метод для экранирования символов MarkdownV2 + private static function escapeMarkdown($text) + { + $specialChars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']; + foreach ($specialChars as $char) { + $text = str_replace($char, '\\' . $char, $text); + } + return $text; + } + + +// Метод для проверки наличия аналогичных записей в ApiErrorLog за текущую дату + private static function shouldSendToTelegram($hash_input, $jsonString) + { + $startOfDay = strtotime('today'); // Метка времени для начала текущего дня + + // Запрос в таблицу ApiErrorLog для поиска аналогичных записей за текущую дату + $count = ApiErrorLog::find() + ->where([ + 'hash_input' => $hash_input, + 'url' => Yii::$app->request->url ?? '', + 'ip' => Yii::$app->request->remoteIP ?? '', + 'payload' => $jsonString, + ]) + ->andWhere(['>=', 'created_at', date('Y-m-d H:i:s', $startOfDay)]) // Фильтр по началу текущего дня + ->count(); + + return $count <= 1; // Отправляем только если нет более одной записи + } + + } diff --git a/erp24/services/TelegramService.php b/erp24/services/TelegramService.php index c75dc0c5..fc79c4c7 100644 --- a/erp24/services/TelegramService.php +++ b/erp24/services/TelegramService.php @@ -20,19 +20,20 @@ class TelegramService { return $client->request('GET', $url); } -public static function sendErrorToTelegramMessage($message) +public static function sendErrorToTelegramMessage($message,$disableNotification) { $botToken = self::TELEGRAM_API_URL; $chatId = self::CHAT_CHANNEL_ID; $apiURL = "https://api.telegram.org/bot{$botToken}/sendMessage"; - + //$message = self::escapeMarkdown($message); $client = new Client(); try { $client->post($apiURL, [ 'json' => [ 'chat_id' => $chatId, 'text' => $message, - 'parse_mode' => 'MarkdownV2' + 'parse_mode' => 'MarkdownV2', + 'disable_notification' => $disableNotification, ], ]); } catch (\Exception $e) { @@ -41,4 +42,20 @@ public static function sendErrorToTelegramMessage($message) } + // Метод для экранирования символов MarkdownV2 + private static function escapeMarkdown($text) + { + // Экранирование символов, зарезервированных в MarkdownV2 + $specialChars = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']; + + foreach ($specialChars as $char) { + // Проверяем, что символ не находится внутри кода + $text = preg_replace_callback('/(?botToken . '/sendMessage'; diff --git a/erp24/views/test-log/index.php b/erp24/views/test-log/index.php new file mode 100644 index 00000000..57be5ce3 --- /dev/null +++ b/erp24/views/test-log/index.php @@ -0,0 +1,18 @@ +title = 'Тестирование логов'; +?> + +
+

title ?>

+ +
+ 'api_error'], ['class' => 'btn btn-danger']) ?> + 'error_log'], ['class' => 'btn btn-warning']) ?> + 'info_log'], ['class' => 'btn btn-info']) ?> +
+
\ No newline at end of file -- 2.39.5