Оптимизация SentryTarget класса
Created on: December 18, 2024
Created on: December 18, 2024
Что можно улучшить
<?php namespace common\components\log; use notamedia\sentry\SentryTarget as BaseSentryTarget; use Sentry\Event; use Sentry\EventHint; use Sentry\Frame; use Sentry\SentrySdk; use Sentry\Severity; use Sentry\Stacktrace; use Sentry\StacktraceBuilder; use Sentry\Tracing\SpanContext; use Sentry\Tracing\TraceId; use Sentry\Tracing\TransactionContext; use Yii; use yii\console\Application as ConsoleApplication; use yii\console\Controller as ConsoleController; use yii\web\Application as WebApplication; use yii\web\Controller as WebController; use yii\log\Logger; use Throwable; class SentryTarget extends BaseSentryTarget { public function export() { if (empty($this->messages)) { return; } $applicationRunId = preg_replace("/-/", '', Yii::$app->getRequest()->applicationRunId); $operationName = $this->determineOperationName(); // Используем timestamp первого сообщения как старт транзакции $firstMessage = reset($this->messages); $startTimestamp = $firstMessage[3] ?? microtime(true); $transactionContext = new TransactionContext(); $transactionContext->setName($operationName); $transactionContext->setOp(Yii::$app->id); $transactionContext->setDescription($operationName); $transactionContext->setTraceId(new TraceId($applicationRunId)); $transactionContext->setStartTimestamp($startTimestamp); $transactionContext->setTags([ 'route' => $operationName, 'app-id' => Yii::$app->id, 'trace-id' => $applicationRunId ]); $transactionContext->setSampled(true); $transaction = SentrySdk::getCurrentHub()->startTransaction($transactionContext); SentrySdk::getCurrentHub()->setSpan($transaction); foreach ($this->messages as $message) { try { [$text, $level, $action, $timestamp, $stackTrace, $memory] = $message; $textExport = is_array($text) ? var_export($text, true) : (string)$text; // Параметры для спанов $spanContext = new SpanContext(); $spanContext->setOp($action); $spanContext->setTags([ 'action' => $action, 'route' => $operationName, 'app-id' => Yii::$app->id, 'trace-id' => $applicationRunId ]); $spanContext->setDescription($textExport); $spanContext->setStartTimestamp($startTimestamp); $spanContext->setSampled(true); $spanContext->setData($this->buildSpanData($message)); // Старт спана $span = $transaction->startChild($spanContext); // Обработка ошибки или предупреждения if (in_array($level, [Logger::LEVEL_ERROR, Logger::LEVEL_WARNING], true)) { if ($text instanceof Throwable) { // Если это реальный Throwable, отправим как исключение SentrySdk::getCurrentHub()->captureException($text); } else { // В остальных случаях создаем ивент вручную $event = Event::createEvent(); $event->setFingerprint([$level, $action, $operationName, Yii::$app->id]); $event->setTags([ 'action' => $action, 'route' => $operationName, 'app-id' => Yii::$app->id, 'trace-id' => $applicationRunId ]); $event->setMessage($textExport); // Установка уровня серьёзности $severity = $level === Logger::LEVEL_WARNING ? Severity::warning() : Severity::error(); $event->setLevel($severity); $event->setStartTimestamp($startTimestamp); // Формирование стектрейса, если доступен if (is_array($stackTrace) && !empty($stackTrace)) { $frames = []; foreach ($stackTrace as $frameData) { $frames[] = new Frame( $frameData['file'] ?? null, $frameData['line'] ?? null, $frameData['function'] ?? null, $frameData['class'] ?? null ); } if (!empty($frames)) { $event->setStacktrace(new Stacktrace($frames)); } } // Если есть информация об исключении в тексте $exceptionHint = null; if (is_array($text) && isset($text['exception']) && $text['exception'] instanceof Throwable) { $exceptionHint = ['exception' => $text['exception']]; } SentrySdk::getCurrentHub()->captureEvent( $event, EventHint::fromArray(array_filter((array)$exceptionHint)) ); } } // Завершение спана $span->finish($timestamp); $startTimestamp = $timestamp; } catch (Throwable $exception) { SentrySdk::getCurrentHub()->captureException($exception); } } // Завершаем транзакцию последним таймштампом $lastMessage = end($this->messages); $finalTimestamp = $lastMessage[3] ?? microtime(true); $transaction->finish($finalTimestamp); } /** * Определяет название операции для транзакции на основе типа приложения и контекста запроса. * * @return string */ private function determineOperationName(): string { if (Yii::$app instanceof ConsoleApplication) { // Для консольных приложений используем имя команды и аргументы $controller = Yii::$app->controller; if ($controller instanceof ConsoleController) { $commandName = Yii::$app->requestedRoute; $args = implode(' ', Yii::$app->request->getArgs()); return "console.{$commandName} {$args}"; } return 'console'; } elseif (Yii::$app instanceof WebApplication) { // Для веб-приложений используем модуль, контроллер и действие $controller = Yii::$app->controller; if ($controller instanceof WebController) { $module = Yii::$app->controller->module->id ?? 'app'; $controllerId = Yii::$app->controller->id; $actionId = Yii::$app->controller->action->id ?? 'index'; return "web.{$module}.{$controllerId}.{$actionId}"; } return 'web'; } // По умолчанию return 'application'; } private function buildSpanData(array $message): array { [$text, $level, $action, $timestamp, $stackTrace, $memoryUsages] = $message; $user = Yii::$app->user ?? null; return [ 'level' => Logger::getLevelName($level), 'action' => $action, 'timestamp' => $timestamp, 'user' => $user ? $user->getId() : 'CLI', 'memory' => $memoryUsages, 'trace' => $stackTrace, 'message' => is_array($text) ? var_export($text, true) : (string)$text, ]; } }Ваш класс SentryTarget
выглядит достаточно продуманным и функциональным. Однако всегда есть возможности для улучшения кода с точки зрения читаемости, производительности и поддержки. Ниже приведены рекомендации, которые помогут оптимизировать и улучшить ваш код:
Использование констант для уровней логирования и тегов:
Вместо жестко закодированных строк используйте константы. Это уменьшит вероятность ошибок и упростит изменение значений в будущем.
phpclass SentryTarget extends BaseSentryTarget { private const LEVEL_ERROR = Logger::LEVEL_ERROR; private const LEVEL_WARNING = Logger::LEVEL_WARNING; private const TAG_ROUTE = 'route'; private const TAG_APP_ID = 'app-id'; private const TAG_TRACE_ID = 'trace-id'; // Остальной код... }
Централизация тегов:
Создайте метод для генерации общих тегов, чтобы избежать дублирования кода.
phpprivate function getCommonTags(string $operationName, string $traceId): array { return [ self::TAG_ROUTE => $operationName, self::TAG_APP_ID => Yii::$app->id, self::TAG_TRACE_ID => $traceId, ]; }
Сокращение повторных вызовов SentrySdk::getCurrentHub()
:
Сохраните ссылку на хаб Sentry в переменную, чтобы избежать многократных вызовов статического метода.
phppublic function export() { if (empty($this->messages)) { return; } $hub = SentrySdk::getCurrentHub(); // Остальной код с использованием $hub вместо повторных вызовов }
Проверка структуры сообщения:
Перед деструктуризацией убедитесь, что массив сообщения содержит ожидаемое количество элементов, чтобы избежать ошибок.
phpforeach ($this->messages as $message) { if (count($message) < 6) { // Логирование или пропуск некорректных сообщений continue; } [$text, $level, $action, $timestamp, $stackTrace, $memory] = $message; // Остальной код... }
Использование строгой типизации:
Добавьте типы параметров и возвращаемых значений для методов, что повысит безопасность и читаемость кода.
phpprivate function determineOperationName(): string { // Код метода... } private function buildSpanData(array $message): array { // Код метода... }
Более детальная обработка ошибок:
Добавьте логирование или дополнительные действия при обработке исключений внутри цикла, чтобы легче было отслеживать проблемы.
phpcatch (Throwable $exception) { // Логирование исключения в Yii лог Yii::error('Error exporting to Sentry: ' . $exception->getMessage(), __METHOD__); // Также отправка исключения в Sentry $hub->captureException($exception); }
Использование встроенных возможностей Sentry для захвата стека:
Вместо ручного создания фреймов стека можно использовать встроенные возможности Sentry для автоматического захвата стека вызовов.
phpif (is_array($stackTrace) && !empty($stackTrace)) { $event->setStacktrace(StacktraceBuilder::fromBacktrace($stackTrace)); }
determineOperationName
Использование вспомогательных методов для формирования названия операции:
Это повысит читаемость и облегчит тестирование.
phpprivate function determineOperationName(): string { if (Yii::$app instanceof ConsoleApplication) { return $this->getConsoleOperationName(); } elseif (Yii::$app instanceof WebApplication) { return $this->getWebOperationName(); } return 'application'; } private function getConsoleOperationName(): string { $controller = Yii::$app->controller; if ($controller instanceof ConsoleController) { $commandName = Yii::$app->requestedRoute; $args = implode(' ', Yii::$app->request->getArgs()); return "console.{$commandName} {$args}"; } return 'console'; } private function getWebOperationName(): string { $controller = Yii::$app->controller; if ($controller instanceof WebController) { $module = $controller->module->id ?? 'app'; $controllerId = $controller->id; $actionId = $controller->action->id ?? 'index'; return "web.{$module}.{$controllerId}.{$actionId}"; } return 'web'; }
Избегайте использования var_export
для данных, содержащих потенциально чувствительную информацию:
Если text
может содержать конфиденциальные данные, рассмотрите возможность их фильтрации или маскировки перед отправкой в Sentry.
phpprivate function exportText($text): string { if (is_array($text)) { // Фильтрация или маскировка чувствительных данных return json_encode($text, JSON_UNESCAPED_UNICODE); } return (string)$text; }
Затем используйте этот метод при формировании сообщений:
php$textExport = $this->exportText($text);
Добавление PHPDoc комментариев:
Это поможет другим разработчикам (и вам в будущем) лучше понимать назначение методов и их параметры.
php/** * Экспортирует накопленные сообщения в Sentry. * * @return void */ public function export(): void { // Код метода... } /** * Определяет название операции для транзакции на основе типа приложения и контекста запроса. * * @return string Название операции. */ private function determineOperationName(): string { // Код метода... } /** * Строит данные для спана на основе сообщения. * * @param array $message Сообщение логирования. * @return array Данные для спана. */ private function buildSpanData(array $message): array { // Код метода... }
Следование PSR-12:
Убедитесь, что ваш код соответствует стандарту PSR-12 для PHP, что повысит его читаемость и облегчит интеграцию с другими проектами.
Написание юнит-тестов:
Добавьте тесты для вашего класса, чтобы гарантировать корректность его работы и облегчить будущие изменения.
Использование match
вместо if-elseif
:
Если вы используете PHP 8.0 или выше, можно использовать конструкцию match
для определения типа приложения.
phpprivate function determineOperationName(): string { return match (true) { Yii::$app instanceof ConsoleApplication => $this->getConsoleOperationName(), Yii::$app instanceof WebApplication => $this->getWebOperationName(), default => 'application', }; }
Проверка, активна ли транзакция перед завершением:
Это поможет избежать потенциальных ошибок при завершении транзакции.
phpif ($transaction->isActive()) { $transaction->finish($finalTimestamp); }
Ниже приведен обновленный пример класса с учетом некоторых из вышеупомянутых рекомендаций:
php<?php namespace common\components\log; use notamedia\sentry\SentryTarget as BaseSentryTarget; use Sentry\Event; use Sentry\EventHint; use Sentry\Frame; use Sentry\SentrySdk; use Sentry\Severity; use Sentry\Stacktrace; use Sentry\Tracing\SpanContext; use Sentry\Tracing\TraceId; use Sentry\Tracing\TransactionContext; use Yii; use yii\console\Application as ConsoleApplication; use yii\console\Controller as ConsoleController; use yii\web\Application as WebApplication; use yii\web\Controller as WebController; use yii\log\Logger; use Throwable; class SentryTarget extends BaseSentryTarget { private const LEVEL_ERROR = Logger::LEVEL_ERROR; private const LEVEL_WARNING = Logger::LEVEL_WARNING; private const TAG_ROUTE = 'route'; private const TAG_APP_ID = 'app-id'; private const TAG_TRACE_ID = 'trace-id'; /** * Экспортирует накопленные сообщения в Sentry. * * @return void */ public function export(): void { if (empty($this->messages)) { return; } $hub = SentrySdk::getCurrentHub(); $applicationRunId = preg_replace("/-/", '', Yii::$app->getRequest()->applicationRunId); $operationName = $this->determineOperationName(); $commonTags = $this->getCommonTags($operationName, $applicationRunId); // Используем timestamp первого сообщения как старт транзакции $firstMessage = reset($this->messages); $startTimestamp = $firstMessage[3] ?? microtime(true); $transactionContext = new TransactionContext(); $transactionContext->setName($operationName); $transactionContext->setOp(Yii::$app->id); $transactionContext->setDescription($operationName); $transactionContext->setTraceId(new TraceId($applicationRunId)); $transactionContext->setStartTimestamp($startTimestamp); $transactionContext->setTags($commonTags); $transactionContext->setSampled(true); $transaction = $hub->startTransaction($transactionContext); $hub->setSpan($transaction); foreach ($this->messages as $message) { if (count($message) < 6) { Yii::warning('Некорректное сообщение для Sentry', __METHOD__); continue; } [$text, $level, $action, $timestamp, $stackTrace, $memory] = $message; $textExport = $this->exportText($text); $spanContext = new SpanContext(); $spanContext->setOp($action); $spanContext->setTags($commonTags + ['action' => $action]); $spanContext->setDescription($textExport); $spanContext->setStartTimestamp($startTimestamp); $spanContext->setSampled(true); $spanContext->setData($this->buildSpanData($message)); $span = $transaction->startChild($spanContext); if (in_array($level, [self::LEVEL_ERROR, self::LEVEL_WARNING], true)) { if ($text instanceof Throwable) { $hub->captureException($text); } else { $event = $this->createSentryEvent($textExport, $level, $action, $commonTags, $stackTrace, $startTimestamp); $hub->captureEvent($event, $this->getExceptionHint($text)); } } $span->finish($timestamp); $startTimestamp = $timestamp; } $lastMessage = end($this->messages); $finalTimestamp = $lastMessage[3] ?? microtime(true); if ($transaction->isActive()) { $transaction->finish($finalTimestamp); } } /** * Определяет название операции для транзакции на основе типа приложения и контекста запроса. * * @return string Название операции. */ private function determineOperationName(): string { return match (true) { Yii::$app instanceof ConsoleApplication => $this->getConsoleOperationName(), Yii::$app instanceof WebApplication => $this->getWebOperationName(), default => 'application', }; } private function getConsoleOperationName(): string { $controller = Yii::$app->controller; if ($controller instanceof ConsoleController) { $commandName = Yii::$app->requestedRoute; $args = implode(' ', Yii::$app->request->getArgs()); return "console.{$commandName} {$args}"; } return 'console'; } private function getWebOperationName(): string { $controller = Yii::$app->controller; if ($controller instanceof WebController) { $module = $controller->module->id ?? 'app'; $controllerId = $controller->id; $actionId = $controller->action->id ?? 'index'; return "web.{$module}.{$controllerId}.{$actionId}"; } return 'web'; } private function getCommonTags(string $operationName, string $traceId): array { return [ self::TAG_ROUTE => $operationName, self::TAG_APP_ID => Yii::$app->id, self::TAG_TRACE_ID => $traceId, ]; } /** * Формирует данные для спана на основе сообщения. * * @param array $message Сообщение логирования. * @return array Данные для спана. */ private function buildSpanData(array $message): array { [$text, $level, $action, $timestamp, $stackTrace, $memoryUsages] = $message; $user = Yii::$app->user ?? null; return [ 'level' => Logger::getLevelName($level), 'action' => $action, 'timestamp' => $timestamp, 'user' => $user ? $user->getId() : 'CLI', 'memory' => $memoryUsages, 'trace' => $stackTrace, 'message' => $this->exportText($text), ]; } /** * Создает Sentry Event на основе предоставленных данных. * * @param string $message Сообщение. * @param string $level Уровень логирования. * @param string $action Действие. * @param array $tags Теги. * @param array|null $stackTrace Стектрейс. * @param float $timestamp Временная метка. * @return Event */ private function createSentryEvent(string $message, string $level, string $action, array $tags, ?array $stackTrace, float $timestamp): Event { $event = Event::createEvent(); $event->setFingerprint([$level, $action, $tags[self::TAG_ROUTE], $tags[self::TAG_APP_ID]]); $event->setTags($tags); $event->setMessage($message); $severity = $level === self::LEVEL_WARNING ? Severity::warning() : Severity::error(); $event->setLevel($severity); $event->setStartTimestamp($timestamp); if (is_array($stackTrace) && !empty($stackTrace)) { $frames = array_map(fn($frameData) => new Frame( $frameData['file'] ?? null, $frameData['line'] ?? null, $frameData['function'] ?? null, $frameData['class'] ?? null ), $stackTrace); if (!empty($frames)) { $event->setStacktrace(new Stacktrace($frames)); } } return $event; } /** * Получает подсказку для события на основе текста сообщения. * * @param mixed $text Текст сообщения. * @return EventHint|null */ private function getExceptionHint($text): ?EventHint { if (is_array($text) && isset($text['exception']) && $text['exception'] instanceof Throwable) { return EventHint::fromArray(['exception' => $text['exception']]); } return null; } /** * Форматирует текст сообщения для отправки в Sentry. * * @param mixed $text Текст сообщения. * @return string */ private function exportText($text): string { if (is_array($text)) { return json_encode($text, JSON_UNESCAPED_UNICODE); } return (string)$text; } }
Логирование внутри класса SentryTarget:
В случае возникновения ошибок при экспорте сообщений в Sentry, полезно логировать эти ошибки в основной лог системы для облегчения отладки.
Конфигурация Sentry SDK:
Убедитесь, что все необходимые настройки Sentry (DSN, уровни логирования, теги по умолчанию и т.д.) правильно сконфигурированы, возможно, через конфигурационные файлы Yii.
Мониторинг производительности:
Если объем логов большой, отслеживайте влияние экспорта в Sentry на производительность приложения и при необходимости оптимизируйте этот процесс (например, используя очереди или асинхронные задачи).
Следуя этим рекомендациям, вы сможете сделать ваш класс SentryTarget
более устойчивым, читаемым и поддерживаемым, а также улучшить общую архитектуру вашего приложения.
Вот есть функция которая Экспортирует накопленные сообщения в Sentry.
Понял, вы хотите получить рекомендации по улучшению функции export
, которая экспортирует накопленные сообщения в Sentry. Рассмотрим текущую реализацию функции и предложим возможные улучшения для повышения эффективности, читаемости и надежности.
export
Для удобства, приведу кратко основную структуру функции export
из вашего класса SentryTarget
:
phppublic function export() { if (empty($this->messages)) { return; } $applicationRunId = preg_replace("/-/", '', Yii::$app->getRequest()->applicationRunId); $operationName = $this->determineOperationName(); // Используем timestamp первого сообщения как старт транзакции $firstMessage = reset($this->messages); $startTimestamp = $firstMessage[3] ?? microtime(true); $transactionContext = new TransactionContext(); // Настройка контекста транзакции... $transaction = SentrySdk::getCurrentHub()->startTransaction($transactionContext); SentrySdk::getCurrentHub()->setSpan($transaction); foreach ($this->messages as $message) { try { [$text, $level, $action, $timestamp, $stackTrace, $memory] = $message; $textExport = is_array($text) ? var_export($text, true) : (string)$text; // Создание и настройка спана $spanContext = new SpanContext(); // Настройка контекста спана... $span = $transaction->startChild($spanContext); // Обработка ошибок и предупреждений if (in_array($level, [Logger::LEVEL_ERROR, Logger::LEVEL_WARNING], true)) { if ($text instanceof Throwable) { SentrySdk::getCurrentHub()->captureException($text); } else { // Создание и настройка события $event = Event::createEvent(); // Настройка события... SentrySdk::getCurrentHub()->captureEvent( $event, EventHint::fromArray(array_filter((array)$exceptionHint)) ); } } // Завершение спана $span->finish($timestamp); $startTimestamp = $timestamp; } catch (Throwable $exception) { SentrySdk::getCurrentHub()->captureException($exception); } } // Завершение транзакции $lastMessage = end($this->messages); $finalTimestamp = $lastMessage[3] ?? microtime(true); $transaction->finish($finalTimestamp); }
export
Оптимизация доступа к Sentry Hub:
Вместо многократных вызовов SentrySdk::getCurrentHub()
, лучше сохранить ссылку на хаб в переменную. Это улучшит читаемость и потенциально повысит производительность.
php$hub = SentrySdk::getCurrentHub();
Использование транзакций и спанов с проверкой на активность:
Перед завершением транзакции убедитесь, что она все еще активна. Это предотвратит возможные ошибки в случае непредвиденных ситуаций.
phpif ($transaction->isActive()) { $transaction->finish($finalTimestamp); }
Улучшение обработки сообщений:
Проверка структуры сообщений:
Перед разбором сообщения убедитесь, что оно содержит ожидаемое количество элементов. Это предотвратит ошибки деструктуризации.
phpif (count($message) < 6) { Yii::warning('Некорректное сообщение для Sentry', __METHOD__); continue; }
Использование строгой типизации:
Убедитесь, что все используемые переменные имеют ожидаемые типы данных.
Централизация тегов и использование констант:
Как уже упоминалось ранее, использование констант для тегов и уровней логирования уменьшит вероятность ошибок и упростит поддержку кода.
phpprivate const LEVEL_ERROR = Logger::LEVEL_ERROR; private const LEVEL_WARNING = Logger::LEVEL_WARNING; private const TAG_ROUTE = 'route'; private const TAG_APP_ID = 'app-id'; private const TAG_TRACE_ID = 'trace-id';
И использование метода для получения общих тегов:
phpprivate function getCommonTags(string $operationName, string $traceId): array { return [ self::TAG_ROUTE => $operationName, self::TAG_APP_ID => Yii::$app->id, self::TAG_TRACE_ID => $traceId, ]; }
Улучшение формирования стектрейса:
Вместо ручного создания фреймов стека, можно использовать встроенные методы Sentry для построения стектрейса.
phpif (is_array($stackTrace) && !empty($stackTrace)) { $event->setStacktrace(StacktraceBuilder::fromBacktrace($stackTrace)); }
Асинхронная отправка данных:
Если объем логов велик, рассмотрите возможность отправки данных в Sentry асинхронно, чтобы не блокировать основной поток выполнения приложения. Это можно реализовать с помощью очередей (например, RabbitMQ, Redis) или фоновых задач.
Фильтрация чувствительных данных:
Убедитесь, что перед отправкой в Sentry вы фильтруете или маскируете любую конфиденциальную информацию, чтобы избежать утечки данных.
phpprivate function exportText($text): string { if (is_array($text)) { // Фильтрация или маскировка чувствительных данных return json_encode($text, JSON_UNESCAPED_UNICODE); } return (string)$text; }
Добавление логирования внутри класса:
В случае возникновения ошибок при экспорте сообщений в Sentry, полезно логировать эти ошибки в основной лог системы для облегчения отладки.
phpcatch (Throwable $exception) { Yii::error('Error exporting to Sentry: ' . $exception->getMessage(), __METHOD__); $hub->captureException($exception); }
Использование типов и строгой типизации:
Добавьте типы параметров и возвращаемых значений для методов, что повысит безопасность и читаемость кода.
php/** * Экспортирует накопленные сообщения в Sentry. * * @return void */ public function export(): void { // Код метода... }
Разделение логики на небольшие методы:
Разделите сложные блоки кода на отдельные методы для повышения читаемости и упрощения тестирования.
phpprivate function createSentryEvent(string $message, string $level, string $action, array $tags, ?array $stackTrace, float $timestamp): Event { // Создание и настройка события... } private function getExceptionHint($text): ?EventHint { // Получение подсказки для события... }
Использование современных возможностей PHP:
Если вы используете PHP 8.0 или выше, можно использовать конструкцию match
для более читаемого определения типа приложения.
phpprivate function determineOperationName(): string { return match (true) { Yii::$app instanceof ConsoleApplication => $this->getConsoleOperationName(), Yii::$app instanceof WebApplication => $this->getWebOperationName(), default => 'application', }; }
Документирование кода:
Добавьте PHPDoc комментарии к методам для улучшения понимания кода другими разработчиками и инструментами анализа.
php/** * Экспортирует накопленные сообщения в Sentry. * * @return void */ public function export(): void { // Код метода... }
export
Ниже приведен обновленный пример функции export
с учетом вышеуказанных рекомендаций:
phppublic function export(): void { if (empty($this->messages)) { return; } $hub = SentrySdk::getCurrentHub(); $applicationRunId = preg_replace("/-/", '', Yii::$app->getRequest()->applicationRunId); $operationName = $this->determineOperationName(); $commonTags = $this->getCommonTags($operationName, $applicationRunId); // Используем timestamp первого сообщения как старт транзакции $firstMessage = reset($this->messages); $startTimestamp = $firstMessage[3] ?? microtime(true); $transactionContext = new TransactionContext(); $transactionContext->setName($operationName); $transactionContext->setOp(Yii::$app->id); $transactionContext->setDescription($operationName); $transactionContext->setTraceId(new TraceId($applicationRunId)); $transactionContext->setStartTimestamp($startTimestamp); $transactionContext->setTags($commonTags); $transactionContext->setSampled(true); $transaction = $hub->startTransaction($transactionContext); $hub->setSpan($transaction); foreach ($this->messages as $message) { if (count($message) < 6) { Yii::warning('Некорректное сообщение для Sentry', __METHOD__); continue; } [$text, $level, $action, $timestamp, $stackTrace, $memory] = $message; $textExport = $this->exportText($text); $spanContext = new SpanContext(); $spanContext->setOp($action); $spanContext->setTags($commonTags + ['action' => $action]); $spanContext->setDescription($textExport); $spanContext->setStartTimestamp($startTimestamp); $spanContext->setSampled(true); $spanContext->setData($this->buildSpanData($message)); $span = $transaction->startChild($spanContext); if (in_array($level, [self::LEVEL_ERROR, self::LEVEL_WARNING], true)) { if ($text instanceof Throwable) { $hub->captureException($text); } else { $event = $this->createSentryEvent($textExport, $level, $action, $commonTags, $stackTrace, $startTimestamp); $hub->captureEvent($event, $this->getExceptionHint($text)); } } $span->finish($timestamp); $startTimestamp = $timestamp; } $lastMessage = end($this->messages); $finalTimestamp = $lastMessage[3] ?? microtime(true); if ($transaction->isActive()) { $transaction->finish($finalTimestamp); } }
Тестирование:
Напишите юнит-тесты для функции export
и связанных методов, чтобы гарантировать корректность их работы и упростить отладку в будущем.
Мониторинг производительности:
Если объем логов большой, отслеживайте влияние экспорта в Sentry на производительность приложения. Рассмотрите возможность использования очередей или фоновых задач для обработки логов.
Конфигурация Sentry SDK:
Убедитесь, что все необходимые настройки Sentry (например, DSN, уровни логирования, теги по умолчанию) правильно сконфигурированы, возможно, через конфигурационные файлы Yii.
Обработка специфичных случаев:
Рассмотрите возможность обработки других уровней логирования или типов сообщений, если это необходимо для вашего приложения.
Документация:
Поддерживайте актуальную документацию к вашему коду, описывая назначение методов и используемые параметры.
Следуя этим рекомендациям, вы сможете сделать функцию export
более эффективной, надежной и удобной для поддержки. Если у вас есть конкретные вопросы или требуется помощь с определенными аспектами функции, пожалуйста, уточните, и я с радостью помогу!
Есть функция которая Экспортирует накопленные сообщения в Sentry, как это работает под капотом, объясни подробно, вот приходит запрос какойто либо через консоль либо через web
Конечно! Давайте подробно разберем, как функция export
из вашего класса SentryTarget
экспортирует накопленные сообщения в Sentry. Мы рассмотрим процесс пошагово, начиная от получения запроса (через консоль или веб) до отправки данных в Sentry.
Функция export
отвечает за отправку логов вашего приложения в сервис Sentry для последующего мониторинга и анализа. Sentry позволяет отслеживать ошибки, предупреждения и производительность приложения. В вашем случае, функция также создает транзакции и спаны для мониторинга производительности.
export
Проверка наличия сообщений для экспорта
phpif (empty($this->messages)) { return; }
$this->messages
пустой), функция завершает работу.Подготовка идентификатора выполнения приложения и определения названия операции
php$applicationRunId = preg_replace("/-/", '', Yii::$app->getRequest()->applicationRunId); $operationName = $this->determineOperationName();
applicationRunId
: Получает уникальный идентификатор выполнения приложения, удаляя все дефисы. Этот идентификатор используется для отслеживания транзакций.operationName
: Вызывает метод determineOperationName()
, который определяет название текущей операции на основе типа приложения (консольное или веб).Инициализация Sentry Hub и создание транзакции
php$hub = SentrySdk::getCurrentHub(); // Используем timestamp первого сообщения как старт транзакции $firstMessage = reset($this->messages); $startTimestamp = $firstMessage[3] ?? microtime(true); $transactionContext = new TransactionContext(); $transactionContext->setName($operationName); $transactionContext->setOp(Yii::$app->id); $transactionContext->setDescription($operationName); $transactionContext->setTraceId(new TraceId($applicationRunId)); $transactionContext->setStartTimestamp($startTimestamp); $transactionContext->setTags([ 'route' => $operationName, 'app-id' => Yii::$app->id, 'trace-id' => $applicationRunId ]); $transactionContext->setSampled(true); $transaction = $hub->startTransaction($transactionContext); $hub->setSpan($transaction);
Обработка каждого сообщения лога
phpforeach ($this->messages as $message) { try { [$text, $level, $action, $timestamp, $stackTrace, $memory] = $message; $textExport = is_array($text) ? var_export($text, true) : (string)$text; // Параметры для спанов $spanContext = new SpanContext(); $spanContext->setOp($action); $spanContext->setTags([ 'action' => $action, 'route' => $operationName, 'app-id' => Yii::$app->id, 'trace-id' => $applicationRunId ]); $spanContext->setDescription($textExport); $spanContext->setStartTimestamp($startTimestamp); $spanContext->setSampled(true); $spanContext->setData($this->buildSpanData($message)); // Старт спана $span = $transaction->startChild($spanContext); // Обработка ошибки или предупреждения if (in_array($level, [Logger::LEVEL_ERROR, Logger::LEVEL_WARNING], true)) { if ($text instanceof Throwable) { // Если это реальный Throwable, отправим как исключение $hub->captureException($text); } else { // В остальных случаях создаем ивент вручную $event = Event::createEvent(); $event->setFingerprint([$level, $action, $operationName, Yii::$app->id]); $event->setTags([ 'action' => $action, 'route' => $operationName, 'app-id' => Yii::$app->id, 'trace-id' => $applicationRunId ]); $event->setMessage($textExport); // Установка уровня серьёзности $severity = $level === Logger::LEVEL_WARNING ? Severity::warning() : Severity::error(); $event->setLevel($severity); $event->setStartTimestamp($startTimestamp); // Формирование стектрейса, если доступен if (is_array($stackTrace) && !empty($stackTrace)) { $frames = []; foreach ($stackTrace as $frameData) { $frames[] = new Frame( $frameData['file'] ?? null, $frameData['line'] ?? null, $frameData['function'] ?? null, $frameData['class'] ?? null ); } if (!empty($frames)) { $event->setStacktrace(new Stacktrace($frames)); } } // Если есть информация об исключении в тексте $exceptionHint = null; if (is_array($text) && isset($text['exception']) && $text['exception'] instanceof Throwable) { $exceptionHint = ['exception' => $text['exception']]; } $hub->captureEvent( $event, EventHint::fromArray(array_filter((array)$exceptionHint)) ); } } // Завершение спана $span->finish($timestamp); $startTimestamp = $timestamp; } catch (Throwable $exception) { $hub->captureException($exception); } }
Что происходит:
$this->messages
.$text
), уровень ($level
), действие ($action
), временная метка ($timestamp
), стек-трейс ($stackTrace
), использование памяти ($memory
).SpanContext
с информацией о текущем действии, тегах, описании и временной метке.buildSpanData($message)
.LEVEL_ERROR
) или предупреждению (LEVEL_WARNING
):
Throwable
: Отправляет исключение напрямую в Sentry через captureException
.Event
), устанавливает его свойства (сообщение, уровень, теги, стек-трейс) и отправляет через captureEvent
.startTimestamp
для следующего спана.Зачем:
Завершение транзакции
php$lastMessage = end($this->messages); $finalTimestamp = $lastMessage[3] ?? microtime(true); $transaction->finish($finalTimestamp);
Функция export
учитывает, откуда пришел запрос — из консоли или из веб. Это влияет на определение названия операции (operationName
), что, в свою очередь, отражается в транзакциях и спанах.
Метод determineOperationName()
отвечает за определение контекста операции:
phpprivate function determineOperationName(): string { if (Yii::$app instanceof ConsoleApplication) { // Для консольных приложений используем имя команды и аргументы $controller = Yii::$app->controller; if ($controller instanceof ConsoleController) { $commandName = Yii::$app->requestedRoute; $args = implode(' ', Yii::$app->request->getArgs()); return "console.{$commandName} {$args}"; } return 'console'; } elseif (Yii::$app instanceof WebApplication) { // Для веб-приложений используем модуль, контроллер и действие $controller = Yii::$app->controller; if ($controller instanceof WebController) { $module = Yii::$app->controller->module->id ?? 'app'; $controllerId = Yii::$app->controller->id; $actionId = Yii::$app->controller->action->id ?? 'index'; return "web.{$module}.{$controllerId}.{$actionId}"; } return 'web'; } // По умолчанию return 'application'; }
Консольные запросы:
console.migrate --migrationPath=@app/migrations
.Веб-запросы:
web.site.index
.По умолчанию:
application
.Транзакция (Transaction
):
Спан (Span
):
Зачем использовать транзакции и спаны:
После обработки всех сообщений и создания необходимых спанов транзакция завершается, и все собранные данные отправляются в Sentry. Sentry агрегирует эту информацию, предоставляя интерфейс для анализа ошибок, предупреждений и производительности.
Sentry SDK:
Контекст приложения:
Управление исключениями:
Throwable
) автоматически отправляются в Sentry через captureException
.Стек-трейсы:
Таймштампы:
Давайте рассмотрим, как функция export
обрабатывает запрос шаг за шагом:
Получение запроса:
yii migrate
.https://example.com/site/index
.Логирование:
Накопление сообщений:
$this->messages
внутри класса SentryTarget
.Вызов функции export
:
export
, которая отправляет накопленные сообщения в Sentry.Создание транзакции:
Обработка сообщений:
Завершение транзакции:
Анализ в Sentry:
Централизованное логирование и мониторинг:
Мониторинг производительности:
Диагностика ошибок:
Различие между типами запросов:
Функция export
эффективно интегрирует ваш PHP-приложение с Sentry, обеспечивая подробное логирование, мониторинг ошибок и анализ производительности. Она учитывает специфику различных типов запросов (консольных и веб), создавая транзакции и спаны, которые позволяют получить глубокое понимание работы приложения.
Рекомендации для дальнейшего улучшения:
Асинхронная отправка логов:
Фильтрация чувствительных данных:
Управление уровнем логирования:
Тестирование:
SentryTarget
, чтобы гарантировать корректность его работы и облегчить будущие изменения.Документация:
Если у вас возникнут дополнительные вопросы или потребуется помощь с конкретными аспектами интеграции, не стесняйтесь обращаться!
Напиши итоговый лучший Вариант одним файлом
Конечно! Ниже представлен улучшенный и оптимизированный вариант класса SentryTarget
для интеграции с Sentry в вашем Yii-приложении. Этот класс учитывает все рекомендации по улучшению, включая повышение читаемости, производительности, безопасности и поддержки.
php<?php namespace common\components\log; use notamedia\sentry\SentryTarget as BaseSentryTarget; use Sentry\Event; use Sentry\EventHint; use Sentry\Frame; use Sentry\SentrySdk; use Sentry\Severity; use Sentry\Stacktrace; use Sentry\Tracing\SpanContext; use Sentry\Tracing\TraceId; use Sentry\Tracing\TransactionContext; use Sentry\StacktraceBuilder; use Yii; use yii\console\Application as ConsoleApplication; use yii\console\Controller as ConsoleController; use yii\web\Application as WebApplication; use yii\web\Controller as WebController; use yii\log\Logger; use Throwable; /** * Класс SentryTarget отвечает за экспорт логов в Sentry с поддержкой транзакций и спанов для мониторинга производительности. */ class SentryTarget extends BaseSentryTarget { // Константы уровней логирования private const LEVEL_ERROR = Logger::LEVEL_ERROR; private const LEVEL_WARNING = Logger::LEVEL_WARNING; // Константы тегов private const TAG_ROUTE = 'route'; private const TAG_APP_ID = 'app-id'; private const TAG_TRACE_ID = 'trace-id'; private const TAG_ACTION = 'action'; /** * Экспортирует накопленные сообщения в Sentry. * * @return void */ public function export(): void { if (empty($this->messages)) { return; } $hub = SentrySdk::getCurrentHub(); $applicationRunId = $this->getApplicationRunId(); $operationName = $this->determineOperationName(); $commonTags = $this->getCommonTags($operationName, $applicationRunId); // Используем timestamp первого сообщения как старт транзакции $firstMessage = reset($this->messages); $startTimestamp = $firstMessage[3] ?? microtime(true); $transactionContext = new TransactionContext(); $transactionContext->setName($operationName); $transactionContext->setOp(Yii::$app->id); $transactionContext->setDescription($operationName); $transactionContext->setTraceId(new TraceId($applicationRunId)); $transactionContext->setStartTimestamp($startTimestamp); $transactionContext->setTags($commonTags); $transactionContext->setSampled(true); $transaction = $hub->startTransaction($transactionContext); $hub->setSpan($transaction); foreach ($this->messages as $message) { if (!$this->isValidMessage($message)) { Yii::warning('Некорректное сообщение для Sentry', __METHOD__); continue; } [$text, $level, $action, $timestamp, $stackTrace, $memory] = $message; $textExport = $this->exportText($text); $spanContext = new SpanContext(); $spanContext->setOp($action); $spanContext->setTags(array_merge($commonTags, [self::TAG_ACTION => $action])); $spanContext->setDescription($textExport); $spanContext->setStartTimestamp($startTimestamp); $spanContext->setSampled(true); $spanContext->setData($this->buildSpanData($message)); $span = $transaction->startChild($spanContext); if (in_array($level, [self::LEVEL_ERROR, self::LEVEL_WARNING], true)) { if ($text instanceof Throwable) { $hub->captureException($text); } else { $event = $this->createSentryEvent($textExport, $level, $action, $commonTags, $stackTrace, $startTimestamp); $hub->captureEvent($event, $this->getExceptionHint($text)); } } $span->finish($timestamp); $startTimestamp = $timestamp; } $lastMessage = end($this->messages); $finalTimestamp = $lastMessage[3] ?? microtime(true); if ($transaction->isActive()) { $transaction->finish($finalTimestamp); } } /** * Получает уникальный идентификатор выполнения приложения без дефисов. * * @return string */ private function getApplicationRunId(): string { return preg_replace("/-/", '', Yii::$app->getRequest()->applicationRunId); } /** * Определяет название операции для транзакции на основе типа приложения и контекста запроса. * * @return string Название операции. */ private function determineOperationName(): string { return match (true) { Yii::$app instanceof ConsoleApplication => $this->getConsoleOperationName(), Yii::$app instanceof WebApplication => $this->getWebOperationName(), default => 'application', }; } /** * Получает название операции для консольного приложения. * * @return string Название операции. */ private function getConsoleOperationName(): string { $controller = Yii::$app->controller; if ($controller instanceof ConsoleController) { $commandName = Yii::$app->requestedRoute; $args = implode(' ', Yii::$app->request->getArgs()); return "console.{$commandName} {$args}"; } return 'console'; } /** * Получает название операции для веб-приложения. * * @return string Название операции. */ private function getWebOperationName(): string { $controller = Yii::$app->controller; if ($controller instanceof WebController) { $module = $controller->module->id ?? 'app'; $controllerId = $controller->id; $actionId = $controller->action->id ?? 'index'; return "web.{$module}.{$controllerId}.{$actionId}"; } return 'web'; } /** * Генерирует общие теги для Sentry событий. * * @param string $operationName Название операции. * @param string $traceId Идентификатор трассировки. * @return array Массив тегов. */ private function getCommonTags(string $operationName, string $traceId): array { return [ self::TAG_ROUTE => $operationName, self::TAG_APP_ID => Yii::$app->id, self::TAG_TRACE_ID => $traceId, ]; } /** * Проверяет корректность структуры сообщения. * * @param array $message Сообщение лога. * @return bool Истина, если сообщение корректно, иначе ложь. */ private function isValidMessage(array $message): bool { return count($message) >= 6; } /** * Формирует данные для спана на основе сообщения. * * @param array $message Сообщение логирования. * @return array Данные для спана. */ private function buildSpanData(array $message): array { [$text, $level, $action, $timestamp, $stackTrace, $memoryUsages] = $message; $user = Yii::$app->user ?? null; return [ 'level' => Logger::getLevelName($level), 'action' => $action, 'timestamp' => $timestamp, 'user' => $user ? $user->getId() : 'CLI', 'memory' => $memoryUsages, 'trace' => $stackTrace, 'message' => $this->exportText($text), ]; } /** * Создает Sentry Event на основе предоставленных данных. * * @param string $message Сообщение. * @param string $level Уровень логирования. * @param string $action Действие. * @param array $tags Теги. * @param array|null $stackTrace Стектрейс. * @param float $timestamp Временная метка. * @return Event */ private function createSentryEvent(string $message, string $level, string $action, array $tags, ?array $stackTrace, float $timestamp): Event { $event = Event::createEvent(); $event->setFingerprint([$level, $action, $tags[self::TAG_ROUTE], $tags[self::TAG_APP_ID]]); $event->setTags($tags); $event->setMessage($message); $severity = $level === self::LEVEL_WARNING ? Severity::warning() : Severity::error(); $event->setLevel($severity); $event->setStartTimestamp($timestamp); if (is_array($stackTrace) && !empty($stackTrace)) { $frames = array_map(fn($frameData) => new Frame( $frameData['file'] ?? null, $frameData['line'] ?? null, $frameData['function'] ?? null, $frameData['class'] ?? null ), $stackTrace); if (!empty($frames)) { $event->setStacktrace(new Stacktrace($frames)); } } return $event; } /** * Получает подсказку для события на основе текста сообщения. * * @param mixed $text Текст сообщения. * @return EventHint|null */ private function getExceptionHint($text): ?EventHint { if (is_array($text) && isset($text['exception']) && $text['exception'] instanceof Throwable) { return EventHint::fromArray(['exception' => $text['exception']]); } return null; } /** * Форматирует текст сообщения для отправки в Sentry. * * @param mixed $text Текст сообщения. * @return string */ private function exportText($text): string { if (is_array($text)) { return json_encode($text, JSON_UNESCAPED_UNICODE); } return (string)$text; } }
Использование Констант:
Централизация Тегов:
getCommonTags
централизует создание общих тегов, избегая дублирования кода.Оптимизация Вызовов Sentry Hub:
SentrySdk::getCurrentHub()
сохраняется в переменной $hub
, что снижает количество вызовов статического метода.Проверка Корректности Сообщений:
isValidMessage
проверяет структуру сообщения перед его обработкой, предотвращая возможные ошибки деструктуризации.Разделение Логики на Методы:
Использование Современных Возможностей PHP:
match
используется для определения типа приложения, что делает код более лаконичным и читаемым.Документирование Кода:
Безопасность и Обработка Данных:
exportText
использует json_encode
вместо var_export
для безопасного преобразования массивов в строки, что снижает риск утечки чувствительных данных.Обработка Исключений:
Асинхронная Отправка Логов:
Фильтрация Чувствительных Данных:
Настройка Уровней Логирования:
Тестирование:
SentryTarget
, чтобы гарантировать корректность его работы и облегчить отладку в будущем.Мониторинг Производительности:
Документация:
Этот улучшенный класс SentryTarget
предоставляет эффективную и надежную интеграцию с Sentry, обеспечивая подробное логирование, мониторинг ошибок и анализ производительности вашего Yii-приложения. Следуя дополнительным рекомендациям, вы сможете обеспечить высокое качество и безопасность вашего приложения, а также упростить его поддержку и развитие.
Сделай так чтобы в консольных командах operationame не попадали аргументы
а попадали как дополнительная информация
Пришли код полностью
Конечно! Ниже представлен обновленный и оптимизированный класс SentryTarget
, который интегрируется с Sentry в вашем Yii-приложении. В этом варианте для консольных команд operationName
не включает аргументы, а аргументы добавляются как дополнительная информация в данные спана.
php<?php namespace common\components\log; use notamedia\sentry\SentryTarget as BaseSentryTarget; use Sentry\Event; use Sentry\EventHint; use Sentry\Frame; use Sentry\SentrySdk; use Sentry\Severity; use Sentry\Stacktrace; use Sentry\Tracing\SpanContext; use Sentry\Tracing\TraceId; use Sentry\Tracing\TransactionContext; use Sentry\StacktraceBuilder; use Yii; use yii\console\Application as ConsoleApplication; use yii\console\Controller as ConsoleController; use yii\web\Application as WebApplication; use yii\web\Controller as WebController; use yii\log\Logger; use Throwable; /** * Класс SentryTarget отвечает за экспорт логов в Sentry с поддержкой транзакций и спанов для мониторинга производительности. */ class SentryTarget extends BaseSentryTarget { // Константы уровней логирования private const LEVEL_ERROR = Logger::LEVEL_ERROR; private const LEVEL_WARNING = Logger::LEVEL_WARNING; // Константы тегов private const TAG_ROUTE = 'route'; private const TAG_APP_ID = 'app-id'; private const TAG_TRACE_ID = 'trace-id'; private const TAG_ACTION = 'action'; private const TAG_ARGS = 'args'; // Тег для аргументов консольной команды /** * Экспортирует накопленные сообщения в Sentry. * * @return void */ public function export(): void { if (empty($this->messages)) { return; } $hub = SentrySdk::getCurrentHub(); $applicationRunId = $this->getApplicationRunId(); $operationName = $this->determineOperationName(); $commonTags = $this->getCommonTags($operationName, $applicationRunId); // Используем timestamp первого сообщения как старт транзакции $firstMessage = reset($this->messages); $startTimestamp = $firstMessage[3] ?? microtime(true); $transactionContext = new TransactionContext(); $transactionContext->setName($operationName); $transactionContext->setOp(Yii::$app->id); $transactionContext->setDescription($operationName); $transactionContext->setTraceId(new TraceId($applicationRunId)); $transactionContext->setStartTimestamp($startTimestamp); $transactionContext->setTags($commonTags); $transactionContext->setSampled(true); $transaction = $hub->startTransaction($transactionContext); $hub->setSpan($transaction); foreach ($this->messages as $message) { if (!$this->isValidMessage($message)) { Yii::warning('Некорректное сообщение для Sentry', __METHOD__); continue; } [$text, $level, $action, $timestamp, $stackTrace, $memory] = $message; $textExport = $this->exportText($text); $spanContext = new SpanContext(); $spanContext->setOp($action); $spanContext->setTags(array_merge($commonTags, [self::TAG_ACTION => $action])); $spanContext->setDescription($textExport); $spanContext->setStartTimestamp($startTimestamp); $spanContext->setSampled(true); $spanContext->setData($this->buildSpanData($message)); $span = $transaction->startChild($spanContext); if (in_array($level, [self::LEVEL_ERROR, self::LEVEL_WARNING], true)) { if ($text instanceof Throwable) { $hub->captureException($text); } else { $event = $this->createSentryEvent($textExport, $level, $action, $commonTags, $stackTrace, $startTimestamp); $hub->captureEvent($event, $this->getExceptionHint($text)); } } $span->finish($timestamp); $startTimestamp = $timestamp; } $lastMessage = end($this->messages); $finalTimestamp = $lastMessage[3] ?? microtime(true); if ($transaction->isActive()) { $transaction->finish($finalTimestamp); } } /** * Получает уникальный идентификатор выполнения приложения без дефисов. * * @return string */ private function getApplicationRunId(): string { // Проверяем, является ли приложение веб-приложением, иначе предполагаем, что это консольное приложение if (Yii::$app instanceof WebApplication) { $request = Yii::$app->getRequest(); return preg_replace("/-/", '', $request->applicationRunId); } elseif (Yii::$app instanceof ConsoleApplication) { // Для консольных приложений может быть другой способ получения applicationRunId // Здесь предполагается, что applicationRunId доступен через Yii::$app->controller $controller = Yii::$app->controller; if ($controller && method_exists($controller, 'getApplicationRunId')) { return preg_replace("/-/", '', $controller->getApplicationRunId()); } } // По умолчанию return uniqid('', true); } /** * Определяет название операции для транзакции на основе типа приложения и контекста запроса. * * @return string Название операции. */ private function determineOperationName(): string { return match (true) { Yii::$app instanceof ConsoleApplication => $this->getConsoleOperationName(), Yii::$app instanceof WebApplication => $this->getWebOperationName(), default => 'application', }; } /** * Получает название операции для консольного приложения без аргументов. * Аргументы добавляются как дополнительная информация. * * @return string Название операции. */ private function getConsoleOperationName(): string { $controller = Yii::$app->controller; if ($controller instanceof ConsoleController) { $commandName = Yii::$app->requestedRoute; return "console.{$commandName}"; } return 'console'; } /** * Получает название операции для веб-приложения. * * @return string Название операции. */ private function getWebOperationName(): string { $controller = Yii::$app->controller; if ($controller instanceof WebController) { $module = $controller->module->id ?? 'app'; $controllerId = $controller->id; $actionId = $controller->action->id ?? 'index'; return "web.{$module}.{$controllerId}.{$actionId}"; } return 'web'; } /** * Генерирует общие теги для Sentry событий. * * @param string $operationName Название операции. * @param string $traceId Идентификатор трассировки. * @return array Массив тегов. */ private function getCommonTags(string $operationName, string $traceId): array { return [ self::TAG_ROUTE => $operationName, self::TAG_APP_ID => Yii::$app->id, self::TAG_TRACE_ID => $traceId, ]; } /** * Проверяет корректность структуры сообщения. * * @param array $message Сообщение лога. * @return bool Истина, если сообщение корректно, иначе ложь. */ private function isValidMessage(array $message): bool { return count($message) >= 6; } /** * Формирует данные для спана на основе сообщения. * * @param array $message Сообщение логирования. * @return array Данные для спана. */ private function buildSpanData(array $message): array { [$text, $level, $action, $timestamp, $stackTrace, $memoryUsages] = $message; $user = Yii::$app->user ?? null; $spanData = [ 'level' => Logger::getLevelName($level), 'action' => $action, 'timestamp' => $timestamp, 'user' => $user ? $user->getId() : 'CLI', 'memory' => $memoryUsages, 'trace' => $stackTrace, 'message' => $this->exportText($text), ]; // Добавляем аргументы для консольных команд if (Yii::$app instanceof ConsoleApplication) { $controller = Yii::$app->controller; if ($controller instanceof ConsoleController && method_exists($controller, 'getCommandArguments')) { $args = $controller->getCommandArguments(); $spanData['args'] = $args; } } return $spanData; } /** * Создает Sentry Event на основе предоставленных данных. * * @param string $message Сообщение. * @param string $level Уровень логирования. * @param string $action Действие. * @param array $tags Теги. * @param array|null $stackTrace Стектрейс. * @param float $timestamp Временная метка. * @return Event */ private function createSentryEvent(string $message, string $level, string $action, array $tags, ?array $stackTrace, float $timestamp): Event { $event = Event::createEvent(); $event->setFingerprint([$level, $action, $tags[self::TAG_ROUTE], $tags[self::TAG_APP_ID]]); $event->setTags($tags); $event->setMessage($message); $severity = $level === self::LEVEL_WARNING ? Severity::warning() : Severity::error(); $event->setLevel($severity); $event->setStartTimestamp($timestamp); if (is_array($stackTrace) && !empty($stackTrace)) { $frames = array_map(fn($frameData) => new Frame( $frameData['file'] ?? null, $frameData['line'] ?? null, $frameData['function'] ?? null, $frameData['class'] ?? null ), $stackTrace); if (!empty($frames)) { $event->setStacktrace(new Stacktrace($frames)); } } return $event; } /** * Получает подсказку для события на основе текста сообщения. * * @param mixed $text Текст сообщения. * @return EventHint|null */ private function getExceptionHint($text): ?EventHint { if (is_array($text) && isset($text['exception']) && $text['exception'] instanceof Throwable) { return EventHint::fromArray(['exception' => $text['exception']]); } return null; } /** * Форматирует текст сообщения для отправки в Sentry. * * @param mixed $text Текст сообщения. * @return string */ private function exportText($text): string { if (is_array($text)) { return json_encode($text, JSON_UNESCAPED_UNICODE); } return (string)$text; } }
Удаление Аргументов из operationName
для Консольных Команд:
В методе getConsoleOperationName()
название операции формируется только на основе имени команды без включения аргументов:
phpprivate function getConsoleOperationName(): string { $controller = Yii::$app->controller; if ($controller instanceof ConsoleController) { $commandName = Yii::$app->requestedRoute; return "console.{$commandName}"; } return 'console'; }
Добавление Аргументов как Дополнительной Информации:
В методе buildSpanData()
, если приложение является консольным, извлекаются аргументы команды и добавляются в данные спана:
phpprivate function buildSpanData(array $message): array { [$text, $level, $action, $timestamp, $stackTrace, $memoryUsages] = $message; $user = Yii::$app->user ?? null; $spanData = [ 'level' => Logger::getLevelName($level), 'action' => $action, 'timestamp' => $timestamp, 'user' => $user ? $user->getId() : 'CLI', 'memory' => $memoryUsages, 'trace' => $stackTrace, 'message' => $this->exportText($text), ]; // Добавляем аргументы для консольных команд if (Yii::$app instanceof ConsoleApplication) { $controller = Yii::$app->controller; if ($controller instanceof ConsoleController && method_exists($controller, 'getCommandArguments')) { $args = $controller->getCommandArguments(); $spanData['args'] = $args; } } return $spanData; }
Примечание: Предполагается, что ваш консольный контроллер имеет метод getCommandArguments()
, который возвращает аргументы команды. Если такого метода нет, его нужно реализовать в вашем контроллере консольных команд. Например:
phpclass YourConsoleController extends ConsoleController { // Другие методы... /** * Возвращает аргументы консольной команды. * * @return array */ public function getCommandArguments(): array { return Yii::$app->request->getArgs(); } }
Добавление Тега для Аргументов:
В классе добавлен новый константный тег TAG_ARGS
, который может быть использован для передачи аргументов в Sentry, если вы решите добавлять их как отдельный тег:
phpprivate const TAG_ARGS = 'args'; // Тег для аргументов консольной команды
Однако в данном примере аргументы добавляются как часть данных спана (spanData
), что позволяет сохранить структуру информации более гибкой.
Улучшение Метода getApplicationRunId()
:
Метод getApplicationRunId()
был расширен для корректного получения applicationRunId
как для веб-приложений, так и для консольных приложений. Если приложение является консольным и контроллер имеет метод getApplicationRunId()
, он используется. В противном случае генерируется уникальный идентификатор с помощью uniqid()
:
phpprivate function getApplicationRunId(): string { // Проверяем, является ли приложение веб-приложением, иначе предполагаем, что это консольное приложение if (Yii::$app instanceof WebApplication) { $request = Yii::$app->getRequest(); return preg_replace("/-/", '', $request->applicationRunId); } elseif (Yii::$app instanceof ConsoleApplication) { // Для консольных приложений может быть другой способ получения applicationRunId // Здесь предполагается, что applicationRunId доступен через Yii::$app->controller $controller = Yii::$app->controller; if ($controller && method_exists($controller, 'getApplicationRunId')) { return preg_replace("/-/", '', $controller->getApplicationRunId()); } } // По умолчанию return uniqid('', true); }
Реализация Метода getCommandArguments()
в Контроллерах Консольных Команд:
Убедитесь, что все ваши консольные контроллеры реализуют метод getCommandArguments()
, который возвращает аргументы команды. Это необходимо для корректного добавления аргументов в данные спана.
Фильтрация Чувствительных Данных:
При добавлении аргументов команд убедитесь, что они не содержат конфиденциальной информации. Если это необходимо, реализуйте механизмы маскировки или фильтрации.
Асинхронная Отправка Логов:
Для повышения производительности и уменьшения задержек при обработке запросов, рассмотрите возможность отправки логов в Sentry асинхронно, используя очереди или фоновые задачи.
Тестирование:
Напишите юнит-тесты для класса SentryTarget
, чтобы убедиться в корректности его работы, особенно в части обработки консольных команд и передачи аргументов.
Документация:
Обновите документацию вашего проекта, описав особенности интеграции с Sentry и использование нового метода передачи аргументов консольных команд.
Данный класс SentryTarget
обеспечивает эффективную интеграцию вашего Yii-приложения с Sentry, учитывая специфику как веб-запросов, так и консольных команд. Аргументы консольных команд не включаются в название операции (operationName
), а добавляются как дополнительная информация в данные спана, что позволяет сохранять чистоту названий операций и при этом иметь доступ к необходимым данным для анализа.
Если у вас возникнут дополнительные вопросы или потребуется дальнейшая помощь, не стесняйтесь обращаться!