Skip to content

Commit 36820e8

Browse files
authored
Call options.log for structured logs (#3187)
1 parent 192b44c commit 36820e8

File tree

6 files changed

+253
-2
lines changed

6 files changed

+253
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- Add `DioException` response data to error breadcrumb ([#3164](https://github.com/getsentry/sentry-dart/pull/3164))
1818
- Bumped `dio` min verion to `5.2.0`
1919
- Log a warning when dropping envelope items ([#3165](https://github.com/getsentry/sentry-dart/pull/3165))
20+
- Call options.log for structured logs ([#3187](https://github.com/getsentry/sentry-dart/pull/3187))
2021

2122
### Dependencies
2223

metrics/metrics-ios.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ apps:
66

77
startupTimeTest:
88
runs: 50
9-
diffMin: 0
9+
diffMin: -10 # For the flaky test case where the app with sentry is faster than the app without sentry.
1010
diffMax: 150
1111

1212
binarySizeTest:

packages/dart/lib/src/protocol/sentry_log_level.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'sentry_level.dart';
2+
13
enum SentryLogLevel {
24
trace('trace'),
35
debug('debug'),
@@ -26,3 +28,25 @@ enum SentryLogLevel {
2628
}
2729
}
2830
}
31+
32+
/// Extension to bridge SentryLogLevel to SentryLevel
33+
extension SentryLogLevelExtension on SentryLogLevel {
34+
/// Converts this SentryLogLevel to the corresponding SentryLevel
35+
/// for use with the diagnostic logging system.
36+
SentryLevel toSentryLevel() {
37+
switch (this) {
38+
case SentryLogLevel.trace:
39+
return SentryLevel.debug;
40+
case SentryLogLevel.debug:
41+
return SentryLevel.debug;
42+
case SentryLogLevel.info:
43+
return SentryLevel.info;
44+
case SentryLogLevel.warn:
45+
return SentryLevel.warning;
46+
case SentryLogLevel.error:
47+
return SentryLevel.error;
48+
case SentryLogLevel.fatal:
49+
return SentryLevel.fatal;
50+
}
51+
}
52+
}

packages/dart/lib/src/sentry_logger.dart

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,66 @@ class SentryLogger {
7070
body: body,
7171
attributes: attributes ?? {},
7272
);
73+
74+
_hub.options.log(
75+
level.toSentryLevel(),
76+
_formatLogMessage(level, body, attributes),
77+
logger: 'sentry_logger',
78+
);
79+
7380
return _hub.captureLog(log);
7481
}
82+
83+
/// Format log message with level and attributes
84+
String _formatLogMessage(
85+
SentryLogLevel level,
86+
String body,
87+
Map<String, SentryLogAttribute>? attributes,
88+
) {
89+
if (attributes == null || attributes.isEmpty) {
90+
return body;
91+
}
92+
93+
final attrsStr = attributes.entries
94+
.map((e) => '"${e.key}": ${_formatAttributeValue(e.value)}')
95+
.join(', ');
96+
97+
return '$body {$attrsStr}';
98+
}
99+
100+
/// Format attribute value based on its type
101+
String _formatAttributeValue(SentryLogAttribute attribute) {
102+
switch (attribute.type) {
103+
case 'string':
104+
if (attribute.value is String) {
105+
return '"${attribute.value}"';
106+
}
107+
break;
108+
case 'boolean':
109+
if (attribute.value is bool) {
110+
return attribute.value.toString();
111+
}
112+
break;
113+
case 'integer':
114+
if (attribute.value is int) {
115+
return attribute.value.toString();
116+
}
117+
break;
118+
case 'double':
119+
if (attribute.value is double) {
120+
final value = attribute.value as double;
121+
// Handle special double values
122+
if (value.isNaN || value.isInfinite) {
123+
return value.toString();
124+
}
125+
// Ensure doubles always show decimal notation to distinguish from ints
126+
// Use toStringAsFixed(1) for whole numbers, toString() for decimals
127+
return value == value.toInt()
128+
? value.toStringAsFixed(1)
129+
: value.toString();
130+
}
131+
break;
132+
}
133+
return attribute.value.toString();
134+
}
75135
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import 'package:test/test.dart';
2+
import 'package:sentry/src/protocol/sentry_log_level.dart';
3+
import 'package:sentry/src/protocol/sentry_level.dart';
4+
5+
void main() {
6+
group('SentryLogLevel', () {
7+
test('toSeverityNumber returns correct values', () {
8+
expect(SentryLogLevel.trace.toSeverityNumber(), 1);
9+
expect(SentryLogLevel.debug.toSeverityNumber(), 5);
10+
expect(SentryLogLevel.info.toSeverityNumber(), 9);
11+
expect(SentryLogLevel.warn.toSeverityNumber(), 13);
12+
expect(SentryLogLevel.error.toSeverityNumber(), 17);
13+
expect(SentryLogLevel.fatal.toSeverityNumber(), 21);
14+
});
15+
});
16+
17+
group('SentryLogLevelExtension', () {
18+
test('toSentryLevel bridges levels correctly', () {
19+
expect(SentryLogLevel.trace.toSentryLevel(), SentryLevel.debug);
20+
expect(SentryLogLevel.debug.toSentryLevel(), SentryLevel.debug);
21+
expect(SentryLogLevel.info.toSentryLevel(), SentryLevel.info);
22+
expect(SentryLogLevel.warn.toSentryLevel(), SentryLevel.warning);
23+
expect(SentryLogLevel.error.toSentryLevel(), SentryLevel.error);
24+
expect(SentryLogLevel.fatal.toSentryLevel(), SentryLevel.fatal);
25+
});
26+
});
27+
}

packages/dart/test/sentry_logger_test.dart

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,114 @@ void main() {
7575

7676
verifyCaptureLog(SentryLogLevel.fatal);
7777
});
78+
79+
test('logs to hub options when provided', () {
80+
final mockLogCallback = _MockSdkLogCallback();
81+
82+
// Set the mock log callback on the fixture hub
83+
fixture.hub.options.log = mockLogCallback.call;
84+
fixture.hub.options.debug = true;
85+
fixture.hub.options.diagnosticLevel = SentryLevel.debug;
86+
87+
final logger = SentryLogger(
88+
() => fixture.timestamp,
89+
hub: fixture.hub,
90+
);
91+
92+
logger.trace('test message', attributes: fixture.attributes);
93+
94+
// Verify that both hub.captureLog and our callback were called
95+
expect(fixture.hub.captureLogCalls.length, 1);
96+
expect(mockLogCallback.calls.length, 1);
97+
98+
// Verify the captured log has the right content
99+
final capturedLog = fixture.hub.captureLogCalls[0].log;
100+
expect(capturedLog.level, SentryLogLevel.trace);
101+
expect(capturedLog.body, 'test message');
102+
expect(capturedLog.attributes, fixture.attributes);
103+
104+
// Verify the log callback was called with the right parameters
105+
final logCall = mockLogCallback.calls[0];
106+
expect(logCall.level, SentryLevel.debug); // trace maps to debug
107+
expect(logCall.message,
108+
'test message {"string": "string", "int": 1, "double": 1.23456789, "bool": true, "double_int": 1.0, "nan": NaN, "positive_infinity": Infinity, "negative_infinity": -Infinity}');
109+
expect(logCall.logger, 'sentry_logger');
110+
});
111+
112+
test('bridges SentryLogLevel to SentryLevel correctly', () {
113+
final mockLogCallback = _MockSdkLogCallback();
114+
115+
// Set the mock log callback on the fixture hub's options
116+
fixture.hub.options.log = mockLogCallback.call;
117+
fixture.hub.options.debug = true;
118+
fixture.hub.options.diagnosticLevel = SentryLevel.debug;
119+
120+
final logger = SentryLogger(
121+
() => fixture.timestamp,
122+
hub: fixture.hub,
123+
);
124+
125+
// Test all log levels to ensure proper bridging
126+
logger.trace('trace message');
127+
logger.debug('debug message');
128+
logger.info('info message');
129+
logger.warn('warn message');
130+
logger.error('error message');
131+
logger.fatal('fatal message');
132+
133+
// Verify that all calls were made to both the hub and the log callback
134+
expect(fixture.hub.captureLogCalls.length, 6);
135+
expect(mockLogCallback.calls.length, 6);
136+
137+
// Verify the bridging is correct
138+
expect(mockLogCallback.calls[0].level, SentryLevel.debug); // trace -> debug
139+
expect(mockLogCallback.calls[1].level, SentryLevel.debug); // debug -> debug
140+
expect(mockLogCallback.calls[2].level, SentryLevel.info); // info -> info
141+
expect(
142+
mockLogCallback.calls[3].level, SentryLevel.warning); // warn -> warning
143+
expect(mockLogCallback.calls[4].level, SentryLevel.error); // error -> error
144+
expect(mockLogCallback.calls[5].level, SentryLevel.fatal); // fatal -> fatal
145+
});
146+
147+
test('handles NaN and infinite values correctly', () {
148+
final mockLogCallback = _MockSdkLogCallback();
149+
150+
// Set the mock log callback on the fixture hub's options
151+
fixture.hub.options.log = mockLogCallback.call;
152+
fixture.hub.options.debug = true;
153+
fixture.hub.options.diagnosticLevel = SentryLevel.debug;
154+
155+
final logger = SentryLogger(
156+
() => fixture.timestamp,
157+
hub: fixture.hub,
158+
);
159+
160+
// Test with special double values
161+
final specialAttributes = <String, SentryLogAttribute>{
162+
'nan': SentryLogAttribute.double(double.nan),
163+
'positive_infinity': SentryLogAttribute.double(double.infinity),
164+
'negative_infinity': SentryLogAttribute.double(double.negativeInfinity),
165+
};
166+
167+
logger.info('special values', attributes: specialAttributes);
168+
169+
// Verify that both hub.captureLog and our callback were called
170+
expect(fixture.hub.captureLogCalls.length, 1);
171+
expect(mockLogCallback.calls.length, 1);
172+
173+
// Verify the captured log has the right content
174+
final capturedLog = fixture.hub.captureLogCalls[0].log;
175+
expect(capturedLog.level, SentryLogLevel.info);
176+
expect(capturedLog.body, 'special values');
177+
expect(capturedLog.attributes, specialAttributes);
178+
179+
// Verify the log callback was called with the right parameters
180+
final logCall = mockLogCallback.calls[0];
181+
expect(logCall.level, SentryLevel.info);
182+
expect(logCall.message,
183+
'special values {"nan": NaN, "positive_infinity": Infinity, "negative_infinity": -Infinity}');
184+
expect(logCall.logger, 'sentry_logger');
185+
});
78186
}
79187

80188
class Fixture {
@@ -85,11 +193,42 @@ class Fixture {
85193
final attributes = <String, SentryLogAttribute>{
86194
'string': SentryLogAttribute.string('string'),
87195
'int': SentryLogAttribute.int(1),
88-
'double': SentryLogAttribute.double(1.0),
196+
'double': SentryLogAttribute.double(1.23456789),
89197
'bool': SentryLogAttribute.bool(true),
198+
'double_int': SentryLogAttribute.double(1.0),
199+
'nan': SentryLogAttribute.double(double.nan),
200+
'positive_infinity': SentryLogAttribute.double(double.infinity),
201+
'negative_infinity': SentryLogAttribute.double(double.negativeInfinity),
90202
};
91203

92204
SentryLogger getSut() {
93205
return SentryLogger(() => timestamp, hub: hub);
94206
}
95207
}
208+
209+
/// Simple mock for SdkLogCallback to track calls
210+
class _MockSdkLogCallback {
211+
final List<_LogCall> calls = [];
212+
213+
void call(
214+
SentryLevel level,
215+
String message, {
216+
String? logger,
217+
Object? exception,
218+
StackTrace? stackTrace,
219+
}) {
220+
calls.add(_LogCall(level, message, logger, exception, stackTrace));
221+
}
222+
}
223+
224+
/// Data class to store log call information
225+
class _LogCall {
226+
final SentryLevel level;
227+
final String message;
228+
final String? logger;
229+
final Object? exception;
230+
final StackTrace? stackTrace;
231+
232+
_LogCall(
233+
this.level, this.message, this.logger, this.exception, this.stackTrace);
234+
}

0 commit comments

Comments
 (0)