diff --git a/composer.json b/composer.json index 86578ac95..f9c46a752 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "psr/http-factory-implementation": "*", "psy/psysh": "^0.10.0 || ^0.11.0", "ramsey/uuid": "^4.7", - "sentry/sentry": "^4.21.0", + "sentry/sentry": "^4.28.0", "symfony/console": "^5.3 || ^6.0 || ^7.0", "symfony/http-foundation": "^5.3 || ^6.0 || ^7.0", "symfony/polyfill-php84": "^1.33", diff --git a/src/sentry/class_map/RuntimeContextManager.php b/src/sentry/class_map/RuntimeContextManager.php new file mode 100644 index 000000000..8ddc2dd57 --- /dev/null +++ b/src/sentry/class_map/RuntimeContextManager.php @@ -0,0 +1,299 @@ +getClient()) { + $baseHub = make(HubInterface::class); + } + $this->baseHub = $baseHub; + $this->globalContext = null; + // Using ContextArrayObject here since the manager is designed to be used in a single-threaded execution environment and does not require the overhead of thread-safe structures. + $context = new ContextArrayObject(); + $this->activeContexts = $context; + $this->executionContextToRuntimeContext = $context; + } + + /** + * Sets the current hub with context-aware behavior. + * + * If a runtime context is active for the current execution key, the hub is + * updated only for that active context. Otherwise, the baseline/global hub + * template is updated. + * + * @return bool Whether the hub was set on an active runtime context + */ + public function setCurrentHub(HubInterface $hub): bool + { + $executionContextKey = $this->getExecutionContextKey(); + + if ($this->hasActiveContextForExecutionContextKey($executionContextKey)) { + $runtimeContextId = $this->executionContextToRuntimeContext[$executionContextKey]; + $this->activeContexts[$runtimeContextId]->setHub($hub); + + return true; + } + + $this->baseHub = $hub; + + if ($this->globalContext !== null) { + $this->globalContext->setHub($hub); + } + + return false; + } + + public function getCurrentHub(): HubInterface + { + return $this->getCurrentContext()->getHub(); + } + + public function getCurrentContext(): RuntimeContext + { + $executionContextKey = $this->getExecutionContextKey(); + + if ($this->hasActiveContextForExecutionContextKey($executionContextKey)) { + $runtimeContextId = $this->executionContextToRuntimeContext[$executionContextKey]; + + return $this->activeContexts[$runtimeContextId]; + } + + return $this->getGlobalContext(); + } + + public function hasActiveContext(): bool + { + return $this->hasActiveContextForExecutionContextKey($this->getExecutionContextKey()); + } + + /** + * Starts an isolated context for the current execution key. + */ + public function startContext(): void + { + $executionContextKey = $this->getExecutionContextKey(); + + if ($this->hasActiveContextForExecutionContextKey($executionContextKey)) { + // Nested start calls for the same execution key should be a no-op. + return; + } + + $this->createContextForExecutionContextKey($executionContextKey); + } + + /** + * Ends and flushes the active context for the current execution key. + * + * When no context is active for the key this is a no-op. + */ + public function endContext(?int $timeout = null): void + { + $executionContextKey = $this->getExecutionContextKey(); + + if (! $this->hasActiveContextForExecutionContextKey($executionContextKey)) { + return; + } + + $runtimeContextId = $this->executionContextToRuntimeContext[$executionContextKey]; + unset($this->executionContextToRuntimeContext[$executionContextKey]); + + $this->removeContextById($runtimeContextId, $timeout); + } + + private function createContextForExecutionContextKey(string $executionContextKey): void + { + $runtimeContextId = $this->generateRuntimeContextId(); + $runtimeContext = new RuntimeContext($runtimeContextId, $this->createHubFromBaseHub()); + + $this->activeContexts[$runtimeContextId] = $runtimeContext; + $this->executionContextToRuntimeContext[$executionContextKey] = $runtimeContextId; + } + + private function removeContextById(string $runtimeContextId, ?int $timeout = null): void + { + if (! isset($this->activeContexts[$runtimeContextId])) { + return; + } + + $runtimeContext = $this->activeContexts[$runtimeContextId]; + unset($this->activeContexts[$runtimeContextId]); + // Remove any key mappings that may still reference this context. + $this->removeExecutionContextMappingsForRuntimeContext($runtimeContextId); + + $logger = $this->getLoggerFromHub($runtimeContext->getHub()); + + $this->flushRuntimeContextResources($runtimeContext, $timeout, $logger); + } + + private function flushRuntimeContextResources(RuntimeContext $runtimeContext, ?int $timeout, LoggerInterface $logger): void + { + $hub = $runtimeContext->getHub(); + + // captureEvent can throw before transport send (for example from scope event processors + // or before_send callbacks), so we isolate failures and continue flushing other resources. + try { + $runtimeContext->getLogsAggregator()->flush($hub); + } catch (Throwable $exception) { + $logger->error('Failed to flush logs while ending a runtime context.', [ + 'exception' => $exception, + 'runtime_context_id' => $runtimeContext->getId(), + ]); + } + + // Keep metrics flush independent from logs flush so one bad callback does not block the rest. + try { + $runtimeContext->getMetricsAggregator()->flush($hub); + } catch (Throwable $exception) { + $logger->error('Failed to flush trace metrics while ending a runtime context.', [ + 'exception' => $exception, + 'runtime_context_id' => $runtimeContext->getId(), + ]); + } + + $client = $hub->getClient(); + + if ($client === null) { + return; + } + + // Custom transports may throw from close(); endContext must stay best-effort and non-fatal. + try { + $client->flush($timeout); + } catch (Throwable $exception) { + $logger->error('Failed to flush the client transport while ending a runtime context.', [ + 'exception' => $exception, + 'runtime_context_id' => $runtimeContext->getId(), + ]); + } + } + + private function removeExecutionContextMappingsForRuntimeContext(string $runtimeContextId): void + { + foreach ($this->executionContextToRuntimeContext as $executionContextKey => $mappedRuntimeContextId) { + if ($mappedRuntimeContextId === $runtimeContextId) { + unset($this->executionContextToRuntimeContext[$executionContextKey]); + } + } + } + + private function hasActiveContextForExecutionContextKey(string $executionContextKey): bool + { + if (! isset($this->executionContextToRuntimeContext[$executionContextKey])) { + return false; + } + + $runtimeContextId = $this->executionContextToRuntimeContext[$executionContextKey]; + + if (! isset($this->activeContexts[$runtimeContextId])) { + // Mapping points to a context that was already evicted/ended; drop the stale index entry. + unset($this->executionContextToRuntimeContext[$executionContextKey]); + + return false; + } + + return true; + } + + private function createHubFromBaseHub(): HubInterface + { + if (! $this->baseHub instanceof Hub) { + return new Hub($this->baseHub->getClient()); + } + + $clonedScope = null; + + $this->baseHub->configureScope(static function (Scope $scope) use (&$clonedScope): void { + $clonedScope = clone $scope; + // Do not inherit active traces into a new runtime context. + $clonedScope->setSpan(null); + $clonedScope->setPropagationContext(PropagationContext::fromDefaults()); + }); + + return new Hub($this->baseHub->getClient(), $clonedScope ?? new Scope()); + } + + private function getLoggerFromHub(HubInterface $hub): LoggerInterface + { + $client = $hub->getClient(); + + if ($client === null) { + return new NullLogger(); + } + + return $client->getOptions()->getLoggerOrNullLogger(); + } + + private function generateRuntimeContextId(): string + { + return \sprintf('%s-%d', str_replace('.', '', uniqid('', true)), mt_rand()); + } + + private function getExecutionContextKey(): string + { + // All supported runtime modes currently use a process-local execution key. + return self::PROCESS_EXECUTION_CONTEXT_KEY; + } + + private function getGlobalContext(): RuntimeContext + { + if ($this->globalContext === null) { + // Lazy fallback keeps baseline behavior when users do not opt into explicit context lifecycle. + $this->globalContext = new RuntimeContext('global', $this->baseHub); + } + + return $this->globalContext; + } +} diff --git a/src/sentry/composer.json b/src/sentry/composer.json index 6ff6596e9..7ca409df3 100644 --- a/src/sentry/composer.json +++ b/src/sentry/composer.json @@ -24,6 +24,7 @@ "require": { "friendsofhyperf/support": "~3.1.61", "hyperf/command": "~3.1.0", + "hyperf/coordinator": "~3.1.70", "hyperf/context": "~3.1.0", "hyperf/coroutine": "~3.1.0", "hyperf/di": "~3.1.0", @@ -33,7 +34,7 @@ "hyperf/http-server": "~3.1.0", "hyperf/support": "~3.1.0", "hyperf/tappable": "~3.1.0", - "sentry/sentry": "^4.21.0", + "sentry/sentry": "^4.28.0", "symfony/polyfill-php85": "^1.33" }, "suggest": { diff --git a/src/sentry/src/Aspect/CoroutineAspect.php b/src/sentry/src/Aspect/CoroutineAspect.php index 151c2cb70..c736a7111 100644 --- a/src/sentry/src/Aspect/CoroutineAspect.php +++ b/src/sentry/src/Aspect/CoroutineAspect.php @@ -58,10 +58,10 @@ protected function handleCreate(ProceedingJoinPoint $proceedingJoinPoint): void Context::getOrSet($key, fn () => Context::get($key, coroutineId: $cid)); } - // Defer the flushing of events until the coroutine completes. - defer(fn () => SentrySdk::flush()); + SentrySdk::startContext(); + + defer(fn () => SentrySdk::endContext()); - // Continue the callable in the new Coroutine. $callable(); }; } diff --git a/src/sentry/src/Aspect/SentrySdkAspect.php b/src/sentry/src/Aspect/SentrySdkAspect.php deleted file mode 100644 index 6954b1635..000000000 --- a/src/sentry/src/Aspect/SentrySdkAspect.php +++ /dev/null @@ -1,72 +0,0 @@ -methodName) { - 'init' => $this->handleInit($proceedingJoinPoint), - 'setCurrentHub' => $this->handleSetCurrentHub($proceedingJoinPoint), - 'getRuntimeContextManager' => $this->handleGetRuntimeContextManager($proceedingJoinPoint), - default => $proceedingJoinPoint->process(), - }; - } - - private function handleInit(ProceedingJoinPoint $proceedingJoinPoint) - { - Context::set( - RuntimeContextManager::class, - new RuntimeContextManager(make(HubInterface::class)) - ); - - return SentrySdk::getCurrentHub(); - } - - private function handleSetCurrentHub(ProceedingJoinPoint $proceedingJoinPoint) - { - $arguments = $proceedingJoinPoint->arguments['keys'] ?? []; - $hub = $arguments['hub']; - // @phpstan-ignore-next-line - Closure::bind(fn () => static::getRuntimeContextManager()->setCurrentHub($hub), null, SentrySdk::class)(); - - return $hub; - } - - private function handleGetRuntimeContextManager(ProceedingJoinPoint $proceedingJoinPoint) - { - return Context::getOrSet( - RuntimeContextManager::class, - fn () => new RuntimeContextManager(make(HubInterface::class)) - ); - } -} diff --git a/src/sentry/src/Aspect/SingletonAspect.php b/src/sentry/src/Aspect/SingletonAspect.php deleted file mode 100644 index a04e63183..000000000 --- a/src/sentry/src/Aspect/SingletonAspect.php +++ /dev/null @@ -1,73 +0,0 @@ -className; - $arguments = $proceedingJoinPoint->getArguments(); - - if (array_key_exists(0, $arguments)) { - $key .= '#' . $arguments[0]; - } - - return match ($className) { - // Singleton Classes - // \Sentry\State\HubAdapter::class, - \Sentry\Logs\Logs::class => Context::getOrSet($key, function () use ($className) { - return Closure::bind(fn () => new $className(), null, $className)(); - }), - \Sentry\Metrics\TraceMetrics::class => Context::getOrSet($key, function () use ($className) { - return new $className(); - }), - - // !!! Don't enable this for now, it may cause some unexpected issues !!! - // \Sentry\Integration\IntegrationRegistry::class => $proceedingJoinPoint->process(), - - // Enums - // \Sentry\CheckInStatus::class, - // \Sentry\EventType::class, - // \Sentry\MonitorScheduleUnit::class, - // \Sentry\Logs\LogLevel::class, - // \Sentry\Tracing\SpanStatus::class, - // \Sentry\Tracing\TransactionSource::class, - // \Sentry\Transport\ResultStatus::class, - // \Sentry\Unit::class => $proceedingJoinPoint->process(), - default => $proceedingJoinPoint->process(), - }; - } -} diff --git a/src/sentry/src/ConfigProvider.php b/src/sentry/src/ConfigProvider.php index a1be9bed8..ef5999564 100644 --- a/src/sentry/src/ConfigProvider.php +++ b/src/sentry/src/ConfigProvider.php @@ -27,14 +27,11 @@ public function __invoke(): array Aspect\GuzzleHttpClientAspect::class, Aspect\LoggerAspect::class, Aspect\RedisAspect::class, - // Aspect\SingletonAspect::class, - Aspect\SentrySdkAspect::class, Metrics\Aspect\CounterAspect::class, Metrics\Aspect\HistogramAspect::class, Tracing\Aspect\AmqpProducerAspect::class, Tracing\Aspect\AsyncQueueJobMessageAspect::class, Tracing\Aspect\CacheAspect::class, - Tracing\Aspect\CoordinatorAspect::class, Tracing\Aspect\CoroutineAspect::class, Tracing\Aspect\DbAspect::class, Tracing\Aspect\DbConnectionAspect::class, @@ -76,7 +73,9 @@ public function __invoke(): array ], 'annotations' => [ 'scan' => [ - 'class_map' => [], + 'class_map' => [ + \Sentry\State\RuntimeContextManager::class => __DIR__ . '/../class_map/RuntimeContextManager.php', + ], ], ], 'publish' => [ diff --git a/src/sentry/src/Metrics/Listener/OnBeforeHandle.php b/src/sentry/src/Metrics/Listener/OnBeforeHandle.php index 74940eea5..bd28da0cc 100644 --- a/src/sentry/src/Metrics/Listener/OnBeforeHandle.php +++ b/src/sentry/src/Metrics/Listener/OnBeforeHandle.php @@ -21,7 +21,6 @@ use Hyperf\Event\Contract\ListenerInterface; use Psr\Container\ContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface; -use Sentry\SentrySdk; use Sentry\Unit; use function FriendsOfHyperf\Sentry\metrics; @@ -114,8 +113,6 @@ function () use ($metrics) { ['worker' => '0'], Unit::megabyte() ); - - SentrySdk::flush(); } ); } diff --git a/src/sentry/src/Metrics/Listener/OnCoroutineServerStart.php b/src/sentry/src/Metrics/Listener/OnCoroutineServerStart.php index e7b28db88..9985eb36b 100644 --- a/src/sentry/src/Metrics/Listener/OnCoroutineServerStart.php +++ b/src/sentry/src/Metrics/Listener/OnCoroutineServerStart.php @@ -19,7 +19,6 @@ use Hyperf\Server\Event\MainCoroutineServerStart; use Psr\Container\ContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface; -use Sentry\SentrySdk; use Sentry\Unit; use function FriendsOfHyperf\Sentry\metrics; @@ -112,8 +111,6 @@ function () use ($metrics) { ['worker' => '0'], Unit::megabyte() ); - - SentrySdk::flush(); } ); } diff --git a/src/sentry/src/Metrics/Listener/OnMetricFactoryReady.php b/src/sentry/src/Metrics/Listener/OnMetricFactoryReady.php index 247e080eb..b2066e07d 100644 --- a/src/sentry/src/Metrics/Listener/OnMetricFactoryReady.php +++ b/src/sentry/src/Metrics/Listener/OnMetricFactoryReady.php @@ -21,7 +21,6 @@ use Hyperf\Event\Contract\ListenerInterface; use Hyperf\Support\System; use Psr\Container\ContainerInterface; -use Sentry\SentrySdk; use Sentry\Unit; use Swoole\Server as SwooleServer; @@ -129,8 +128,6 @@ function () use ($metrics, $serverStatsFactory, $workerId) { ['worker' => (string) $workerId], Unit::megabyte() ); - - SentrySdk::flush(); } ); } diff --git a/src/sentry/src/Metrics/Listener/OnWorkerStart.php b/src/sentry/src/Metrics/Listener/OnWorkerStart.php index 3a2638c6f..613585a0d 100644 --- a/src/sentry/src/Metrics/Listener/OnWorkerStart.php +++ b/src/sentry/src/Metrics/Listener/OnWorkerStart.php @@ -19,7 +19,6 @@ use Hyperf\Framework\Event\BeforeWorkerStart; use Psr\Container\ContainerInterface; use Psr\EventDispatcher\EventDispatcherInterface; -use Sentry\SentrySdk; use Sentry\Unit; use Swoole\Server; @@ -119,8 +118,6 @@ function () use ($metrics, $event) { ['worker' => (string) ($event->workerId ?? 0)], Unit::megabyte() ); - - SentrySdk::flush(); } ); } diff --git a/src/sentry/src/Metrics/Listener/PoolWatcher.php b/src/sentry/src/Metrics/Listener/PoolWatcher.php index 1c651cbaf..52ccfb96f 100644 --- a/src/sentry/src/Metrics/Listener/PoolWatcher.php +++ b/src/sentry/src/Metrics/Listener/PoolWatcher.php @@ -18,7 +18,6 @@ use Hyperf\Pool\Pool; use Hyperf\Server\Event\MainCoroutineServerStart; use Psr\Container\ContainerInterface; -use Sentry\SentrySdk; use function FriendsOfHyperf\Sentry\metrics; @@ -89,8 +88,6 @@ function () use ($pool, $workerId, $poolName) { 'worker' => (string) $workerId, ] ); - - SentrySdk::flush(); } ); } diff --git a/src/sentry/src/Metrics/Listener/QueueWatcher.php b/src/sentry/src/Metrics/Listener/QueueWatcher.php index 70c93ec5e..5f7a0d682 100644 --- a/src/sentry/src/Metrics/Listener/QueueWatcher.php +++ b/src/sentry/src/Metrics/Listener/QueueWatcher.php @@ -18,7 +18,6 @@ use Hyperf\Coordinator\Timer; use Hyperf\Event\Contract\ListenerInterface; use Psr\Container\ContainerInterface; -use Sentry\SentrySdk; use function FriendsOfHyperf\Sentry\metrics; @@ -83,8 +82,6 @@ function () { ['queue' => $name] ); } - - SentrySdk::flush(); } ); } diff --git a/src/sentry/src/Tracing/Aspect/CoordinatorAspect.php b/src/sentry/src/Tracing/Aspect/CoordinatorAspect.php index 8f58d5390..f81dfeaac 100644 --- a/src/sentry/src/Tracing/Aspect/CoordinatorAspect.php +++ b/src/sentry/src/Tracing/Aspect/CoordinatorAspect.php @@ -32,10 +32,6 @@ public function __construct(protected Feature $feature) public function process(ProceedingJoinPoint $proceedingJoinPoint) { - if (! $this->feature->isTracingSpanEnabled('coordinator')) { - return $proceedingJoinPoint->process(); - } - $timeout = $proceedingJoinPoint->arguments['keys']['timeout'] ?? -1; return trace( diff --git a/src/sentry/src/Tracing/Aspect/CoroutineAspect.php b/src/sentry/src/Tracing/Aspect/CoroutineAspect.php index 7e3471826..07665d281 100644 --- a/src/sentry/src/Tracing/Aspect/CoroutineAspect.php +++ b/src/sentry/src/Tracing/Aspect/CoroutineAspect.php @@ -27,6 +27,9 @@ use function Hyperf\Coroutine\defer; use function Sentry\continueTrace; +/** + * Run after FriendsOfHyperf\Sentry\Aspect\CoroutineAspect. + */ class CoroutineAspect extends AbstractAspect { public const CONTEXT_KEYS = [ @@ -70,6 +73,8 @@ function (Scope $scope) use ($proceedingJoinPoint, $callingOnFunction) { Context::getOrSet($key, fn () => Context::get($key, coroutineId: $cid)); } + SentrySdk::startContext(); + // Start a new transaction for the coroutine preparation phase. $transaction = startTransaction( continueTrace($span->toTraceparent(), $span->toBaggage()) @@ -82,7 +87,7 @@ function (Scope $scope) use ($proceedingJoinPoint, $callingOnFunction) { // Defer the finishing of the transaction and flushing of events until the coroutine completes. defer(function () use ($transaction) { $transaction->finish(); - SentrySdk::flush(); + SentrySdk::endContext(); }); return trace( diff --git a/src/sentry/src/Tracing/Listener/EventHandleListener.php b/src/sentry/src/Tracing/Listener/EventHandleListener.php index 2d10cfe06..4dcb28fae 100644 --- a/src/sentry/src/Tracing/Listener/EventHandleListener.php +++ b/src/sentry/src/Tracing/Listener/EventHandleListener.php @@ -452,8 +452,6 @@ protected function handleCommandFinished(CommandEvent\AfterExecute $event): void $parentSpan->finish(); SentrySdk::getCurrentHub()->popScope(); } - - SentrySdk::flush(); } } diff --git a/src/sentry/src/Util/ContextArrayObject.php b/src/sentry/src/Util/ContextArrayObject.php new file mode 100644 index 000000000..634f50653 --- /dev/null +++ b/src/sentry/src/Util/ContextArrayObject.php @@ -0,0 +1,57 @@ +getArrayObject()[$offset]); + } + + public function offsetGet($offset): mixed + { + return $this->getArrayObject()[$offset] ?? null; + } + + public function offsetSet($offset, $value): void + { + $this->getArrayObject()[$offset] = $value; + } + + public function offsetUnset($offset): void + { + unset($this->getArrayObject()[$offset]); + } + + public function count(): int + { + return count($this->getArrayObject()); + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->getArrayObject() ?? []); + } + + private function getArrayObject(?int $id = null): ?ArrayObject + { + return \Hyperf\Engine\Coroutine::getContextFor($id); + } +} diff --git a/src/sentry/src/Util/CoroutineBacktraceHelper.php b/src/sentry/src/Util/CoroutineBacktraceHelper.php index f523fe30a..acdc2e46b 100644 --- a/src/sentry/src/Util/CoroutineBacktraceHelper.php +++ b/src/sentry/src/Util/CoroutineBacktraceHelper.php @@ -29,6 +29,10 @@ class CoroutineBacktraceHelper 'Multiplex\Socket\Client->loop', 'Multiplex\Socket\Client->heartbeat', 'FriendsOfHyperf\Sentry\HttpClient\HttpClient->loop', 'FriendsOfHyperf\Sentry\Transport\CoHttpTransport->loop', + 'FriendsOfHyperf\Sentry\Transport\CoHttpTransport->FriendsOfHyperf\Sentry\Transport\{closure}', + 'FriendsOfHyperf\Sentry\Metrics\Listener\OnBeforeHandle->process', + 'FriendsOfHyperf\Sentry\Metrics\Listener\OnMetricFactoryReady->process', + 'FriendsOfHyperf\Sentry\Metrics\Listener\QueueWatcher->process', 'Hyperf\Kafka\Producer->loop', 'Hyperf\Metric\Listener\OnMetricFactoryReady->process', 'Hyperf\Metric\Listener\QueueWatcher->process',