diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b46c7884b..66e8db206d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - `Sentry.metrics.count(...)` - `Sentry.metrics.distribution(...)` +### Enhancements + +- Refactor Logging API to be consistent with Metrics ([#3463](https://github.com/getsentry/sentry-dart/pull/3463)) + ### Dependencies - Bump Android SDK from v8.28.0 to v8.30.0 ([#3451](https://github.com/getsentry/sentry-dart/pull/3451)) diff --git a/packages/dart/lib/sentry.dart b/packages/dart/lib/sentry.dart index c809b5e2af..76177fb4e9 100644 --- a/packages/dart/lib/sentry.dart +++ b/packages/dart/lib/sentry.dart @@ -60,7 +60,6 @@ export 'src/utils/tracing_utils.dart'; export 'src/utils/url_details.dart'; // ignore: invalid_export_of_internal_element export 'src/utils/breadcrumb_log_level.dart'; -export 'src/sentry_logger.dart'; export 'src/telemetry/telemetry.dart'; // ignore: invalid_export_of_internal_element export 'src/utils/internal_logger.dart' show SentryInternalLogger; diff --git a/packages/dart/lib/src/logs_enricher_integration.dart b/packages/dart/lib/src/logs_enricher_integration.dart deleted file mode 100644 index ee39b41a96..0000000000 --- a/packages/dart/lib/src/logs_enricher_integration.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'dart:async'; -import 'package:meta/meta.dart'; - -import 'sdk_lifecycle_hooks.dart'; -import 'utils/os_utils.dart'; -import 'integration.dart'; -import 'hub.dart'; -import 'protocol/sentry_attribute.dart'; -import 'sentry_options.dart'; - -@internal -class LogsEnricherIntegration extends Integration { - static const integrationName = 'LogsEnricher'; - - @override - FutureOr call(Hub hub, SentryOptions options) { - if (options.enableLogs) { - options.lifecycleRegistry.registerCallback( - (event) async { - final os = getSentryOperatingSystem(); - - if (os.name != null) { - event.log.attributes['os.name'] = SentryAttribute.string( - os.name ?? '', - ); - } - if (os.version != null) { - event.log.attributes['os.version'] = SentryAttribute.string( - os.version ?? '', - ); - } - }, - ); - options.sdk.addIntegration(integrationName); - } - } -} diff --git a/packages/dart/lib/src/protocol.dart b/packages/dart/lib/src/protocol.dart index cb94d143b7..7347c0e88a 100644 --- a/packages/dart/lib/src/protocol.dart +++ b/packages/dart/lib/src/protocol.dart @@ -41,6 +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 'telemetry/log/log.dart'; +export 'telemetry/log/log_level.dart'; export 'protocol/sentry_attribute.dart'; diff --git a/packages/dart/lib/src/sdk_lifecycle_hooks.dart b/packages/dart/lib/src/sdk_lifecycle_hooks.dart index c0cbcaf343..36912dc523 100644 --- a/packages/dart/lib/src/sdk_lifecycle_hooks.dart +++ b/packages/dart/lib/src/sdk_lifecycle_hooks.dart @@ -67,8 +67,8 @@ class SdkLifecycleRegistry { } @internal -class OnBeforeCaptureLog extends SdkLifecycleEvent { - OnBeforeCaptureLog(this.log); +class OnProcessLog extends SdkLifecycleEvent { + OnProcessLog(this.log); final SentryLog log; } diff --git a/packages/dart/lib/src/sentry.dart b/packages/dart/lib/src/sentry.dart index 8e598c9b4b..9c658e5643 100644 --- a/packages/dart/lib/src/sentry.dart +++ b/packages/dart/lib/src/sentry.dart @@ -30,8 +30,8 @@ import 'tracing.dart'; import 'transport/data_category.dart'; import 'transport/task_queue.dart'; import 'feature_flags_integration.dart'; -import 'sentry_logger.dart'; -import 'logs_enricher_integration.dart'; +import 'telemetry/log/logger.dart'; +import 'telemetry/log/logger_setup_integration.dart'; /// Configuration options callback typedef OptionsConfiguration = FutureOr Function(SentryOptions); @@ -113,8 +113,8 @@ class Sentry { } options.addIntegration(MetricsSetupIntegration()); + options.addIntegration(LoggerSetupIntegration()); options.addIntegration(FeatureFlagsIntegration()); - options.addIntegration(LogsEnricherIntegration()); options.addIntegration(InMemoryTelemetryProcessorIntegration()); options.addEventProcessor(EnricherEventProcessor(options)); diff --git a/packages/dart/lib/src/sentry_client.dart b/packages/dart/lib/src/sentry_client.dart index bcd4a45419..af9850f847 100644 --- a/packages/dart/lib/src/sentry_client.dart +++ b/packages/dart/lib/src/sentry_client.dart @@ -18,6 +18,7 @@ import 'sentry_exception_factory.dart'; import 'sentry_options.dart'; import 'sentry_stack_trace_factory.dart'; import 'sentry_trace_context_header.dart'; +import 'telemetry/log/log_capture_pipeline.dart'; import 'telemetry/metric/metric.dart'; import 'telemetry/metric/metric_capture_pipeline.dart'; import 'transport/client_report_transport.dart'; @@ -44,6 +45,7 @@ String get defaultIpAddress => _defaultIpAddress; class SentryClient { final SentryOptions _options; final Random? _random; + final LogCapturePipeline _logCapturePipeline; final MetricCapturePipeline _metricCapturePipeline; static final _emptySentryId = Future.value(SentryId.empty()); @@ -53,7 +55,8 @@ class SentryClient { /// Instantiates a client using [SentryOptions] factory SentryClient(SentryOptions options, - {MetricCapturePipeline? metricCapturePipeline}) { + {LogCapturePipeline? logCapturePipeline, + MetricCapturePipeline? metricCapturePipeline}) { if (options.sendClientReports) { options.recorder = ClientReportRecorder(options.clock); } @@ -79,11 +82,15 @@ class SentryClient { options.transport = SpotlightHttpTransport(options, options.transport); } return SentryClient._( - options, metricCapturePipeline ?? MetricCapturePipeline(options)); + options, + logCapturePipeline ?? LogCapturePipeline(options), + metricCapturePipeline ?? MetricCapturePipeline(options), + ); } /// Instantiates a client using [SentryOptions] - SentryClient._(this._options, this._metricCapturePipeline) + SentryClient._( + this._options, this._logCapturePipeline, this._metricCapturePipeline) : _random = _options.sampleRate == null ? null : Random(); /// Reports an [event] to Sentry.io. @@ -495,98 +502,8 @@ class SentryClient { } @internal - FutureOr captureLog( - SentryLog log, { - Scope? scope, - }) async { - if (!_options.enableLogs) { - return; - } - - if (scope != null) { - final merged = Map.of(scope.attributes)..addAll(log.attributes); - log.attributes = merged; - } - - log.attributes['sentry.sdk.name'] = SentryAttribute.string( - _options.sdk.name, - ); - log.attributes['sentry.sdk.version'] = SentryAttribute.string( - _options.sdk.version, - ); - final environment = _options.environment; - if (environment != null) { - log.attributes['sentry.environment'] = SentryAttribute.string( - environment, - ); - } - final release = _options.release; - if (release != null) { - log.attributes['sentry.release'] = SentryAttribute.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'] = SentryAttribute.string( - span.context.spanId.toString(), - ); - } - - final user = scope?.user; - final id = user?.id; - final email = user?.email; - final name = user?.name; - if (id != null) { - log.attributes['user.id'] = SentryAttribute.string(id); - } - if (name != null) { - log.attributes['user.name'] = SentryAttribute.string(name); - } - if (email != null) { - log.attributes['user.email'] = SentryAttribute.string(email); - } - - final beforeSendLog = _options.beforeSendLog; - SentryLog? processedLog = log; - if (beforeSendLog != null) { - try { - final callbackResult = beforeSendLog(log); - - if (callbackResult is Future) { - processedLog = await callbackResult; - } else { - processedLog = callbackResult; - } - } catch (exception, stackTrace) { - _options.log( - SentryLevel.error, - 'The beforeSendLog callback threw an exception', - exception: exception, - stackTrace: stackTrace, - ); - if (_options.automatedTestMode) { - rethrow; - } - } - } - - if (processedLog != null) { - await _options.lifecycleRegistry - .dispatchCallback(OnBeforeCaptureLog(processedLog)); - _options.telemetryProcessor.addLog(processedLog); - } else { - _options.recorder.recordLostEvent( - DiscardReason.beforeSend, - DataCategory.logItem, - ); - } - } + FutureOr captureLog(SentryLog log, {Scope? scope}) => + _logCapturePipeline.captureLog(log, scope: scope); Future captureMetric(SentryMetric metric, {Scope? scope}) => _metricCapturePipeline.captureMetric(metric, scope: scope); diff --git a/packages/dart/lib/src/sentry_logger.dart b/packages/dart/lib/src/sentry_logger.dart deleted file mode 100644 index a643ac8eec..0000000000 --- a/packages/dart/lib/src/sentry_logger.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:async'; -import 'hub.dart'; -import 'hub_adapter.dart'; -import 'protocol/sentry_log.dart'; -import 'protocol/sentry_log_level.dart'; -import 'protocol/sentry_attribute.dart'; -import 'sentry_options.dart'; -import 'sentry_logger_formatter.dart'; -import 'utils.dart'; - -class SentryLogger { - SentryLogger(this._clock, {Hub? hub}) : _hub = hub ?? HubAdapter(); - - final ClockProvider _clock; - final Hub _hub; - - late final fmt = SentryLoggerFormatter(this); - - FutureOr trace( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.trace, body, attributes: attributes); - } - - FutureOr debug( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.debug, body, attributes: attributes); - } - - FutureOr info( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.info, body, attributes: attributes); - } - - FutureOr warn( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.warn, body, attributes: attributes); - } - - FutureOr error( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.error, body, attributes: attributes); - } - - FutureOr fatal( - String body, { - Map? attributes, - }) { - return _captureLog(SentryLogLevel.fatal, body, attributes: attributes); - } - - // Helper - - FutureOr _captureLog( - SentryLogLevel level, - String body, { - Map? attributes, - }) { - final log = SentryLog( - timestamp: _clock(), - level: level, - body: body, - attributes: attributes ?? {}, - ); - - _hub.options.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? attributes, - ) { - if (attributes == null || attributes.isEmpty) { - return body; - } - return '$body ${attributes.toFormattedString()}'; - } -} diff --git a/packages/dart/lib/src/sentry_options.dart b/packages/dart/lib/src/sentry_options.dart index d36e29d2c9..bb1cc755b0 100644 --- a/packages/dart/lib/src/sentry_options.dart +++ b/packages/dart/lib/src/sentry_options.dart @@ -12,6 +12,7 @@ import 'noop_client.dart'; import 'platform/platform.dart'; import 'sentry_exception_factory.dart'; import 'sentry_stack_trace_factory.dart'; +import 'telemetry/log/noop_logger.dart'; import 'telemetry/metric/noop_metrics.dart'; import 'telemetry/processing/processor.dart'; import 'transport/noop_transport.dart'; @@ -560,7 +561,8 @@ class SentryOptions { /// Enabling this option may change grouping. bool includeModuleInStackTrace = false; - late final SentryLogger logger = SentryLogger(clock); + @internal + late SentryLogger logger = const NoOpSentryLogger(); @internal late SentryMetrics metrics = const NoOpSentryMetrics(); diff --git a/packages/dart/lib/src/sentry_logger_formatter.dart b/packages/dart/lib/src/telemetry/log/default_logger.dart similarity index 56% rename from packages/dart/lib/src/sentry_logger_formatter.dart rename to packages/dart/lib/src/telemetry/log/default_logger.dart index aff7d21e51..40ee263589 100644 --- a/packages/dart/lib/src/sentry_logger_formatter.dart +++ b/packages/dart/lib/src/telemetry/log/default_logger.dart @@ -1,13 +1,113 @@ import 'dart:async'; -import 'protocol/sentry_attribute.dart'; -import 'sentry_template_string.dart'; -import 'sentry_logger.dart'; -class SentryLoggerFormatter { - SentryLoggerFormatter(this._logger); +import '../../../sentry.dart'; +import '../../sentry_template_string.dart'; +import '../../utils/internal_logger.dart'; - final SentryLogger _logger; +typedef CaptureLogCallback = FutureOr Function(SentryLog log); +typedef ScopeProvider = Scope Function(); +final class DefaultSentryLogger implements SentryLogger { + final CaptureLogCallback _captureLogCallback; + final ClockProvider _clockProvider; + final ScopeProvider _scopeProvider; + + late final SentryLoggerFormatter _formatter = + _DefaultSentryLoggerFormatter(this); + + DefaultSentryLogger({ + required CaptureLogCallback captureLogCallback, + required ClockProvider clockProvider, + required ScopeProvider scopeProvider, + }) : _captureLogCallback = captureLogCallback, + _clockProvider = clockProvider, + _scopeProvider = scopeProvider; + + @override + FutureOr trace( + String body, { + Map? attributes, + }) { + return _captureLog(SentryLogLevel.trace, body, attributes: attributes); + } + + @override + FutureOr debug( + String body, { + Map? attributes, + }) { + return _captureLog(SentryLogLevel.debug, body, attributes: attributes); + } + + @override + FutureOr info( + String body, { + Map? attributes, + }) { + return _captureLog(SentryLogLevel.info, body, attributes: attributes); + } + + @override + FutureOr warn( + String body, { + Map? attributes, + }) { + return _captureLog(SentryLogLevel.warn, body, attributes: attributes); + } + + @override + FutureOr error( + String body, { + Map? attributes, + }) { + return _captureLog(SentryLogLevel.error, body, attributes: attributes); + } + + @override + FutureOr fatal( + String body, { + Map? attributes, + }) { + return _captureLog(SentryLogLevel.fatal, body, attributes: attributes); + } + + @override + SentryLoggerFormatter get fmt => _formatter; + + // Helpers + + FutureOr _captureLog( + SentryLogLevel level, + String body, { + Map? attributes, + }) { + internalLogger.debug(() => + 'Sentry.logger.${level.value}("$body") called with attributes ${_formatAttributes(attributes)}'); + + final log = SentryLog( + timestamp: _clockProvider(), + level: level, + body: body, + traceId: _scopeProvider().propagationContext.traceId, + spanId: _scopeProvider().span?.context.spanId, + attributes: attributes ?? {}, + ); + + return _captureLogCallback(log); + } + + String _formatAttributes(Map? attributes) { + final formatted = attributes?.toFormattedString() ?? ''; + return formatted.isEmpty ? '' : ' $formatted'; + } +} + +final class _DefaultSentryLoggerFormatter implements SentryLoggerFormatter { + _DefaultSentryLoggerFormatter(this._logger); + + final DefaultSentryLogger _logger; + + @override FutureOr trace( String templateBody, List arguments, { @@ -23,6 +123,7 @@ class SentryLoggerFormatter { ); } + @override FutureOr debug( String templateBody, List arguments, { @@ -38,6 +139,7 @@ class SentryLoggerFormatter { ); } + @override FutureOr info( String templateBody, List arguments, { @@ -53,6 +155,7 @@ class SentryLoggerFormatter { ); } + @override FutureOr warn( String templateBody, List arguments, { @@ -68,6 +171,7 @@ class SentryLoggerFormatter { ); } + @override FutureOr error( String templateBody, List arguments, { @@ -83,6 +187,7 @@ class SentryLoggerFormatter { ); } + @override FutureOr fatal( String templateBody, List arguments, { diff --git a/packages/dart/lib/src/protocol/sentry_log.dart b/packages/dart/lib/src/telemetry/log/log.dart similarity index 73% rename from packages/dart/lib/src/protocol/sentry_log.dart rename to packages/dart/lib/src/telemetry/log/log.dart index 6612b8d700..ac0a211544 100644 --- a/packages/dart/lib/src/protocol/sentry_log.dart +++ b/packages/dart/lib/src/telemetry/log/log.dart @@ -1,10 +1,12 @@ -import 'sentry_attribute.dart'; -import 'sentry_id.dart'; -import 'sentry_log_level.dart'; +import '../../protocol/sentry_attribute.dart'; +import '../../protocol/sentry_id.dart'; +import '../../protocol/span_id.dart'; +import 'log_level.dart'; class SentryLog { DateTime timestamp; - SentryId traceId; + SentryId? traceId; + SpanId? spanId; SentryLogLevel level; String body; Map attributes; @@ -14,10 +16,12 @@ class SentryLog { /// by the time processing completes, it is guaranteed to be a valid non-empty trace id. SentryLog({ required this.timestamp, + // TODO(major-v10): this should be required non-null SentryId? traceId, required this.level, required this.body, required this.attributes, + this.spanId, this.severityNumber, }) : traceId = traceId ?? SentryId.empty(); @@ -25,6 +29,7 @@ class SentryLog { return { 'timestamp': timestamp.toIso8601String(), 'trace_id': traceId.toString(), + if (spanId != null) 'span_id': spanId.toString(), 'level': level.value, 'body': body, 'attributes': diff --git a/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart new file mode 100644 index 0000000000..4abf24b6df --- /dev/null +++ b/packages/dart/lib/src/telemetry/log/log_capture_pipeline.dart @@ -0,0 +1,82 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +import '../../../sentry.dart'; +import '../../client_reports/discard_reason.dart'; +import '../../transport/data_category.dart'; +import '../../utils/internal_logger.dart'; +import '../default_attributes.dart'; + +@internal +class LogCapturePipeline { + final SentryOptions _options; + + LogCapturePipeline(this._options); + + FutureOr captureLog(SentryLog log, {Scope? scope}) async { + if (!_options.enableLogs) { + internalLogger + .debug('$LogCapturePipeline: Logs disabled, dropping ${log.body}'); + return; + } + + try { + if (scope != null) { + // Populate traceId from scope if not already set + // TODO(major-v10): this can be removed once we make the traceId required on the log + if (log.traceId == null || log.traceId == SentryId.empty()) { + log.traceId = scope.propagationContext.traceId; + } + log.attributes.addAllIfAbsent(scope.attributes); + } + + await _options.lifecycleRegistry + .dispatchCallback(OnProcessLog(log)); + + log.attributes.addAllIfAbsent(defaultAttributes(_options, scope: scope)); + + final beforeSendLog = _options.beforeSendLog; + SentryLog? processedLog = log; + if (beforeSendLog != null) { + try { + final callbackResult = beforeSendLog(log); + + if (callbackResult is Future) { + processedLog = await callbackResult; + } else { + processedLog = callbackResult; + } + } catch (exception, stackTrace) { + internalLogger.error( + 'The beforeSendLog callback threw an exception', + error: exception, + stackTrace: stackTrace, + ); + if (_options.automatedTestMode) { + rethrow; + } + } + } + + if (processedLog == null) { + _options.recorder + .recordLostEvent(DiscardReason.beforeSend, DataCategory.logItem); + internalLogger.debug( + '$LogCapturePipeline: Log "${log.body}" dropped by beforeSendLog'); + return; + } + + _options.telemetryProcessor.addLog(processedLog); + } catch (exception, stackTrace) { + internalLogger.error( + 'Error capturing log "${log.body}"', + error: exception, + stackTrace: stackTrace, + ); + if (_options.automatedTestMode) { + rethrow; + } + } + } +} diff --git a/packages/dart/lib/src/protocol/sentry_log_level.dart b/packages/dart/lib/src/telemetry/log/log_level.dart similarity index 96% rename from packages/dart/lib/src/protocol/sentry_log_level.dart rename to packages/dart/lib/src/telemetry/log/log_level.dart index 5fd441e8c9..aa90878613 100644 --- a/packages/dart/lib/src/protocol/sentry_log_level.dart +++ b/packages/dart/lib/src/telemetry/log/log_level.dart @@ -1,4 +1,4 @@ -import 'sentry_level.dart'; +import '../../protocol/sentry_level.dart'; enum SentryLogLevel { trace('trace'), diff --git a/packages/dart/lib/src/telemetry/log/logger.dart b/packages/dart/lib/src/telemetry/log/logger.dart new file mode 100644 index 0000000000..0b8f253832 --- /dev/null +++ b/packages/dart/lib/src/telemetry/log/logger.dart @@ -0,0 +1,96 @@ +import 'dart:async'; + +import '../../../sentry.dart'; + +// TODO(major-v10): refactor FutureOr to void + +/// Interface for emitting custom logs to Sentry. +/// +/// Access via [Sentry.logger]. +abstract interface class SentryLogger { + /// Logs a message at TRACE level. + FutureOr trace( + String body, { + Map? attributes, + }); + + /// Logs a message at DEBUG level. + FutureOr debug( + String body, { + Map? attributes, + }); + + /// Logs a message at INFO level. + FutureOr info( + String body, { + Map? attributes, + }); + + /// Logs a message at WARN level. + FutureOr warn( + String body, { + Map? attributes, + }); + + /// Logs a message at ERROR level. + FutureOr error( + String body, { + Map? attributes, + }); + + /// Logs a message at FATAL level. + FutureOr fatal( + String body, { + Map? attributes, + }); + + /// Provides formatted logging with template strings. + SentryLoggerFormatter get fmt; +} + +/// Interface for formatted logging with template strings. +/// +/// Access via [SentryLogger.fmt]. +abstract interface class SentryLoggerFormatter { + /// Logs a formatted message at TRACE level. + FutureOr trace( + String templateBody, + List arguments, { + Map? attributes, + }); + + /// Logs a formatted message at DEBUG level. + FutureOr debug( + String templateBody, + List arguments, { + Map? attributes, + }); + + /// Logs a formatted message at INFO level. + FutureOr info( + String templateBody, + List arguments, { + Map? attributes, + }); + + /// Logs a formatted message at WARN level. + FutureOr warn( + String templateBody, + List arguments, { + Map? attributes, + }); + + /// Logs a formatted message at ERROR level. + FutureOr error( + String templateBody, + List arguments, { + Map? attributes, + }); + + /// Logs a formatted message at FATAL level. + FutureOr fatal( + String templateBody, + List arguments, { + Map? attributes, + }); +} diff --git a/packages/dart/lib/src/telemetry/log/logger_setup_integration.dart b/packages/dart/lib/src/telemetry/log/logger_setup_integration.dart new file mode 100644 index 0000000000..9eb48cffae --- /dev/null +++ b/packages/dart/lib/src/telemetry/log/logger_setup_integration.dart @@ -0,0 +1,32 @@ +import '../../../sentry.dart'; +import '../../utils/internal_logger.dart'; +import 'default_logger.dart'; +import 'noop_logger.dart'; + +/// Integration that sets up the default Sentry logger implementation. +class LoggerSetupIntegration extends Integration { + static const integrationName = 'LoggerSetup'; + + @override + void call(Hub hub, SentryOptions options) { + if (!options.enableLogs) { + internalLogger.debug('$integrationName: Logs disabled, skipping setup'); + return; + } + + if (options.logger is! NoOpSentryLogger) { + internalLogger.debug( + '$integrationName: Custom logger already configured, skipping setup'); + return; + } + + options.logger = DefaultSentryLogger( + captureLogCallback: hub.captureLog, + clockProvider: options.clock, + scopeProvider: () => hub.scope, + ); + + options.sdk.addIntegration(integrationName); + internalLogger.debug('$integrationName: Logger configured successfully'); + } +} diff --git a/packages/dart/lib/src/telemetry/log/noop_logger.dart b/packages/dart/lib/src/telemetry/log/noop_logger.dart new file mode 100644 index 0000000000..0381bdcb88 --- /dev/null +++ b/packages/dart/lib/src/telemetry/log/noop_logger.dart @@ -0,0 +1,95 @@ +import 'dart:async'; + +import '../../protocol/sentry_attribute.dart'; +import 'logger.dart'; + +final class NoOpSentryLogger implements SentryLogger { + const NoOpSentryLogger(); + + static const _formatter = _NoOpSentryLoggerFormatter(); + + @override + FutureOr trace( + String body, { + Map? attributes, + }) {} + + @override + FutureOr debug( + String body, { + Map? attributes, + }) {} + + @override + FutureOr info( + String body, { + Map? attributes, + }) {} + + @override + FutureOr warn( + String body, { + Map? attributes, + }) {} + + @override + FutureOr error( + String body, { + Map? attributes, + }) {} + + @override + FutureOr fatal( + String body, { + Map? attributes, + }) {} + + @override + SentryLoggerFormatter get fmt => _formatter; +} + +final class _NoOpSentryLoggerFormatter implements SentryLoggerFormatter { + const _NoOpSentryLoggerFormatter(); + + @override + FutureOr trace( + String templateBody, + List arguments, { + Map? attributes, + }) {} + + @override + FutureOr debug( + String templateBody, + List arguments, { + Map? attributes, + }) {} + + @override + FutureOr info( + String templateBody, + List arguments, { + Map? attributes, + }) {} + + @override + FutureOr warn( + String templateBody, + List arguments, { + Map? attributes, + }) {} + + @override + FutureOr error( + String templateBody, + List arguments, { + Map? attributes, + }) {} + + @override + FutureOr fatal( + String templateBody, + List arguments, { + Map? attributes, + }) {} +} diff --git a/packages/dart/lib/src/telemetry/processing/processor.dart b/packages/dart/lib/src/telemetry/processing/processor.dart index 778d7accb7..43c2ab32ae 100644 --- a/packages/dart/lib/src/telemetry/processing/processor.dart +++ b/packages/dart/lib/src/telemetry/processing/processor.dart @@ -58,6 +58,9 @@ class DefaultTelemetryProcessor implements TelemetryProcessor { } _logBuffer.add(log); + + internalLogger.debug(() => + '$runtimeType: Log "${log.body}" (${log.level.name}) added to buffer'); } @override @@ -70,6 +73,9 @@ class DefaultTelemetryProcessor implements TelemetryProcessor { } _metricBuffer.add(metric); + + internalLogger.debug(() => + '$runtimeType: Metric "${metric.name}" (${metric.value}) added to buffer'); } @override diff --git a/packages/dart/lib/src/telemetry/telemetry.dart b/packages/dart/lib/src/telemetry/telemetry.dart index f1bbe2e574..98c0f23ba0 100644 --- a/packages/dart/lib/src/telemetry/telemetry.dart +++ b/packages/dart/lib/src/telemetry/telemetry.dart @@ -1,2 +1,4 @@ export 'metric/metrics.dart'; export 'metric/metric.dart'; +export 'log/log.dart'; +export 'log/logger.dart'; diff --git a/packages/dart/test/logs_enricher_integration_test.dart b/packages/dart/test/logs_enricher_integration_test.dart deleted file mode 100644 index 38644c1c74..0000000000 --- a/packages/dart/test/logs_enricher_integration_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -@TestOn('vm') -library; - -import 'package:sentry/src/logs_enricher_integration.dart'; -import 'package:test/test.dart'; -import 'package:sentry/src/hub.dart'; -import 'package:sentry/src/protocol/sentry_log.dart'; -import 'package:sentry/src/protocol/sentry_attribute.dart'; -import 'package:sentry/src/protocol/sentry_id.dart'; -import 'package:sentry/src/protocol/sentry_log_level.dart'; -import 'test_utils.dart'; -import 'package:sentry/src/utils/os_utils.dart'; - -void main() { - SentryLog givenLog() { - return SentryLog( - timestamp: DateTime.now(), - traceId: SentryId.newId(), - level: SentryLogLevel.info, - body: 'test', - attributes: { - 'attribute': SentryAttribute.string('value'), - }, - ); - } - - group('LogsEnricherIntegration', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - test('adds itself to sdk.integrations if enableLogs is true', () { - fixture.options.enableLogs = true; - fixture.addIntegration(); - - expect( - fixture.options.sdk.integrations - .contains(LogsEnricherIntegration.integrationName), - true, - ); - }); - - test('does not add itself to sdk.integrations if enableLogs is false', () { - fixture.options.enableLogs = false; - fixture.addIntegration(); - - expect( - fixture.options.sdk.integrations - .contains(LogsEnricherIntegration.integrationName), - false, - ); - }); - - test( - 'adds os.name and os.version to log attributes on OnBeforeCaptureLog lifecycle event', - () async { - fixture.options.enableLogs = true; - fixture.addIntegration(); - - final log = givenLog(); - await fixture.hub.captureLog(log); - - final os = getSentryOperatingSystem(); - - expect(log.attributes['os.name']?.value, os.name); - expect(log.attributes['os.version']?.value, os.version); - }); - - test( - 'does not add os.name and os.version to log attributes if enableLogs is false', - () async { - fixture.options.enableLogs = false; - fixture.addIntegration(); - - final log = givenLog(); - await fixture.hub.captureLog(log); - - expect(log.attributes['os.name'], isNull); - expect(log.attributes['os.version'], isNull); - }); - }); -} - -class Fixture { - final options = defaultTestOptions(); - late final hub = Hub(options); - late final LogsEnricherIntegration integration; - - Fixture() { - options.enableLogs = true; - integration = LogsEnricherIntegration(); - } - - void addIntegration() { - integration.call(hub, options); - } -} diff --git a/packages/dart/test/mocks/mock_hub.dart b/packages/dart/test/mocks/mock_hub.dart index 6b8d530cae..2499af1a29 100644 --- a/packages/dart/test/mocks/mock_hub.dart +++ b/packages/dart/test/mocks/mock_hub.dart @@ -111,8 +111,8 @@ class MockHub with NoSuchMethodProvider implements Hub { } @override - FutureOr captureLog(SentryLog log) async { - captureLogCalls.add(CaptureLogCall(log, null)); + FutureOr captureLog(SentryLog log, {Scope? scope}) async { + captureLogCalls.add(CaptureLogCall(log, scope)); } @override diff --git a/packages/dart/test/mocks/mock_log_capture_pipeline.dart b/packages/dart/test/mocks/mock_log_capture_pipeline.dart new file mode 100644 index 0000000000..0e88e6b4da --- /dev/null +++ b/packages/dart/test/mocks/mock_log_capture_pipeline.dart @@ -0,0 +1,19 @@ +import 'dart:async'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/log/log_capture_pipeline.dart'; + +import 'mock_sentry_client.dart'; + +class MockLogCapturePipeline extends LogCapturePipeline { + MockLogCapturePipeline(super.options); + + final List captureLogCalls = []; + + int get callCount => captureLogCalls.length; + + @override + FutureOr captureLog(SentryLog log, {Scope? scope}) async { + captureLogCalls.add(CaptureLogCall(log, scope)); + } +} diff --git a/packages/dart/test/sentry_client_lifecycle_test.dart b/packages/dart/test/sentry_client_lifecycle_test.dart index 060cb6e261..9e94217e6c 100644 --- a/packages/dart/test/sentry_client_lifecycle_test.dart +++ b/packages/dart/test/sentry_client_lifecycle_test.dart @@ -16,49 +16,6 @@ void main() { setUp(() => fixture = Fixture()); - group('Logs', () { - SentryLog givenLog() { - return SentryLog( - timestamp: DateTime.now(), - traceId: SentryId.newId(), - level: SentryLogLevel.info, - body: 'test', - attributes: { - 'attribute': SentryAttribute.string('value'), - }, - ); - } - - test('captureLog triggers OnBeforeCaptureLog', () async { - fixture.options.enableLogs = true; - fixture.options.environment = 'test-environment'; - fixture.options.release = 'test-release'; - - final log = givenLog(); - - final scope = Scope(fixture.options); - final span = MockSpan(); - scope.span = span; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - fixture.options.lifecycleRegistry - .registerCallback((event) { - event.log.attributes['test'] = SentryAttribute.string('test-value'); - }); - - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.attributes['test']?.value, "test-value"); - expect(capturedLog.attributes['test']?.type, 'string'); - }); - }); - group('SentryEvent', () { test('captureEvent triggers OnBeforeSendEvent', () async { fixture.options.enableLogs = true; diff --git a/packages/dart/test/sentry_client_sdk_lifecycle_test.dart b/packages/dart/test/sentry_client_sdk_lifecycle_test.dart index 060cb6e261..9de1f753c7 100644 --- a/packages/dart/test/sentry_client_sdk_lifecycle_test.dart +++ b/packages/dart/test/sentry_client_sdk_lifecycle_test.dart @@ -29,7 +29,7 @@ void main() { ); } - test('captureLog triggers OnBeforeCaptureLog', () async { + test('captureLog triggers OnProcessLog', () async { fixture.options.enableLogs = true; fixture.options.environment = 'test-environment'; fixture.options.release = 'test-release'; @@ -45,7 +45,7 @@ void main() { fixture.options.telemetryProcessor = mockProcessor; fixture.options.lifecycleRegistry - .registerCallback((event) { + .registerCallback((event) { event.log.attributes['test'] = SentryAttribute.string('test-value'); }); diff --git a/packages/dart/test/sentry_client_test.dart b/packages/dart/test/sentry_client_test.dart index c2c7eb1e6f..0d7b38e91f 100644 --- a/packages/dart/test/sentry_client_test.dart +++ b/packages/dart/test/sentry_client_test.dart @@ -23,11 +23,11 @@ import 'package:http/http.dart' as http; import 'mocks.dart'; import 'mocks/mock_client_report_recorder.dart'; import 'mocks/mock_hub.dart'; +import 'mocks/mock_log_capture_pipeline.dart'; import 'mocks/mock_metric_capture_pipeline.dart'; import 'mocks/mock_telemetry_processor.dart'; import 'mocks/mock_transport.dart'; import 'test_utils.dart'; -import 'utils/url_details_test.dart'; void main() { group('SentryClient captures message', () { @@ -1721,8 +1721,13 @@ void main() { fixture = Fixture(); }); - SentryLog givenLog() { - return SentryLog( + test('delegates to log pipeline', () async { + final pipeline = MockLogCapturePipeline(fixture.options); + final client = + SentryClient(fixture.options, logCapturePipeline: pipeline); + final scope = Scope(fixture.options); + + final log = SentryLog( timestamp: DateTime.now(), traceId: SentryId.newId(), level: SentryLogLevel.info, @@ -1731,333 +1736,40 @@ void main() { 'attribute': SentryAttribute.string('value'), }, ); - } - - test('disabled by default', () async { - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - - await client.captureLog(log); - - expect(mockProcessor.addedLogs, isEmpty); - }); - - test('should capture logs as envelope', () async { - fixture.options.enableLogs = true; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - - await client.captureLog(log); - - expect(mockProcessor.addedLogs.length, 1); - - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.traceId, log.traceId); - expect(capturedLog.level, log.level); - expect(capturedLog.body, log.body); - expect(capturedLog.attributes['attribute']?.value, - log.attributes['attribute']?.value); - }); - - test('should add additional info to attributes', () async { - fixture.options.enableLogs = true; - fixture.options.environment = 'test-environment'; - fixture.options.release = 'test-release'; - - final log = givenLog(); - - final scope = Scope(fixture.options); - final span = MockSpan(); - scope.span = span; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; await client.captureLog(log, scope: scope); - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect( - capturedLog.attributes['sentry.sdk.name']?.value, - fixture.options.sdk.name, - ); - expect( - capturedLog.attributes['sentry.sdk.name']?.type, - 'string', - ); - expect( - capturedLog.attributes['sentry.sdk.version']?.value, - fixture.options.sdk.version, - ); - expect( - capturedLog.attributes['sentry.sdk.version']?.type, - 'string', - ); - expect( - capturedLog.attributes['sentry.environment']?.value, - fixture.options.environment, - ); - expect( - capturedLog.attributes['sentry.environment']?.type, - 'string', - ); - expect( - capturedLog.attributes['sentry.release']?.value, - fixture.options.release, - ); - expect( - capturedLog.attributes['sentry.release']?.type, - 'string', - ); - expect( - capturedLog.attributes['sentry.trace.parent_span_id']?.value, - span.context.spanId.toString(), - ); - expect( - capturedLog.attributes['sentry.trace.parent_span_id']?.type, - 'string', - ); - }); - - test('should use attributes from given scope', () async { - fixture.options.enableLogs = true; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - final log = givenLog(); - - final scope = Scope(fixture.options); - scope.setAttributes({'from_scope': SentryAttribute.int(12)}); - - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - expect(capturedLog.attributes['from_scope']?.value, 12); - }); - - test('per-log attributes override scope on same key', () async { - fixture.options.enableLogs = true; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - final log = givenLog(); - - final scope = Scope(fixture.options); - scope.setAttributes({ - 'overridden': SentryAttribute.string('fromScope'), - 'kept': SentryAttribute.bool(true), - }); - - log.attributes['overridden'] = SentryAttribute.string('fromLog'); - log.attributes['logOnly'] = SentryAttribute.double(1.23); - - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final captured = mockProcessor.addedLogs.first; - - expect(captured.attributes['overridden']?.value, 'fromLog'); - expect(captured.attributes['kept']?.value, true); - expect(captured.attributes['logOnly']?.type, 'double'); + expect(pipeline.callCount, 1); + expect(pipeline.captureLogCalls.first.log, same(log)); + expect(pipeline.captureLogCalls.first.scope, same(scope)); }); + }); - test('should add user info to attributes', () async { - fixture.options.enableLogs = true; - - final log = givenLog(); - final scope = Scope(fixture.options); - final user = SentryUser( - id: '123', - email: 'test@test.com', - name: 'test-name', - ); - await scope.setUser(user); - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect( - capturedLog.attributes['user.id']?.value, - user.id, - ); - expect( - capturedLog.attributes['user.id']?.type, - 'string', - ); - - expect( - capturedLog.attributes['user.name']?.value, - user.name, - ); - expect( - capturedLog.attributes['user.name']?.type, - 'string', - ); + group('SentryClient captureMetric', () { + late Fixture fixture; - expect( - capturedLog.attributes['user.email']?.value, - user.email, - ); - expect( - capturedLog.attributes['user.email']?.type, - 'string', - ); + setUp(() { + fixture = Fixture(); }); - test('should set trace id from propagation context', () async { - fixture.options.enableLogs = true; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); + test('delegates to metric pipeline', () async { + final pipeline = MockMetricCapturePipeline(fixture.options); + final client = + SentryClient(fixture.options, metricCapturePipeline: pipeline); final scope = Scope(fixture.options); - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.traceId, scope.propagationContext.traceId); - }); - - test( - '$BeforeSendLogCallback returning null drops the log and record it as lost', - () async { - fixture.options.enableLogs = true; - fixture.options.beforeSendLog = (log) => null; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - - await client.captureLog(log); - - expect(mockProcessor.addedLogs.length, 0); - - expect( - fixture.recorder.discardedEvents.first.reason, - DiscardReason.beforeSend, - ); - expect( - fixture.recorder.discardedEvents.first.category, - DataCategory.logItem, + final metric = SentryCounterMetric( + timestamp: DateTime.now().toUtc(), + name: 'test-metric', + value: 1, + traceId: SentryId.newId(), ); - }); - - test('$BeforeSendLogCallback returning a log modifies it', () async { - fixture.options.enableLogs = true; - fixture.options.beforeSendLog = (log) { - log.body = 'modified'; - return log; - }; - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - - await client.captureLog(log); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.body, 'modified'); - }); - - test('$BeforeSendLogCallback returning a log async modifies it', () async { - fixture.options.enableLogs = true; - fixture.options.beforeSendLog = (log) async { - await Future.delayed(Duration(milliseconds: 100)); - log.body = 'modified'; - return log; - }; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - - await client.captureLog(log); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.body, 'modified'); - }); - - test('$BeforeSendLogCallback throwing is caught', () async { - fixture.options.enableLogs = true; - fixture.options.automatedTestMode = false; - - fixture.options.beforeSendLog = (log) { - throw Exception('test'); - }; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - final log = givenLog(); - await client.captureLog(log); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; - - expect(capturedLog.body, 'test'); - }); - - test('OnBeforeCaptureLog lifecycle event is called', () async { - fixture.options.enableLogs = true; - fixture.options.environment = 'test-environment'; - fixture.options.release = 'test-release'; - - final log = givenLog(); - - final scope = Scope(fixture.options); - final span = MockSpan(); - scope.span = span; - - final client = fixture.getSut(); - final mockProcessor = MockTelemetryProcessor(); - fixture.options.telemetryProcessor = mockProcessor; - - fixture.options.lifecycleRegistry - .registerCallback((event) { - event.log.attributes['test'] = SentryAttribute.string('test-value'); - }); - - await client.captureLog(log, scope: scope); - - expect(mockProcessor.addedLogs.length, 1); - final capturedLog = mockProcessor.addedLogs.first; + await client.captureMetric(metric, scope: scope); - expect(capturedLog.attributes['test']?.value, "test-value"); - expect(capturedLog.attributes['test']?.type, 'string'); + expect(pipeline.callCount, 1); + expect(pipeline.captureMetricCalls.first.metric, same(metric)); + expect(pipeline.captureMetricCalls.first.scope, same(scope)); }); }); diff --git a/packages/dart/test/sentry_logger_formatter_test.dart b/packages/dart/test/sentry_logger_formatter_test.dart deleted file mode 100644 index 443fade82a..0000000000 --- a/packages/dart/test/sentry_logger_formatter_test.dart +++ /dev/null @@ -1,429 +0,0 @@ -import 'package:test/test.dart'; -import 'package:sentry/src/sentry_logger_formatter.dart'; -import 'package:sentry/src/sentry_logger.dart'; -import 'package:sentry/src/protocol/sentry_attribute.dart'; - -void main() { - final fixture = Fixture(); - - void verifyPassedAttributes(Map attributes) { - expect(attributes['foo'].type, 'string'); - expect(attributes['foo'].value, 'bar'); - } - - void verifyBasicTemplate(String body, Map attributes) { - expect(body, 'Hello, World!'); - expect(attributes['sentry.message.template'].type, 'string'); - expect(attributes['sentry.message.template'].value, 'Hello, %s!'); - expect(attributes['sentry.message.parameter.0'].type, 'string'); - expect(attributes['sentry.message.parameter.0'].value, 'World'); - verifyPassedAttributes(attributes); - } - - void verifyTemplateWithMultipleArguments( - String body, Map attributes) { - expect(body, 'Name: Alice, Age: 30, Active: true, Score: 95.5'); - expect(attributes['sentry.message.template'].type, 'string'); - expect(attributes['sentry.message.template'].value, - 'Name: %s, Age: %s, Active: %s, Score: %s'); - expect(attributes['sentry.message.parameter.0'].type, 'string'); - expect(attributes['sentry.message.parameter.0'].value, 'Alice'); - expect(attributes['sentry.message.parameter.1'].type, 'integer'); - expect(attributes['sentry.message.parameter.1'].value, 30); - expect(attributes['sentry.message.parameter.2'].type, 'boolean'); - expect(attributes['sentry.message.parameter.2'].value, true); - expect(attributes['sentry.message.parameter.3'].type, 'double'); - expect(attributes['sentry.message.parameter.3'].value, 95.5); - verifyPassedAttributes(attributes); - } - - group('format basic template', () { - test('for trace', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.trace( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.traceCalls.length, 1); - final message = logger.traceCalls[0].message; - final attributes = logger.traceCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - - test('for debug', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.debug( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.debugCalls.length, 1); - final message = logger.debugCalls[0].message; - final attributes = logger.debugCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - - test('for info', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.info( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.infoCalls.length, 1); - final message = logger.infoCalls[0].message; - final attributes = logger.infoCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - - test('for warn', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.warn( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.warnCalls.length, 1); - final message = logger.warnCalls[0].message; - final attributes = logger.warnCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - - test('for error', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.error( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.errorCalls.length, 1); - final message = logger.errorCalls[0].message; - final attributes = logger.errorCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - - test('for fatal', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.fatal( - "Hello, %s!", - ["World"], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.fatalCalls.length, 1); - final message = logger.fatalCalls[0].message; - final attributes = logger.fatalCalls[0].attributes!; - verifyBasicTemplate(message, attributes); - }); - }); - - group('template with multiple arguments', () { - test('for trace', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.trace( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.traceCalls.length, 1); - final message = logger.traceCalls[0].message; - final attributes = logger.traceCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for trace', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.trace( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.traceCalls.length, 1); - final message = logger.traceCalls[0].message; - final attributes = logger.traceCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for debug', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.debug( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.debugCalls.length, 1); - final message = logger.debugCalls[0].message; - final attributes = logger.debugCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for info', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.info( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.infoCalls.length, 1); - final message = logger.infoCalls[0].message; - final attributes = logger.infoCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for warn', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.warn( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.warnCalls.length, 1); - final message = logger.warnCalls[0].message; - final attributes = logger.warnCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for error', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.error( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.errorCalls.length, 1); - final message = logger.errorCalls[0].message; - final attributes = logger.errorCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - - test('for fatal', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.fatal( - "Name: %s, Age: %s, Active: %s, Score: %s", - ['Alice', 30, true, 95.5], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.fatalCalls.length, 1); - final message = logger.fatalCalls[0].message; - final attributes = logger.fatalCalls[0].attributes!; - verifyTemplateWithMultipleArguments(message, attributes); - }); - }); - - group('template with no arguments', () { - test('for trace', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.trace( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.traceCalls.length, 1); - final message = logger.traceCalls[0].message; - final attributes = logger.traceCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - - test('for debug', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.debug( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.debugCalls.length, 1); - final message = logger.debugCalls[0].message; - final attributes = logger.debugCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - - test('for info', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.info( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.infoCalls.length, 1); - final message = logger.infoCalls[0].message; - final attributes = logger.infoCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - - test('for warn', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.warn( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.warnCalls.length, 1); - final message = logger.warnCalls[0].message; - final attributes = logger.warnCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - - test('for error', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.error( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.errorCalls.length, 1); - final message = logger.errorCalls[0].message; - final attributes = logger.errorCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - - test('for fatal', () { - final logger = MockLogger(); - final sut = fixture.getSut(logger); - - sut.fatal( - "Hello, World!", - [], - attributes: {'foo': SentryAttribute.string('bar')}, - ); - - expect(logger.fatalCalls.length, 1); - final message = logger.fatalCalls[0].message; - final attributes = logger.fatalCalls[0].attributes!; - - expect(message, 'Hello, World!'); - expect(attributes['sentry.message.template'], isNull); - expect(attributes['sentry.message.parameter.0'], isNull); - verifyPassedAttributes(attributes); - }); - }); -} - -class Fixture { - SentryLoggerFormatter getSut(SentryLogger logger) { - return SentryLoggerFormatter(logger); - } -} - -class MockLogger implements SentryLogger { - var traceCalls = []; - var debugCalls = []; - var infoCalls = []; - var warnCalls = []; - var errorCalls = []; - var fatalCalls = []; - - @override - SentryLoggerFormatter get fmt => throw UnimplementedError(); - - @override - Future trace(String message, {Map? attributes}) async { - traceCalls.add((message: message, attributes: attributes)); - return; - } - - @override - Future debug(String message, {Map? attributes}) async { - debugCalls.add((message: message, attributes: attributes)); - return; - } - - @override - Future info(String message, {Map? attributes}) async { - infoCalls.add((message: message, attributes: attributes)); - return; - } - - @override - Future warn(String message, {Map? attributes}) async { - warnCalls.add((message: message, attributes: attributes)); - return; - } - - @override - Future error(String message, {Map? attributes}) async { - errorCalls.add((message: message, attributes: attributes)); - return; - } - - @override - Future fatal(String message, {Map? attributes}) async { - fatalCalls.add((message: message, attributes: attributes)); - return; - } -} - -typedef LoggerCall = ({String message, Map? attributes}); diff --git a/packages/dart/test/sentry_logger_test.dart b/packages/dart/test/sentry_logger_test.dart deleted file mode 100644 index 37e760a204..0000000000 --- a/packages/dart/test/sentry_logger_test.dart +++ /dev/null @@ -1,252 +0,0 @@ -import 'package:sentry/sentry.dart'; -import 'package:test/test.dart'; -import 'test_utils.dart'; -import 'mocks/mock_hub.dart'; - -void main() { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - }); - - void verifyCaptureLog(SentryLogLevel level) { - expect(fixture.hub.captureLogCalls.length, 1); - final capturedLog = fixture.hub.captureLogCalls[0].log; - - expect(capturedLog.timestamp, fixture.timestamp); - expect(capturedLog.level, level); - expect(capturedLog.body, 'test'); - expect(capturedLog.attributes, fixture.attributes); - } - - test('sentry logger', () { - final logger = fixture.getSut(); - - logger.info('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.info); - }); - - test('trace', () { - final logger = fixture.getSut(); - - logger.info('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.info); - }); - - test('debug', () { - final logger = fixture.getSut(); - - logger.debug('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.debug); - }); - - test('info', () { - final logger = fixture.getSut(); - - logger.info('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.info); - }); - - test('warn', () { - final logger = fixture.getSut(); - - logger.warn('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.warn); - }); - - test('error', () { - final logger = fixture.getSut(); - - logger.error('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.error); - }); - - test('fatal', () { - final logger = fixture.getSut(); - - logger.fatal('test', attributes: fixture.attributes); - - verifyCaptureLog(SentryLogLevel.fatal); - }); - - test('logs to hub options when provided', () { - final mockLogCallback = _MockSdkLogCallback(); - - // Set the mock log callback on the fixture hub - fixture.hub.options.log = mockLogCallback.call; - fixture.hub.options.debug = true; - fixture.hub.options.diagnosticLevel = SentryLevel.debug; - - final logger = SentryLogger( - () => fixture.timestamp, - 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); - - // Verify the captured log has the right content - final capturedLog = fixture.hub.captureLogCalls[0].log; - expect(capturedLog.level, SentryLogLevel.trace); - expect(capturedLog.body, 'test message'); - expect(capturedLog.attributes, fixture.attributes); - - // Verify the log callback was called with the right parameters - final logCall = mockLogCallback.calls[0]; - expect(logCall.level, SentryLevel.debug); // trace maps to debug - expect(logCall.message, - 'test message {"string": "string", "int": 1, "double": 1.23456789, "bool": true, "double_int": 1.0, "nan": NaN, "positive_infinity": Infinity, "negative_infinity": -Infinity}'); - expect(logCall.logger, 'sentry_logger'); - }); - - test('bridges SentryLogLevel to SentryLevel correctly', () { - final mockLogCallback = _MockSdkLogCallback(); - - // Set the mock log callback on the fixture hub's options - fixture.hub.options.log = mockLogCallback.call; - fixture.hub.options.debug = true; - fixture.hub.options.diagnosticLevel = SentryLevel.debug; - - final logger = SentryLogger( - () => fixture.timestamp, - 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 both the hub and the log callback - expect(fixture.hub.captureLogCalls.length, 6); - 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 - }); - - test('handles NaN and infinite values correctly', () { - final mockLogCallback = _MockSdkLogCallback(); - - // Set the mock log callback on the fixture hub's options - fixture.hub.options.log = mockLogCallback.call; - fixture.hub.options.debug = true; - fixture.hub.options.diagnosticLevel = SentryLevel.debug; - - final logger = SentryLogger( - () => fixture.timestamp, - hub: fixture.hub, - ); - - // Test with special double values - final specialAttributes = { - 'nan': SentryAttribute.double(double.nan), - 'positive_infinity': SentryAttribute.double(double.infinity), - 'negative_infinity': SentryAttribute.double(double.negativeInfinity), - }; - - logger.info('special values', attributes: specialAttributes); - - // Verify that both hub.captureLog and our callback were called - expect(fixture.hub.captureLogCalls.length, 1); - expect(mockLogCallback.calls.length, 1); - - // Verify the captured log has the right content - final capturedLog = fixture.hub.captureLogCalls[0].log; - expect(capturedLog.level, SentryLogLevel.info); - expect(capturedLog.body, 'special values'); - expect(capturedLog.attributes, specialAttributes); - - // Verify the log callback was called with the right parameters - final logCall = mockLogCallback.calls[0]; - expect(logCall.level, SentryLevel.info); - expect(logCall.message, - 'special values {"nan": NaN, "positive_infinity": Infinity, "negative_infinity": -Infinity}'); - expect(logCall.logger, 'sentry_logger'); - }); - - // This is mostly an edge case but let's cover it just in case - test('per-log attributes override fmt template attributes on same key', () { - final logger = fixture.getSut(); - - logger.fmt.info( - 'Hello, %s!', - ['World'], - attributes: { - 'sentry.message.template': SentryAttribute.string('OVERRIDE'), - 'sentry.message.parameter.0': SentryAttribute.string('Earth'), - }, - ); - - final attrs = fixture.hub.captureLogCalls[0].log.attributes; - expect(attrs['sentry.message.template']?.value, 'OVERRIDE'); - expect(attrs['sentry.message.parameter.0']?.value, 'Earth'); - }); -} - -class Fixture { - final options = defaultTestOptions(); - final hub = MockHub(); - final timestamp = DateTime.fromMicrosecondsSinceEpoch(0); - - final attributes = { - 'string': SentryAttribute.string('string'), - 'int': SentryAttribute.int(1), - 'double': SentryAttribute.double(1.23456789), - 'bool': SentryAttribute.bool(true), - 'double_int': SentryAttribute.double(1.0), - 'nan': SentryAttribute.double(double.nan), - 'positive_infinity': SentryAttribute.double(double.infinity), - 'negative_infinity': SentryAttribute.double(double.negativeInfinity), - }; - - SentryLogger getSut() { - return SentryLogger(() => timestamp, 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); -} diff --git a/packages/dart/test/sentry_test.dart b/packages/dart/test/sentry_test.dart index ab688fdfd7..4c7bac028e 100644 --- a/packages/dart/test/sentry_test.dart +++ b/packages/dart/test/sentry_test.dart @@ -6,7 +6,6 @@ import 'dart:isolate'; import 'package:sentry/sentry.dart'; import 'package:sentry/src/dart_exception_type_identifier.dart'; import 'package:sentry/src/event_processor/deduplication_event_processor.dart'; -import 'package:sentry/src/logs_enricher_integration.dart'; import 'package:sentry/src/feature_flags_integration.dart'; import 'package:sentry/src/telemetry/metric/metrics_setup_integration.dart'; import 'package:sentry/src/telemetry/processing/processor_integration.dart'; @@ -321,27 +320,6 @@ void main() { ); }, onPlatform: {'browser': Skip()}); - test('should add logsEnricherIntegration', () async { - late SentryOptions optionsReference; - final options = defaultTestOptions(); - - await Sentry.init( - options: options, - (options) { - options.dsn = fakeDsn; - optionsReference = options; - }, - appRunner: appRunner, - ); - - expect( - optionsReference.integrations - .whereType() - .length, - 1, - ); - }); - test('should add $InMemoryTelemetryProcessorIntegration', () async { late SentryOptions optionsReference; final options = defaultTestOptions(); diff --git a/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart b/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart new file mode 100644 index 0000000000..5c6179f99e --- /dev/null +++ b/packages/dart/test/telemetry/log/log_capture_pipeline_test.dart @@ -0,0 +1,257 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/client_reports/discard_reason.dart'; +import 'package:sentry/src/telemetry/log/log_capture_pipeline.dart'; +import 'package:sentry/src/transport/data_category.dart'; +import 'package:test/test.dart'; + +import '../../mocks/mock_client_report_recorder.dart'; +import '../../mocks/mock_telemetry_processor.dart'; +import '../../test_utils.dart'; + +void main() { + group('$LogCapturePipeline', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + SentryLog givenLog() { + return SentryLog( + timestamp: DateTime.now(), + traceId: SentryId.newId(), + level: SentryLogLevel.info, + body: 'test', + attributes: { + 'attribute': SentryAttribute.string('value'), + }, + ); + } + + group('when capturing a log', () { + test('forwards to telemetry processor', () async { + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs.length, 1); + expect(fixture.processor.addedLogs.first, same(log)); + }); + + test('adds default attributes', () async { + await fixture.scope.setUser(SentryUser(id: 'user-id')); + fixture.scope.setAttributes({ + 'scope-key': SentryAttribute.string('scope-value'), + }); + + final log = givenLog() + ..attributes['custom'] = SentryAttribute.string('log-value'); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + final attributes = log.attributes; + expect(attributes['scope-key']?.value, 'scope-value'); + expect(attributes['custom']?.value, 'log-value'); + expect(attributes[SemanticAttributesConstants.sentryEnvironment]?.value, + 'test-env'); + expect(attributes[SemanticAttributesConstants.sentryRelease]?.value, + 'test-release'); + expect(attributes[SemanticAttributesConstants.sentrySdkName]?.value, + fixture.options.sdk.name); + expect(attributes[SemanticAttributesConstants.sentrySdkVersion]?.value, + fixture.options.sdk.version); + expect( + attributes[SemanticAttributesConstants.userId]?.value, 'user-id'); + }); + + test('prefers log attributes over scope attributes', () async { + fixture.scope.setAttributes({ + 'overridden': SentryAttribute.string('scope-value'), + 'kept': SentryAttribute.bool(true), + }); + + final log = givenLog() + ..attributes['overridden'] = SentryAttribute.string('log-value') + ..attributes['logOnly'] = SentryAttribute.double(1.23); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + final attributes = log.attributes; + expect(attributes['overridden']?.value, 'log-value'); + expect(attributes['kept']?.value, true); + expect(attributes['logOnly']?.type, 'double'); + }); + + test('dispatches OnProcessLog after scope merge but before beforeSendLog', + () async { + final operations = []; + bool hasScopeAttrInCallback = false; + + fixture.scope.setAttributes({ + 'scope-attr': SentryAttribute.string('scope-value'), + }); + + fixture.options.lifecycleRegistry + .registerCallback((event) { + operations.add('onProcessLog'); + hasScopeAttrInCallback = + event.log.attributes.containsKey('scope-attr'); + }); + + fixture.options.beforeSendLog = (log) { + operations.add('beforeSendLog'); + return log; + }; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(operations, ['onProcessLog', 'beforeSendLog']); + expect(hasScopeAttrInCallback, isTrue); + }); + + test('keeps attributes added by lifecycle callbacks', () async { + fixture.options.lifecycleRegistry + .registerCallback((event) { + event.log.attributes['callback-key'] = + SentryAttribute.string('callback-value'); + event.log.attributes[SemanticAttributesConstants.sentryEnvironment] = + SentryAttribute.string('callback-env'); + }); + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + final attributes = log.attributes; + expect(attributes['callback-key']?.value, 'callback-value'); + expect(attributes[SemanticAttributesConstants.sentryEnvironment]?.value, + 'callback-env'); + }); + + test('does not add user attributes when sendDefaultPii is false', + () async { + fixture.options.sendDefaultPii = false; + await fixture.scope.setUser(SentryUser(id: 'user-id')); + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect( + log.attributes.containsKey(SemanticAttributesConstants.userId), + isFalse, + ); + }); + }); + + group('when logs are disabled', () { + test('does not add logs to processor', () async { + fixture.options.enableLogs = false; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs, isEmpty); + }); + }); + + group('when beforeSendLog is configured', () { + test('returning null drops the log', () async { + fixture.options.beforeSendLog = (_) => null; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs, isEmpty); + }); + + test('returning null records lost event in client report', () async { + fixture.options.beforeSendLog = (_) => null; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.recorder.discardedEvents.length, 1); + expect(fixture.recorder.discardedEvents.first.reason, + DiscardReason.beforeSend); + expect(fixture.recorder.discardedEvents.first.category, + DataCategory.logItem); + }); + + test('can mutate the log', () async { + fixture.options.beforeSendLog = (log) { + log.body = 'modified-body'; + log.attributes['added-key'] = SentryAttribute.string('added'); + return log; + }; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs.length, 1); + final captured = fixture.processor.addedLogs.first; + expect(captured.body, 'modified-body'); + expect(captured.attributes['added-key']?.value, 'added'); + }); + + test('async callback is awaited', () async { + fixture.options.beforeSendLog = (log) async { + await Future.delayed(Duration(milliseconds: 10)); + log.body = 'async-modified'; + return log; + }; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs.length, 1); + final captured = fixture.processor.addedLogs.first; + expect(captured.body, 'async-modified'); + }); + + test('exception in callback is caught and log is still captured', + () async { + fixture.options.automatedTestMode = false; + fixture.options.beforeSendLog = (log) { + throw Exception('test'); + }; + + final log = givenLog(); + + await fixture.pipeline.captureLog(log, scope: fixture.scope); + + expect(fixture.processor.addedLogs.length, 1); + final captured = fixture.processor.addedLogs.first; + expect(captured.body, 'test'); + }); + }); + }); +} + +class Fixture { + final options = defaultTestOptions() + ..environment = 'test-env' + ..release = 'test-release' + ..sendDefaultPii = true + ..enableLogs = true; + + final processor = MockTelemetryProcessor(); + final recorder = MockClientReportRecorder(); + + late final Scope scope; + late final LogCapturePipeline pipeline; + + Fixture() { + options.telemetryProcessor = processor; + options.recorder = recorder; + scope = Scope(options); + pipeline = LogCapturePipeline(options); + } +} diff --git a/packages/dart/test/protocol/sentry_log_level_test.dart b/packages/dart/test/telemetry/log/log_level_test.dart similarity index 94% rename from packages/dart/test/protocol/sentry_log_level_test.dart rename to packages/dart/test/telemetry/log/log_level_test.dart index 8139859c0e..4ee72df62d 100644 --- a/packages/dart/test/protocol/sentry_log_level_test.dart +++ b/packages/dart/test/telemetry/log/log_level_test.dart @@ -1,5 +1,5 @@ import 'package:test/test.dart'; -import 'package:sentry/src/protocol/sentry_log_level.dart'; +import 'package:sentry/src/telemetry/log/log_level.dart'; import 'package:sentry/src/protocol/sentry_level.dart'; void main() { diff --git a/packages/dart/test/protocol/sentry_log_test.dart b/packages/dart/test/telemetry/log/log_test.dart similarity index 95% rename from packages/dart/test/protocol/sentry_log_test.dart rename to packages/dart/test/telemetry/log/log_test.dart index b138e5340a..b3f08521b5 100644 --- a/packages/dart/test/protocol/sentry_log_test.dart +++ b/packages/dart/test/telemetry/log/log_test.dart @@ -5,10 +5,12 @@ void main() { test('$SentryLog to json', () { final timestamp = DateTime.now(); final traceId = SentryId.newId(); + final spanId = SpanId.newId(); final logItem = SentryLog( timestamp: timestamp, traceId: traceId, + spanId: spanId, level: SentryLogLevel.info, body: 'fixture-body', attributes: { @@ -25,6 +27,7 @@ void main() { expect(json, { 'timestamp': timestamp.toIso8601String(), 'trace_id': traceId.toString(), + 'span_id': spanId.toString(), 'level': 'info', 'body': 'fixture-body', 'attributes': { diff --git a/packages/dart/test/telemetry/log/logger_formatter_test.dart b/packages/dart/test/telemetry/log/logger_formatter_test.dart new file mode 100644 index 0000000000..3c74f2385d --- /dev/null +++ b/packages/dart/test/telemetry/log/logger_formatter_test.dart @@ -0,0 +1,295 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/log/default_logger.dart'; +import 'package:test/test.dart'; + +import '../../test_utils.dart'; + +void main() { + group('$_DefaultSentryLoggerFormatter', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + void verifyPassedAttributes(Map attributes) { + expect(attributes['foo']?.type, 'string'); + expect(attributes['foo']?.value, 'bar'); + } + + void verifyBasicTemplate(SentryLog log) { + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template']?.type, 'string'); + expect(log.attributes['sentry.message.template']?.value, 'Hello, %s!'); + expect(log.attributes['sentry.message.parameter.0']?.type, 'string'); + expect(log.attributes['sentry.message.parameter.0']?.value, 'World'); + verifyPassedAttributes(log.attributes); + } + + void verifyTemplateWithMultipleArguments(SentryLog log) { + expect(log.body, 'Name: Alice, Age: 30, Active: true, Score: 95.5'); + expect(log.attributes['sentry.message.template']?.type, 'string'); + expect(log.attributes['sentry.message.template']?.value, + 'Name: %s, Age: %s, Active: %s, Score: %s'); + expect(log.attributes['sentry.message.parameter.0']?.type, 'string'); + expect(log.attributes['sentry.message.parameter.0']?.value, 'Alice'); + expect(log.attributes['sentry.message.parameter.1']?.type, 'integer'); + expect(log.attributes['sentry.message.parameter.1']?.value, 30); + expect(log.attributes['sentry.message.parameter.2']?.type, 'boolean'); + expect(log.attributes['sentry.message.parameter.2']?.value, true); + expect(log.attributes['sentry.message.parameter.3']?.type, 'double'); + expect(log.attributes['sentry.message.parameter.3']?.value, 95.5); + verifyPassedAttributes(log.attributes); + } + + group('format basic template', () { + test('for trace', () async { + await fixture.logger.fmt.trace( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + + test('for debug', () async { + await fixture.logger.fmt.debug( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + + test('for info', () async { + await fixture.logger.fmt.info( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + + test('for warn', () async { + await fixture.logger.fmt.warn( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + + test('for error', () async { + await fixture.logger.fmt.error( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + + test('for fatal', () async { + await fixture.logger.fmt.fatal( + "Hello, %s!", + ["World"], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyBasicTemplate(fixture.capturedLogs[0]); + }); + }); + + group('template with multiple arguments', () { + test('for trace', () async { + await fixture.logger.fmt.trace( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + + test('for debug', () async { + await fixture.logger.fmt.debug( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + + test('for info', () async { + await fixture.logger.fmt.info( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + + test('for warn', () async { + await fixture.logger.fmt.warn( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + + test('for error', () async { + await fixture.logger.fmt.error( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + + test('for fatal', () async { + await fixture.logger.fmt.fatal( + "Name: %s, Age: %s, Active: %s, Score: %s", + ['Alice', 30, true, 95.5], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + verifyTemplateWithMultipleArguments(fixture.capturedLogs[0]); + }); + }); + + group('template with no arguments', () { + test('for trace', () async { + await fixture.logger.fmt.trace( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + + test('for debug', () async { + await fixture.logger.fmt.debug( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + + test('for info', () async { + await fixture.logger.fmt.info( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + + test('for warn', () async { + await fixture.logger.fmt.warn( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + + test('for error', () async { + await fixture.logger.fmt.error( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + + test('for fatal', () async { + await fixture.logger.fmt.fatal( + "Hello, World!", + [], + attributes: {'foo': SentryAttribute.string('bar')}, + ); + + expect(fixture.capturedLogs.length, 1); + final log = fixture.capturedLogs[0]; + expect(log.body, 'Hello, World!'); + expect(log.attributes['sentry.message.template'], isNull); + expect(log.attributes['sentry.message.parameter.0'], isNull); + verifyPassedAttributes(log.attributes); + }); + }); + }); +} + +// Used to reference the private class in the group name +// ignore: camel_case_types +class _DefaultSentryLoggerFormatter {} + +class Fixture { + final capturedLogs = []; + final options = defaultTestOptions(); + late final SentryLogger logger; + late final Scope scope; + + Fixture() { + scope = Scope(options); + logger = DefaultSentryLogger( + captureLogCallback: (log, {scope}) { + capturedLogs.add(log); + }, + clockProvider: () => DateTime.now(), + scopeProvider: () => scope, + ); + } +} diff --git a/packages/dart/test/telemetry/log/logger_setup_integration_test.dart b/packages/dart/test/telemetry/log/logger_setup_integration_test.dart new file mode 100644 index 0000000000..24ef14af52 --- /dev/null +++ b/packages/dart/test/telemetry/log/logger_setup_integration_test.dart @@ -0,0 +1,168 @@ +import 'dart:async'; + +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/log/default_logger.dart'; +import 'package:sentry/src/telemetry/log/logger_setup_integration.dart'; +import 'package:sentry/src/telemetry/log/noop_logger.dart'; +import 'package:test/test.dart'; + +import '../../test_utils.dart'; + +void main() { + group('$LoggerSetupIntegration', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + group('when logs are enabled', () { + test('configures DefaultSentryLogger', () { + fixture.options.enableLogs = true; + + fixture.sut.call(fixture.hub, fixture.options); + + expect(fixture.options.logger, isA()); + }); + + test('adds integration to SDK', () { + fixture.options.enableLogs = true; + + fixture.sut.call(fixture.hub, fixture.options); + + expect( + fixture.options.sdk.integrations, + contains(LoggerSetupIntegration.integrationName), + ); + }); + + test('does not override existing non-noop logger', () { + fixture.options.enableLogs = true; + final customLogger = _CustomSentryLogger(); + fixture.options.logger = customLogger; + + fixture.sut.call(fixture.hub, fixture.options); + + expect(fixture.options.logger, same(customLogger)); + }); + }); + + group('when logs are disabled', () { + test('does not configure logger', () { + fixture.options.enableLogs = false; + + fixture.sut.call(fixture.hub, fixture.options); + + expect(fixture.options.logger, isA()); + }); + + test('does not add integration to SDK', () { + fixture.options.enableLogs = false; + + fixture.sut.call(fixture.hub, fixture.options); + + expect( + fixture.options.sdk.integrations, + isNot(contains(LoggerSetupIntegration.integrationName)), + ); + }); + }); + }); +} + +class Fixture { + final options = defaultTestOptions(); + + late final Hub hub; + late final LoggerSetupIntegration sut; + + Fixture() { + hub = Hub(options); + sut = LoggerSetupIntegration(); + } +} + +class _CustomSentryLogger implements SentryLogger { + @override + FutureOr trace( + String body, { + Map? attributes, + }) {} + + @override + FutureOr debug( + String body, { + Map? attributes, + }) {} + + @override + FutureOr info( + String body, { + Map? attributes, + }) {} + + @override + FutureOr warn( + String body, { + Map? attributes, + }) {} + + @override + FutureOr error( + String body, { + Map? attributes, + }) {} + + @override + FutureOr fatal( + String body, { + Map? attributes, + }) {} + + @override + SentryLoggerFormatter get fmt => _CustomSentryLoggerFormatter(); +} + +class _CustomSentryLoggerFormatter implements SentryLoggerFormatter { + @override + FutureOr trace( + String templateBody, + List arguments, { + Map? attributes, + }) {} + + @override + FutureOr debug( + String templateBody, + List arguments, { + Map? attributes, + }) {} + + @override + FutureOr info( + String templateBody, + List arguments, { + Map? attributes, + }) {} + + @override + FutureOr warn( + String templateBody, + List arguments, { + Map? attributes, + }) {} + + @override + FutureOr error( + String templateBody, + List arguments, { + Map? attributes, + }) {} + + @override + FutureOr fatal( + String templateBody, + List arguments, { + Map? attributes, + }) {} +} diff --git a/packages/dart/test/telemetry/log/logger_test.dart b/packages/dart/test/telemetry/log/logger_test.dart new file mode 100644 index 0000000000..ffb37a0530 --- /dev/null +++ b/packages/dart/test/telemetry/log/logger_test.dart @@ -0,0 +1,178 @@ +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/telemetry/log/default_logger.dart'; +import 'package:test/test.dart'; + +import '../../test_utils.dart'; + +void main() { + group('$DefaultSentryLogger', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + void verifyCaptureLog(SentryLogLevel level) { + expect(fixture.capturedLogs.length, 1); + final capturedLog = fixture.capturedLogs[0]; + + expect(capturedLog.level, level); + expect(capturedLog.body, 'test'); + // The attributes might have additional trace context, so check key attributes + expect(capturedLog.attributes['string']?.value, 'string'); + } + + test('info', () async { + final logger = fixture.getSut(); + + await logger.info('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.info); + }); + + test('trace', () async { + final logger = fixture.getSut(); + + await logger.trace('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.trace); + }); + + test('debug', () async { + final logger = fixture.getSut(); + + await logger.debug('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.debug); + }); + + test('warn', () async { + final logger = fixture.getSut(); + + await logger.warn('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.warn); + }); + + test('error', () async { + final logger = fixture.getSut(); + + await logger.error('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.error); + }); + + test('fatal', () async { + final logger = fixture.getSut(); + + await logger.fatal('test', attributes: fixture.attributes); + + verifyCaptureLog(SentryLogLevel.fatal); + }); + + // This is mostly an edge case but let's cover it just in case + test('per-log attributes override fmt template attributes on same key', + () async { + final logger = fixture.getSut(); + + await logger.fmt.info( + 'Hello, %s!', + ['World'], + attributes: { + 'sentry.message.template': SentryAttribute.string('OVERRIDE'), + 'sentry.message.parameter.0': SentryAttribute.string('Earth'), + }, + ); + + final attrs = fixture.capturedLogs[0].attributes; + expect(attrs['sentry.message.template']?.value, 'OVERRIDE'); + expect(attrs['sentry.message.parameter.0']?.value, 'Earth'); + }); + + test('sets trace id from default scope propagation context', () async { + final logger = fixture.getSut(); + + await logger.info('test'); + + expect(fixture.capturedLogs.length, 1); + final capturedLog = fixture.capturedLogs[0]; + expect(capturedLog.traceId, fixture.scope.propagationContext.traceId); + }); + + test('sets span id when span is active on default scope', () async { + final span = _MockSpan(); + fixture.scope.span = span; + + final logger = fixture.getSut(); + + await logger.info('test'); + + expect(fixture.capturedLogs.length, 1); + final capturedLog = fixture.capturedLogs[0]; + expect(capturedLog.spanId, span.context.spanId); + }); + + test('sets timestamp from clock provider', () async { + final logger = fixture.getSut(); + + await logger.info('test'); + + expect(fixture.capturedLogs.length, 1); + final capturedLog = fixture.capturedLogs[0]; + expect(capturedLog.timestamp, fixture.timestamp); + }); + + test('includes attributes when provided', () async { + final logger = fixture.getSut(); + + await logger + .info('test', attributes: {'key': SentryAttribute.string('value')}); + + expect(fixture.capturedLogs.length, 1); + final capturedLog = fixture.capturedLogs[0]; + expect(capturedLog.attributes['key']?.value, 'value'); + }); + }); +} + +class Fixture { + final options = defaultTestOptions(); + final timestamp = DateTime.fromMicrosecondsSinceEpoch(0); + final capturedLogs = []; + late final Scope scope; + + final attributes = { + 'string': SentryAttribute.string('string'), + 'int': SentryAttribute.int(1), + 'double': SentryAttribute.double(1.23456789), + 'bool': SentryAttribute.bool(true), + 'double_int': SentryAttribute.double(1.0), + 'nan': SentryAttribute.double(double.nan), + 'positive_infinity': SentryAttribute.double(double.infinity), + 'negative_infinity': SentryAttribute.double(double.negativeInfinity), + }; + + Fixture() { + scope = Scope(options); + } + + SentryLogger getSut() { + return DefaultSentryLogger( + captureLogCallback: (log, {scope}) { + capturedLogs.add(log); + }, + clockProvider: () => timestamp, + scopeProvider: () => scope, + ); + } +} + +class _MockSpan implements ISentrySpan { + final SentrySpanContext _context = SentrySpanContext(operation: 'test'); + + @override + SentrySpanContext get context => _context; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} diff --git a/packages/dart/test/telemetry/processing/processor_integration_test.dart b/packages/dart/test/telemetry/processing/processor_integration_test.dart index b0a41d07f4..169ecf9f97 100644 --- a/packages/dart/test/telemetry/processing/processor_integration_test.dart +++ b/packages/dart/test/telemetry/processing/processor_integration_test.dart @@ -88,6 +88,7 @@ class _Fixture { SentryLog createLog() { return SentryLog( timestamp: DateTime.now().toUtc(), + traceId: SentryId.newId(), level: SentryLogLevel.info, body: 'test log', attributes: {}, diff --git a/packages/dart/test/telemetry/processing/processor_test.dart b/packages/dart/test/telemetry/processing/processor_test.dart index 46fefd0299..2c4914942d 100644 --- a/packages/dart/test/telemetry/processing/processor_test.dart +++ b/packages/dart/test/telemetry/processing/processor_test.dart @@ -119,6 +119,7 @@ class Fixture { SentryLog createLog({String body = 'test log'}) { return SentryLog( timestamp: DateTime.now().toUtc(), + traceId: SentryId.newId(), level: SentryLogLevel.info, body: body, attributes: {}, diff --git a/packages/flutter/lib/src/integrations/load_contexts_integration.dart b/packages/flutter/lib/src/integrations/load_contexts_integration.dart index 3d4791d5b1..b46a7a7494 100644 --- a/packages/flutter/lib/src/integrations/load_contexts_integration.dart +++ b/packages/flutter/lib/src/integrations/load_contexts_integration.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'package:sentry/sentry.dart'; import 'package:collection/collection.dart'; import 'package:sentry/src/event_processor/enricher/enricher_event_processor.dart'; -import 'package:sentry/src/logs_enricher_integration.dart'; import '../native/sentry_native_binding.dart'; import '../sentry_flutter_options.dart'; import '../utils/internal_logger.dart'; @@ -24,7 +23,7 @@ class LoadContextsIntegration implements Integration { final SentryNativeBinding _native; Map? _cachedAttributes; SentryFlutterOptions? _options; - SdkLifecycleCallback? _logCallback; + SdkLifecycleCallback? _logCallback; SdkLifecycleCallback? _metricCallback; LoadContextsIntegration(this._native); @@ -51,28 +50,19 @@ class LoadContextsIntegration implements Integration { options.addEventProcessor(enricherEventProcessor); } if (options.enableLogs) { - final logsEnricherIntegration = options.integrations.firstWhereOrNull( - (element) => element is LogsEnricherIntegration, - ); - if (logsEnricherIntegration != null) { - // Contexts from native cover the os.name and os.version attributes, - // so we can remove the logsEnricherIntegration. - options.removeIntegration(logsEnricherIntegration); - } - _logCallback = (event) async { try { final attributes = await _nativeContextAttributes(); event.log.attributes.addAllIfAbsent(attributes); } catch (exception, stackTrace) { internalLogger.error( - 'LoadContextsIntegration failed to load contexts for $OnBeforeCaptureLog', + 'LoadContextsIntegration failed to load contexts for $OnProcessLog', error: exception, stackTrace: stackTrace, ); } }; - options.lifecycleRegistry.registerCallback( + options.lifecycleRegistry.registerCallback( _logCallback!, ); } @@ -104,8 +94,7 @@ class LoadContextsIntegration implements Integration { if (options == null) return; if (_logCallback != null) { - options.lifecycleRegistry - .removeCallback(_logCallback!); + options.lifecycleRegistry.removeCallback(_logCallback!); _logCallback = null; } if (_metricCallback != null) { diff --git a/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart b/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart index 4883ad1bb1..7936dffde7 100644 --- a/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart +++ b/packages/flutter/lib/src/integrations/replay_telemetry_integration.dart @@ -15,7 +15,7 @@ class ReplayTelemetryIntegration implements Integration { ReplayTelemetryIntegration(this._native); SentryFlutterOptions? _options; - SdkLifecycleCallback? _onBeforeCaptureLog; + SdkLifecycleCallback? _onProcessLog; SdkLifecycleCallback? _onProcessMetric; @override @@ -28,7 +28,7 @@ class ReplayTelemetryIntegration implements Integration { _options = options; - _onBeforeCaptureLog = (OnBeforeCaptureLog event) { + _onProcessLog = (OnProcessLog event) { _addReplayAttributes( hub.scope.replayId, event.log.attributes, @@ -46,8 +46,7 @@ class ReplayTelemetryIntegration implements Integration { ); }; - options.lifecycleRegistry - .registerCallback(_onBeforeCaptureLog!); + options.lifecycleRegistry.registerCallback(_onProcessLog!); options.lifecycleRegistry .registerCallback(_onProcessMetric!); options.sdk.addIntegration(integrationName); @@ -76,13 +75,12 @@ class ReplayTelemetryIntegration implements Integration { @override Future close() async { final options = _options; - final onBeforeCaptureLog = _onBeforeCaptureLog; + final onProcessLog = _onProcessLog; final onProcessMetric = _onProcessMetric; if (options != null) { - if (onBeforeCaptureLog != null) { - options.lifecycleRegistry - .removeCallback(onBeforeCaptureLog); + if (onProcessLog != null) { + options.lifecycleRegistry.removeCallback(onProcessLog); } if (onProcessMetric != null) { options.lifecycleRegistry @@ -91,7 +89,7 @@ class ReplayTelemetryIntegration implements Integration { } _options = null; - _onBeforeCaptureLog = null; + _onProcessLog = null; _onProcessMetric = null; } } diff --git a/packages/flutter/test/integrations/load_contexts_integration_test.dart b/packages/flutter/test/integrations/load_contexts_integration_test.dart index 5c07b1648a..f74f261bad 100644 --- a/packages/flutter/test/integrations/load_contexts_integration_test.dart +++ b/packages/flutter/test/integrations/load_contexts_integration_test.dart @@ -5,7 +5,6 @@ library; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; -import 'package:sentry/src/logs_enricher_integration.dart'; import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/integrations/load_contexts_integration.dart'; @@ -689,20 +688,6 @@ void main() { expect(log.attributes['device.family']?.value, 'fixture-device-family'); }); - test('removes logsEnricherIntegration', () async { - final integration = LogsEnricherIntegration(); - fixture.options.addIntegration(integration); - - fixture.options.enableLogs = true; - await fixture.registerIntegration(); - - expect( - fixture.options.integrations - .any((element) => element is LogsEnricherIntegration), - isFalse, - ); - }); - test( 'does not add os and device attributes to log if enableLogs is false', () async { @@ -731,8 +716,8 @@ void main() { final log = givenLog(); await fixture.hub.captureLog(log); - expect(log.attributes['os.name'], isNull); - expect(log.attributes['os.version'], isNull); + // os.name and os.version are set by defaultAttributes() from Dart-level + // OS detection, not from native loadContexts(), so we only check device.* expect(log.attributes['device.brand'], isNull); expect(log.attributes['device.model'], isNull); expect(log.attributes['device.family'], isNull); @@ -816,16 +801,14 @@ void main() { await fixture.registerIntegration(); expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessLog], isNotEmpty, ); fixture.sut.close(); expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessLog], isEmpty, ); }); @@ -845,8 +828,7 @@ void main() { isEmpty, ); expect( - fixture - .options.lifecycleRegistry.lifecycleCallbacks[OnBeforeCaptureLog], + fixture.options.lifecycleRegistry.lifecycleCallbacks[OnProcessLog], isEmpty, ); }); diff --git a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart index 460c0c0c0a..ab7d010348 100644 --- a/packages/flutter/test/integrations/replay_telemetry_integration_test.dart +++ b/packages/flutter/test/integrations/replay_telemetry_integration_test.dart @@ -194,7 +194,7 @@ class Fixture { when(hub.scope).thenReturn(scope); when(hub.captureLog(any)).thenAnswer((invocation) async { final log = invocation.positionalArguments.first as SentryLog; - await options.lifecycleRegistry.dispatchCallback(OnBeforeCaptureLog(log)); + await options.lifecycleRegistry.dispatchCallback(OnProcessLog(log)); }); when(nativeBinding.replayId).thenReturn(null); } diff --git a/packages/logging/lib/src/logging_integration.dart b/packages/logging/lib/src/logging_integration.dart index 948faf1df4..061e699c69 100644 --- a/packages/logging/lib/src/logging_integration.dart +++ b/packages/logging/lib/src/logging_integration.dart @@ -1,3 +1,5 @@ +// ignore_for_file: invalid_use_of_internal_member + import 'dart:async'; import 'package:meta/meta.dart';