Skip to content

Commit f9f8d13

Browse files
authored
ref(logs): use RingBuffer for logs when not using log_flush_threshold (#2058)
1 parent 4489a03 commit f9f8d13

3 files changed

Lines changed: 216 additions & 10 deletions

File tree

src/Logs/LogsAggregator.php

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,19 @@
1313
use Sentry\State\Scope;
1414
use Sentry\Util\Arr;
1515
use Sentry\Util\Str;
16+
use Sentry\Util\TelemetryStorage;
1617

1718
/**
1819
* @internal
1920
*/
2021
final class LogsAggregator
2122
{
23+
private const LOGS_BUFFER_SIZE = 1000;
24+
2225
/**
23-
* @var Log[]
26+
* @var TelemetryStorage<Log>|null
2427
*/
25-
private $logs = [];
28+
private $logs;
2629

2730
/**
2831
* @param string $message see sprintf for a description of format
@@ -155,25 +158,24 @@ public function add(
155158
$sdkLogger->log($log->getPsrLevel(), "Logs item: {$log->getBody()}", $log->attributes()->toSimpleArray());
156159
}
157160

158-
$this->logs[] = $log;
159-
160161
$logFlushThreshold = $options->getLogFlushThreshold();
162+
$logs = $this->getStorage($logFlushThreshold);
161163

162-
if ($logFlushThreshold !== null && \count($this->logs) >= $logFlushThreshold) {
164+
$logs->push($log);
165+
166+
if ($logFlushThreshold !== null && \count($logs) >= $logFlushThreshold) {
163167
$this->flush($hub);
164168
}
165169
}
166170

167171
public function flush(?HubInterface $hub = null): ?EventId
168172
{
169-
if (empty($this->logs)) {
173+
if ($this->logs === null || $this->logs->isEmpty()) {
170174
return null;
171175
}
172176

173177
$hub = $hub ?? SentrySdk::getCurrentHub();
174-
$event = Event::createLogs()->setLogs($this->logs);
175-
176-
$this->logs = [];
178+
$event = Event::createLogs()->setLogs($this->logs->drain());
177179

178180
return $hub->captureEvent($event);
179181
}
@@ -183,7 +185,7 @@ public function flush(?HubInterface $hub = null): ?EventId
183185
*/
184186
public function all(): array
185187
{
186-
return $this->logs;
188+
return $this->logs !== null ? $this->logs->toArray() : [];
187189
}
188190

189191
/**
@@ -223,4 +225,21 @@ private function getTraceData(HubInterface $hub): array
223225
/** @var array{trace_id: string, parent_span_id: string|null} $traceData */
224226
return $traceData;
225227
}
228+
229+
/**
230+
* @return TelemetryStorage<Log>
231+
*/
232+
private function getStorage(?int $logFlushThreshold = null): TelemetryStorage
233+
{
234+
if ($this->logs === null) {
235+
/** @var TelemetryStorage<Log> $logs */
236+
$logs = $logFlushThreshold !== null
237+
? TelemetryStorage::unbounded()
238+
: TelemetryStorage::bounded(self::LOGS_BUFFER_SIZE);
239+
240+
$this->logs = $logs;
241+
}
242+
243+
return $this->logs;
244+
}
226245
}

