feat(sentry): Implement Sentry tracing (#50)
This commit is contained in:
@@ -6,3 +6,7 @@ TELECART_CACHE_DRIVER=redis
|
|||||||
#TELECART_REDIS_HOST=redis
|
#TELECART_REDIS_HOST=redis
|
||||||
#TELECART_REDIS_PORT=6379
|
#TELECART_REDIS_PORT=6379
|
||||||
#TELECART_REDIS_DATABASE=0
|
#TELECART_REDIS_DATABASE=0
|
||||||
|
|
||||||
|
SENTRY_ENABLED=false
|
||||||
|
SENTRY_DSN=
|
||||||
|
SENTRY_ENABLE_LOGS=false
|
||||||
|
|||||||
@@ -5,3 +5,7 @@ TELECART_CACHE_DRIVER=mysql
|
|||||||
TELECART_REDIS_HOST=redis
|
TELECART_REDIS_HOST=redis
|
||||||
TELECART_REDIS_PORT=6379
|
TELECART_REDIS_PORT=6379
|
||||||
TELECART_REDIS_DATABASE=0
|
TELECART_REDIS_DATABASE=0
|
||||||
|
|
||||||
|
SENTRY_ENABLED=false
|
||||||
|
SENTRY_DSN=
|
||||||
|
SENTRY_ENABLE_LOGS=false
|
||||||
|
|||||||
@@ -36,7 +36,8 @@
|
|||||||
"ramsey/uuid": "^4.2",
|
"ramsey/uuid": "^4.2",
|
||||||
"symfony/http-foundation": "^5.4",
|
"symfony/http-foundation": "^5.4",
|
||||||
"symfony/console": "^5.4",
|
"symfony/console": "^5.4",
|
||||||
"dragonmantank/cron-expression": "^3.5"
|
"dragonmantank/cron-expression": "^3.5",
|
||||||
|
"sentry/sentry": "^4.19"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"doctrine/sql-formatter": "^1.3",
|
"doctrine/sql-formatter": "^1.3",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use Openguru\OpenCartFramework\MaintenanceTasks\MaintenanceTasksService;
|
|||||||
use Openguru\OpenCartFramework\MaintenanceTasks\MaintenanceTasksServiceProvider;
|
use Openguru\OpenCartFramework\MaintenanceTasks\MaintenanceTasksServiceProvider;
|
||||||
use Openguru\OpenCartFramework\Migrations\MigrationsServiceProvider;
|
use Openguru\OpenCartFramework\Migrations\MigrationsServiceProvider;
|
||||||
use Openguru\OpenCartFramework\Router\Router;
|
use Openguru\OpenCartFramework\Router\Router;
|
||||||
use Openguru\OpenCartFramework\Support\ExecutionTimeProfiler;
|
use Openguru\OpenCartFramework\Sentry\SentryService;
|
||||||
use Psr\Log\LoggerAwareInterface;
|
use Psr\Log\LoggerAwareInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
@@ -27,11 +27,6 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
|
|
||||||
private LoggerInterface $logger;
|
private LoggerInterface $logger;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var ExecutionTimeProfiler
|
|
||||||
*/
|
|
||||||
private ExecutionTimeProfiler $profiler;
|
|
||||||
|
|
||||||
public static function getInstance(): Application
|
public static function getInstance(): Application
|
||||||
{
|
{
|
||||||
return static::$instance;
|
return static::$instance;
|
||||||
@@ -39,13 +34,6 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
|
|
||||||
private function bootKernelServices(): void
|
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) {
|
$this->singleton(Container::class, function (Container $container) {
|
||||||
return $container;
|
return $container;
|
||||||
});
|
});
|
||||||
@@ -56,8 +44,6 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
return new Settings($container->getConfigValue());
|
return new Settings($container->getConfigValue());
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->loadEnvironmentVariables();
|
|
||||||
|
|
||||||
$errorHandler = new ErrorHandler(
|
$errorHandler = new ErrorHandler(
|
||||||
$this->get(LoggerInterface::class),
|
$this->get(LoggerInterface::class),
|
||||||
$this,
|
$this,
|
||||||
@@ -68,16 +54,23 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
|
|
||||||
public function boot(): Application
|
public function boot(): Application
|
||||||
{
|
{
|
||||||
|
$this->loadEnvironmentVariables();
|
||||||
|
|
||||||
|
$action = $_GET['api_action'] ?? null;
|
||||||
|
|
||||||
|
SentryService::init($action);
|
||||||
|
|
||||||
|
SentryService::measure('boot_kernel_services', 'app.boot', function () {
|
||||||
$this->bootKernelServices();
|
$this->bootKernelServices();
|
||||||
|
});
|
||||||
|
|
||||||
|
SentryService::measure('dependency_registration', 'app.boot', function () {
|
||||||
$this->initializeEventDispatcher(static::$events);
|
$this->initializeEventDispatcher(static::$events);
|
||||||
|
|
||||||
DependencyRegistration::register($this, $this->serviceProviders);
|
DependencyRegistration::register($this, $this->serviceProviders);
|
||||||
|
});
|
||||||
|
|
||||||
static::$instance = $this;
|
static::$instance = $this;
|
||||||
|
|
||||||
$this->profiler->addCheckpoint('Bootstrap Application');
|
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,18 +90,25 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
throw new InvalidArgumentException('Invalid action: ' . $controller . '->' . $method);
|
throw new InvalidArgumentException('Invalid action: ' . $controller . '->' . $method);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->profiler->addCheckpoint('Handle Middlewares.');
|
$span = SentryService::startSpan('handle_middlewares', 'http.handle');
|
||||||
|
|
||||||
|
try {
|
||||||
$next = fn($req) => $this->call($controller, $method);
|
$next = fn($req) => $this->call($controller, $method);
|
||||||
|
|
||||||
foreach (array_reverse($this->middlewareStack) as $class) {
|
foreach (array_reverse($this->middlewareStack) as $class) {
|
||||||
$instance = $this->get($class);
|
$instance = $this->get($class);
|
||||||
$next = static fn($req) => $instance->handle($req, $next);
|
$next = static fn($req) => $instance->handle($req, $next);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
SentryService::endSpan($span);
|
||||||
|
}
|
||||||
|
|
||||||
|
$span = SentryService::startSpan('handle_controller', 'http.handle');
|
||||||
|
|
||||||
|
try {
|
||||||
$response = $next($request);
|
$response = $next($request);
|
||||||
|
} finally {
|
||||||
$this->profiler->addCheckpoint('Handle HTTP request.');
|
SentryService::endSpan($span);
|
||||||
|
}
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
@@ -117,9 +117,11 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
{
|
{
|
||||||
$this->boot();
|
$this->boot();
|
||||||
|
|
||||||
|
SentryService::measure('handle_request', 'http.handle', function () {
|
||||||
$request = Request::createFromGlobals();
|
$request = Request::createFromGlobals();
|
||||||
$response = $this->handleRequest($request);
|
$response = $this->handleRequest($request);
|
||||||
$response->send();
|
$response->send();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function withServiceProviders(array $serviceProviders): Application
|
public function withServiceProviders(array $serviceProviders): Application
|
||||||
@@ -182,6 +184,7 @@ class Application extends Container implements LoggerAwareInterface
|
|||||||
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 = Dotenv::createMutable(__DIR__ . '/../');
|
||||||
$dotenv->load();
|
$dotenv->load();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use Openguru\OpenCartFramework\Exceptions\ActionNotFoundException;
|
|||||||
use Openguru\OpenCartFramework\Exceptions\HttpNotFoundException;
|
use Openguru\OpenCartFramework\Exceptions\HttpNotFoundException;
|
||||||
use Openguru\OpenCartFramework\Exceptions\InvalidApiTokenException;
|
use Openguru\OpenCartFramework\Exceptions\InvalidApiTokenException;
|
||||||
use Openguru\OpenCartFramework\Exceptions\NonLoggableExceptionInterface;
|
use Openguru\OpenCartFramework\Exceptions\NonLoggableExceptionInterface;
|
||||||
|
use Openguru\OpenCartFramework\Sentry\SentryService;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@@ -110,6 +111,8 @@ class ErrorHandler
|
|||||||
|
|
||||||
public function handleShutdown(): void
|
public function handleShutdown(): void
|
||||||
{
|
{
|
||||||
|
SentryService::finishTransaction();
|
||||||
|
|
||||||
$error = error_get_last();
|
$error = error_get_last();
|
||||||
|
|
||||||
if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
|
if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR], true)) {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
namespace Openguru\OpenCartFramework\QueryBuilder\Connections;
|
namespace Openguru\OpenCartFramework\QueryBuilder\Connections;
|
||||||
|
|
||||||
|
use Openguru\OpenCartFramework\Sentry\SentryService;
|
||||||
use Openguru\OpenCartFramework\Support\Utils;
|
use Openguru\OpenCartFramework\Support\Utils;
|
||||||
use PDO;
|
use PDO;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
use Sentry\Tracing\SpanContext;
|
||||||
|
|
||||||
class MySqlConnection implements ConnectionInterface
|
class MySqlConnection implements ConnectionInterface
|
||||||
{
|
{
|
||||||
@@ -25,10 +27,23 @@ class MySqlConnection implements ConnectionInterface
|
|||||||
|
|
||||||
public function select(string $sql, array $bindings = []): array
|
public function select(string $sql, array $bindings = []): array
|
||||||
{
|
{
|
||||||
|
$span = SentryService::startSpan($sql, 'db.mysql.query', function (SpanContext $context) use ($sql) {
|
||||||
|
$context->setData([
|
||||||
|
'db.system' => 'mysql',
|
||||||
|
'db.statement' => $sql,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
$statement = $this->pdo->prepare($sql);
|
$statement = $this->pdo->prepare($sql);
|
||||||
$statement->execute($bindings);
|
$statement->execute($bindings);
|
||||||
|
|
||||||
return $statement->fetchAll(PDO::FETCH_ASSOC);
|
$results = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} finally {
|
||||||
|
SentryService::endSpan($span);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function escape($value): string
|
public function escape($value): string
|
||||||
@@ -88,10 +103,19 @@ class MySqlConnection implements ConnectionInterface
|
|||||||
|
|
||||||
public function statement(string $sql, array $bindings = []): bool
|
public function statement(string $sql, array $bindings = []): bool
|
||||||
{
|
{
|
||||||
|
$span = SentryService::startSpan($sql, 'db.mysql.query', function (SpanContext $context) use ($sql) {
|
||||||
|
$context->setData([
|
||||||
|
'db.system' => 'mysql',
|
||||||
|
'db.statement' => $sql,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
$statement = $this->pdo->prepare($sql);
|
$statement = $this->pdo->prepare($sql);
|
||||||
|
|
||||||
if (! $statement) {
|
if (! $statement) {
|
||||||
$this->lastError = $this->pdo->errorInfo();
|
$this->lastError = $this->pdo->errorInfo();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,8 +123,12 @@ class MySqlConnection implements ConnectionInterface
|
|||||||
|
|
||||||
if (! $success) {
|
if (! $success) {
|
||||||
$this->lastError = $statement->errorInfo();
|
$this->lastError = $statement->errorInfo();
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
SentryService::endSpan($span);
|
||||||
|
}
|
||||||
|
|
||||||
return $success;
|
return $success;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Openguru\OpenCartFramework\Sentry;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Sentry\SentrySdk;
|
||||||
|
use Sentry\Tracing\Span;
|
||||||
|
use Sentry\Tracing\SpanContext;
|
||||||
|
use Sentry\Tracing\Transaction;
|
||||||
|
use Sentry\Tracing\TransactionContext;
|
||||||
|
use SplObjectStorage;
|
||||||
|
|
||||||
|
use function Sentry\init;
|
||||||
|
use function Sentry\startTransaction;
|
||||||
|
|
||||||
|
class SentryService
|
||||||
|
{
|
||||||
|
private static ?string $currentAction = null;
|
||||||
|
public static ?Transaction $currentTransaction = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores parent span for each created child span to correctly restore nesting.
|
||||||
|
*
|
||||||
|
* @var SplObjectStorage<Span, Span|null>|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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ use Openguru\OpenCartFramework\QueryBuilder\Builder;
|
|||||||
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;
|
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;
|
||||||
use Openguru\OpenCartFramework\QueryBuilder\Table;
|
use Openguru\OpenCartFramework\QueryBuilder\Table;
|
||||||
|
use Openguru\OpenCartFramework\Sentry\SentryService;
|
||||||
use Openguru\OpenCartFramework\Support\Arr;
|
use Openguru\OpenCartFramework\Support\Arr;
|
||||||
use Openguru\OpenCartFramework\Support\PaginationHelper;
|
use Openguru\OpenCartFramework\Support\PaginationHelper;
|
||||||
use Openguru\OpenCartFramework\Support\Str;
|
use Openguru\OpenCartFramework\Support\Str;
|
||||||
@@ -157,6 +158,7 @@ class ProductsService
|
|||||||
->get();
|
->get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$span = SentryService::startSpan('crop_images', 'image.process');
|
||||||
$productsImagesMap = [];
|
$productsImagesMap = [];
|
||||||
foreach ($productsImages as $item) {
|
foreach ($productsImages as $item) {
|
||||||
$productId = $item['product_id'];
|
$productId = $item['product_id'];
|
||||||
@@ -174,6 +176,8 @@ class ProductsService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SentryService::endSpan($span);
|
||||||
|
|
||||||
$debug = [];
|
$debug = [];
|
||||||
if (env('APP_DEBUG')) {
|
if (env('APP_DEBUG')) {
|
||||||
$debug = [
|
$debug = [
|
||||||
|
|||||||
Reference in New Issue
Block a user