Skip to content

Commit 75284dc

Browse files
authored
Improve SentryLogBatcher flush logic (#3211)
1 parent 0a52e23 commit 75284dc

File tree

7 files changed

+218
-61
lines changed

7 files changed

+218
-61
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
- Call options.log for structured logs ([#3187](https://github.com/getsentry/sentry-dart/pull/3187))
2424
- Remove async usage from `FlutterErrorIntegration` ([#3202](https://github.com/getsentry/sentry-dart/pull/3202))
2525
- Tag all spans during app start with start type info ([#3190](https://github.com/getsentry/sentry-dart/pull/3190))
26+
- Improve `SentryLogBatcher` flush logic ([#3211](https://github.com/getsentry/sentry-dart/pull/3187))
2627

2728
### Dependencies
2829

packages/dart/lib/src/sentry_envelope.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:convert';
2+
import 'dart:typed_data';
23

34
import 'client_reports/client_report.dart';
45
import 'protocol.dart';
@@ -9,6 +10,7 @@ import 'sentry_item_type.dart';
910
import 'sentry_options.dart';
1011
import 'sentry_trace_context_header.dart';
1112
import 'utils.dart';
13+
import 'package:meta/meta.dart';
1214

1315
/// Class representation of `Envelope` file.
1416
class SentryEnvelope {
@@ -96,6 +98,37 @@ class SentryEnvelope {
9698
);
9799
}
98100

101+
/// Create a [SentryEnvelope] containing raw log data payload.
102+
/// This is used by the log batcher to send pre-encoded log batches.
103+
@internal
104+
factory SentryEnvelope.fromLogsData(
105+
List<List<int>> encodedLogs,
106+
SdkVersion sdkVersion,
107+
) {
108+
// Create the payload in the format expected by Sentry
109+
// Format: {"items": [log1, log2, ...]}
110+
final builder = BytesBuilder(copy: false);
111+
builder.add(utf8.encode('{"items":['));
112+
for (int i = 0; i < encodedLogs.length; i++) {
113+
if (i > 0) {
114+
builder.add(utf8.encode(','));
115+
}
116+
builder.add(encodedLogs[i]);
117+
}
118+
builder.add(utf8.encode(']}'));
119+
120+
return SentryEnvelope(
121+
SentryEnvelopeHeader(
122+
null,
123+
sdkVersion,
124+
),
125+
[
126+
SentryEnvelopeItem.fromLogsData(
127+
builder.takeBytes(), encodedLogs.length),
128+
],
129+
);
130+
}
131+
99132
/// Stream binary data representation of `Envelope` file encoded.
100133
Stream<List<int>> envelopeStream(SentryOptions options) async* {
101134
yield utf8JsonEncoder.convert(header.toJson());

packages/dart/lib/src/sentry_envelope_item.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'sentry_attachment/sentry_attachment.dart';
66
import 'sentry_envelope_item_header.dart';
77
import 'sentry_item_type.dart';
88
import 'utils.dart';
9+
import 'package:meta/meta.dart';
910

1011
/// Item holding header information and JSON encoded data.
1112
class SentryEnvelopeItem {
@@ -78,6 +79,21 @@ class SentryEnvelopeItem {
7879
);
7980
}
8081

82+
/// Create a [SentryEnvelopeItem] which holds pre-encoded log data.
83+
/// This is used by the log batcher to send pre-encoded log batches.
84+
@internal
85+
factory SentryEnvelopeItem.fromLogsData(List<int> payload, int logsCount) {
86+
return SentryEnvelopeItem(
87+
SentryEnvelopeItemHeader(
88+
SentryItemType.log,
89+
itemCount: logsCount,
90+
contentType: 'application/vnd.sentry.items.log+json',
91+
),
92+
() => payload,
93+
originalObject: null,
94+
);
95+
}
96+
8197
/// Header with info about type and length of data in bytes.
8298
final SentryEnvelopeItemHeader header;
8399

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,97 @@
11
import 'dart:async';
2-
import 'sentry_envelope.dart';
32
import 'sentry_options.dart';
43
import 'protocol/sentry_log.dart';
4+
import 'protocol/sentry_level.dart';
5+
import 'sentry_envelope.dart';
6+
import 'utils.dart';
57
import 'package:meta/meta.dart';
68

79
@internal
810
class SentryLogBatcher {
9-
SentryLogBatcher(this._options, {Duration? flushTimeout, int? maxBufferSize})
10-
: _flushTimeout = flushTimeout ?? Duration(seconds: 5),
11-
_maxBufferSize = maxBufferSize ?? 100;
11+
SentryLogBatcher(
12+
this._options, {
13+
Duration? flushTimeout,
14+
int? maxBufferSizeBytes,
15+
}) : _flushTimeout = flushTimeout ?? Duration(seconds: 5),
16+
_maxBufferSizeBytes = maxBufferSizeBytes ??
17+
1024 * 1024; // 1MB default per BatchProcessor spec
1218

1319
final SentryOptions _options;
1420
final Duration _flushTimeout;
15-
final int _maxBufferSize;
21+
final int _maxBufferSizeBytes;
1622

17-
final _logBuffer = <SentryLog>[];
23+
// Store encoded log data instead of raw logs to avoid re-serialization
24+
final List<List<int>> _encodedLogs = [];
25+
int _encodedLogsSize = 0;
1826

1927
Timer? _flushTimer;
2028

29+
/// Adds a log to the buffer.
2130
void addLog(SentryLog log) {
22-
_logBuffer.add(log);
31+
try {
32+
final encodedLog = utf8JsonEncoder.convert(log.toJson());
2333

24-
_flushTimer?.cancel();
34+
_encodedLogs.add(encodedLog);
35+
_encodedLogsSize += encodedLog.length;
2536

26-
if (_logBuffer.length >= _maxBufferSize) {
27-
return flush();
28-
} else {
29-
_flushTimer = Timer(_flushTimeout, flush);
37+
// Flush if size threshold is reached
38+
if (_encodedLogsSize >= _maxBufferSizeBytes) {
39+
// Buffer size exceeded, flush immediately
40+
_performFlushLogs();
41+
} else if (_flushTimer == null) {
42+
// Start timeout only when first item is added
43+
_startTimer();
44+
}
45+
// Note: We don't restart the timer on subsequent additions per spec
46+
} catch (error) {
47+
_options.log(
48+
SentryLevel.error,
49+
'Failed to encode log: $error',
50+
);
3051
}
3152
}
3253

54+
/// Flushes the buffer immediately, sending all buffered logs.
3355
void flush() {
56+
_performFlushLogs();
57+
}
58+
59+
void _startTimer() {
60+
_flushTimer = Timer(_flushTimeout, () {
61+
_options.log(
62+
SentryLevel.debug,
63+
'SentryLogBatcher: Timer fired, calling performCaptureLogs().',
64+
);
65+
_performFlushLogs();
66+
});
67+
}
68+
69+
void _performFlushLogs() {
70+
// Reset timer state first
3471
_flushTimer?.cancel();
3572
_flushTimer = null;
3673

37-
final logs = List<SentryLog>.from(_logBuffer);
38-
_logBuffer.clear();
74+
// Reset buffer on function exit
75+
final logsToSend = List<List<int>>.from(_encodedLogs);
76+
_encodedLogs.clear();
77+
_encodedLogsSize = 0;
3978

40-
if (logs.isEmpty) {
79+
if (logsToSend.isEmpty) {
80+
_options.log(
81+
SentryLevel.debug,
82+
'SentryLogBatcher: No logs to flush.',
83+
);
4184
return;
4285
}
4386

44-
final envelope = SentryEnvelope.fromLogs(
45-
logs,
46-
_options.sdk,
47-
);
48-
49-
// TODO: Make sure the Android SDK understands the log envelope type.
50-
_options.transport.send(envelope);
87+
try {
88+
final envelope = SentryEnvelope.fromLogsData(logsToSend, _options.sdk);
89+
_options.transport.send(envelope);
90+
} catch (error) {
91+
_options.log(
92+
SentryLevel.error,
93+
'Failed to send batched logs: $error',
94+
);
95+
}
5196
}
5297
}

packages/dart/test/sentry_envelope_item_test.dart

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,32 @@ void main() {
130130

131131
expect(sut.header.contentType, 'application/vnd.sentry.items.log+json');
132132
expect(sut.header.type, SentryItemType.log);
133+
expect(sut.header.itemCount, 2);
133134
expect(actualData, expectedData);
134135
});
136+
137+
test('fromLogsData', () async {
138+
final payload =
139+
utf8.encode('{"items":[{"test":"data1"},{"test":"data2"}]');
140+
final logsCount = 2;
141+
142+
final sut = SentryEnvelopeItem.fromLogsData(payload, logsCount);
143+
144+
expect(sut.header.contentType, 'application/vnd.sentry.items.log+json');
145+
expect(sut.header.type, SentryItemType.log);
146+
expect(sut.header.itemCount, logsCount);
147+
148+
final actualData = await sut.dataFactory();
149+
expect(actualData, payload);
150+
});
151+
152+
test('fromLogsData null original object', () async {
153+
final payload = utf8.encode('{"items":[{"test":"data"}]}');
154+
final logsCount = 1;
155+
156+
final sut = SentryEnvelopeItem.fromLogsData(payload, logsCount);
157+
158+
expect(sut.originalObject, null);
159+
});
135160
});
136161
}

packages/dart/test/sentry_envelope_test.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,42 @@ void main() {
173173
expect(actualItemData, expectedItemData);
174174
});
175175

176+
test('fromLogsData', () async {
177+
final encodedLogs = [
178+
utf8.encode(
179+
'{"timestamp":"2023-01-01T00:00:00.000Z","level":"info","body":"test1","attributes":{}}'),
180+
utf8.encode(
181+
'{"timestamp":"2023-01-01T00:00:01.000Z","level":"info","body":"test2","attributes":{}}'),
182+
];
183+
184+
final sdkVersion =
185+
SdkVersion(name: 'fixture-name', version: 'fixture-version');
186+
final sut = SentryEnvelope.fromLogsData(encodedLogs, sdkVersion);
187+
188+
expect(sut.header.eventId, null);
189+
expect(sut.header.sdkVersion, sdkVersion);
190+
expect(sut.items.length, 1);
191+
192+
final expectedEnvelopeItem = SentryEnvelopeItem.fromLogsData(
193+
// The envelope should create the final payload with {"items": [...]} wrapper
194+
utf8.encode('{"items":[') +
195+
encodedLogs[0] +
196+
utf8.encode(',') +
197+
encodedLogs[1] +
198+
utf8.encode(']}'),
199+
2, // logsCount
200+
);
201+
202+
expect(sut.items[0].header.contentType,
203+
expectedEnvelopeItem.header.contentType);
204+
expect(sut.items[0].header.type, expectedEnvelopeItem.header.type);
205+
expect(sut.items[0].header.itemCount, 2);
206+
207+
final actualItem = await sut.items[0].dataFactory();
208+
final expectedItem = await expectedEnvelopeItem.dataFactory();
209+
expect(actualItem, expectedItem);
210+
});
211+
176212
test('max attachment size', () async {
177213
final attachment = SentryAttachment.fromLoader(
178214
loader: () => Uint8List.fromList([1, 2, 3, 4]),

0 commit comments

Comments
 (0)