Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- Add `DioException` response data to error breadcrumb ([#3164](https://github.com/getsentry/sentry-dart/pull/3164))
- Bumped `dio` min verion to `5.2.0`
- Log a warning when dropping envelope items ([#3165](https://github.com/getsentry/sentry-dart/pull/3165))
- Call options.log for structured logs ([#3187](https://github.com/getsentry/sentry-dart/pull/3187))

### Dependencies

Expand Down
2 changes: 1 addition & 1 deletion packages/dart/lib/src/protocol/sentry_log.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class SentryLog {
return {
'timestamp': timestamp.toIso8601String(),
'trace_id': traceId.toString(),
'level': level.value,
'level': level.name,
'body': body,
'attributes':
attributes.map((key, value) => MapEntry(key, value.toJson())),
Expand Down
73 changes: 64 additions & 9 deletions packages/dart/lib/src/protocol/sentry_log_level.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,45 @@
enum SentryLogLevel {
trace('trace'),
debug('debug'),
info('info'),
warn('warn'),
error('error'),
fatal('fatal');
import 'package:meta/meta.dart';
import 'sentry_level.dart';

final String value;
const SentryLogLevel(this.value);
/// Severity of the logged [Event].
@immutable
class SentryLogLevel {
const SentryLogLevel._(this.name, this.ordinal);

static const trace = SentryLogLevel._('trace', 1);
static const debug = SentryLogLevel._('debug', 5);
static const info = SentryLogLevel._('info', 9);
static const warn = SentryLogLevel._('warn', 13);
static const error = SentryLogLevel._('error', 17);
static const fatal = SentryLogLevel._('fatal', 21);

/// API name of the level as it is encoded in the JSON protocol.
final String name;
final int ordinal;

factory SentryLogLevel.fromName(String name) {
switch (name) {
case 'fatal':
return SentryLogLevel.fatal;
case 'error':
return SentryLogLevel.error;
case 'warn':
return SentryLogLevel.warn;
case 'info':
return SentryLogLevel.info;
case 'debug':
return SentryLogLevel.debug;
case 'trace':
return SentryLogLevel.trace;
}
return SentryLogLevel.debug;
}

/// For use with Dart's
/// [`log`](https://api.dart.dev/stable/2.12.4/dart-developer/log.html)
/// function.
/// These levels are inspired by
/// https://pub.dev/documentation/logging/latest/logging/Level-class.html
int toSeverityNumber() {
switch (this) {
case SentryLogLevel.trace:
Expand All @@ -24,5 +55,29 @@ enum SentryLogLevel {
case SentryLogLevel.fatal:
return 21;
}
throw StateError('Unreachable code');
}
}

/// Extension to bridge SentryLogLevel to SentryLevel
extension SentryLogLevelExtension on SentryLogLevel {
/// Converts this SentryLogLevel to the corresponding SentryLevel
/// for use with the diagnostic logging system.
SentryLevel toSentryLevel() {
switch (this) {
case SentryLogLevel.trace:
return SentryLevel.debug;
case SentryLogLevel.debug:
return SentryLevel.debug;
case SentryLogLevel.info:
return SentryLevel.info;
case SentryLogLevel.warn:
return SentryLevel.warning;
case SentryLogLevel.error:
return SentryLevel.error;
case SentryLogLevel.fatal:
return SentryLevel.fatal;
}
throw StateError('Unreachable code');
}
}
50 changes: 49 additions & 1 deletion packages/dart/lib/src/sentry_logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import 'sentry_options.dart';
import 'sentry_logger_formatter.dart';

class SentryLogger {
SentryLogger(this._clock, {Hub? hub}) : _hub = hub ?? HubAdapter();
SentryLogger(this._clock, this._log, {Hub? hub}) : _hub = hub ?? HubAdapter();

final ClockProvider _clock;
final SdkLogCallback _log;
final Hub _hub;

late final fmt = SentryLoggerFormatter(this);
Expand Down Expand Up @@ -70,6 +71,53 @@ class SentryLogger {
body: body,
attributes: attributes ?? {},
);

_log(
level.toSentryLevel(),
_formatLogMessage(level, body, attributes),
logger: 'sentry_logger',
);

return _hub.captureLog(log);
}

/// Format log message with level and attributes
String _formatLogMessage(
SentryLogLevel level,
String body,
Map<String, SentryLogAttribute>? attributes,
) {
if (attributes == null || attributes.isEmpty) {
return body;
}

final attrsStr = attributes.entries
.map((e) => '"${e.key}": ${_formatAttributeValue(e.value)}')
.join(', ');

return '$body {$attrsStr}';
}

/// Format attribute value based on its type
String _formatAttributeValue(SentryLogAttribute attribute) {
switch (attribute.type) {
case 'string':
if (attribute.value is String) {
return '"${attribute.value}"';
}
case 'boolean':
if (attribute.value is bool) {
return attribute.value.toString();
}
case 'integer':
if (attribute.value is int) {
return attribute.value.toString();
}
case 'double':
if (attribute.value is double) {
return attribute.value.toStringAsFixed(1);
}
}
return attribute.value.toString();
}
}
2 changes: 1 addition & 1 deletion packages/dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ class SentryOptions {
/// Enabling this option may change grouping.
bool includeModuleInStackTrace = false;

late final SentryLogger logger = SentryLogger(clock);
late final SentryLogger logger = SentryLogger(clock, log);

@internal
SentryLogBatcher logBatcher = NoopLogBatcher();
Expand Down
52 changes: 52 additions & 0 deletions packages/dart/test/protocol/sentry_log_level_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:test/test.dart';
import 'package:sentry/src/protocol/sentry_log_level.dart';
import 'package:sentry/src/protocol/sentry_level.dart';

void main() {
group('SentryLogLevel', () {
test('fromName returns correct levels', () {
expect(SentryLogLevel.fromName('trace'), SentryLogLevel.trace);
expect(SentryLogLevel.fromName('debug'), SentryLogLevel.debug);
expect(SentryLogLevel.fromName('info'), SentryLogLevel.info);
expect(SentryLogLevel.fromName('warn'), SentryLogLevel.warn);
expect(SentryLogLevel.fromName('error'), SentryLogLevel.error);
expect(SentryLogLevel.fromName('fatal'), SentryLogLevel.fatal);
expect(SentryLogLevel.fromName('unknown'), SentryLogLevel.debug);
});

test('toSeverityNumber returns correct values', () {
expect(SentryLogLevel.trace.toSeverityNumber(), 1);
expect(SentryLogLevel.debug.toSeverityNumber(), 5);
expect(SentryLogLevel.info.toSeverityNumber(), 9);
expect(SentryLogLevel.warn.toSeverityNumber(), 13);
expect(SentryLogLevel.error.toSeverityNumber(), 17);
expect(SentryLogLevel.fatal.toSeverityNumber(), 21);
});

test('properties are correct', () {
expect(SentryLogLevel.trace.name, 'trace');
expect(SentryLogLevel.trace.ordinal, 1);
expect(SentryLogLevel.debug.name, 'debug');
expect(SentryLogLevel.debug.ordinal, 5);
expect(SentryLogLevel.info.name, 'info');
expect(SentryLogLevel.info.ordinal, 9);
expect(SentryLogLevel.warn.name, 'warn');
expect(SentryLogLevel.warn.ordinal, 13);
expect(SentryLogLevel.error.name, 'error');
expect(SentryLogLevel.error.ordinal, 17);
expect(SentryLogLevel.fatal.name, 'fatal');
expect(SentryLogLevel.fatal.ordinal, 21);
});
});

group('SentryLogLevelExtension', () {
test('toSentryLevel bridges levels correctly', () {
expect(SentryLogLevel.trace.toSentryLevel(), SentryLevel.debug);
expect(SentryLogLevel.debug.toSentryLevel(), SentryLevel.debug);
expect(SentryLogLevel.info.toSentryLevel(), SentryLevel.info);
expect(SentryLogLevel.warn.toSentryLevel(), SentryLevel.warning);
expect(SentryLogLevel.error.toSentryLevel(), SentryLevel.error);
expect(SentryLogLevel.fatal.toSentryLevel(), SentryLevel.fatal);
});
});
}
79 changes: 78 additions & 1 deletion packages/dart/test/sentry_logger_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,56 @@ void main() {

verifyCaptureLog(SentryLogLevel.fatal);
});

test('logs to injected callback when provided', () {
final mockLogCallback = _MockSdkLogCallback();
final logger = SentryLogger(
() => fixture.timestamp,
mockLogCallback.call,
hub: fixture.hub,
);

logger.trace('test message', attributes: fixture.attributes);

// Verify that both hub.captureLog and our callback were called
expect(fixture.hub.captureLogCalls.length, 1);
expect(mockLogCallback.calls.length, 1);

final logCall = mockLogCallback.calls[0];
expect(logCall.level, SentryLevel.debug);
expect(logCall.message,
'test message {"string": "string", "int": 1, "double": 1.0, "bool": true}');
expect(logCall.logger, 'sentry_logger');
});

test('bridges SentryLogLevel to SentryLevel correctly', () {
final mockLogCallback = _MockSdkLogCallback();
final logger = SentryLogger(
() => fixture.timestamp,
mockLogCallback.call,
hub: fixture.hub,
);

// Test all log levels to ensure proper bridging
logger.trace('trace message');
logger.debug('debug message');
logger.info('info message');
logger.warn('warn message');
logger.error('error message');
logger.fatal('fatal message');

// Verify that all calls were made to the log callback
expect(mockLogCallback.calls.length, 6);

// Verify the bridging is correct
expect(mockLogCallback.calls[0].level, SentryLevel.debug); // trace -> debug
expect(mockLogCallback.calls[1].level, SentryLevel.debug); // debug -> debug
expect(mockLogCallback.calls[2].level, SentryLevel.info); // info -> info
expect(
mockLogCallback.calls[3].level, SentryLevel.warning); // warn -> warning
expect(mockLogCallback.calls[4].level, SentryLevel.error); // error -> error
expect(mockLogCallback.calls[5].level, SentryLevel.fatal); // fatal -> fatal
});
}

class Fixture {
Expand All @@ -90,6 +140,33 @@ class Fixture {
};

SentryLogger getSut() {
return SentryLogger(() => timestamp, hub: hub);
return SentryLogger(() => timestamp, options.log, hub: hub);
}
}

/// Simple mock for SdkLogCallback to track calls
class _MockSdkLogCallback {
final List<_LogCall> calls = [];

void call(
SentryLevel level,
String message, {
String? logger,
Object? exception,
StackTrace? stackTrace,
}) {
calls.add(_LogCall(level, message, logger, exception, stackTrace));
}
}

/// Data class to store log call information
class _LogCall {
final SentryLevel level;
final String message;
final String? logger;
final Object? exception;
final StackTrace? stackTrace;

_LogCall(
this.level, this.message, this.logger, this.exception, this.stackTrace);
}
Loading