diff --git a/src/sentry/publish/sentry.php b/src/sentry/publish/sentry.php index b29c88d38..dd3187711 100644 --- a/src/sentry/publish/sentry.php +++ b/src/sentry/publish/sentry.php @@ -40,6 +40,10 @@ // @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), + '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/sentry/src/Aspect/SingletonAspect.php b/src/sentry/src/Aspect/SingletonAspect.php index bcd559415..5c0aec6e4 100644 --- a/src/sentry/src/Aspect/SingletonAspect.php +++ b/src/sentry/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/sentry/src/ConfigProvider.php b/src/sentry/src/ConfigProvider.php index c2b1caf19..31a99d4fd 100644 --- a/src/sentry/src/ConfigProvider.php +++ b/src/sentry/src/ConfigProvider.php @@ -27,6 +27,8 @@ public function __invoke(): array Aspect\LoggerAspect::class, Aspect\RedisAspect::class, Aspect\SingletonAspect::class, + Metrics\Aspect\CounterAspect::class, + Metrics\Aspect\HistogramAspect::class, Tracing\Aspect\AmqpProducerAspect::class, Tracing\Aspect\AsyncQueueJobMessageAspect::class, Tracing\Aspect\CacheAspect::class, @@ -60,6 +62,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/sentry/src/Constants.php b/src/sentry/src/Constants.php index 6540efdc4..33b344c37 100644 --- a/src/sentry/src/Constants.php +++ b/src/sentry/src/Constants.php @@ -38,4 +38,6 @@ class Constants public const BAGGAGE = 'baggage'; public const TRACEPARENT = 'traceparent'; + + public const RUN_IN_COMMAND = 'sentry.run_in_command'; } diff --git a/src/sentry/src/Factory/ClientBuilderFactory.php b/src/sentry/src/Factory/ClientBuilderFactory.php index aaa970ab1..ce478b03b 100644 --- a/src/sentry/src/Factory/ClientBuilderFactory.php +++ b/src/sentry/src/Factory/ClientBuilderFactory.php @@ -31,6 +31,7 @@ class ClientBuilderFactory 'http_chanel_size', // deprecated, will be removed in v3.2 'http_concurrent_limit', // deprecated, will be removed in v3.2 'logs_channel_level', + 'metrics_interval', 'transport_channel_size', 'transport_concurrent_limit', 'tracing', diff --git a/src/sentry/src/Feature.php b/src/sentry/src/Feature.php index 6f052b672..6b0169af5 100644 --- a/src/sentry/src/Feature.php +++ b/src/sentry/src/Feature.php @@ -31,6 +31,22 @@ 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 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); diff --git a/src/sentry/src/Integration.php b/src/sentry/src/Integration.php index 7d934ba68..e26f0e80e 100644 --- a/src/sentry/src/Integration.php +++ b/src/sentry/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/sentry/src/Metrics/Annotation/Counter.php b/src/sentry/src/Metrics/Annotation/Counter.php new file mode 100644 index 000000000..b4cc6aaf5 --- /dev/null +++ b/src/sentry/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 () => TraceMetrics::getInstance()->flush()); + + TraceMetrics::getInstance() + ->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/sentry/src/Metrics/Aspect/HistogramAspect.php b/src/sentry/src/Metrics/Aspect/HistogramAspect.php new file mode 100644 index 000000000..5ea1cb2f1 --- /dev/null +++ b/src/sentry/src/Metrics/Aspect/HistogramAspect.php @@ -0,0 +1,78 @@ +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; + } + + $startAt = microtime(true); + + return tap($proceedingJoinPoint->process(), function () use ($name, $proceedingJoinPoint, $startAt) { + defer(fn () => TraceMetrics::getInstance()->flush()); + + TraceMetrics::getInstance()->distribution( + $name, + (microtime(true) - $startAt) * 1000, + [ + 'class' => $proceedingJoinPoint->className, + 'method' => $proceedingJoinPoint->methodName, + ], + Unit::second() + ); + }); + } + + 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/sentry/src/Metrics/CoroutineServerStats.php b/src/sentry/src/Metrics/CoroutineServerStats.php new file mode 100644 index 000000000..d150366f7 --- /dev/null +++ b/src/sentry/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/sentry/src/Metrics/Event/MetricFactoryReady.php b/src/sentry/src/Metrics/Event/MetricFactoryReady.php new file mode 100644 index 000000000..bffe095d0 --- /dev/null +++ b/src/sentry/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/sentry/src/Metrics/Listener/OnBeforeHandle.php b/src/sentry/src/Metrics/Listener/OnBeforeHandle.php new file mode 100644 index 000000000..b4954ee83 --- /dev/null +++ b/src/sentry/src/Metrics/Listener/OnBeforeHandle.php @@ -0,0 +1,41 @@ + Context::set(Constants::RUN_IN_COMMAND, true), + $event instanceof AfterExecute => Context::destroy(Constants::RUN_IN_COMMAND), + default => null, + }; + } +} diff --git a/src/sentry/src/Metrics/Listener/OnCoroutineServerStart.php b/src/sentry/src/Metrics/Listener/OnCoroutineServerStart.php new file mode 100644 index 000000000..109ca8ea0 --- /dev/null +++ b/src/sentry/src/Metrics/Listener/OnCoroutineServerStart.php @@ -0,0 +1,124 @@ +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()); + + // 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 () => TraceMetrics::getInstance()->flush()); + + $this->trySet('gc_', $metrics, gc_status()); + $this->trySet('', $metrics, getrusage()); + + TraceMetrics::getInstance()->gauge( + 'memory_usage', + (float) memory_get_usage(), + [], + Unit::byte() + ); + TraceMetrics::getInstance()->gauge( + 'memory_peak_usage', + (float) memory_get_peak_usage(), + [], + Unit::byte() + ); + }); + + Coroutine::create(function () use ($timerId) { + CoordinatorManager::until(Constants::WORKER_EXIT)->yield(); + $this->timer->clear($timerId); + }); + } +} diff --git a/src/sentry/src/Metrics/Listener/OnMetricFactoryReady.php b/src/sentry/src/Metrics/Listener/OnMetricFactoryReady.php new file mode 100644 index 000000000..37eb6652f --- /dev/null +++ b/src/sentry/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->isMetricsEnabled()) { + 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 (! Context::get(\FriendsOfHyperf\Sentry\Constants::RUN_IN_COMMAND, false)) { + 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 () => TraceMetrics::getInstance()->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(); + TraceMetrics::getInstance()->gauge( + 'sys_load', + round($load[0] / System::getCpuCoresNum(), 2), + ['worker' => (string) $workerId], + ); + TraceMetrics::getInstance()->gauge( + 'metric_process_memory_usage', + (float) memory_get_usage(), + ['worker' => (string) $workerId], + Unit::byte() + ); + TraceMetrics::getInstance()->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/sentry/src/Metrics/Listener/OnWorkerStart.php b/src/sentry/src/Metrics/Listener/OnWorkerStart.php new file mode 100644 index 000000000..770ef9b8c --- /dev/null +++ b/src/sentry/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->isMetricsEnabled()) { + 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 () => TraceMetrics::getInstance()->flush()); + + $server = $this->container->get(Server::class); + $serverStats = $server->stats(); + $this->trySet('gc_', $metrics, gc_status()); + $this->trySet('', $metrics, getrusage()); + + TraceMetrics::getInstance()->gauge( + 'worker_request_count', + (float) $serverStats['worker_request_count'], + ['worker' => (string) ($event->workerId ?? 0)], + ); + TraceMetrics::getInstance()->gauge( + 'worker_dispatch_count', + (float) $serverStats['worker_dispatch_count'], + ['worker' => (string) ($event->workerId ?? 0)], + ); + TraceMetrics::getInstance()->gauge( + 'memory_usage', + (float) memory_get_usage(), + ['worker' => (string) ($event->workerId ?? 0)], + Unit::byte() + ); + TraceMetrics::getInstance()->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/sentry/src/Metrics/Listener/PoolWatcher.php b/src/sentry/src/Metrics/Listener/PoolWatcher.php new file mode 100644 index 000000000..789c42815 --- /dev/null +++ b/src/sentry/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 () => TraceMetrics::getInstance()->flush()); + + TraceMetrics::getInstance()->gauge( + $this->getPrefix() . '_connections_in_use', + (float) $pool->getCurrentConnections(), + [ + 'pool' => $poolName, + 'worker' => (string) $workerId, + ] + ); + TraceMetrics::getInstance()->gauge( + $this->getPrefix() . '_connections_in_waiting', + (float) $pool->getConnectionsInChannel(), + [ + 'pool' => $poolName, + 'worker' => (string) $workerId, + ] + ); + TraceMetrics::getInstance()->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/sentry/src/Metrics/Listener/QueueWatcher.php b/src/sentry/src/Metrics/Listener/QueueWatcher.php new file mode 100644 index 000000000..22ad95609 --- /dev/null +++ b/src/sentry/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 () => TraceMetrics::getInstance()->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(); + + TraceMetrics::getInstance()->gauge( + 'queue_waiting', + (float) $info['waiting'], + ['queue' => $name] + ); + TraceMetrics::getInstance()->gauge( + 'queue_delayed', + (float) $info['delayed'], + ['queue' => $name] + ); + TraceMetrics::getInstance()->gauge( + 'queue_failed', + (float) $info['failed'], + ['queue' => $name] + ); + TraceMetrics::getInstance()->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/sentry/src/Metrics/Listener/RedisPoolWatcher.php b/src/sentry/src/Metrics/Listener/RedisPoolWatcher.php new file mode 100644 index 000000000..0fbe5c154 --- /dev/null +++ b/src/sentry/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/sentry/src/Metrics/Listener/RequestWatcher.php b/src/sentry/src/Metrics/Listener/RequestWatcher.php new file mode 100644 index 000000000..f707af2f8 --- /dev/null +++ b/src/sentry/src/Metrics/Listener/RequestWatcher.php @@ -0,0 +1,79 @@ +stats->accept_count; + ++$this->stats->request_count; + ++$this->stats->connection_num; + + $request = $event->request; + $startAt = microtime(true); + + Coroutine::defer(function () use ($request, $startAt) { + ++$this->stats->close_count; + ++$this->stats->response_count; + --$this->stats->connection_num; + + TraceMetrics::getInstance()->distribution( + 'http_requests', + microtime(true) - $startAt, + [ + 'request_path' => $this->getPath($request), + 'request_method' => $request->getMethod(), + ] + ); + + TraceMetrics::getInstance()->flush(); + }); + } + } + + 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/sentry/src/Metrics/Traits/MetricSetter.php b/src/sentry/src/Metrics/Traits/MetricSetter.php new file mode 100644 index 000000000..ef839740b --- /dev/null +++ b/src/sentry/src/Metrics/Traits/MetricSetter.php @@ -0,0 +1,33 @@ +gauge( + $metricsKey, + (float) $stats[$key], + ['worker' => (string) $workerId], + $unit + ); + } + } + } +}