From 0c5b9478098e53c990909c08eaf2631e5b4dbb9c Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Fri, 18 Apr 2025 15:21:57 +0200 Subject: [PATCH] Logging service: - added: reset() - added: `skipAutodetection` option for module, method, and metadata - added: unregisterInterface() - Created FileAndLineNumber mixin Signed-off-by: Hans Kokx --- lib/src/providers/service_provider.dart | 8 +- .../logging/logging_interface_extensions.dart | 26 ++++ lib/src/services/logging/logging_service.dart | 130 +++++++++++++----- pubspec.yaml | 3 + 4 files changed, 130 insertions(+), 37 deletions(-) create mode 100644 lib/src/services/logging/logging_interface_extensions.dart diff --git a/lib/src/providers/service_provider.dart b/lib/src/providers/service_provider.dart index 9396e99..a0d88e0 100644 --- a/lib/src/providers/service_provider.dart +++ b/lib/src/providers/service_provider.dart @@ -38,9 +38,7 @@ class ArcaneServiceProvider extends InheritedNotifier { /// This always returns `true`, meaning dependents will always be notified /// when this widget is rebuilt. @override - bool updateShouldNotify(ArcaneServiceProvider oldWidget) { - return true; - } + bool updateShouldNotify(_) => true; /// Retrieves the nearest `ArcaneServiceProvider` in the widget tree. /// @@ -105,7 +103,7 @@ extension ServiceProvider on BuildContext { /// to notify listeners of changes. Services are typically registered in /// `ArcaneServiceProvider` and can be accessed using the `serviceOfType` /// method on `BuildContext`. -abstract class ArcaneService with ChangeNotifier { +abstract class ArcaneService with ChangeNotifier { static T? of(BuildContext context) => - context.serviceOfType(); + context.serviceOfType(); } diff --git a/lib/src/services/logging/logging_interface_extensions.dart b/lib/src/services/logging/logging_interface_extensions.dart new file mode 100644 index 0000000..0f0459a --- /dev/null +++ b/lib/src/services/logging/logging_interface_extensions.dart @@ -0,0 +1,26 @@ +mixin class FileAndLineNumber { + static String get _parts => StackTrace.current + .toString() + .split("\n")[2] + .split(RegExp("#2"))[1] + .trim(); + + static String? get module => + _parts.split(".").firstOrNull?.replaceFirst("new ", ""); + static String? get method { + if (_parts.length <= 1) return null; + return _parts[1].split(" ").firstOrNull?.replaceAll(" fileAndLineParts = [ + ...?_parts.split("(package:").lastOrNull?.split(":"), + ]; + + if (fileAndLineParts.length <= 2) { + return fileAndLineParts.firstOrNull; + } + + return "${fileAndLineParts[0]}:${fileAndLineParts[1]}"; + } +} diff --git a/lib/src/services/logging/logging_service.dart b/lib/src/services/logging/logging_service.dart index 1d82c11..40551cd 100644 --- a/lib/src/services/logging/logging_service.dart +++ b/lib/src/services/logging/logging_service.dart @@ -1,7 +1,7 @@ import "dart:async"; +import "package:arcane_framework/src/services/logging/logging_interface_extensions.dart"; import "package:arcane_helper_utils/arcane_helper_utils.dart"; -import "package:flutter/foundation.dart"; part "logging_enums.dart"; part "logging_interface.dart"; @@ -12,7 +12,7 @@ part "logging_interface.dart"; /// The `ArcaneLogger` provides a centralized way to log messages across /// different parts of an application. It supports multiple logging interfaces, /// metadata, and platform-specific error handling. -class ArcaneLogger { +class ArcaneLogger with FileAndLineNumber { ArcaneLogger._internal(); static final ArcaneLogger _instance = ArcaneLogger._internal(); @@ -115,13 +115,61 @@ class ArcaneLogger { /// ``` /// void log( + /// The message to be logged String message, { + /// The Dart class from which the `log` call was invoked. This is useful + /// in determining which part of the code called the log event. If the + /// [module] is not specified and [skipAutodetection] is set to [false], + /// [ArcaneLogger] will attempt to derive this value automatically. However, + /// this may fail in some cases and could potentially adversely impact + /// performance. String? module, + + /// The method from which the `log` call was invoked. This is useful + /// in determining which part of the code called the log event. If the + /// [method] is not specified and [skipAutodetection] is set to [false], + /// [ArcaneLogger] will attempt to derive this value automatically. However, + /// this may fail in some cases and could potentially adversely impact + /// performance. String? method, + + /// This value defines the severity of the log message. The default value is + /// [Level.debug]. Level level = Level.debug, + + /// A [StackTrace] can be passed into the `log` call for further processing + /// by the registered [LoggingInterface]s. StackTrace? stackTrace, + + /// The provided [metadata] will be merged with any previously registered + /// persistent metadata. If the [module] and/or [method] are provided, these + /// values will be merged with the [metadata] as well, otherwise if one or + /// none of these values are provided and [skipAutodetection] is set to + /// [false] (default), the module, method, and/or [filenameAndLineNumber] + /// will be automatically determined and added to the [metadata] if the + /// values are not already present. Map? metadata, + + /// The [extra] parameter can be used to pass _any_ object into the + /// registered [LoggingInterface]s. Object? extra, + + /// If set to [true], this parameter will skip automatically trying to + /// determine the current log message's [module], [method], and + /// [filenameAndLineNumber]. + /// + /// If set to [true] and the [filenameAndLineNumber] are desired, they + /// should be calculated by the [LoggingInterface] and added as as + /// [metadata]. + /// + /// If this value is [false] (default), the [module] and [method] will only + /// be added to the [metadata] if they are not otherwise provided. However, + /// the [filenameAndLineNumber] will automatically be added _unless_ it is + /// already in the [metadata] provided. + /// + /// When set to [true], the automatic generation of these values _may_ + /// impact performance. + bool skipAutodetection = false, }) { if (!I._initialized) { throw Exception("ArcaneLogger has not yet been initialized."); @@ -129,37 +177,41 @@ class ArcaneLogger { metadata ??= {}; - final String now = DateTime.now().toIso8601String(); - metadata.putIfAbsent("timestamp", () => now); - - try { - final List parts = StackTrace.current - .toString() - .split("\n")[2] - .split(RegExp("#2"))[1] - .trimLeft() - .split("."); - - module ??= parts.first.replaceFirst("new ", ""); - method ??= parts[1].split(" ").first.replaceAll(" fileAndLineParts = StackTrace.current - .toString() - .split("\n")[2] - .split(RegExp("#2"))[1] - .trim() - .split("(package:") - .last - .split(":"); - - final String fileAndLine = - "${fileAndLineParts[0]}:${fileAndLineParts[1]}"; + metadata.putIfAbsent("timestamp", () => DateTime.now().toIso8601String()); + if (module.isNotEmptyOrNull) { metadata.putIfAbsent("module", () => module!); - if (method.isNotNullOrEmpty) - metadata.putIfAbsent("method", () => method!); - metadata.putIfAbsent("filenameAndLineNumber", () => fileAndLine); - } catch (_) {} + } else if (!metadata.containsKey("module") && + metadata["module"].isNullOrEmpty) { + try { + if (FileAndLineNumber.module.isNotNullOrEmpty) { + metadata.putIfAbsent("module", () => FileAndLineNumber.module!); + } + } catch (_) {} + } + + if (method.isNotEmptyOrNull) { + metadata.putIfAbsent("method", () => method!); + } else if (!metadata.containsKey("method") && + metadata["method"].isNullOrEmpty) { + try { + if (FileAndLineNumber.method.isNotNullOrEmpty) { + metadata.putIfAbsent("method", () => FileAndLineNumber.method!); + } + } catch (_) {} + } + + if (!metadata.containsKey("filenameAndLineNumber") && + metadata["filenameAndLineNumber"].isNullOrEmpty) { + try { + if (FileAndLineNumber.fileAndLine.isNotNullOrEmpty) { + metadata.putIfAbsent( + "filenameAndLineNumber", + () => FileAndLineNumber.fileAndLine!, + ); + } + } catch (_) {} + } metadata.addAll(additionalMetadata); @@ -211,6 +263,18 @@ class ArcaneLogger { return I; } + /// Unregisters a [LoggingInterface] from the [ArcaneLogger], if it was + /// previously registered. + Future unregisterInterface( + LoggingInterface interface, + ) async { + if (!initialized) await _init(); + + I._interfaces.remove(interface); + + return I; + } + /// Unregisters a `List` of [LoggingInterface] from the [ArcaneLogger], if /// they were previously registered. Future unregisterInterfaces( @@ -287,7 +351,9 @@ class ArcaneLogger { /// Clears all persistent metadata. void clearPersistentMetadata() => _additionalMetadata.clear(); - @visibleForTesting + /// Resets the Arcane logging service by clearing all persistent metadata, + /// clearing all registered [LoggingInterface]s and marking the logging + /// service as no longer being initialized. void reset() { I._interfaces.clear(); I._initialized = false; diff --git a/pubspec.yaml b/pubspec.yaml index fc05500..d8bbc70 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,3 +25,6 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.4.5 + +dependency_overrides: + analyzer: 7.3.0