diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/.env.example b/module/oc_telegram_shop/upload/oc_telegram_shop/.env.example index 9c0c6d8..cd945c9 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/.env.example +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/.env.example @@ -6,3 +6,7 @@ TELECART_CACHE_DRIVER=redis #TELECART_REDIS_HOST=redis #TELECART_REDIS_PORT=6379 #TELECART_REDIS_DATABASE=0 + +SENTRY_ENABLED=false +SENTRY_DSN= +SENTRY_ENABLE_LOGS=false diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/.env.production b/module/oc_telegram_shop/upload/oc_telegram_shop/.env.production index 2aa08b4..e0b036a 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/.env.production +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/.env.production @@ -5,3 +5,7 @@ TELECART_CACHE_DRIVER=mysql TELECART_REDIS_HOST=redis TELECART_REDIS_PORT=6379 TELECART_REDIS_DATABASE=0 + +SENTRY_ENABLED=false +SENTRY_DSN= +SENTRY_ENABLE_LOGS=false diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/composer.json b/module/oc_telegram_shop/upload/oc_telegram_shop/composer.json index ceb293d..ad79b14 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/composer.json +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/composer.json @@ -36,7 +36,8 @@ "ramsey/uuid": "^4.2", "symfony/http-foundation": "^5.4", "symfony/console": "^5.4", - "dragonmantank/cron-expression": "^3.5" + "dragonmantank/cron-expression": "^3.5", + "sentry/sentry": "^4.19" }, "require-dev": { "doctrine/sql-formatter": "^1.3", diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Application.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Application.php index 7598cdd..56abdc7 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Application.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Application.php @@ -12,7 +12,7 @@ use Openguru\OpenCartFramework\MaintenanceTasks\MaintenanceTasksService; use Openguru\OpenCartFramework\MaintenanceTasks\MaintenanceTasksServiceProvider; use Openguru\OpenCartFramework\Migrations\MigrationsServiceProvider; use Openguru\OpenCartFramework\Router\Router; -use Openguru\OpenCartFramework\Support\ExecutionTimeProfiler; +use Openguru\OpenCartFramework\Sentry\SentryService; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Response; @@ -27,11 +27,6 @@ class Application extends Container implements LoggerAwareInterface private LoggerInterface $logger; - /** - * @var ExecutionTimeProfiler - */ - private ExecutionTimeProfiler $profiler; - public static function getInstance(): Application { return static::$instance; @@ -39,13 +34,6 @@ class Application extends Container implements LoggerAwareInterface private function bootKernelServices(): void { - $this->singleton(ExecutionTimeProfiler::class, function () { - return new ExecutionTimeProfiler(); - }); - - $this->profiler = $this->get(ExecutionTimeProfiler::class); - $this->profiler->start(); - $this->singleton(Container::class, function (Container $container) { return $container; }); @@ -56,8 +44,6 @@ class Application extends Container implements LoggerAwareInterface return new Settings($container->getConfigValue()); }); - $this->loadEnvironmentVariables(); - $errorHandler = new ErrorHandler( $this->get(LoggerInterface::class), $this, @@ -68,16 +54,23 @@ class Application extends Container implements LoggerAwareInterface public function boot(): Application { - $this->bootKernelServices(); + $this->loadEnvironmentVariables(); - $this->initializeEventDispatcher(static::$events); + $action = $_GET['api_action'] ?? null; - DependencyRegistration::register($this, $this->serviceProviders); + SentryService::init($action); + + SentryService::measure('boot_kernel_services', 'app.boot', function () { + $this->bootKernelServices(); + }); + + SentryService::measure('dependency_registration', 'app.boot', function () { + $this->initializeEventDispatcher(static::$events); + DependencyRegistration::register($this, $this->serviceProviders); + }); static::$instance = $this; - $this->profiler->addCheckpoint('Bootstrap Application'); - return $this; } @@ -97,18 +90,25 @@ class Application extends Container implements LoggerAwareInterface throw new InvalidArgumentException('Invalid action: ' . $controller . '->' . $method); } - $this->profiler->addCheckpoint('Handle Middlewares.'); + $span = SentryService::startSpan('handle_middlewares', 'http.handle'); - $next = fn($req) => $this->call($controller, $method); - - foreach (array_reverse($this->middlewareStack) as $class) { - $instance = $this->get($class); - $next = static fn($req) => $instance->handle($req, $next); + try { + $next = fn($req) => $this->call($controller, $method); + foreach (array_reverse($this->middlewareStack) as $class) { + $instance = $this->get($class); + $next = static fn($req) => $instance->handle($req, $next); + } + } finally { + SentryService::endSpan($span); } - $response = $next($request); + $span = SentryService::startSpan('handle_controller', 'http.handle'); - $this->profiler->addCheckpoint('Handle HTTP request.'); + try { + $response = $next($request); + } finally { + SentryService::endSpan($span); + } return $response; } @@ -117,9 +117,11 @@ class Application extends Container implements LoggerAwareInterface { $this->boot(); - $request = Request::createFromGlobals(); - $response = $this->handleRequest($request); - $response->send(); + SentryService::measure('handle_request', 'http.handle', function () { + $request = Request::createFromGlobals(); + $response = $this->handleRequest($request); + $response->send(); + }); } public function withServiceProviders(array $serviceProviders): Application @@ -179,9 +181,10 @@ class Application extends Container implements LoggerAwareInterface */ private function loadEnvironmentVariables(): void { - if (!defined('BP_PHAR_BASE_PATH') || !defined('BP_REAL_BASE_PATH')) { + if (! defined('BP_PHAR_BASE_PATH') || ! defined('BP_REAL_BASE_PATH')) { $dotenv = Dotenv::createMutable(__DIR__ . '/../'); $dotenv->load(); + return; } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php index 91ee4cb..0ffdf28 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/ErrorHandler.php @@ -8,6 +8,7 @@ use Openguru\OpenCartFramework\Exceptions\ActionNotFoundException; use Openguru\OpenCartFramework\Exceptions\HttpNotFoundException; use Openguru\OpenCartFramework\Exceptions\InvalidApiTokenException; use Openguru\OpenCartFramework\Exceptions\NonLoggableExceptionInterface; +use Openguru\OpenCartFramework\Sentry\SentryService; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Psr\Log\LoggerInterface; @@ -110,6 +111,8 @@ class ErrorHandler public function handleShutdown(): void { + SentryService::finishTransaction(); + $error = error_get_last(); if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR], true)) { diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/QueryBuilder/Connections/MySqlConnection.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/QueryBuilder/Connections/MySqlConnection.php index c3f804e..3221e74 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/QueryBuilder/Connections/MySqlConnection.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/QueryBuilder/Connections/MySqlConnection.php @@ -2,9 +2,11 @@ namespace Openguru\OpenCartFramework\QueryBuilder\Connections; +use Openguru\OpenCartFramework\Sentry\SentryService; use Openguru\OpenCartFramework\Support\Utils; use PDO; use RuntimeException; +use Sentry\Tracing\SpanContext; class MySqlConnection implements ConnectionInterface { @@ -25,10 +27,23 @@ class MySqlConnection implements ConnectionInterface public function select(string $sql, array $bindings = []): array { - $statement = $this->pdo->prepare($sql); - $statement->execute($bindings); + $span = SentryService::startSpan($sql, 'db.mysql.query', function (SpanContext $context) use ($sql) { + $context->setData([ + 'db.system' => 'mysql', + 'db.statement' => $sql, + ]); + }); - return $statement->fetchAll(PDO::FETCH_ASSOC); + try { + $statement = $this->pdo->prepare($sql); + $statement->execute($bindings); + + $results = $statement->fetchAll(PDO::FETCH_ASSOC); + } finally { + SentryService::endSpan($span); + } + + return $results; } public function escape($value): string @@ -88,18 +103,31 @@ class MySqlConnection implements ConnectionInterface public function statement(string $sql, array $bindings = []): bool { - $statement = $this->pdo->prepare($sql); + $span = SentryService::startSpan($sql, 'db.mysql.query', function (SpanContext $context) use ($sql) { + $context->setData([ + 'db.system' => 'mysql', + 'db.statement' => $sql, + ]); + }); - if (! $statement) { - $this->lastError = $this->pdo->errorInfo(); - return false; - } + try { + $statement = $this->pdo->prepare($sql); - $success = $statement->execute($bindings); + if (! $statement) { + $this->lastError = $this->pdo->errorInfo(); - if (! $success) { - $this->lastError = $statement->errorInfo(); - return false; + return false; + } + + $success = $statement->execute($bindings); + + if (! $success) { + $this->lastError = $statement->errorInfo(); + + return false; + } + } finally { + SentryService::endSpan($span); } return $success; @@ -147,7 +175,7 @@ class MySqlConnection implements ConnectionInterface public function insert(string $table, array $data): bool { $placeholders = implode(',', array_fill(0, count($data), '?')); - $columns = implode(',', array_map(static fn ($key) => "`${key}`", array_keys($data))); + $columns = implode(',', array_map(static fn($key) => "`${key}`", array_keys($data))); $sql = sprintf('INSERT INTO `%s` (%s) VALUES (%s)', $table, $columns, $placeholders); return $this->statement($sql, array_values($data)); diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Sentry/SentryService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Sentry/SentryService.php new file mode 100644 index 0000000..70dde01 --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/framework/Sentry/SentryService.php @@ -0,0 +1,137 @@ +|null + */ + private static ?SplObjectStorage $spanParents = null; + + public static array $excludeActions = [ + 'health', + ]; + + private static function resolveOptionsFromEnv(): array + { + return [ + 'dsn' => env('SENTRY_DSN'), + 'send_default_pii' => true, + 'enable_logs' => (bool) filter_var(env('SENTRY_ENABLE_LOGS', false), FILTER_VALIDATE_BOOLEAN), + 'traces_sample_rate' => (float) env('SENTRY_TRACES_SAMPLE_RATE', 1.0), + ]; + } + + private static function isSentryEnabled(): bool + { + return self::$currentAction !== null + && ! in_array(self::$currentAction, self::$excludeActions, true) + && env('SENTRY_ENABLED', false); + } + + public static function init(?string $action): void + { + $options = self::resolveOptionsFromEnv(); + self::$currentAction = $action; + + if (! self::isSentryEnabled()) { + return; + } + + init($options); + + $transactionContext = TransactionContext::make() + ->setName(self::$currentAction) + ->setOp('http.server'); + + self::$currentTransaction = startTransaction($transactionContext); + SentrySdk::getCurrentHub()->setSpan(self::$currentTransaction); + } + + public static function startSpan(string $description, string $op, ?Closure $closure = null): ?Span + { + $parent = self::resolveParent(); + + if (! self::isSentryEnabled() || ! $parent) { + return null; + } + + $context = SpanContext::make() + ->setOp($op) + ->setDescription($description); + + if ($closure) { + $closure($context); + } + + $span = $parent->startChild($context); + + if (self::$spanParents === null) { + self::$spanParents = new SplObjectStorage(); + } + self::$spanParents[$span] = $parent; + + SentrySdk::getCurrentHub()->setSpan($span); + + return $span; + } + + public static function endSpan(?Span $span = null): void + { + if (! $span || ! self::isSentryEnabled()) { + return; + } + + $parent = null; + if (self::$spanParents !== null && self::$spanParents->contains($span)) { + $parent = self::$spanParents[$span] ?? null; + self::$spanParents->detach($span); + } + + $span->finish(); + SentrySdk::getCurrentHub()->setSpan($parent ?? self::$currentTransaction); + } + + public static function resolveParent(): ?Span + { + return SentrySdk::getCurrentHub()->getSpan() ?? self::$currentTransaction; + } + + public static function measure(string $description, string $op, Closure $closure, ?Closure $params = null): void + { + $span = self::startSpan($description, $op, $params); + + try { + $closure(); + } finally { + self::endSpan($span); + } + } + + public static function finishTransaction(): void + { + if (self::$currentTransaction) { + self::$currentTransaction->finish(); + self::$currentTransaction = null; + } + } +} diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php index c386091..5f40104 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Services/ProductsService.php @@ -16,6 +16,7 @@ use Openguru\OpenCartFramework\QueryBuilder\Builder; use Openguru\OpenCartFramework\QueryBuilder\JoinClause; use Openguru\OpenCartFramework\QueryBuilder\RawExpression; use Openguru\OpenCartFramework\QueryBuilder\Table; +use Openguru\OpenCartFramework\Sentry\SentryService; use Openguru\OpenCartFramework\Support\Arr; use Openguru\OpenCartFramework\Support\PaginationHelper; use Openguru\OpenCartFramework\Support\Str; @@ -157,6 +158,7 @@ class ProductsService ->get(); } + $span = SentryService::startSpan('crop_images', 'image.process'); $productsImagesMap = []; foreach ($productsImages as $item) { $productId = $item['product_id']; @@ -174,6 +176,8 @@ class ProductsService } } + SentryService::endSpan($span); + $debug = []; if (env('APP_DEBUG')) { $debug = [