Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2f4f785
add log models
denrase May 7, 2025
20506d4
Log in envelope
denrase May 7, 2025
c5f5d6e
add type annotation
denrase May 7, 2025
1a2c038
add cl entry
denrase May 7, 2025
16c4b42
remove unused imports
denrase May 7, 2025
f4ec20b
capture log in client
denrase May 7, 2025
9bf5514
add severity number
denrase May 7, 2025
9cb7821
remove parent SnetryLog, Rename SentryLogItem to SentryLog
denrase May 7, 2025
f50a6ce
Merge branch 'feat/logs-envelope' into feat/logs-client
denrase May 7, 2025
3dee73c
update from model feedback
denrase May 7, 2025
6c74691
add comment to attrib
denrase May 7, 2025
2b9b62e
Merge branch 'feat/logs-envelope' into feat/logs-client
denrase May 7, 2025
ea79978
infer severity number from level
denrase May 7, 2025
39f4130
make default constructor provate for attribute
denrase May 7, 2025
302d22a
Merge branch 'feat/logs-envelope' into feat/logs-client
denrase May 7, 2025
9163b43
add sdk name & version to log attributes
denrase May 7, 2025
58bbb6a
set sentry attributes to log
denrase May 7, 2025
bd95978
Merge branch 'feat/logs' into feat/logs-client
denrase May 7, 2025
5050c90
change client api to take single log
denrase May 7, 2025
a58b17d
update comment with correct dart types
denrase May 7, 2025
874a4ce
add todo for android
denrase May 7, 2025
8ed3a83
set trace id
denrase May 8, 2025
de3a856
disable logs per default
denrase May 8, 2025
3ad3ff0
add BeforeSendLogCallback
denrase May 8, 2025
345d4f6
add cl entry
denrase May 8, 2025
74e69d9
update test text to match api
denrase May 8, 2025
87d52e6
rename var
denrase May 8, 2025
b58cd37
always use propagation context for trace id
denrase May 12, 2025
969dd3c
add comment for empty trace id
denrase May 12, 2025
1a206f4
update comment
denrase May 13, 2025
58535ae
remove experimental, add comments
denrase May 13, 2025
b7f74b3
[Structured Logs]: Buffering and Flushing of Logs (#2930)
denrase May 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Logs: Models & Envelopes ([#2916](https://github.com/getsentry/sentry-dart/pull/2916))
- Logs: Integrate in Sentry Client ([#2920](https://github.com/getsentry/sentry-dart/pull/2920))

## 9.0.0-beta.2

Expand Down
3 changes: 3 additions & 0 deletions dart/lib/src/noop_sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,7 @@ class NoOpSentryClient implements SentryClient {
Future<SentryId> captureFeedback(SentryFeedback feedback,
{Scope? scope, Hint? hint}) async =>
SentryId.empty();

@override
Future<void> captureLog(SentryLog log, {Scope? scope}) async {}
}
4 changes: 2 additions & 2 deletions dart/lib/src/protocol/sentry_log.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@

SentryLog({
required this.timestamp,
required this.traceId,
SentryId? traceId,
required this.level,
required this.body,
required this.attributes,
this.severityNumber,
});
}) : traceId = traceId ?? SentryId.empty();

Check warning on line 20 in dart/lib/src/protocol/sentry_log.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/protocol/sentry_log.dart#L20

Added line #L20 was not covered by tests

Map<String, dynamic> toJson() {
return {
Expand Down
6 changes: 3 additions & 3 deletions dart/lib/src/protocol/sentry_log_attribute.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ class SentryLogAttribute {
return SentryLogAttribute._(value, 'string');
}

factory SentryLogAttribute.boolean(bool value) {
factory SentryLogAttribute.bool(bool value) {
return SentryLogAttribute._(value, 'boolean');
}

factory SentryLogAttribute.integer(int value) {
factory SentryLogAttribute.int(int value) {
return SentryLogAttribute._(value, 'integer');
}

factory SentryLogAttribute.double(double value) {
return SentryLogAttribute._(value, 'double');
}

// In the future the SDK will also support string[], boolean[], integer[], double[] values.
// In the future the SDK will also support List<String>, List<bool>, List<int>, List<double> values.
Map<String, dynamic> toJson() {
return {
'value': value,
Expand Down
69 changes: 69 additions & 0 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,75 @@ class SentryClient {
);
}

@internal
Future<void> captureLog(
SentryLog log, {
Scope? scope,
}) async {
if (!_options.enableLogs) {
return;
}

log.attributes['sentry.sdk.name'] = SentryLogAttribute.string(
_options.sdk.name,
);
log.attributes['sentry.sdk.version'] = SentryLogAttribute.string(
_options.sdk.version,
);
final environment = _options.environment;
if (environment != null) {
log.attributes['sentry.environment'] = SentryLogAttribute.string(
environment,
);
}
final release = _options.release;
if (release != null) {
log.attributes['sentry.release'] = SentryLogAttribute.string(
release,
);
}
final span = scope?.span;
final propagationContext = scope?.propagationContext;
if (span != null) {
log.attributes['sentry.trace.parent_span_id'] = SentryLogAttribute.string(
span.context.spanId.toString(),
);
log.traceId = span.context.traceId;
} else if (propagationContext != null) {
log.traceId = propagationContext.traceId;
}

final beforeSendLog = _options.beforeSendLog;
SentryLog? processedLog = log;
if (beforeSendLog != null) {
try {
processedLog = beforeSendLog(log);
if (processedLog == null) {
return;
}
} catch (exception, stackTrace) {
_options.logger(
SentryLevel.error,
'The beforeSendLog callback threw an exception',
exception: exception,
stackTrace: stackTrace,
);
if (_options.automatedTestMode) {
rethrow;
}
}
}

// TODO: Batch in separate PR, so we can send multiple logs at once.
final envelope = SentryEnvelope.fromLogs(
[processedLog ?? log],
_options.sdk,
);

// TODO: Make sure the Android SDK understands the log envelope type.
await captureEnvelope(envelope);
}

void close() {
_options.httpClient.close();
}
Expand Down
10 changes: 10 additions & 0 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import 'sentry_exception_factory.dart';
import 'sentry_stack_trace_factory.dart';
import 'transport/noop_transport.dart';
import 'version.dart';
import 'package:meta/meta.dart' as meta;

// TODO: shutdownTimeout, flushTimeoutMillis
// 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
Expand Down Expand Up @@ -198,6 +199,9 @@ class SentryOptions {
/// Can return true to emit the metric, or false to drop it.
BeforeMetricCallback? beforeMetricCallback;

@meta.experimental
BeforeSendLogCallback? beforeSendLog;

/// Sets the release. SDK will try to automatically configure a release out of the box
/// See [docs for further information](https://docs.sentry.io/platforms/flutter/configuration/releases/)
String? release;
Expand Down Expand Up @@ -531,6 +535,9 @@ class SentryOptions {
/// This is opt-in, as it can lead to existing exception beeing grouped as new ones.
bool groupExceptions = false;

@meta.experimental
bool enableLogs = false;

SentryOptions({String? dsn, Platform? platform, RuntimeChecker? checker}) {
this.dsn = dsn;
if (platform != null) {
Expand Down Expand Up @@ -660,6 +667,9 @@ typedef BeforeMetricCallback = bool Function(
Map<String, String>? tags,
});

@meta.experimental
typedef BeforeSendLogCallback = SentryLog? Function(SentryLog log);

/// Used to provide timestamp for logging.
typedef ClockProvider = DateTime Function();

Expand Down
14 changes: 13 additions & 1 deletion dart/test/mocks/mock_sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient {
List<CaptureMessageCall> captureMessageCalls = [];
List<CaptureEnvelopeCall> captureEnvelopeCalls = [];
List<CaptureTransactionCall> captureTransactionCalls = [];

List<CaptureFeedbackCall> captureFeedbackCalls = [];
List<CaptureLogCall> captureLogCalls = [];
int closeCalls = 0;

@override
Expand Down Expand Up @@ -84,6 +84,11 @@ class MockSentryClient with NoSuchMethodProvider implements SentryClient {
return SentryId.newId();
}

@override
Future<void> captureLog(SentryLog log, {Scope? scope}) async {
captureLogCalls.add(CaptureLogCall(log, scope));
}

@override
void close() {
closeCalls = closeCalls + 1;
Expand Down Expand Up @@ -173,3 +178,10 @@ class CaptureTransactionCall {

CaptureTransactionCall(this.transaction, this.traceContext, this.hint);
}

class CaptureLogCall {
final SentryLog log;
final Scope? scope;

CaptureLogCall(this.log, this.scope);
}
12 changes: 10 additions & 2 deletions dart/test/mocks/mock_transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class MockTransport implements Transport {
List<SentryEnvelope> envelopes = [];
List<SentryEvent> events = [];
List<String> statsdItems = [];
List<Map<String, dynamic>> logs = [];

int _calls = 0;
String _exceptions = '';
Expand All @@ -31,7 +32,7 @@ class MockTransport implements Transport {
try {
envelopes.add(envelope);
if (parseFromEnvelope) {
await _eventFromEnvelope(envelope);
await _parseEnvelope(envelope);
}
} catch (e, stack) {
_exceptions += '$e\n$stack\n\n';
Expand All @@ -41,14 +42,21 @@ class MockTransport implements Transport {
return envelope.header.eventId ?? SentryId.empty();
}

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

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

if (statSdRegex.hasMatch(envelopeItem)) {
statsdItems.add(envelopeItem);
} else if (envelopeItem.contains('items') &&
envelopeItem.contains('timestamp') &&
envelopeItem.contains('trace_id') &&
envelopeItem.contains('level') &&
envelopeItem.contains('body')) {
final envelopeItemJson = jsonDecode(envelopeItem) as Map<String, dynamic>;
logs.add(envelopeItemJson);
} else {
final envelopeItemJson = jsonDecode(envelopeItem) as Map<String, dynamic>;
events.add(SentryEvent.fromJson(envelopeItemJson));
Expand Down
8 changes: 4 additions & 4 deletions dart/test/protocol/sentry_log_attribute_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ void main() {
});
});

test('$SentryLogAttribute boolean to json', () {
final attribute = SentryLogAttribute.boolean(true);
test('$SentryLogAttribute bool to json', () {
final attribute = SentryLogAttribute.bool(true);
final json = attribute.toJson();
expect(json, {
'value': true,
'type': 'boolean',
});
});

test('$SentryLogAttribute integer to json', () {
final attribute = SentryLogAttribute.integer(1);
test('$SentryLogAttribute int to json', () {
final attribute = SentryLogAttribute.int(1);
final json = attribute.toJson();

expect(json, {
Expand Down
4 changes: 2 additions & 2 deletions dart/test/protocol/sentry_log_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ void main() {
body: 'fixture-body',
attributes: {
'test': SentryLogAttribute.string('fixture-test'),
'test2': SentryLogAttribute.boolean(true),
'test3': SentryLogAttribute.integer(9001),
'test2': SentryLogAttribute.bool(true),
'test3': SentryLogAttribute.int(9001),
'test4': SentryLogAttribute.double(9000.1),
},
severityNumber: 1,
Expand Down
Loading
Loading