Skip to content

Commit 71c84eb

Browse files
authored
feat(metrics): add metric_flush_threshold flag (#2059)
1 parent f9f8d13 commit 71c84eb

5 files changed

Lines changed: 164 additions & 17 deletions

File tree

src/Metrics/MetricsAggregator.php

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
use Sentry\Tracing\SpanId;
1818
use Sentry\Tracing\TraceId;
1919
use Sentry\Unit;
20-
use Sentry\Util\RingBuffer;
20+
use Sentry\Util\TelemetryStorage;
2121

2222
/**
2323
* @internal
@@ -29,22 +29,17 @@ final class MetricsAggregator
2929
*/
3030
public const METRICS_BUFFER_SIZE = 1000;
3131

32-
/**
33-
* @var RingBuffer<Metric>
34-
*/
35-
private $metrics;
36-
37-
public function __construct()
38-
{
39-
$this->metrics = new RingBuffer(self::METRICS_BUFFER_SIZE);
40-
}
41-
4232
private const METRIC_TYPES = [
4333
CounterMetric::TYPE => CounterMetric::class,
4434
DistributionMetric::TYPE => DistributionMetric::class,
4535
GaugeMetric::TYPE => GaugeMetric::class,
4636
];
4737

38+
/**
39+
* @var TelemetryStorage<Metric>|null
40+
*/
41+
private $metrics;
42+
4843
/**
4944
* @param int|float $value
5045
* @param array<string, int|float|string|bool> $attributes
@@ -58,6 +53,7 @@ public function add(
5853
): void {
5954
$hub = SentrySdk::getCurrentHub();
6055
$client = $hub->getClient();
56+
$metricFlushThreshold = null;
6157

6258
if (!\is_int($value) && !\is_float($value)) {
6359
if ($client !== null) {
@@ -67,20 +63,24 @@ public function add(
6763
return;
6864
}
6965

70-
if ($client instanceof Client) {
66+
if ($client !== null) {
7167
$options = $client->getOptions();
68+
$metricFlushThreshold = $options->getMetricFlushThreshold();
7269

7370
if ($options->getEnableMetrics() === false) {
7471
return;
7572
}
7673

7774
$defaultAttributes = [
78-
'sentry.sdk.name' => $client->getSdkIdentifier(),
79-
'sentry.sdk.version' => $client->getSdkVersion(),
8075
'sentry.environment' => $options->getEnvironment() ?? Event::DEFAULT_ENVIRONMENT,
8176
'server.address' => $options->getServerName(),
8277
];
8378

79+
if ($client instanceof Client) {
80+
$defaultAttributes['sentry.sdk.name'] = $client->getSdkIdentifier();
81+
$defaultAttributes['sentry.sdk.version'] = $client->getSdkVersion();
82+
}
83+
8484
if ($options->shouldSendDefaultPii()) {
8585
$hub->configureScope(static function (Scope $scope) use (&$defaultAttributes) {
8686
$user = $scope->getUser();
@@ -122,12 +122,17 @@ public function add(
122122
}
123123
}
124124

125-
$this->metrics->push($metric);
125+
$metrics = $this->getStorage($metricFlushThreshold);
126+
$metrics->push($metric);
127+
128+
if ($metricFlushThreshold !== null && \count($metrics) >= $metricFlushThreshold) {
129+
$this->flush($hub);
130+
}
126131
}
127132

128133
public function flush(?HubInterface $hub = null): ?EventId
129134
{
130-
if ($this->metrics->isEmpty()) {
135+
if ($this->metrics === null || $this->metrics->isEmpty()) {
131136
return null;
132137
}
133138

@@ -151,4 +156,21 @@ private function getTraceContext(HubInterface $hub): array
151156
/** @var array{trace_id: string, span_id: string} $traceContext */
152157
return $traceContext;
153158
}
159+
160+
/**
161+
* @return TelemetryStorage<Metric>
162+
*/
163+
private function getStorage(?int $metricFlushThreshold = null): TelemetryStorage
164+
{
165+
if ($this->metrics === null) {
166+
/** @var TelemetryStorage<Metric> $metrics */
167+
$metrics = $metricFlushThreshold !== null
168+
? TelemetryStorage::unbounded()
169+
: TelemetryStorage::bounded(self::METRICS_BUFFER_SIZE);
170+
171+
$this->metrics = $metrics;
172+
}
173+
174+
return $this->metrics;
175+
}
154176
}