src/Util/TelemetryStorage.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Util;
6+
7+
/**
8+
* Creates a new data container for Telemetry data such as Logs or Metrics.
9+
* If a size parameter is passed, it will create a RingBuffer under the hood to restrict the number of
10+
* items, if no size is used then it will be backed by a regular array.
11+
*
12+
* The TelemetryStorage operates under the same constraints as the RingBuffer, meaning that it's possible to
13+
* add/remove from the front and the back, but it's not possible to remove from the middle or based on an offset.
14+
* To do that, one has to either drain or convert it into an array (which will be basically free if unbounded)
15+
*
16+
* @template T
17+
*
18+
* @internal
19+
*/
20+
class TelemetryStorage implements \Countable
21+
{
22+
/**
23+
* @var T[]|RingBuffer<T>
24+
*/
25+
private $data;
26+
27+
private function __construct(?int $size = null)
28+
{
29+
if ($size !== null) {
30+
$this->data = new RingBuffer($size);
31+
} else {
32+
$this->data = [];
33+
}
34+
}
35+
36+
public function count(): int
37+
{
38+
return \count($this->data);
39+
}
40+
41+
/**
42+
* @param T $value
43+
*/
44+
public function push($value): void
45+
{
46+
if ($this->data instanceof RingBuffer) {
47+
$this->data->push($value);
48+
} else {
49+
$this->data[] = $value;
50+
}
51+
}
52+
53+
/**
54+
* @return T[]
55+
*/
56+
public function drain(): array
57+
{
58+
if ($this->data instanceof RingBuffer) {
59+
return $this->data->drain();
60+
}
61+
$data = $this->data;
62+
$this->data = [];
63+
64+
return $data;
65+
}
66+
67+
/**
68+
* @return T[]
69+
*/
70+
public function toArray(): array
71+
{
72+
if ($this->data instanceof RingBuffer) {
73+
return $this->data->toArray();
74+
}
75+
76+
return $this->data;
77+
}
78+
79+
public function isEmpty(): bool
80+
{
81+
if ($this->data instanceof RingBuffer) {
82+
return $this->data->isEmpty();
83+
}
84+
85+
return empty($this->data);
86+
}
87+
88+
/**
89+
* Creates a new TelemetryStorage that is not bounded in size. This version should only be used if there
90+
* is another flushing signal available.
91+
*
92+
* @return self<T>
93+
*/
94+
public static function unbounded(): self
95+
{
96+
return new self();
97+
}
98+
99+
/**
100+
* Creates a TelemetryStorage that has an upper bound of $size. It will drop the oldest items when new items
101+
* are added while being at capacity.
102+
*
103+
* @return self<T>
104+
*/
105+
public static function bounded(int $size): self
106+
{
107+
return new self($size);
108+
}
109+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Tests\Util;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Sentry\Util\TelemetryStorage;
9+
10+
final class TelemetryStorageTest extends TestCase
11+
{
12+
public function testUnboundedPushAndToArray(): void
13+
{
14+
$storage = TelemetryStorage::unbounded();
15+
$storage->push('foo');
16+
$storage->push('bar');
17+
18+
$result = $storage->toArray();
19+
$this->assertSame(2, $storage->count());
20+
$this->assertEquals(['foo', 'bar'], $result);
21+
}
22+
23+
public function testUnboundedDrainClearsStorage(): void
24+
{
25+
$storage = TelemetryStorage::unbounded();
26+
$storage->push('foo');
27+
$storage->push('bar');
28+
29+
$this->assertSame(2, $storage->count());
30+
$result = $storage->drain();
31+
$this->assertTrue($storage->isEmpty());
32+
$this->assertEquals(['foo', 'bar'], $result);
33+
}
34+
35+
public function testUnboundedIsEmpty(): void
36+
{
37+
$storage = TelemetryStorage::unbounded();
38+
$this->assertTrue($storage->isEmpty());
39+
40+
$storage->push('foo');
41+
42+
$this->assertFalse($storage->isEmpty());
43+
}
44+
45+
public function testBoundedCapacityOverwritesOldestItems(): void
46+
{
47+
$storage = TelemetryStorage::bounded(2);
48+
$storage->push('foo');
49+
$storage->push('bar');
50+
$storage->push('baz');
51+
52+
$this->assertSame(2, $storage->count());
53+
$this->assertEquals(['bar', 'baz'], $storage->toArray());
54+
}
55+
56+
public function testBoundedDrainReturnsLogicalOrderAndClearsStorage(): void
57+
{
58+
$storage = TelemetryStorage::bounded(2);
59+
$storage->push('foo');
60+
$storage->push('bar');
61+
$storage->push('baz');
62+
63+
$this->assertSame(2, $storage->count());
64+
$result = $storage->drain();
65+
$this->assertTrue($storage->isEmpty());
66+
$this->assertEquals(['bar', 'baz'], $result);
67+
}
68+
69+
public function testBoundedCapacityOneKeepsLatestItem(): void
70+
{
71+
$storage = TelemetryStorage::bounded(1);
72+
$storage->push('foo');
73+
$storage->push('bar');
74+
75+
$this->assertCount(1, $storage);
76+
$this->assertEquals(['bar'], $storage->toArray());
77+
}
78+
}

0 commit comments

Comments
 (0)