Skip to content

Sentry Structured Logs #2919

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## Unreleased

### 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))
- [Structured Logs]: Buffering and Flushing of Logs ([#2930](https://github.com/getsentry/sentry-dart/pull/2930))
## 9.0.0-RC

### Various fixes & improvements
Expand Down
12 changes: 12 additions & 0 deletions dart/lib/src/noop_log_batcher.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import 'dart:async';

import 'sentry_log_batcher.dart';
import 'protocol/sentry_log.dart';

class NoopLogBatcher implements SentryLogBatcher {
@override

Check warning on line 7 in dart/lib/src/noop_log_batcher.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/noop_log_batcher.dart#L7

Added line #L7 was not covered by tests
FutureOr<void> addLog(SentryLog log) {}

@override

Check warning on line 10 in dart/lib/src/noop_log_batcher.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/noop_log_batcher.dart#L10

Added line #L10 was not covered by tests
Future<void> flush() async {}
}
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 {}
}
3 changes: 3 additions & 0 deletions dart/lib/src/protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ export 'protocol/span_status.dart';
export 'sentry_event_like.dart';
export 'protocol/sentry_feature_flag.dart';
export 'protocol/sentry_feature_flags.dart';
export 'protocol/sentry_log.dart';
export 'protocol/sentry_log_level.dart';
export 'protocol/sentry_log_attribute.dart';
35 changes: 35 additions & 0 deletions dart/lib/src/protocol/sentry_log.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'sentry_id.dart';
import 'sentry_log_level.dart';
import 'sentry_log_attribute.dart';

class SentryLog {
DateTime timestamp;
SentryId traceId;
SentryLogLevel level;
String body;
Map<String, SentryLogAttribute> attributes;
int? severityNumber;

/// The traceId is initially an empty default value and is populated during event processing;
/// by the time processing completes, it is guaranteed to be a valid non-empty trace id.
SentryLog({
required this.timestamp,
SentryId? traceId,
required this.level,
required this.body,
required this.attributes,
this.severityNumber,
}) : traceId = traceId ?? SentryId.empty();

Map<String, dynamic> toJson() {
return {
'timestamp': timestamp.toIso8601String(),
'trace_id': traceId.toString(),
'level': level.value,
'body': body,
'attributes':
attributes.map((key, value) => MapEntry(key, value.toJson())),
'severity_number': severityNumber ?? level.toSeverityNumber(),
};
}
}
30 changes: 30 additions & 0 deletions dart/lib/src/protocol/sentry_log_attribute.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class SentryLogAttribute {
final dynamic value;
final String type;

const SentryLogAttribute._(this.value, this.type);

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

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

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 List<String>, List<bool>, List<int>, List<double> values.
Map<String, dynamic> toJson() {
return {
'value': value,
'type': type,
};
}
}
28 changes: 28 additions & 0 deletions dart/lib/src/protocol/sentry_log_level.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
enum SentryLogLevel {
trace('trace'),
debug('debug'),
info('info'),
warn('warn'),
error('error'),
fatal('fatal');

final String value;
const SentryLogLevel(this.value);

int toSeverityNumber() {
switch (this) {
case SentryLogLevel.trace:
return 1;
case SentryLogLevel.debug:
return 5;
case SentryLogLevel.info:
return 9;
case SentryLogLevel.warn:
return 13;
case SentryLogLevel.error:
return 17;
case SentryLogLevel.fatal:
return 21;
}
}
}
71 changes: 71 additions & 0 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import 'type_check_hint.dart';
import 'utils/isolate_utils.dart';
import 'utils/regex_utils.dart';
import 'utils/stacktrace_utils.dart';
import 'sentry_log_batcher.dart';
import 'version.dart';

/// Default value for [SentryUser.ipAddress]. It gets set when an event does not have
Expand Down Expand Up @@ -75,6 +76,9 @@ class SentryClient {
if (enableFlutterSpotlight) {
options.transport = SpotlightHttpTransport(options, options.transport);
}
if (options.enableLogs) {
options.logBatcher = SentryLogBatcher(options);
}
return SentryClient._(options);
}

Expand Down Expand Up @@ -485,6 +489,73 @@ 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 propagationContext = scope?.propagationContext;
if (propagationContext != null) {
log.traceId = propagationContext.traceId;
}
final span = scope?.span;
if (span != null) {
log.attributes['sentry.trace.parent_span_id'] = SentryLogAttribute.string(
span.context.spanId.toString(),
);
}

final beforeSendLog = _options.beforeSendLog;
SentryLog? processedLog = log;
if (beforeSendLog != null) {
try {
final callbackResult = beforeSendLog(log);

if (callbackResult is Future<SentryLog?>) {
processedLog = await callbackResult;
} else {
processedLog = callbackResult;
}
} catch (exception, stackTrace) {
_options.logger(
SentryLevel.error,
'The beforeSendLog callback threw an exception',
exception: exception,
stackTrace: stackTrace,
);
if (_options.automatedTestMode) {
rethrow;
}
}
}
if (processedLog != null) {
_options.logBatcher.addLog(processedLog);
}
}