src/Options.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,32 @@ public function setLogFlushThreshold(?int $logFlushThreshold): self
210210
return $this;
211211
}
212212

213+
/**
214+
* Gets the number of buffered metrics that trigger an immediate flush.
215+
*/
216+
public function getMetricFlushThreshold(): ?int
217+
{
218+
/**
219+
* @var int|null $metricFlushThreshold
220+
*/
221+
$metricFlushThreshold = $this->options['metric_flush_threshold'];
222+
223+
return $metricFlushThreshold;
224+
}
225+
226+
/**
227+
* Sets the number of buffered metrics that trigger an immediate flush.
228+
* null will never trigger an immediate flush.
229+
*/
230+
public function setMetricFlushThreshold(?int $metricFlushThreshold): self
231+
{
232+
$options = array_merge($this->options, ['metric_flush_threshold' => $metricFlushThreshold]);
233+
234+
$this->options = $this->resolver->resolve($options);
235+
236+
return $this;
237+
}
238+
213239
/**
214240
* Sets if metrics should be enabled or not.
215241
*/
@@ -1365,6 +1391,7 @@ private function configureOptions(OptionsResolver $resolver): void
13651391
'enable_logs' => false,
13661392
'log_flush_threshold' => null,
13671393
'enable_metrics' => true,
1394+
'metric_flush_threshold' => null,
13681395
'traces_sample_rate' => null,
13691396
'traces_sampler' => null,
13701397
'profiles_sample_rate' => null,
@@ -1443,6 +1470,7 @@ private function configureOptions(OptionsResolver $resolver): void
14431470
$resolver->setAllowedTypes('enable_logs', 'bool');
14441471
$resolver->setAllowedTypes('log_flush_threshold', ['null', 'int']);
14451472
$resolver->setAllowedTypes('enable_metrics', 'bool');
1473+
$resolver->setAllowedTypes('metric_flush_threshold', ['null', 'int']);
14461474
$resolver->setAllowedTypes('traces_sample_rate', ['null', 'int', 'float']);
14471475
$resolver->setAllowedTypes('traces_sampler', ['null', 'callable']);
14481476
$resolver->setAllowedTypes('profiles_sample_rate', ['null', 'int', 'float']);
@@ -1496,6 +1524,7 @@ private function configureOptions(OptionsResolver $resolver): void
14961524
$resolver->setAllowedValues('class_serializers', \Closure::fromCallable([$this, 'validateClassSerializersOption']));
14971525
$resolver->setAllowedValues('context_lines', \Closure::fromCallable([$this, 'validateContextLinesOption']));
14981526
$resolver->setAllowedValues('log_flush_threshold', \Closure::fromCallable([$this, 'validateLogFlushThresholdOption']));
1527+
$resolver->setAllowedValues('metric_flush_threshold', \Closure::fromCallable([$this, 'validateMetricFlushThresholdOption']));
14991528

15001529
$resolver->setNormalizer('dsn', \Closure::fromCallable([$this, 'normalizeDsnOption']));
15011530

@@ -1671,4 +1700,14 @@ private function validateLogFlushThresholdOption(?int $logFlushThreshold): bool
16711700
{
16721701
return $logFlushThreshold === null || $logFlushThreshold > 0;
16731702
}
1703+
1704+
/**
1705+
* Validates that the value passed to the "metric_flush_threshold" option is valid.
1706+
*
1707+
* @param int|null $metricFlushThreshold The value to validate
1708+
*/
1709+
private function validateMetricFlushThresholdOption(?int $metricFlushThreshold): bool
1710+
{
1711+
return $metricFlushThreshold === null || $metricFlushThreshold > 0;
1712+
}
16741713
}

src/functions.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
* integrations?: IntegrationInterface[]|callable(IntegrationInterface[]): IntegrationInterface[],
5252
* logger?: LoggerInterface|null,
5353
* log_flush_threshold?: int|null,
54+
* metric_flush_threshold?: int|null,
5455
* max_breadcrumbs?: int,
5556
* max_request_body_size?: "none"|"never"|"small"|"medium"|"always",
5657
* max_value_length?: int,

