Skip to content
Open
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
2 changes: 2 additions & 0 deletions packages/dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@ export 'src/utils/url_details.dart';
// ignore: invalid_export_of_internal_element
export 'src/utils/breadcrumb_log_level.dart';
export 'src/sentry_logger.dart';
// ignore: invalid_export_of_internal_element
export 'src/utils/debug_logger.dart' show SentryDebugLogger;
14 changes: 13 additions & 1 deletion packages/dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class SentryOptions {

set debug(bool newValue) {
_debug = newValue;
_configureDebugLogger();
if (_debug == true &&
(log == noOpLog || diagnosticLog?.logger == noOpLog)) {
log = debugLog;
Expand All @@ -175,7 +176,18 @@ class SentryOptions {
bool _debug = false;

/// minimum LogLevel to be used if debug is enabled
SentryLevel diagnosticLevel = _defaultDiagnosticLevel;
SentryLevel get diagnosticLevel => _diagnosticLevel;

set diagnosticLevel(SentryLevel newValue) {
_diagnosticLevel = newValue;
_configureDebugLogger();
}

SentryLevel _diagnosticLevel = _defaultDiagnosticLevel;

void _configureDebugLogger() {
SentryDebugLogger.configure(isEnabled: _debug, minLevel: _diagnosticLevel);
}

/// Sentry client name used for the HTTP authHeader and userAgent eg
/// sentry.{language}.{platform}/{version} eg sentry.java.android/2.0.0 would be a valid case
Expand Down
106 changes: 106 additions & 0 deletions packages/dart/lib/src/utils/debug_logger.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import 'dart:developer' as dev;

import 'package:meta/meta.dart';

import '../../sentry.dart';

/// Lightweight isolate compatible diagnostic logger for the Sentry SDK.
///
/// Logger naming convention:
/// - `sentry` – core dart package
/// - `sentry.flutter` – flutter package
/// - `sentry.{integration}` – integration packages (dio, hive, etc.)
///
/// Each package should have at least one top-level instance.
///
/// Example:
/// ```dart
/// const debugLogger = SentryDebugLogger('sentry.flutter');
///
/// debugLogger.warning('My Message')
///```
@internal
class SentryDebugLogger {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think SentryInternalLogger would be a more appropriate name.

final String _name;

const SentryDebugLogger(this._name);

static bool _isEnabled = false;
static SentryLevel _minLevel = SentryLevel.warning;

@visibleForTesting
static bool get isEnabled => _isEnabled;

@visibleForTesting
static SentryLevel get minLevel => _minLevel;

/// Configure logging for the current isolate.
///
/// This needs to be called for each new spawned isolate before logging.
static void configure({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this ever changing during runtime? I don't understand why we now have both an instance and static vars. If we go the instance, can't we just call `Logger(name, minLevel)'? If it needs to be enabled later, we can just set it on the instance.

So either static only, or instance only, no need to mix both.

Copy link
Contributor Author

@buenaflor buenaflor Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we now have both an instance and static vars

the reason why it's an instance is so we have the benefit of creating multiple logger instance with the same configuration within the same isolate

the configuration is static per isolate, I don't think we will ever have a use case where we want multiple loggers with different minLevels

Example: Flutter

const logger = SentryDebugLogger(sentry.flutter)

// SentryDebugLogger.configure(...) called in SentryOptions
logger.info('info in main isolate)' // by default uses the main isolate static config

// spawn a new isolate
// within that isolate
// SentryDebugLogger.configure(...) called directly after isolate has spawned
logger.info('info in other isolate') // you can still use that same logger instance

  • if we went the route with static only we would lose the ability to directly associate a package to a specific logger

    • SentryDebugLogger.info(name: 'sentry.flutter', message: 'xyz')
    • and repeating name: 'sentry.flutter', hundreds of time is error prone
  • if we went the route with instance only we would need to create a new logger instance in each new isolate which can get messy quickly because then you would have to differentiate between the main isolate logger and the current isolate logger and the code interchanges heavily where it's easy to use the wrong one (take a look at android replay recorder and envelope sender)

Logger(name, minLevel)

this doesn't work because you can only configure the logger through the static function

Copy link
Contributor Author

@buenaflor buenaflor Dec 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what we could do is to separate the config and have only the config be static so we don't mix it into the log class

/// Per-isolate logging configuration.
@internal
class SentryLogConfig {
  static bool isEnabled = false;
  static SentryLevel minLevel = SentryLevel.warning;

  /// Configure logging for the current isolate.
  static void configure({
    required bool isEnabled,
    SentryLevel minLevel = SentryLevel.warning,
  }) {
    SentryLogConfig.isEnabled = isEnabled;
    SentryLogConfig.minLevel = minLevel;
  }
}


// within the logger:
@pragma('vm:prefer-inline')
void _log(SentryLevel level, String message, {Object? error, StackTrace? stackTrace}) {
  if (!SentryLogConfig.isEnabled) return;
  if (level.ordinal < SentryLogConfig.minLevel.ordinal) return;
  
  dev.log(
    '[${level.name}] $message',
    name: _name,
    level: level.toDartLogLevel(),
    error: error,
    stackTrace: stackTrace,
    time: DateTime.now(),
  );
}

alternative 1 - update the config on the instance:

const logger = SentryDebugLogger(sentry.flutter);

// later at some point
logger.updateConfig(isEnabled: xyz, name: 'sentry.flutter');

caveat: we would need to update the config for every new logger

alternative 2 - update the config using a callback on a global config instance:

const loggerConfig = LoggerConfig() // updated later at some point when Sentry initializes
final logger = SentryDebugLogger(sentry.flutter, () => loggerConfig);

required bool isEnabled,
SentryLevel minLevel = SentryLevel.warning,
}) {
_isEnabled = isEnabled;
_minLevel = minLevel;
}

void debug(
String message, {
Object? error,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case where we would log an error + stacktrace as debug? Same for info and probably also warning.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm not sure, just added it for consistency

StackTrace? stackTrace,
}) =>
_log(SentryLevel.debug, message, error: error, stackTrace: stackTrace);

void info(
String message, {
Object? error,
StackTrace? stackTrace,
}) =>
_log(SentryLevel.info, message, error: error, stackTrace: stackTrace);

void warning(
String message, {
Object? error,
StackTrace? stackTrace,
}) =>
_log(SentryLevel.warning, message, error: error, stackTrace: stackTrace);

void error(
String message, {
Object? error,
StackTrace? stackTrace,
}) =>
_log(SentryLevel.error, message, error: error, stackTrace: stackTrace);

void fatal(
String message, {
Object? error,
StackTrace? stackTrace,
}) =>
_log(SentryLevel.fatal, message, error: error, stackTrace: stackTrace);

@pragma('vm:prefer-inline')
void _log(
SentryLevel level,
String message, {
Object? error,
StackTrace? stackTrace,
}) {
if (!_isEnabled) return;
if (level.ordinal < _minLevel.ordinal) return;

dev.log(
'[${level.name}] $message',
name: _name,
level: level.toDartLogLevel(),
error: error,
stackTrace: stackTrace,
time: DateTime.now(),
);
}
}

/// Logger for the Sentry Dart SDK.
@internal
const debugLogger = SentryDebugLogger('sentry');
158 changes: 158 additions & 0 deletions packages/dart/test/utils/debug_logger_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import 'package:sentry/sentry.dart';
import 'package:sentry/src/utils/debug_logger.dart';
import 'package:test/test.dart';

import '../test_utils.dart';

void main() {
group(SentryDebugLogger, () {
tearDown(() {
// Reset to default state after each test
SentryDebugLogger.configure(isEnabled: false);
});

group('configuration', () {
test('enables logging when isEnabled is true', () {
SentryDebugLogger.configure(isEnabled: true);

expect(SentryDebugLogger.isEnabled, isTrue);
});

test('disables logging when isEnabled is false', () {
SentryDebugLogger.configure(isEnabled: false);

expect(SentryDebugLogger.isEnabled, isFalse);
});

test('sets minimum level', () {
SentryDebugLogger.configure(
isEnabled: true,
minLevel: SentryLevel.error,
);

expect(SentryDebugLogger.isEnabled, isTrue);
expect(SentryDebugLogger.minLevel, equals(SentryLevel.error));
});

test('defaults minLevel to warning', () {
SentryDebugLogger.configure(isEnabled: true);

expect(SentryDebugLogger.minLevel, equals(SentryLevel.warning));
});

test('SentryOptions.debug enables logger', () {
final options = defaultTestOptions();

expect(options.debug, isFalse);
options.debug = true;

expect(SentryDebugLogger.isEnabled, isTrue);
});

test('SentryOptions.diagnosticLevel sets minLevel when set before debug',
() {
final options = defaultTestOptions();

options.diagnosticLevel = SentryLevel.error;
options.debug = true;

expect(SentryDebugLogger.isEnabled, isTrue);
expect(SentryDebugLogger.minLevel, equals(SentryLevel.error));
});

test(
'SentryOptions.diagnosticLevel updates minLevel when set after debug',
() {
final options = defaultTestOptions();

options.debug = true;
expect(SentryDebugLogger.minLevel, equals(SentryLevel.warning));

options.diagnosticLevel = SentryLevel.error;

expect(SentryDebugLogger.isEnabled, isTrue);
expect(SentryDebugLogger.minLevel, equals(SentryLevel.error));
});
});

group('logging when enabled', () {
setUp(() {
SentryDebugLogger.configure(
isEnabled: true, minLevel: SentryLevel.debug);
});

test('debug logs without throwing', () {
expect(
() => debugLogger.debug('debug message'),
returnsNormally,
);
});

test('info logs without throwing', () {
expect(
() => debugLogger.info('info message'),
returnsNormally,
);
});

test('warning logs without throwing', () {
expect(
() => debugLogger.warning('warning message'),
returnsNormally,
);
});

test('error logs without throwing', () {
expect(
() => debugLogger.error('error message'),
returnsNormally,
);
});

test('fatal logs without throwing', () {
expect(
() => debugLogger.fatal('fatal message'),
returnsNormally,
);
});

test('accepts error object', () {
expect(
() => debugLogger.error(
'error occurred',
error: Exception('test exception'),
),
returnsNormally,
);
});

test('accepts stackTrace', () {
expect(
() => debugLogger.error(
'error occurred',
error: Exception('test'),
stackTrace: StackTrace.current,
),
returnsNormally,
);
});
});

group('logger instances', () {
test('debugLogger constant is available', () {
expect(debugLogger, isA<SentryDebugLogger>());
});

test('can create logger with custom name', () {
const customLogger = SentryDebugLogger('sentry.flutter');

SentryDebugLogger.configure(isEnabled: true);

expect(
() => customLogger.info('test from flutter logger'),
returnsNormally,
);
});
});
});
}
81 changes: 0 additions & 81 deletions packages/flutter/lib/src/isolate/isolate_logger.dart

This file was deleted.

Loading
Loading