void close() {
_options.httpClient.close();
}
Expand Down
15 changes: 15 additions & 0 deletions dart/lib/src/sentry_envelope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,21 @@ class SentryEnvelope {
);
}

factory SentryEnvelope.fromLogs(
List<SentryLog> items,
SdkVersion sdkVersion,
) {
return SentryEnvelope(
SentryEnvelopeHeader(
null,
sdkVersion,
),
[
SentryEnvelopeItem.fromLogs(items),
],
);
}

/// Stream binary data representation of `Envelope` file encoded.
Stream<List<int>> envelopeStream(SentryOptions options) async* {
yield utf8JsonEncoder.convert(header.toJson());
Expand Down
15 changes: 15 additions & 0 deletions dart/lib/src/sentry_envelope_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ class SentryEnvelopeItem {
);
}

factory SentryEnvelopeItem.fromLogs(List<SentryLog> items) {
final payload = {
'items': items.map((e) => e.toJson()).toList(),
};
return SentryEnvelopeItem(
SentryEnvelopeItemHeader(
SentryItemType.log,
itemCount: items.length,
contentType: 'application/vnd.sentry.items.log+json',
),
() => utf8JsonEncoder.convert(payload),
originalObject: payload,
);
}

/// Header with info about type and length of data in bytes.
final SentryEnvelopeItemHeader header;

Expand Down
4 changes: 4 additions & 0 deletions dart/lib/src/sentry_envelope_item_header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
class SentryEnvelopeItemHeader {
SentryEnvelopeItemHeader(
this.type, {
this.itemCount,
this.contentType,
this.fileName,
this.attachmentType,
Expand All @@ -10,6 +11,8 @@ class SentryEnvelopeItemHeader {
/// Type of encoded data.
final String type;

final int? itemCount;

final String? contentType;

final String? fileName;
Expand All @@ -19,6 +22,7 @@ class SentryEnvelopeItemHeader {
/// Item header encoded as JSON
Future<Map<String, dynamic>> toJson(int length) async {
return {
if (itemCount != null) 'item_count': itemCount,
if (contentType != null) 'content_type': contentType,
if (fileName != null) 'filename': fileName,
if (attachmentType != null) 'attachment_type': attachmentType,
Expand Down
1 change: 1 addition & 0 deletions dart/lib/src/sentry_item_type.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ class SentryItemType {
static const String clientReport = 'client_report';
static const String profile = 'profile';
static const String statsd = 'statsd';
static const String log = 'log';
static const String unknown = '__unknown__';
}
52 changes: 52 additions & 0 deletions dart/lib/src/sentry_log_batcher.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'dart:async';
import 'sentry_envelope.dart';
import 'sentry_options.dart';
import 'protocol/sentry_log.dart';
import 'package:meta/meta.dart';

@internal
class SentryLogBatcher {
SentryLogBatcher(this._options, {Duration? flushTimeout, int? maxBufferSize})
: _flushTimeout = flushTimeout ?? Duration(seconds: 5),
_maxBufferSize = maxBufferSize ?? 100;

final SentryOptions _options;
final Duration _flushTimeout;
final int _maxBufferSize;

final _logBuffer = <SentryLog>[];

Timer? _flushTimer;

void addLog(SentryLog log) {
_logBuffer.add(log);

_flushTimer?.cancel();

if (_logBuffer.length >= _maxBufferSize) {
return flush();
} else {
_flushTimer = Timer(_flushTimeout, flush);
}
}

void flush() {
_flushTimer?.cancel();
_flushTimer = null;

final logs = List<SentryLog>.from(_logBuffer);
_logBuffer.clear();

if (logs.isEmpty) {
return;
}

final envelope = SentryEnvelope.fromLogs(
logs,
_options.sdk,
);

// TODO: Make sure the Android SDK understands the log envelope type.
_options.transport.send(envelope);
}
}
Loading
Loading