tests/Metrics/TraceMetricsTest.php

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,59 @@ public function testDistributionMetrics(): void
7373
$this->assertArrayHasKey('foo', $metric->getAttributes()->toSimpleArray());
7474
}
7575

76-
public function testMetricsBufferFull(): void
76+
public function testFlushesImmediatelyWhenMetricFlushThresholdIsReached(): void
7777
{
78+
HubAdapter::getInstance()->bindClient(new Client(new Options([
79+
'metric_flush_threshold' => 2,
80+
]), StubTransport::getInstance()));
81+
82+
traceMetrics()->count('first-metric', 1, ['foo' => 'bar']);
83+
84+
$this->assertCount(0, StubTransport::$events);
85+
86+
traceMetrics()->count('second-metric', 2, ['foo' => 'bar']);
87+
88+
$this->assertCount(1, StubTransport::$events);
89+
$event = StubTransport::$events[0];
90+
91+
$this->assertCount(2, $event->getMetrics());
92+
$this->assertSame('first-metric', $event->getMetrics()[0]->getName());
93+
$this->assertSame('second-metric', $event->getMetrics()[1]->getName());
94+
}
95+
96+
public function testDoesNotFlushImmediatelyWhenMetricFlushThresholdIsNull(): void
97+
{
98+
HubAdapter::getInstance()->bindClient(new Client(new Options([
99+
'metric_flush_threshold' => null,
100+
]), StubTransport::getInstance()));
101+
102+
traceMetrics()->count('first-metric', 1, ['foo' => 'bar']);
103+
traceMetrics()->count('second-metric', 2, ['foo' => 'bar']);
104+
105+
$this->assertCount(0, StubTransport::$events);
106+
107+
traceMetrics()->flush();
108+
109+
$this->assertCount(1, StubTransport::$events);
110+
$this->assertCount(2, StubTransport::$events[0]->getMetrics());
111+
}
112+
113+
public function testMetricsBufferFullWhenMetricFlushThresholdIsNull(): void
114+
{
115+
HubAdapter::getInstance()->bindClient(new Client(new Options([
116+
'metric_flush_threshold' => null,
117+
]), StubTransport::getInstance()));
118+
78119
for ($i = 0; $i < MetricsAggregator::METRICS_BUFFER_SIZE + 100; ++$i) {
79120
traceMetrics()->count('test', 1, ['foo' => 'bar']);
80121
}
122+
81123
traceMetrics()->flush();
124+
82125
$this->assertCount(1, StubTransport::$events);
83126
$event = StubTransport::$events[0];
84127
$metrics = $event->getMetrics();
128+
85129
$this->assertCount(MetricsAggregator::METRICS_BUFFER_SIZE, $metrics);
86130
}
87131

tests/OptionsTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,20 @@ public static function optionsDataProvider(): \Generator
114114
'setLogFlushThreshold',
115115
];
116116

117+
yield [
118+
'metric_flush_threshold',
119+
10,
120+
'getMetricFlushThreshold',
121+
'setMetricFlushThreshold',
122+
];
123+
124+
yield [
125+
'metric_flush_threshold',
126+
null,
127+
'getMetricFlushThreshold',
128+
'setMetricFlushThreshold',
129+
];
130+
117131
yield [
118132
'traces_sample_rate',
119133
0.5,
@@ -682,6 +696,33 @@ public static function logFlushThresholdOptionIsValidatedCorrectlyDataProvider()
682696
];
683697
}
684698

699+
/**
700+
* @dataProvider metricFlushThresholdOptionIsValidatedCorrectlyDataProvider
701+
*/
702+
public function testMetricFlushThresholdOptionIsValidatedCorrectly(bool $isValid, $value): void
703+
{
704+
if (!$isValid) {
705+
$this->expectException(InvalidOptionsException::class);
706+
}
707+
708+
$options = new Options(['metric_flush_threshold' => $value]);
709+
710+
$this->assertSame($value, $options->getMetricFlushThreshold());
711+
}
712+
713+
public static function metricFlushThresholdOptionIsValidatedCorrectlyDataProvider(): array
714+
{
715+
return [
716+
[false, -1],
717+
[false, 0],
718+
[true, 1],
719+
[true, 10],
720+
[true, null],
721+
[false, 'string'],
722+
[false, '1'],
723+
];
724+
}
725+
685726
/**
686727
* @backupGlobals enabled
687728
*/

0 commit comments

Comments
 (0)