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_PORT=6379
|
||||
#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_PORT=6379
|
||||
TELECART_REDIS_DATABASE=0
|
||||
|
||||
SENTRY_ENABLED=false
|
||||
SENTRY_DSN=
|
||||
SENTRY_ENABLE_LOGS=false
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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\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 = [
|
||||
|
||||
Reference in New Issue
Block a user