From f848978bbca67cde25236b0b6c7339ccc5cac690 Mon Sep 17 00:00:00 2001 From: Deeka Wong <8337659+huangdijia@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:24:20 +0800 Subject: [PATCH] fix(sentry): flush global telemetry buffers periodically --- src/sentry/publish/sentry.php | 3 + src/sentry/src/ConfigProvider.php | 1 + src/sentry/src/Feature.php | 5 ++ .../Listener/TelemetryFlushListener.php | 79 +++++++++++++++++ .../Listener/TelemetryFlushListenerTest.php | 87 +++++++++++++++++++ 5 files changed, 175 insertions(+) create mode 100644 src/sentry/src/Metrics/Listener/TelemetryFlushListener.php create mode 100644 tests/Sentry/Metrics/Listener/TelemetryFlushListenerTest.php diff --git a/src/sentry/publish/sentry.php b/src/sentry/publish/sentry.php index fb99e720a..4377bdd69 100644 --- a/src/sentry/publish/sentry.php +++ b/src/sentry/publish/sentry.php @@ -44,6 +44,9 @@ 'enable_logs' => env('SENTRY_ENABLE_LOGS', true), // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#log_flush_threshold + // Reaching the threshold triggers an automatic flush, and the telemetry + // flush listener also periodically flushes as a fallback. Memory usage + // grows linearly with the threshold, so keep it <= 5000. 'log_flush_threshold' => env('SENTRY_LOG_FLUSH_THRESHOLD') === null ? null : (int) env('SENTRY_LOG_FLUSH_THRESHOLD'), // @see: https://docs.sentry.io/platforms/php/configuration/options/#before_send_log diff --git a/src/sentry/src/ConfigProvider.php b/src/sentry/src/ConfigProvider.php index 4fe185a72..9adf629a9 100644 --- a/src/sentry/src/ConfigProvider.php +++ b/src/sentry/src/ConfigProvider.php @@ -64,6 +64,7 @@ public function __invoke(): array Metrics\Listener\OnCoroutineServerStart::class, Metrics\Listener\OnMetricFactoryReady::class, Metrics\Listener\OnWorkerStart::class, + Metrics\Listener\TelemetryFlushListener::class, Metrics\Listener\QueueWatcher::class, Metrics\Listener\RedisPoolWatcher::class, Metrics\Listener\RequestWatcher::class, diff --git a/src/sentry/src/Feature.php b/src/sentry/src/Feature.php index de7f25995..0b1300ce8 100644 --- a/src/sentry/src/Feature.php +++ b/src/sentry/src/Feature.php @@ -35,6 +35,11 @@ public function isMetricsEnabled(bool $default = true): bool return (bool) $this->config->get('sentry.enable_metrics', $default); } + public function isLogsEnabled(bool $default = true): bool + { + return (bool) $this->config->get('sentry.enable_logs', $default); + } + public function isDefaultMetricsEnabled(bool $default = true): bool { if (! $this->isMetricsEnabled()) { diff --git a/src/sentry/src/Metrics/Listener/TelemetryFlushListener.php b/src/sentry/src/Metrics/Listener/TelemetryFlushListener.php new file mode 100644 index 000000000..af09a6ae7 --- /dev/null +++ b/src/sentry/src/Metrics/Listener/TelemetryFlushListener.php @@ -0,0 +1,79 @@ +timer = $timer ?? new Timer(); + } + + public function listen(): array + { + return [ + MetricFactoryReady::class, + ]; + } + + /** + * @param object|MetricFactoryReady $event + */ + public function process(object $event): void + { + if ($this->ticking) { + return; + } + + if (! $this->feature->isMetricsEnabled() && ! $this->feature->isLogsEnabled()) { + return; + } + + $this->ticking = true; + + $this->timer->tick( + $this->feature->getMetricsInterval(), + function (): void { + // End this tick coroutine's own runtime context (if any), so the + // following flush operations target the global context aggregators. + SentrySdk::endContext(); + + try { + Logs::getInstance()->flush(); + } catch (Throwable) { + } + + try { + TraceMetrics::getInstance()->flush(); + } catch (Throwable) { + } + } + ); + } +} diff --git a/tests/Sentry/Metrics/Listener/TelemetryFlushListenerTest.php b/tests/Sentry/Metrics/Listener/TelemetryFlushListenerTest.php new file mode 100644 index 000000000..b3f1365df --- /dev/null +++ b/tests/Sentry/Metrics/Listener/TelemetryFlushListenerTest.php @@ -0,0 +1,87 @@ + + */ + public array $closures = []; + + public function tick(float $timeout, callable $closure, string $identifier = Constants::WORKER_EXIT): int + { + ++$this->tickCount; + $this->closures[$this->tickCount] = $closure; + + return $this->tickCount; + } +} + +beforeEach(function () { + $this->feature = m::mock(Feature::class); + $this->feature->shouldReceive('getMetricsInterval')->andReturn(10); + + $this->container = m::mock(ContainerInterface::class); + $this->timer = new FakeTimer(); +}); + +afterEach(function () { + m::close(); +}); + +test('process schedules a single tick for repeated calls', function () { + $this->feature->shouldReceive('isMetricsEnabled')->andReturn(true); + $this->feature->shouldReceive('isLogsEnabled')->andReturn(true); + + $listener = new TelemetryFlushListener($this->container, $this->feature, $this->timer); + + $listener->process(new MetricFactoryReady()); + $listener->process(new MetricFactoryReady()); + + expect($this->timer->tickCount)->toBe(1); +}); + +test('saved tick closure can be invoked without throwing', function () { + $this->feature->shouldReceive('isMetricsEnabled')->andReturn(true); + $this->feature->shouldReceive('isLogsEnabled')->andReturn(true); + + $listener = new TelemetryFlushListener($this->container, $this->feature, $this->timer); + $listener->process(new MetricFactoryReady()); + + $closure = $this->timer->closures[1]; + + $closure(false); + + expect(true)->toBeTrue(); +}); + +test('no tick is scheduled when logs and metrics are disabled', function () { + $this->feature->shouldReceive('isMetricsEnabled')->andReturn(false); + $this->feature->shouldReceive('isLogsEnabled')->andReturn(false); + + $listener = new TelemetryFlushListener($this->container, $this->feature, $this->timer); + + $listener->process(new MetricFactoryReady()); + + expect($this->timer->tickCount)->toBe(0); +});