diff --git a/composer.json b/composer.json index 7a93a9e..3720a95 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "support": { "issues": "https://github.com/friendsofhyperf/components/issues", "source": "https://github.com/friendsofhyperf/components", - "docs": "https://hyperf.fans", + "docs": "https://docs.hdj.me", "pull-request": "https://github.com/friendsofhyperf/components/pulls" }, "require": { @@ -32,7 +32,7 @@ "hyperf/http-server": "~3.2.0", "hyperf/support": "~3.2.0", "hyperf/tappable": "~3.2.0", - "sentry/sentry": "^4.18.0", + "sentry/sentry": "^4.19.0", "symfony/polyfill-php85": "^1.33" }, "suggest": { diff --git a/publish/sentry.php b/publish/sentry.php index b29c88d..b9e47c5 100644 --- a/publish/sentry.php +++ b/publish/sentry.php @@ -40,6 +40,12 @@ // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#enable_logs 'enable_logs' => env('SENTRY_ENABLE_LOGS', true), + // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#enable_metrics + 'enable_metrics' => env('SENTRY_ENABLE_METRICS', true), + 'enable_default_metrics' => env('SENTRY_ENABLE_DEFAULT_METRICS', true), + 'enable_command_metrics' => env('SENTRY_ENABLE_COMMAND_METRICS', true), + 'metrics_interval' => (int) env('SENTRY_METRICS_INTERVAL', 10), + 'logs_channel_level' => env('SENTRY_LOGS_CHANNEL_LEVEL', Sentry\Logs\LogLevel::debug()), // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send_default_pii diff --git a/src/Aspect/SingletonAspect.php b/src/Aspect/SingletonAspect.php index bcd5594..5c0aec6 100644 --- a/src/Aspect/SingletonAspect.php +++ b/src/Aspect/SingletonAspect.php @@ -22,10 +22,13 @@ class SingletonAspect extends AbstractAspect \Sentry\EventType::class . '::getInstance', \Sentry\MonitorScheduleUnit::class . '::getInstance', \Sentry\Integration\IntegrationRegistry::class . '::getInstance', + \Sentry\Logs\LogLevel::class . '::getInstance', + \Sentry\Metrics\TraceMetrics::class . '::getInstance', \Sentry\State\HubAdapter::class . '::getInstance', \Sentry\Tracing\SpanStatus::class . '::getInstance', \Sentry\Tracing\TransactionSource::class . '::getInstance', \Sentry\Transport\ResultStatus::class . '::getInstance', + \Sentry\Unit::class . '::getInstance', ]; public function process(ProceedingJoinPoint $proceedingJoinPoint) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index e00948e..58923ad 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -26,12 +26,15 @@ public function __invoke(): array Aspect\GuzzleHttpClientAspect::class, Aspect\LoggerAspect::class, Aspect\SingletonAspect::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, Tracing\Aspect\ElasticsearchAspect::class, Tracing\Aspect\ElasticsearchRequestAspect::class, Tracing\Aspect\FilesystemAspect::class, @@ -40,6 +43,7 @@ public function __invoke(): array Tracing\Aspect\KafkaProducerAspect::class, Tracing\Aspect\RpcAspect::class, Tracing\Aspect\RpcEndpointAspect::class, + Tracing\Aspect\RedisConnectionAspect::class, Tracing\Aspect\TraceAnnotationAspect::class, Tracing\Aspect\ViewRenderAspect::class, ], @@ -56,6 +60,14 @@ public function __invoke(): array Listener\SetupSentryListener::class, Listener\EventHandleListener::class => PHP_INT_MAX - 1, Crons\Listener\EventHandleListener::class => PHP_INT_MAX - 1, + Metrics\Listener\DBPoolWatcher::class, + Metrics\Listener\OnBeforeHandle::class, + Metrics\Listener\OnCoroutineServerStart::class, + Metrics\Listener\OnMetricFactoryReady::class, + Metrics\Listener\OnWorkerStart::class, + Metrics\Listener\QueueWatcher::class, + Metrics\Listener\RedisPoolWatcher::class, + Metrics\Listener\RequestWatcher::class, Tracing\Listener\EventHandleListener::class => PHP_INT_MAX, // !! Make sure it is the first one to handle the event ], 'annotations' => [ diff --git a/src/Constants.php b/src/Constants.php index a2d9373..87c4991 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -15,19 +15,11 @@ class Constants { public const TRACE_CARRIER = 'sentry.tracing.trace_carrier'; - public const TRACE_RPC_SERVER_ADDRESS = 'sentry.tracing.rpc.server.address'; - - public const TRACE_RPC_SERVER_PORT = 'sentry.tracing.rpc.server.port'; - - public const TRACE_ELASTICSEARCH_REQUEST_DATA = 'sentry.tracing.elasticsearch.request.data'; - - public const CRON_CHECKIN_ID = 'sentry.crons.checkin_id'; - - public const DISABLE_COROUTINE_TRACING = 'sentry.tracing.disable_coroutine_tracing'; - public const SENTRY_TRACE = 'sentry-trace'; public const BAGGAGE = 'baggage'; public const TRACEPARENT = 'traceparent'; + + public static bool $runningInCommand = false; } diff --git a/src/Crons/Listener/EventHandleListener.php b/src/Crons/Listener/EventHandleListener.php index a35f691..c083e8d 100644 --- a/src/Crons/Listener/EventHandleListener.php +++ b/src/Crons/Listener/EventHandleListener.php @@ -11,9 +11,8 @@ namespace FriendsOfHyperf\Sentry\Crons\Listener; -use FriendsOfHyperf\Sentry\Constants; use FriendsOfHyperf\Sentry\Feature; -use Hyperf\Context\Context; +use FriendsOfHyperf\Sentry\SentryContext; use Hyperf\Contract\ConfigInterface; use Hyperf\Contract\StdoutLoggerInterface; use Hyperf\Crontab\Event; @@ -90,13 +89,13 @@ protected function handleCrontabTaskStarting(Event\BeforeExecute $event, array $ monitorConfig: $monitorConfig, ); - Context::set(Constants::CRON_CHECKIN_ID, $checkInId); + SentryContext::setCronCheckInId($checkInId); } protected function handleCrontabTaskFinished(Event\AfterExecute $event): void { - /** @var null|string $checkInId */ - $checkInId = Context::get(Constants::CRON_CHECKIN_ID); + $checkInId = SentryContext::getCronCheckInId(); + if (! $checkInId) { return; } @@ -113,8 +112,8 @@ protected function handleCrontabTaskFinished(Event\AfterExecute $event): void protected function handleCrontabTaskFailed(Event\FailToExecute $event): void { - /** @var null|string $checkInId */ - $checkInId = Context::get(Constants::CRON_CHECKIN_ID); + $checkInId = SentryContext::getCronCheckInId(); + if (! $checkInId) { return; } diff --git a/src/Factory/ClientBuilderFactory.php b/src/Factory/ClientBuilderFactory.php index ab50761..19f5608 100644 --- a/src/Factory/ClientBuilderFactory.php +++ b/src/Factory/ClientBuilderFactory.php @@ -29,6 +29,9 @@ class ClientBuilderFactory 'ignore_commands', 'integrations', 'logs_channel_level', + 'enable_default_metrics', + 'enable_command_metrics', + 'metrics_interval', 'transport_channel_size', 'transport_concurrent_limit', 'tracing', diff --git a/src/Feature.php b/src/Feature.php index 593f8d0..47e5222 100644 --- a/src/Feature.php +++ b/src/Feature.php @@ -11,7 +11,6 @@ namespace FriendsOfHyperf\Sentry; -use Hyperf\Context\Context; use Hyperf\Contract\ConfigInterface; use Sentry\SentrySdk; @@ -31,6 +30,40 @@ public function isBreadcrumbEnabled(string $key, bool $default = true): bool return (bool) $this->config->get('sentry.breadcrumbs.' . $key, $default); } + public function isMetricsEnabled(bool $default = true): bool + { + return (bool) $this->config->get('sentry.enable_metrics', $default); + } + + public function isDefaultMetricsEnabled(bool $default = true): bool + { + if (! $this->isMetricsEnabled()) { + return false; + } + + return (bool) $this->config->get('sentry.enable_default_metrics', $default); + } + + public function isCommandMetricsEnabled(bool $default = true): bool + { + if (! $this->isMetricsEnabled()) { + return false; + } + + return (bool) $this->config->get('sentry.enable_command_metrics', $default); + } + + public function getMetricsInterval(int $default = 10): int + { + $interval = (int) $this->config->get('sentry.metrics_interval', $default); + + if ($interval < 5) { + return 5; + } + + return $interval; + } + public function isTracingEnabled(string $key, bool $default = true): bool { return (bool) $this->config->get('sentry.tracing.' . $key, $default); @@ -54,14 +87,4 @@ public function isCronsEnabled(): bool { return (bool) $this->config->get('sentry.crons.enable', true); } - - public static function disableCoroutineTracing(): void - { - Context::set(Constants::DISABLE_COROUTINE_TRACING, true); - } - - public static function isDisableCoroutineTracing(): bool - { - return (bool) Context::get(Constants::DISABLE_COROUTINE_TRACING); - } } diff --git a/src/Function.php b/src/Function.php index bfddc62..9c64787 100644 --- a/src/Function.php +++ b/src/Function.php @@ -13,6 +13,7 @@ use FriendsOfHyperf\Sentry\Tracing\Tracer; use Hyperf\Context\ApplicationContext; +use Sentry\Metrics\TraceMetrics; use Sentry\State\Scope; use Sentry\Tracing\SpanContext; use Sentry\Tracing\Transaction; @@ -26,6 +27,11 @@ function feature(): Feature return ApplicationContext::getContainer()->get(Feature::class); } +function metrics(): TraceMetrics +{ + return TraceMetrics::getInstance(); +} + /** * Starts a new Transaction and returns it. This is the entry point to manual tracing instrumentation. */ diff --git a/src/Integration.php b/src/Integration.php index 7d934ba..e26f0e8 100644 --- a/src/Integration.php +++ b/src/Integration.php @@ -15,6 +15,7 @@ use Sentry\Event; use Sentry\Integration\IntegrationInterface; use Sentry\Logs\Logs; +use Sentry\Metrics\TraceMetrics; use Sentry\SentrySdk; use Sentry\State\Scope; use Sentry\Tracing\Span; @@ -111,6 +112,7 @@ public static function flushEvents(): void $client->flush(); Logs::getInstance()->flush(); + TraceMetrics::getInstance()->flush(); } } diff --git a/src/Metrics/Annotation/Counter.php b/src/Metrics/Annotation/Counter.php new file mode 100644 index 0000000..b4cc6aa --- /dev/null +++ b/src/Metrics/Annotation/Counter.php @@ -0,0 +1,23 @@ +feature->isMetricsEnabled()) { + $metadata = $proceedingJoinPoint->getAnnotationMetadata(); + $source = $this->fromCamelCase($proceedingJoinPoint->className . '::' . $proceedingJoinPoint->methodName); + + /** @var null|Counter $annotation */ + $annotation = $metadata->method[Counter::class] ?? null; + + if ($annotation) { + $name = $annotation->name ?: $source; + } else { + $name = $source; + } + + defer(fn () => metrics()->flush()); + + metrics()->count($name, 1, [ + 'class' => $proceedingJoinPoint->className, + 'method' => $proceedingJoinPoint->methodName, + ]); + } + + return $proceedingJoinPoint->process(); + } + + private function fromCamelCase(string $input): string + { + preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); + $ret = $matches[0]; + foreach ($ret as &$match) { + $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match); + } + return implode('_', $ret); + } +} diff --git a/src/Metrics/Aspect/HistogramAspect.php b/src/Metrics/Aspect/HistogramAspect.php new file mode 100644 index 0000000..50845cf --- /dev/null +++ b/src/Metrics/Aspect/HistogramAspect.php @@ -0,0 +1,70 @@ +feature->isMetricsEnabled()) { + return $proceedingJoinPoint->process(); + } + + $metadata = $proceedingJoinPoint->getAnnotationMetadata(); + $source = $this->fromCamelCase($proceedingJoinPoint->className . '::' . $proceedingJoinPoint->methodName); + /** @var null|Histogram $annotation */ + $annotation = $metadata->method[Histogram::class] ?? null; + if ($annotation) { + $name = $annotation->name ?: $source; + } else { + $name = $source; + } + + $timer = Timer::make($name, [ + 'class' => $proceedingJoinPoint->className, + 'method' => $proceedingJoinPoint->methodName, + ]); + + return tap($proceedingJoinPoint->process(), function () use ($timer) { + defer(fn () => $timer->end(true)); + }); + } + + private function fromCamelCase(string $input): string + { + preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); + $ret = $matches[0]; + foreach ($ret as &$match) { + $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match); + } + return implode('_', $ret); + } +} diff --git a/src/Metrics/CoroutineServerStats.php b/src/Metrics/CoroutineServerStats.php new file mode 100644 index 0000000..d150366 --- /dev/null +++ b/src/Metrics/CoroutineServerStats.php @@ -0,0 +1,56 @@ + 1, + 'idle_worker_num' => 0, + ]; + + public function __get($name) + { + return $this->stats[$name] ?? 0; + } + + public function __set($name, $value) + { + $this->stats[$name] = $value; + } + + public function toArray(): array + { + return $this->stats; + } +} diff --git a/src/Metrics/Event/MetricFactoryReady.php b/src/Metrics/Event/MetricFactoryReady.php new file mode 100644 index 0000000..bffe095 --- /dev/null +++ b/src/Metrics/Event/MetricFactoryReady.php @@ -0,0 +1,19 @@ +container->get(ConfigInterface::class); + $poolNames = array_keys($config->get('databases', ['default' => []])); + + foreach ($poolNames as $poolName) { + $workerId = (int) ($event->workerId ?? 0); + $pool = $this + ->container + ->get(PoolFactory::class) + ->getPool($poolName); + $this->watch($pool, $poolName, $workerId); + } + } +} diff --git a/src/Metrics/Listener/OnBeforeHandle.php b/src/Metrics/Listener/OnBeforeHandle.php new file mode 100644 index 0000000..995cd66 --- /dev/null +++ b/src/Metrics/Listener/OnBeforeHandle.php @@ -0,0 +1,115 @@ +timer = new Timer(); + } + + public function listen(): array + { + return [ + BeforeHandle::class, + ]; + } + + /** + * @param object|BeforeHandle $event + */ + public function process(object $event): void + { + if ( + ! $event instanceof BeforeHandle + || ! $event->getCommand()->getApplication()->isAutoExitEnabled() // Only enable in the command with auto exit. + || ! $this->feature->isCommandMetricsEnabled() + ) { + return; + } + + Constants::$runningInCommand = true; + + if (! $this->feature->isDefaultMetricsEnabled()) { + return; + } + + // The following metrics MUST be collected in worker. + $metrics = [ + 'memory_usage', + 'memory_peak_usage', + 'gc_runs', + 'gc_collected', + 'gc_threshold', + 'gc_roots', + 'ru_oublock', + 'ru_inblock', + 'ru_msgsnd', + 'ru_msgrcv', + 'ru_maxrss', + 'ru_ixrss', + 'ru_idrss', + 'ru_minflt', + 'ru_majflt', + 'ru_nsignals', + 'ru_nvcsw', + 'ru_nivcsw', + 'ru_nswap', + 'ru_utime_tv_usec', + 'ru_utime_tv_sec', + 'ru_stime_tv_usec', + 'ru_stime_tv_sec', + ]; + + $timerId = $this->timer->tick($this->feature->getMetricsInterval(), function () use ($metrics) { + defer(fn () => metrics()->flush()); + + $this->trySet('gc_', $metrics, gc_status()); + $this->trySet('', $metrics, getrusage()); + + metrics()->gauge( + 'memory_usage', + (float) memory_get_usage(), + ['worker' => '0'], + Unit::byte() + ); + metrics()->gauge( + 'memory_peak_usage', + (float) memory_get_peak_usage(), + ['worker' => '0'], + Unit::byte() + ); + }); + + Coroutine::create(function () use ($timerId) { + CoordinatorManager::until(\Hyperf\Coordinator\Constants::WORKER_EXIT)->yield(); + $this->timer->clear($timerId); + }); + } +} diff --git a/src/Metrics/Listener/OnCoroutineServerStart.php b/src/Metrics/Listener/OnCoroutineServerStart.php new file mode 100644 index 0000000..f062877 --- /dev/null +++ b/src/Metrics/Listener/OnCoroutineServerStart.php @@ -0,0 +1,128 @@ +timer = new Timer(); + } + + public function listen(): array + { + return [ + MainCoroutineServerStart::class, + ]; + } + + /** + * @param object|MainCoroutineServerStart $event + */ + public function process(object $event): void + { + if (! $this->feature->isMetricsEnabled()) { + return; + } + + if ($this->running) { + return; + } + + $this->running = true; + + $eventDispatcher = $this->container->get(EventDispatcherInterface::class); + $eventDispatcher->dispatch(new MetricFactoryReady()); + + if (! $this->feature->isDefaultMetricsEnabled()) { + return; + } + + // The following metrics MUST be collected in worker. + $metrics = [ + // 'worker_request_count', + // 'worker_dispatch_count', + 'memory_usage', + 'memory_peak_usage', + 'gc_runs', + 'gc_collected', + 'gc_threshold', + 'gc_roots', + 'ru_oublock', + 'ru_inblock', + 'ru_msgsnd', + 'ru_msgrcv', + 'ru_maxrss', + 'ru_ixrss', + 'ru_idrss', + 'ru_minflt', + 'ru_majflt', + 'ru_nsignals', + 'ru_nvcsw', + 'ru_nivcsw', + 'ru_nswap', + 'ru_utime_tv_usec', + 'ru_utime_tv_sec', + 'ru_stime_tv_usec', + 'ru_stime_tv_sec', + ]; + + $timerId = $this->timer->tick($this->feature->getMetricsInterval(), function () use ($metrics) { + defer(fn () => metrics()->flush()); + + $this->trySet('gc_', $metrics, gc_status()); + $this->trySet('', $metrics, getrusage()); + + metrics()->gauge( + 'memory_usage', + (float) memory_get_usage(), + ['worker' => '0'], + Unit::byte() + ); + metrics()->gauge( + 'memory_peak_usage', + (float) memory_get_peak_usage(), + ['worker' => '0'], + Unit::byte() + ); + }); + + Coroutine::create(function () use ($timerId) { + CoordinatorManager::until(Constants::WORKER_EXIT)->yield(); + $this->timer->clear($timerId); + }); + } +} diff --git a/src/Metrics/Listener/OnMetricFactoryReady.php b/src/Metrics/Listener/OnMetricFactoryReady.php new file mode 100644 index 0000000..153bb50 --- /dev/null +++ b/src/Metrics/Listener/OnMetricFactoryReady.php @@ -0,0 +1,140 @@ +timer = new Timer(); + } + + public function listen(): array + { + return [ + MetricFactoryReady::class, + ]; + } + + /** + * @param object|MetricFactoryReady $event + */ + public function process(object $event): void + { + if (! $this->feature->isDefaultMetricsEnabled()) { + return; + } + + $workerId = $event->workerId; + $metrics = [ + 'sys_load', + 'event_num', + 'signal_listener_num', + 'aio_task_num', + 'aio_worker_num', + 'c_stack_size', + 'coroutine_num', + 'coroutine_peak_num', + 'coroutine_last_cid', + 'connection_num', + 'accept_count', + 'close_count', + 'worker_num', + 'idle_worker_num', + 'tasking_num', + 'request_count', + 'timer_num', + 'timer_round', + 'swoole_timer_num', + 'swoole_timer_round', + 'metric_process_memory_usage', + 'metric_process_memory_peak_usage', + ]; + + $serverStatsFactory = null; + + if (! SentryConstants::$runningInCommand) { + if ($this->container->has(SwooleServer::class) && $server = $this->container->get(SwooleServer::class)) { + if ($server instanceof SwooleServer) { + $serverStatsFactory = fn (): array => $server->stats(); + } + } + + if (! $serverStatsFactory) { + $serverStatsFactory = fn (): array => $this->container->get(CoroutineServerStats::class)->toArray(); + } + } + + $timerId = $this->timer->tick($this->feature->getMetricsInterval(), function () use ($metrics, $serverStatsFactory, $workerId) { + defer(fn () => metrics()->flush()); + + $this->trySet('', $metrics, Coroutine::stats(), $workerId); + $this->trySet('timer_', $metrics, Timer::stats(), $workerId); + + if ($serverStatsFactory) { + $this->trySet('', $metrics, $serverStatsFactory(), $workerId); + } + + if (class_exists('Swoole\Timer')) { + $this->trySet('swoole_timer_', $metrics, \Swoole\Timer::stats(), $workerId); + } + + $load = sys_getloadavg(); + metrics()->gauge( + 'sys_load', + round($load[0] / System::getCpuCoresNum(), 2), + ['worker' => (string) $workerId], + ); + metrics()->gauge( + 'metric_process_memory_usage', + (float) memory_get_usage(), + ['worker' => (string) $workerId], + Unit::byte() + ); + metrics()->gauge( + 'metric_process_memory_peak_usage', + (float) memory_get_peak_usage(), + ['worker' => (string) $workerId], + Unit::byte() + ); + }); + + Coroutine::create(function () use ($timerId) { + CoordinatorManager::until(Constants::WORKER_EXIT)->yield(); + $this->timer->clear($timerId); + }); + } +} diff --git a/src/Metrics/Listener/OnWorkerStart.php b/src/Metrics/Listener/OnWorkerStart.php new file mode 100644 index 0000000..2637252 --- /dev/null +++ b/src/Metrics/Listener/OnWorkerStart.php @@ -0,0 +1,130 @@ +timer = new Timer(); + } + + public function listen(): array + { + return [ + BeforeWorkerStart::class, + ]; + } + + /** + * @param object|BeforeWorkerStart $event + */ + public function process(object $event): void + { + if (! $this->feature->isDefaultMetricsEnabled()) { + return; + } + + $workerId = $event->workerId; + $eventDispatcher = $this->container->get(EventDispatcherInterface::class); + $eventDispatcher->dispatch(new MetricFactoryReady($workerId)); + + // The following metrics MUST be collected in worker. + $metrics = [ + 'worker_request_count', + 'worker_dispatch_count', + 'memory_usage', + 'memory_peak_usage', + 'gc_runs', + 'gc_collected', + 'gc_threshold', + 'gc_roots', + 'ru_oublock', + 'ru_inblock', + 'ru_msgsnd', + 'ru_msgrcv', + 'ru_maxrss', + 'ru_ixrss', + 'ru_idrss', + 'ru_minflt', + 'ru_majflt', + 'ru_nsignals', + 'ru_nvcsw', + 'ru_nivcsw', + 'ru_nswap', + 'ru_utime_tv_usec', + 'ru_utime_tv_sec', + 'ru_stime_tv_usec', + 'ru_stime_tv_sec', + ]; + + $timerId = $this->timer->tick($this->feature->getMetricsInterval(), function () use ($metrics, $event) { + defer(fn () => metrics()->flush()); + + $server = $this->container->get(Server::class); + $serverStats = $server->stats(); + $this->trySet('gc_', $metrics, gc_status()); + $this->trySet('', $metrics, getrusage()); + + metrics()->gauge( + 'worker_request_count', + (float) $serverStats['worker_request_count'], + ['worker' => (string) ($event->workerId ?? 0)], + ); + metrics()->gauge( + 'worker_dispatch_count', + (float) $serverStats['worker_dispatch_count'], + ['worker' => (string) ($event->workerId ?? 0)], + ); + metrics()->gauge( + 'memory_usage', + (float) memory_get_usage(), + ['worker' => (string) ($event->workerId ?? 0)], + Unit::byte() + ); + metrics()->gauge( + 'memory_peak_usage', + (float) memory_get_peak_usage(), + ['worker' => (string) ($event->workerId ?? 0)], + Unit::byte() + ); + }); + + Coroutine::create(function () use ($timerId) { + CoordinatorManager::until(Constants::WORKER_EXIT)->yield(); + $this->timer->clear($timerId); + }); + } +} diff --git a/src/Metrics/Listener/PoolWatcher.php b/src/Metrics/Listener/PoolWatcher.php new file mode 100644 index 0000000..5655255 --- /dev/null +++ b/src/Metrics/Listener/PoolWatcher.php @@ -0,0 +1,106 @@ +timer = new Timer(); + } + + /** + * @return string[] returns the events that you want to listen + */ + public function listen(): array + { + return [ + BeforeWorkerStart::class, + MainCoroutineServerStart::class, + ]; + } + + /** + * Get the metric name prefix for this pool type (e.g., 'redis', 'mysql'). + * + * @return string The prefix used in metric names like '{prefix}_connections_in_use' + */ + abstract public function getPrefix(): string; + + /** + * @param object|BeforeWorkerStart|MainCoroutineServerStart $event + */ + abstract public function process(object $event): void; + + public function watch(Pool $pool, string $poolName, int $workerId): void + { + if (! $this->feature->isMetricsEnabled()) { + return; + } + + $timerId = $this->timer->tick($this->feature->getMetricsInterval(), function () use ( + $pool, + $workerId, + $poolName + ) { + defer(fn () => metrics()->flush()); + + metrics()->gauge( + $this->getPrefix() . '_connections_in_use', + (float) $pool->getCurrentConnections(), + [ + 'pool' => $poolName, + 'worker' => (string) $workerId, + ] + ); + metrics()->gauge( + $this->getPrefix() . '_connections_in_waiting', + (float) $pool->getConnectionsInChannel(), + [ + 'pool' => $poolName, + 'worker' => (string) $workerId, + ] + ); + metrics()->gauge( + $this->getPrefix() . '_max_connections', + (float) $pool->getOption()->getMaxConnections(), + [ + 'pool' => $poolName, + 'worker' => (string) $workerId, + ] + ); + }); + + Coroutine::create(function () use ($timerId) { + CoordinatorManager::until(Constants::WORKER_EXIT)->yield(); + $this->timer->clear($timerId); + }); + } +} diff --git a/src/Metrics/Listener/QueueWatcher.php b/src/Metrics/Listener/QueueWatcher.php new file mode 100644 index 0000000..96f6206 --- /dev/null +++ b/src/Metrics/Listener/QueueWatcher.php @@ -0,0 +1,96 @@ +timer = new Timer(); + } + + /** + * @return string[] returns the events that you want to listen + */ + public function listen(): array + { + return [ + MetricFactoryReady::class, + ]; + } + + /** + * @param object|MetricFactoryReady $event + */ + public function process(object $event): void + { + if (! $this->feature->isMetricsEnabled()) { + return; + } + + $timerId = $this->timer->tick($this->feature->getMetricsInterval(), function () { + defer(fn () => metrics()->flush()); + + $config = $this->container->get(ConfigInterface::class); + $queues = array_keys($config->get('async_queue', [])); + + foreach ($queues as $name) { + $queue = $this->container->get(DriverFactory::class)->get($name); + $info = $queue->info(); + + metrics()->gauge( + 'queue_waiting', + (float) $info['waiting'], + ['queue' => $name] + ); + metrics()->gauge( + 'queue_delayed', + (float) $info['delayed'], + ['queue' => $name] + ); + metrics()->gauge( + 'queue_failed', + (float) $info['failed'], + ['queue' => $name] + ); + metrics()->gauge( + 'queue_timeout', + (float) $info['timeout'], + ['queue' => $name] + ); + } + }); + + Coroutine::create(function () use ($timerId) { + CoordinatorManager::until(Constants::WORKER_EXIT)->yield(); + $this->timer->clear($timerId); + }); + } +} diff --git a/src/Metrics/Listener/RedisPoolWatcher.php b/src/Metrics/Listener/RedisPoolWatcher.php new file mode 100644 index 0000000..0fbe5c1 --- /dev/null +++ b/src/Metrics/Listener/RedisPoolWatcher.php @@ -0,0 +1,43 @@ +container->get(ConfigInterface::class); + $poolNames = array_keys($config->get('redis', ['default' => []])); + + foreach ($poolNames as $poolName) { + $workerId = (int) ($event->workerId ?? 0); + $pool = $this + ->container + ->get(PoolFactory::class) + ->getPool($poolName); + $this->watch($pool, $poolName, $workerId); + } + } +} diff --git a/src/Metrics/Listener/RequestWatcher.php b/src/Metrics/Listener/RequestWatcher.php new file mode 100644 index 0000000..10bfd66 --- /dev/null +++ b/src/Metrics/Listener/RequestWatcher.php @@ -0,0 +1,73 @@ +stats->accept_count; + ++$this->stats->request_count; + ++$this->stats->connection_num; + + $request = $event->request; + $timer = Timer::make('http_requests', [ + 'request_path' => $this->getPath($request), + 'request_method' => $request->getMethod(), + ]); + + Coroutine::defer(function () use ($timer) { + ++$this->stats->close_count; + ++$this->stats->response_count; + --$this->stats->connection_num; + + $timer->end(true); + }); + } + } + + protected function getPath(ServerRequestInterface $request): string + { + $dispatched = $request->getAttribute(Dispatched::class); + if (! $dispatched) { + return $request->getUri()->getPath(); + } + if (! $dispatched->handler) { + return 'not_found'; + } + return $dispatched->handler->route; + } +} diff --git a/src/Metrics/Timer.php b/src/Metrics/Timer.php new file mode 100644 index 0000000..5cdefb5 --- /dev/null +++ b/src/Metrics/Timer.php @@ -0,0 +1,62 @@ +startAt = microtime(true); + } + + public function __destruct() + { + $this->end(); + } + + public static function make( + string $name, + array $attributes = [], + ?Unit $unit = null, + ): static { + return new static($name, $attributes, $unit); + } + + public function end(bool $flush = false): void + { + if ($this->ended) { + return; + } + + metrics()->distribution( + $this->name, + (int) ((microtime(true) - $this->startAt) * 1000), + $this->attributes, + $this->unit ?? Unit::second() + ); + + $flush && metrics()->flush(); + + $this->ended = true; + } +} diff --git a/src/Metrics/Traits/MetricSetter.php b/src/Metrics/Traits/MetricSetter.php new file mode 100644 index 0000000..89708ae --- /dev/null +++ b/src/Metrics/Traits/MetricSetter.php @@ -0,0 +1,38 @@ +gauge( + $metricsKey, + (float) $stats[$key], + ['worker' => (string) $workerId], + $unit + ); + } + } + } + + // protected function spawnDefaultMetrics() + // { + // } +} diff --git a/src/SentryContext.php b/src/SentryContext.php new file mode 100644 index 0000000..0be693a --- /dev/null +++ b/src/SentryContext.php @@ -0,0 +1,161 @@ +getInstance(); - $messageId = method_exists($job, 'getId') ? $job->getId() : SentryUid::generate(); + $messageId = method_exists($job, 'getId') ? call_user_func([$job, 'getId']) : SentryUid::generate(); $destinationName = Context::get('sentry.messaging.destination.name', 'default'); $bodySize = (fn ($job) => strlen($this->packer->pack($job)))->call($driver, $job); $data = [ @@ -106,7 +107,7 @@ function (Scope $scope) use ($proceedingJoinPoint, $messageId, $destinationName, $carrier = Carrier::fromArray([])->with($extra); } - Context::set(Constants::TRACE_CARRIER, $carrier); + SentryContext::setCarrier($carrier); return $proceedingJoinPoint->process(); }, @@ -140,7 +141,7 @@ protected function buildSpanDataOfRedisDriver(RedisDriver $driver): array protected function handleSerialize(ProceedingJoinPoint $proceedingJoinPoint) { return with($proceedingJoinPoint->process(), function ($result) { - if (is_array($result) && $carrier = Context::get(Constants::TRACE_CARRIER)) { + if (is_array($result) && $carrier = SentryContext::getCarrier()) { $result[Constants::TRACE_CARRIER] = $carrier->toJson(); } @@ -156,7 +157,7 @@ protected function handleUnserialize(ProceedingJoinPoint $proceedingJoinPoint) $carrier = $data['job'] ?? null; if ($carrier) { - Context::set(Constants::TRACE_CARRIER, Carrier::fromJson($carrier)); + SentryContext::setCarrier(Carrier::fromJson($carrier)); } return $proceedingJoinPoint->process(); diff --git a/src/Tracing/Aspect/CoroutineAspect.php b/src/Tracing/Aspect/CoroutineAspect.php index 0d08c22..bc01701 100644 --- a/src/Tracing/Aspect/CoroutineAspect.php +++ b/src/Tracing/Aspect/CoroutineAspect.php @@ -13,6 +13,7 @@ use FriendsOfHyperf\Sentry\Feature; use FriendsOfHyperf\Sentry\Integration; +use FriendsOfHyperf\Sentry\SentryContext; use FriendsOfHyperf\Sentry\Util\CoroutineBacktraceHelper; use Hyperf\Context\Context; use Hyperf\Di\Aop\AbstractAspect; @@ -46,7 +47,7 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint) { if ( ! $this->feature->isTracingSpanEnabled('coroutine') - || Feature::isDisableCoroutineTracing() + || SentryContext::isTracingDisabled() ) { return $proceedingJoinPoint->process(); } diff --git a/src/Tracing/Aspect/DbAspect.php b/src/Tracing/Aspect/DbAspect.php index f8e9790..94edd63 100644 --- a/src/Tracing/Aspect/DbAspect.php +++ b/src/Tracing/Aspect/DbAspect.php @@ -12,30 +12,43 @@ namespace FriendsOfHyperf\Sentry\Tracing\Aspect; use FriendsOfHyperf\Sentry\Feature; +use FriendsOfHyperf\Sentry\SentryContext; use FriendsOfHyperf\Sentry\Util\SqlParser; -use Hyperf\DB\DB; use Hyperf\DB\Pool\PoolFactory; use Hyperf\Di\Aop\AbstractAspect; use Hyperf\Di\Aop\ProceedingJoinPoint; +use PDO; use Psr\Container\ContainerInterface; use Sentry\State\Scope; use Sentry\Tracing\SpanContext; +use WeakMap; use function FriendsOfHyperf\Sentry\trace; +use function Hyperf\Tappable\tap; /** * @property string $poolName + * @property PDO $connection + * @property array $config */ class DbAspect extends AbstractAspect { public array $classes = [ - DB::class . '::__call', + 'Hyperf\DB\MySQLConnection::reconnect', + 'Hyperf\DB\DB::getConnection', + 'Hyperf\DB\DB::__call', ]; + /** + * @var WeakMap<\Hyperf\DB\AbstractConnection,array> + */ + private WeakMap $serverCache; + public function __construct( protected ContainerInterface $container, protected Feature $feature ) { + $this->serverCache = new WeakMap(); } public function process(ProceedingJoinPoint $proceedingJoinPoint) @@ -44,24 +57,46 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint) return $proceedingJoinPoint->process(); } + if ($proceedingJoinPoint->methodName === 'reconnect') { + return tap($proceedingJoinPoint->process(), function () use ($proceedingJoinPoint) { + /** @var \Hyperf\DB\AbstractConnection $connection */ + $connection = $proceedingJoinPoint->getInstance(); + $this->serverCache[$connection] = $connection->getConfig(); + }); + } + + if ($proceedingJoinPoint->methodName === 'getConnection') { + return tap($proceedingJoinPoint->process(), function ($connection) { + /** @var \Hyperf\DB\AbstractConnection $connection */ + $server = $this->serverCache[$connection] ?? null; + if ($server !== null) { + SentryContext::setDbServerAddress($server['host'] ?? 'localhost'); + SentryContext::setDbServerPort((int) ($server['port'] ?? 3306)); + } + }); + } + $arguments = $proceedingJoinPoint->arguments['keys']; - $poolName = (fn () => $this->poolName)->call($proceedingJoinPoint->getInstance()); - /** @var \Hyperf\Pool\Pool $pool */ - $pool = $this->container->get(PoolFactory::class)->getPool($poolName); $operation = $arguments['name']; $database = ''; $driver = 'unknown'; - $table = ''; + + /** @var \Hyperf\DB\DB $instance */ + $instance = $proceedingJoinPoint->getInstance(); + $poolName = (fn () => $this->poolName)->call($instance); + /** @var \Hyperf\Pool\Pool $pool */ + $pool = $this->container->get(PoolFactory::class)->getPool($poolName); if ($pool instanceof \Hyperf\DB\Pool\Pool) { $config = $pool->getConfig(); $database = $config['database'] ?? ''; - $driver = $config['driver'] ?? 'unknown'; + $driver = $config['driver'] ?? $driver; } $sql = $arguments['arguments']['query'] ?? ''; $sqlParse = SqlParser::parse($sql); $table = $sqlParse['table']; $operation = $sqlParse['operation']; + $data = [ 'db.system' => $driver, 'db.name' => $database, @@ -72,8 +107,8 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint) 'db.pool.max_idle_time' => $pool->getOption()->getMaxIdleTime(), 'db.pool.idle' => $pool->getConnectionsInChannel(), 'db.pool.using' => $pool->getCurrentConnections(), - // 'server.host' => '', - // 'server.port' => '', + 'server.host' => SentryContext::getDbServerAddress() ?? 'localhost', + 'server.port' => SentryContext::getDbServerPort() ?? 3306, ]; if ($this->feature->isTracingTagEnabled('db.sql.bindings', true)) { diff --git a/src/Tracing/Aspect/DbConnectionAspect.php b/src/Tracing/Aspect/DbConnectionAspect.php new file mode 100644 index 0000000..a7e5623 --- /dev/null +++ b/src/Tracing/Aspect/DbConnectionAspect.php @@ -0,0 +1,68 @@ +serverCache = new WeakMap(); + } + + public function process(ProceedingJoinPoint $proceedingJoinPoint) + { + return tap($proceedingJoinPoint->process(), function ($pdo) use ($proceedingJoinPoint) { + if (! $this->feature->isTracingSpanEnabled('db')) { + return; + } + + if ($proceedingJoinPoint->methodName === 'createPdoConnection') { + $dsn = $proceedingJoinPoint->arguments['keys']['dsn'] ?? ''; + $pattern = '/host=([^;]+);port=(\d+);?/'; + if (preg_match($pattern, $dsn, $matches)) { + $host = $matches[1]; + $port = $matches[2]; + $this->serverCache[$pdo] = ['host' => $host, 'port' => $port]; + } + return; + } + + Context::getOrSet(self::class, function () use ($pdo) { + $server = $this->serverCache[$pdo] ?? null; + + if (is_array($server)) { + SentryContext::setDbServerAddress($server['host']); + SentryContext::setDbServerPort((int) $server['port']); + } + + return true; + }); + }); + } +} diff --git a/src/Tracing/Aspect/ElasticsearchAspect.php b/src/Tracing/Aspect/ElasticsearchAspect.php index d842954..b1dc8ac 100644 --- a/src/Tracing/Aspect/ElasticsearchAspect.php +++ b/src/Tracing/Aspect/ElasticsearchAspect.php @@ -11,9 +11,8 @@ namespace FriendsOfHyperf\Sentry\Tracing\Aspect; -use FriendsOfHyperf\Sentry\Constants; use FriendsOfHyperf\Sentry\Feature; -use Hyperf\Context\Context; +use FriendsOfHyperf\Sentry\SentryContext; use Hyperf\Di\Aop\AbstractAspect; use Hyperf\Di\Aop\ProceedingJoinPoint; use Sentry\State\Scope; @@ -59,8 +58,8 @@ function (Scope $scope) use ($proceedingJoinPoint) { ]); } - $data = (array) Context::get(Constants::TRACE_ELASTICSEARCH_REQUEST_DATA, []); - $scope->getSpan()?->setData($data); + $data = SentryContext::getElasticsearchSpanData(); + $data && $scope->getSpan()?->setData($data); }); }, SpanContext::make() diff --git a/src/Tracing/Aspect/ElasticsearchRequestAspect.php b/src/Tracing/Aspect/ElasticsearchRequestAspect.php index c633d65..f9c25da 100644 --- a/src/Tracing/Aspect/ElasticsearchRequestAspect.php +++ b/src/Tracing/Aspect/ElasticsearchRequestAspect.php @@ -11,8 +11,8 @@ namespace FriendsOfHyperf\Sentry\Tracing\Aspect; -use FriendsOfHyperf\Sentry\Constants; -use Hyperf\Context\Context; +use FriendsOfHyperf\Sentry\Feature; +use FriendsOfHyperf\Sentry\SentryContext; use Hyperf\Di\Aop\AbstractAspect; use Hyperf\Di\Aop\ProceedingJoinPoint; use Psr\Http\Message\RequestInterface; @@ -23,19 +23,25 @@ class ElasticsearchRequestAspect extends AbstractAspect 'Elastic\Elasticsearch\Client::sendRequest', ]; + public function __construct(protected Feature $feature) + { + } + public function process(ProceedingJoinPoint $proceedingJoinPoint) { - $arguments = $proceedingJoinPoint->arguments['keys'] ?? []; - $request = $arguments['request'] ?? null; - - if ($request instanceof RequestInterface) { - $data = [ - 'server.address' => $request->getUri()->getHost(), - 'server.port' => $request->getUri()->getPort(), - 'http.request.method' => $request->getMethod(), - 'url.full' => $this->getFullUrl($request), - ]; - Context::set(Constants::TRACE_ELASTICSEARCH_REQUEST_DATA, $data); + if ($this->feature->isTracingSpanEnabled('elasticsearch')) { + $arguments = $proceedingJoinPoint->arguments['keys'] ?? []; + $request = $arguments['request'] ?? null; + + if ($request instanceof RequestInterface) { + $data = [ + 'server.address' => $request->getUri()->getHost(), + 'server.port' => $request->getUri()->getPort(), + 'http.request.method' => $request->getMethod(), + 'url.full' => $this->getFullUrl($request), + ]; + SentryContext::setElasticsearchSpanData($data); + } } return $proceedingJoinPoint->process(); diff --git a/src/Tracing/Aspect/GrpcAspect.php b/src/Tracing/Aspect/GrpcAspect.php index 4ea0888..0dcd896 100644 --- a/src/Tracing/Aspect/GrpcAspect.php +++ b/src/Tracing/Aspect/GrpcAspect.php @@ -15,6 +15,8 @@ use FriendsOfHyperf\Sentry\Feature; use Hyperf\Di\Aop\AbstractAspect; use Hyperf\Di\Aop\ProceedingJoinPoint; +use Hyperf\GrpcClient\BaseClient; +use Hyperf\GrpcClient\GrpcClient; use Sentry\SentrySdk; use Sentry\State\Scope; use Sentry\Tracing\SpanContext; @@ -38,12 +40,20 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint) return $proceedingJoinPoint->process(); } + /** @var BaseClient $instance */ + $instance = $proceedingJoinPoint->getInstance(); + /** @var GrpcClient $grpcClient */ + $grpcClient = $instance->_getGrpcClient(); + [$serverAddress, $serverPort] = (fn () => [$this->host ?? null, $this->port ?? null])->call($grpcClient); + $method = $proceedingJoinPoint->arguments['keys']['method']; $options = $proceedingJoinPoint->arguments['keys']['options']; $data = [ 'rpc.system' => 'grpc', 'rpc.method' => $method, 'rpc.options' => $options, + 'server.address' => (string) ($serverAddress ?? 'unknown'), + 'server.port' => $serverPort, ]; $parent = SentrySdk::getCurrentHub()->getSpan(); diff --git a/src/Tracing/Aspect/GuzzleHttpClientAspect.php b/src/Tracing/Aspect/GuzzleHttpClientAspect.php index 1080a7c..39035f6 100644 --- a/src/Tracing/Aspect/GuzzleHttpClientAspect.php +++ b/src/Tracing/Aspect/GuzzleHttpClientAspect.php @@ -66,7 +66,7 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint) } return trace( - function (Scope $scope) use ($proceedingJoinPoint, $options, $guzzleConfig) { + function (Scope $scope) use ($proceedingJoinPoint, $options, $guzzleConfig, $request) { if ($span = $scope->getSpan()) { // Inject trace context $options['headers'] = array_replace($options['headers'] ?? [], [ @@ -79,7 +79,7 @@ function (Scope $scope) use ($proceedingJoinPoint, $options, $guzzleConfig) { $onStats = $options['on_stats'] ?? null; // Add or override the on_stats option to record the request duration. - $proceedingJoinPoint->arguments['keys']['options']['on_stats'] = function (TransferStats $stats) use ($options, $guzzleConfig, $onStats, $span) { + $proceedingJoinPoint->arguments['keys']['options']['on_stats'] = function (TransferStats $stats) use ($options, $guzzleConfig, $onStats, $request, $span) { $request = $stats->getRequest(); $uri = $request->getUri(); $span->setData([ @@ -100,6 +100,8 @@ function (Scope $scope) use ($proceedingJoinPoint, $options, $guzzleConfig) { 'http.guzzle.config' => $guzzleConfig, 'http.guzzle.options' => $options ?? [], 'duration' => $stats->getTransferTime() * 1000, // in milliseconds + 'server.address' => $request->getUri()->getHost(), + 'server.port' => $request->getUri()->getPort(), ]); if ($response = $stats->getResponse()) { diff --git a/src/Tracing/Aspect/RedisConnectionAspect.php b/src/Tracing/Aspect/RedisConnectionAspect.php new file mode 100644 index 0000000..9af7667 --- /dev/null +++ b/src/Tracing/Aspect/RedisConnectionAspect.php @@ -0,0 +1,87 @@ +slotNodeCache = new WeakMap(); + } + + public function process(ProceedingJoinPoint $proceedingJoinPoint) + { + return tap($proceedingJoinPoint->process(), function ($result) use ($proceedingJoinPoint) { + if (! $this->feature->isTracingSpanEnabled('redis')) { + return; + } + + $redisConnection = $proceedingJoinPoint->getInstance(); + $connection = (fn () => $this->connection ?? null)->call($redisConnection); + + if ($connection instanceof Redis) { // Redis or RedisSentinel + SentryContext::setRedisServerAddress($connection->getHost()); + SentryContext::setRedisServerPort($connection->getPort()); + } + + if ($connection instanceof RedisCluster) { // RedisCluster + $arguments = $proceedingJoinPoint->arguments['keys']['arguments'] ?? []; + $key = $arguments[0] ?? null; + if (is_string($key)) { + $node = $this->getClusterNodeBySlot($connection, $key); + if ($node !== null) { + SentryContext::setRedisServerAddress($node['host']); + SentryContext::setRedisServerPort((int) $node['port']); + } + } + } + }); + } + + private function getClusterNodeBySlot(RedisCluster $rc, string $key) + { + // $slot = $rc->cluster('CLUSTER', 'KEYSLOT', $key); + $slot = RedisClusterKeySlot::get($key); + $slots = ($this->slotNodeCache[$rc] ??= $rc->cluster('CLUSTER', 'SLOTS')); // @phpstan-ignore-line + + foreach ($slots as $range) { + [$start, $end, $master] = $range; + if ($slot >= $start && $slot <= $end) { + // $master = [host, port, nodeId] + return [ + 'host' => $master[0], + 'port' => $master[1], + 'nodeId' => $master[2] ?? null, + ]; + } + } + + return null; + } +} diff --git a/src/Tracing/Aspect/RpcAspect.php b/src/Tracing/Aspect/RpcAspect.php index 51aa90a..573324e 100644 --- a/src/Tracing/Aspect/RpcAspect.php +++ b/src/Tracing/Aspect/RpcAspect.php @@ -13,6 +13,7 @@ use FriendsOfHyperf\Sentry\Constants; use FriendsOfHyperf\Sentry\Feature; +use FriendsOfHyperf\Sentry\SentryContext; use FriendsOfHyperf\Sentry\Util\Carrier; use Hyperf\Context\Context; use Hyperf\Contract\ConfigInterface; @@ -76,7 +77,7 @@ private function handleGenerateRpcPath(ProceedingJoinPoint $proceedingJoinPoint) }; // https://github.com/open-telemetry/semantic-conventions/blob/main/docs/rpc/rpc-spans.md - Context::set(static::SPAN_CONTEXT, SpanContext::make() + SentryContext::setRpcSpanContext(SpanContext::make() ->setOp('rpc.client') ->setDescription($path) ->setOrigin('auto.rpc') @@ -91,8 +92,7 @@ private function handleGenerateRpcPath(ProceedingJoinPoint $proceedingJoinPoint) private function handleSend(ProceedingJoinPoint $proceedingJoinPoint) { - /** @var null|SpanContext $spanContext */ - $spanContext = Context::get(static::SPAN_CONTEXT); + $spanContext = SentryContext::getRpcSpanContext(); if (! $spanContext) { return $proceedingJoinPoint->process(); @@ -116,18 +116,16 @@ function (Scope $scope) use ($proceedingJoinPoint) { if ($this->feature->isTracingTagEnabled('rpc.result')) { $span?->setData(['rpc.result' => $result]); } - if (Context::has(Constants::TRACE_RPC_SERVER_ADDRESS)) { - $span?->setData([ - 'server.address' => Context::get(Constants::TRACE_RPC_SERVER_ADDRESS), - 'server.port' => Context::get(Constants::TRACE_RPC_SERVER_PORT), - ]); - } + $span?->setData([ + 'server.address' => SentryContext::getRpcServerAddress() ?? 'unknown', + 'server.port' => SentryContext::getRpcServerPort() ?? 0, + ]); }); }, $spanContext ); } finally { - Context::destroy(static::SPAN_CONTEXT); + SentryContext::destroyRpcSpanContext(); } } } diff --git a/src/Tracing/Aspect/RpcEndpointAspect.php b/src/Tracing/Aspect/RpcEndpointAspect.php index e83ae8e..8300be1 100644 --- a/src/Tracing/Aspect/RpcEndpointAspect.php +++ b/src/Tracing/Aspect/RpcEndpointAspect.php @@ -11,8 +11,8 @@ namespace FriendsOfHyperf\Sentry\Tracing\Aspect; -use FriendsOfHyperf\Sentry\Constants; -use Hyperf\Context\Context; +use FriendsOfHyperf\Sentry\Feature; +use FriendsOfHyperf\Sentry\SentryContext; use Hyperf\Di\Aop\AbstractAspect; use Hyperf\Di\Aop\ProceedingJoinPoint; @@ -26,18 +26,26 @@ class RpcEndpointAspect extends AbstractAspect 'Hyperf\JsonRpc\JsonRpcPoolTransporter::getConnection', ]; + public function __construct(protected Feature $feature) + { + } + public function process(ProceedingJoinPoint $proceedingJoinPoint) { return tap($proceedingJoinPoint->process(), function ($result) { + if (! $this->feature->isTracingSpanEnabled('rpc')) { + return; + } + // RpcMultiplex if ($result instanceof \Hyperf\RpcMultiplex\Socket) { - Context::set(Constants::TRACE_RPC_SERVER_ADDRESS, $result->getName()); - Context::set(Constants::TRACE_RPC_SERVER_PORT, $result->getPort()); + SentryContext::setRpcServerAddress($result->getName()); + SentryContext::setRpcServerPort($result->getPort()); } // JsonRpcHttpTransporter if ($result instanceof \Hyperf\LoadBalancer\Node) { - Context::set(Constants::TRACE_RPC_SERVER_ADDRESS, $result->host); - Context::set(Constants::TRACE_RPC_SERVER_PORT, $result->port); + SentryContext::setRpcServerAddress($result->host); + SentryContext::setRpcServerPort($result->port); } // JsonRpcPoolTransporter if ($result instanceof \Hyperf\JsonRpc\Pool\RpcConnection) { @@ -47,8 +55,8 @@ public function process(ProceedingJoinPoint $proceedingJoinPoint) /** @var null|\Hyperf\Engine\Contract\Socket\SocketOptionInterface $option */ $option = $socket->getSocketOption(); if ($option instanceof \Hyperf\Engine\Contract\Socket\SocketOptionInterface) { - Context::set(Constants::TRACE_RPC_SERVER_ADDRESS, $option->getHost()); - Context::set(Constants::TRACE_RPC_SERVER_PORT, $option->getPort()); + SentryContext::setRpcServerAddress($option->getHost()); + SentryContext::setRpcServerPort($option->getPort()); } } } diff --git a/src/Tracing/Listener/EventHandleListener.php b/src/Tracing/Listener/EventHandleListener.php index f20fab5..2d8e610 100644 --- a/src/Tracing/Listener/EventHandleListener.php +++ b/src/Tracing/Listener/EventHandleListener.php @@ -15,6 +15,7 @@ use FriendsOfHyperf\Sentry\Constants; use FriendsOfHyperf\Sentry\Feature; use FriendsOfHyperf\Sentry\Integration; +use FriendsOfHyperf\Sentry\SentryContext; use FriendsOfHyperf\Sentry\Util\Carrier; use FriendsOfHyperf\Sentry\Util\CoContainer; use FriendsOfHyperf\Sentry\Util\SqlParser; @@ -22,7 +23,6 @@ use Hyperf\Amqp\Message\ConsumerMessage; use Hyperf\AsyncQueue\Event as AsyncQueueEvent; use Hyperf\Command\Event as CommandEvent; -use Hyperf\Context\Context; use Hyperf\Contract\ConfigInterface; use Hyperf\Coroutine\Coroutine; use Hyperf\Crontab\Event as CrontabEvent; @@ -192,6 +192,8 @@ protected function handleDbQueryExecuted(DbEvent\QueryExecuted $event): void 'db.pool.max_idle_time' => $pool->getOption()->getMaxIdleTime(), 'db.pool.idle' => $pool->getConnectionsInChannel(), 'db.pool.using' => $pool->getCurrentConnections(), + 'server.address' => SentryContext::getDbServerAddress() ?? 'localhost', + 'server.port' => SentryContext::getDbServerPort() ?? 3306, ]; if ($this->feature->isTracingTagEnabled('db.sql.bindings', true)) { @@ -496,6 +498,8 @@ function (Scope $scope) use ($event) { 'db.redis.pool.idle' => $pool->getConnectionsInChannel(), 'db.redis.pool.using' => $pool->getCurrentConnections(), 'duration' => $event->time * 1000, + 'server.address' => SentryContext::getRedisServerAddress() ?? 'localhost', + 'server.port' => SentryContext::getRedisServerPort() ?? 6379, ]) ->setStartTimestamp(microtime(true) - $event->time / 1000) ); @@ -568,7 +572,7 @@ protected function handleAmqpMessageProcessing(AmqpEvent\BeforeConsume $event): $applicationHeaders = $amqpMessage->has('application_headers') ? $amqpMessage->get('application_headers') : null; if ($applicationHeaders && isset($applicationHeaders[Constants::TRACE_CARRIER])) { $carrier = Carrier::fromJson($applicationHeaders[Constants::TRACE_CARRIER]); - Context::set(Constants::TRACE_CARRIER, $carrier); + SentryContext::setCarrier($carrier); } } @@ -638,7 +642,7 @@ protected function handleKafkaMessageProcessing(KafkaEvent\BeforeConsume $event) foreach ($message->getHeaders() as $header) { if ($header->getHeaderKey() === Constants::TRACE_CARRIER) { $carrier = Carrier::fromJson($header->getValue()); - Context::set(Constants::TRACE_CARRIER, $carrier); + SentryContext::setCarrier($carrier); break; } } @@ -694,8 +698,7 @@ protected function handleAsyncQueueJobProcessing(AsyncQueueEvent\BeforeHandle $e return; } - /** @var null|Carrier $carrier */ - $carrier = Context::get(Constants::TRACE_CARRIER, null, Coroutine::parentId()); + $carrier = SentryContext::getCarrier(Coroutine::parentId()); $job = $event->getMessage()->job(); $transaction = startTransaction( diff --git a/src/Util/RedisClusterKeySlot.php b/src/Util/RedisClusterKeySlot.php new file mode 100644 index 0000000..bc87676 --- /dev/null +++ b/src/Util/RedisClusterKeySlot.php @@ -0,0 +1,69 @@ +> 8) ^ ord($key[$i])) & 0xFF]) & 0xFFFF; + } + + // 得到 slot + return $crc % 16384; + } +}