Skip to content

Commit 592c046

Browse files
authored
Logs: Integrate in Sentry Client (#2920)
1 parent 09f8959 commit 592c046

20 files changed

+638
-17
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
### Features
66

77
- Logs: Models & Envelopes ([#2916](https://github.com/getsentry/sentry-dart/pull/2916))
8+
- Logs: Integrate in Sentry Client ([#2920](https://github.com/getsentry/sentry-dart/pull/2920))
9+
- [Structured Logs]: Buffering and Flushing of Logs ([#2930](https://github.com/getsentry/sentry-dart/pull/2930))
810

911
## 9.0.0-beta.2
1012

dart/lib/src/noop_log_batcher.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import 'dart:async';
2+
3+
import 'sentry_log_batcher.dart';
4+
import 'protocol/sentry_log.dart';
5+
6+
class NoopLogBatcher implements SentryLogBatcher {
7+
@override
8+
FutureOr<void> addLog(SentryLog log) {}
9+
10+
@override
11+
Future<void> flush() async {}
12+
}

dart/lib/src/noop_sentry_client.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,7 @@ class NoOpSentryClient implements SentryClient {
6666
Future<SentryId> captureFeedback(SentryFeedback feedback,
6767
{Scope? scope, Hint? hint}) async =>
6868
SentryId.empty();
69+
70+
@override
71+
Future<void> captureLog(SentryLog log, {Scope? scope}) async {}
6972
}

dart/lib/src/protocol/sentry_log.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ class SentryLog {
1010
Map<String, SentryLogAttribute> attributes;
1111
int? severityNumber;
1212

13+
/// The traceId is initially an empty default value and is populated during event processing;
14+
/// by the time processing completes, it is guaranteed to be a valid non-empty trace id.
1315
SentryLog({
1416
required this.timestamp,
15-
required this.traceId,
17+
SentryId? traceId,
1618
required this.level,
1719
required this.body,
1820
required this.attributes,
1921
this.severityNumber,
20-
});
22+
}) : traceId = traceId ?? SentryId.empty();
2123

2224
Map<String, dynamic> toJson() {
2325
return {

dart/lib/src/protocol/sentry_log_attribute.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@ class SentryLogAttribute {
88
return SentryLogAttribute._(value, 'string');
99
}
1010

11-
factory SentryLogAttribute.boolean(bool value) {
11+
factory SentryLogAttribute.bool(bool value) {
1212
return SentryLogAttribute._(value, 'boolean');
1313
}
1414

15-
factory SentryLogAttribute.integer(int value) {
15+
factory SentryLogAttribute.int(int value) {
1616
return SentryLogAttribute._(value, 'integer');
1717
}
1818

1919
factory SentryLogAttribute.double(double value) {
2020
return SentryLogAttribute._(value, 'double');
2121
}
2222

23-
// In the future the SDK will also support string[], boolean[], integer[], double[] values.
23+
// In the future the SDK will also support List<String>, List<bool>, List<int>, List<double> values.
2424
Map<String, dynamic> toJson() {
2525
return {
2626
'value': value,

dart/lib/src/sentry_client.dart

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import 'type_check_hint.dart';
2727
import 'utils/isolate_utils.dart';
2828
import 'utils/regex_utils.dart';
2929
import 'utils/stacktrace_utils.dart';
30+
import 'sentry_log_batcher.dart';
3031
import 'version.dart';
3132

3233
/// Default value for [SentryUser.ipAddress]. It gets set when an event does not have
@@ -75,6 +76,9 @@ class SentryClient {
7576
if (enableFlutterSpotlight) {
7677
options.transport = SpotlightHttpTransport(options, options.transport);
7778
}
79+
if (options.enableLogs) {
80+
options.logBatcher = SentryLogBatcher(options);
81+
}
7882
return SentryClient._(options);
7983
}
8084

@@ -485,6 +489,73 @@ class SentryClient {
485489
);
486490
}
487491

492+
@internal
493+
Future<void> captureLog(
494+
SentryLog log, {
495+
Scope? scope,
496+
}) async {
497+
if (!_options.enableLogs) {
498+
return;
499+
}
500+
501+
log.attributes['sentry.sdk.name'] = SentryLogAttribute.string(
502+
_options.sdk.name,
503+
);
504+
log.attributes['sentry.sdk.version'] = SentryLogAttribute.string(
505+
_options.sdk.version,
506+
);
507+
final environment = _options.environment;
508+
if (environment != null) {
509+
log.attributes['sentry.environment'] = SentryLogAttribute.string(
510+
environment,
511+
);
512+
}
513+
final release = _options.release;
514+
if (release != null) {
515+
log.attributes['sentry.release'] = SentryLogAttribute.string(
516+
release,
517+
);
518+
}
519+
520+
final propagationContext = scope?.propagationContext;
521+
if (propagationContext != null) {
522+
log.traceId = propagationContext.traceId;
523+
}
524+
final span = scope?.span;
525+
if (span != null) {
526+
log.attributes['sentry.trace.parent_span_id'] = SentryLogAttribute.string(
527+
span.context.spanId.toString(),
528+
);
529+
}
530+
531+
final beforeSendLog = _options.beforeSendLog;
532+
SentryLog? processedLog = log;
533+
if (beforeSendLog != null) {
534+
try {
535+
final callbackResult = beforeSendLog(log);
536+
537+
if (callbackResult is Future<SentryLog?>) {
538+
processedLog = await callbackResult;
539+
} else {
540+
processedLog = callbackResult;
541+
}
542+
} catch (exception, stackTrace) {
543+
_options.logger(
544+
SentryLevel.error,
545+
'The beforeSendLog callback threw an exception',
546+
exception: exception,
547+
stackTrace: stackTrace,
548+
);
549+
if (_options.automatedTestMode) {
550+
rethrow;
551+
}
552+
}
553+
}
554+
if (processedLog != null) {
555+
_options.logBatcher.addLog(processedLog);
556+
}
557+
}
558+
488559
void close() {
489560
_options.httpClient.close();
490561
}

dart/lib/src/sentry_log_batcher.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import 'dart:async';
2+
import 'sentry_envelope.dart';
3+
import 'sentry_options.dart';
4+
import 'protocol/sentry_log.dart';
5+
import 'package:meta/meta.dart';
6+
7+
@internal
8+
class SentryLogBatcher {
9+
SentryLogBatcher(this._options, {Duration? flushTimeout, int? maxBufferSize})
10+
: _flushTimeout = flushTimeout ?? Duration(seconds: 5),
11+
_maxBufferSize = maxBufferSize ?? 100;
12+
13+
final SentryOptions _options;
14+
final Duration _flushTimeout;
15+
final int _maxBufferSize;
16+
17+
final _logBuffer = <SentryLog>[];
18+
19+
Timer? _flushTimer;
20+
21+
void addLog(SentryLog log) {
22+
_logBuffer.add(log);
23+
24+
_flushTimer?.cancel();
25+
26+
if (_logBuffer.length >= _maxBufferSize) {
27+
return flush();
28+
} else {
29+
_flushTimer = Timer(_flushTimeout, flush);
30+
}
31+
}
32+
33+
void flush() {
34+
_flushTimer?.cancel();
35+
_flushTimer = null;
36+
37+
final logs = List<SentryLog>.from(_logBuffer);
38+
_logBuffer.clear();
39+
40+
if (logs.isEmpty) {
41+
return;
42+
}
43+
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);
51+
}
52+
}

dart/lib/src/sentry_options.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import 'sentry_exception_factory.dart';
1515
import 'sentry_stack_trace_factory.dart';
1616
import 'transport/noop_transport.dart';
1717
import 'version.dart';
18+
import 'sentry_log_batcher.dart';
19+
import 'noop_log_batcher.dart';
1820

1921
// TODO: shutdownTimeout, flushTimeoutMillis
2022
// https://api.dart.dev/stable/2.10.2/dart-io/HttpClient/close.html doesn't have a timeout param, we'd need to implement manually
@@ -198,6 +200,10 @@ class SentryOptions {
198200
/// Can return true to emit the metric, or false to drop it.
199201
BeforeMetricCallback? beforeMetricCallback;
200202

203+
/// This function is called right before a log is about to be sent.
204+
/// Can return a modified log or null to drop the log.
205+
BeforeSendLogCallback? beforeSendLog;
206+
201207
/// Sets the release. SDK will try to automatically configure a release out of the box
202208
/// See [docs for further information](https://docs.sentry.io/platforms/flutter/configuration/releases/)
203209
String? release;
@@ -531,6 +537,14 @@ class SentryOptions {
531537
/// This is opt-in, as it can lead to existing exception beeing grouped as new ones.
532538
bool groupExceptions = false;
533539

540+
/// Enable to capture and send logs to Sentry.
541+
///
542+
/// Disabled by default.
543+
bool enableLogs = false;
544+
545+
@internal
546+
SentryLogBatcher logBatcher = NoopLogBatcher();
547+
534548
SentryOptions({String? dsn, Platform? platform, RuntimeChecker? checker}) {
535549
this.dsn = dsn;
536550
if (platform != null) {
@@ -660,6 +674,10 @@ typedef BeforeMetricCallback = bool Function(
660674
Map<String, String>? tags,
661675
});
662676

677+
/// This function is called right before a log is about to be sent.
678+
/// Can return a modified log or null to drop the log.
679+
typedef BeforeSendLogCallback = FutureOr<SentryLog?> Function(SentryLog log);
680+
663681
/// Used to provide timestamp for logging.
664682
typedef ClockProvider = DateTime Function();
665683

dart/test/mocks/mock_log_batcher.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'dart:async';
2+
3+
import 'package:sentry/src/protocol/sentry_log.dart';
4+
import 'package:sentry/src/sentry_log_batcher.dart';
5+
6+
class MockLogBatcher implements SentryLogBatcher {
7+
final addLogCalls = <SentryLog>[];
8+
final flushCalls = <void>[];
9+
10+
@override
11+
FutureOr<void> addLog(SentryLog log) {
12+
addLogCalls.add(log);
13+
}
14+
15+
@override
16+
Future<void> flush() async {
17+
flushCalls.add(null);
18+
}
19+
}

dart/test/mocks/mock_sentry_client.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient {
88
List<CaptureMessageCall> captureMessageCalls = [];
99
List<CaptureEnvelopeCall> captureEnvelopeCalls = [];
1010
List<CaptureTransactionCall> captureTransactionCalls = [];
11-
1211
List<CaptureFeedbackCall> captureFeedbackCalls = [];
12+
List<CaptureLogCall> captureLogCalls = [];
1313
int closeCalls = 0;
1414

1515
@override
@@ -84,6 +84,11 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient {
8484
return SentryId.newId();
8585
}
8686

87+
@override
88+
Future<void> captureLog(SentryLog log, {Scope? scope}) async {
89+
captureLogCalls.add(CaptureLogCall(log, scope));
90+
}
91+
8792
@override
8893
void close() {
8994
closeCalls = closeCalls + 1;
@@ -173,3 +178,10 @@ class CaptureTransactionCall {
173178

174179
CaptureTransactionCall(this.transaction, this.traceContext, this.hint);
175180
}
181+
182+
class CaptureLogCall {
183+
final SentryLog log;
184+
final Scope? scope;
185+
186+
CaptureLogCall(this.log, this.scope);
187+
}

dart/test/mocks/mock_transport.dart

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class MockTransport implements Transport {
77
List<SentryEnvelope> envelopes = [];
88
List<SentryEvent> events = [];
99
List<String> statsdItems = [];
10+
List<Map<String, dynamic>> logs = [];
1011

1112
int _calls = 0;
1213
String _exceptions = '';
@@ -31,7 +32,7 @@ class MockTransport implements Transport {
3132
try {
3233
envelopes.add(envelope);
3334
if (parseFromEnvelope) {
34-
await _eventFromEnvelope(envelope);
35+
await _parseEnvelope(envelope);
3536
}
3637
} catch (e, stack) {
3738
_exceptions += '$e\n$stack\n\n';
@@ -41,14 +42,21 @@ class MockTransport implements Transport {
4142
return envelope.header.eventId ?? SentryId.empty();
4243
}
4344

44-
Future<void> _eventFromEnvelope(SentryEnvelope envelope) async {
45+
Future<void> _parseEnvelope(SentryEnvelope envelope) async {
4546
final RegExp statSdRegex = RegExp('^(?!{).+@.+:.+\\|.+', multiLine: true);
4647

4748
final envelopeItemData = await envelope.items.first.dataFactory();
4849
final envelopeItem = utf8.decode(envelopeItemData);
4950

5051
if (statSdRegex.hasMatch(envelopeItem)) {
5152
statsdItems.add(envelopeItem);
53+
} else if (envelopeItem.contains('items') &&
54+
envelopeItem.contains('timestamp') &&
55+
envelopeItem.contains('trace_id') &&
56+
envelopeItem.contains('level') &&
57+
envelopeItem.contains('body')) {
58+
final envelopeItemJson = jsonDecode(envelopeItem) as Map<String, dynamic>;
59+
logs.add(envelopeItemJson);
5260
} else {
5361
final envelopeItemJson = jsonDecode(envelopeItem) as Map<String, dynamic>;
5462
events.add(SentryEvent.fromJson(envelopeItemJson));

dart/test/protocol/sentry_log_attribute_test.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ void main() {
1111
});
1212
});
1313

14-
test('$SentryLogAttribute boolean to json', () {
15-
final attribute = SentryLogAttribute.boolean(true);
14+
test('$SentryLogAttribute bool to json', () {
15+
final attribute = SentryLogAttribute.bool(true);
1616
final json = attribute.toJson();
1717
expect(json, {
1818
'value': true,
1919
'type': 'boolean',
2020
});
2121
});
2222

23-
test('$SentryLogAttribute integer to json', () {
24-
final attribute = SentryLogAttribute.integer(1);
23+
test('$SentryLogAttribute int to json', () {
24+
final attribute = SentryLogAttribute.int(1);
2525
final json = attribute.toJson();
2626

2727
expect(json, {

dart/test/protocol/sentry_log_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ void main() {
1313
body: 'fixture-body',
1414
attributes: {
1515
'test': SentryLogAttribute.string('fixture-test'),
16-
'test2': SentryLogAttribute.boolean(true),
17-
'test3': SentryLogAttribute.integer(9001),
16+
'test2': SentryLogAttribute.bool(true),
17+
'test3': SentryLogAttribute.int(9001),
1818
'test4': SentryLogAttribute.double(9000.1),
1919
},
2020
severityNumber: 1,

0 commit comments

Comments
 (0)