diff --git a/CHANGELOG.md b/CHANGELOG.md index 74772ee..387bc9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 2.0.3 + +### Logging Service + +- [CHANGE] `LogEvent` JSON serialization now delegates recursive nested + `metadata` / `extra` encode-decode handling to `arcane_helper_utils` JSON + extensions (`toJsonValue`, `toJsonMap`, `fromJsonValue`, `fromJsonMap`). +- [CHANGE] Refactored `LogInterceptor` to an interface-style contract with a + factory constructor for callback interceptors, so reusable class-based + interceptors can be implemented without superclass callback plumbing. + ## 2.0.2 ### Logging Service diff --git a/lib/src/services/logging/log_event.dart b/lib/src/services/logging/log_event.dart index a375329..30f6f0f 100644 --- a/lib/src/services/logging/log_event.dart +++ b/lib/src/services/logging/log_event.dart @@ -1,5 +1,9 @@ part of "logging_service.dart"; +/// Immutable payload representing a single log entry. +/// +/// This model is used internally by the logging pipeline and supports +/// JSON round-tripping for persisted or forwarded log events. class LogEvent { static const Object _sentinel = Object(); @@ -11,12 +15,69 @@ class LogEvent { this.extra, }); + /// Human-readable log message. final String message; + + /// Optional structured context for the log event. + /// + /// Values may contain nested maps/lists and are serialized recursively. final Map? metadata; + + /// Optional log level override for this event. final Level? level; + + /// Optional captured stack trace associated with the event. final StackTrace? stackTrace; + + /// Optional additional payload attached to the event. + /// + /// This may be any value and is serialized recursively where possible. final Object? extra; + /// Deserializes a [LogEvent] from a JSON map. + /// + /// Nested maps and lists in `metadata` and `extra` are decoded recursively. + /// `level` is matched by enum name. `stackTrace` is restored as a + /// string-backed [StackTrace] whose [toString] returns the original text. + factory LogEvent.fromJson(Map json) { + final rawStackTrace = json["stackTrace"]; + final rawLevel = json["level"]; + + return LogEvent( + message: json["message"] as String, + metadata: json["metadata"].fromJsonMap(), + level: rawLevel != null + ? Level.values.firstWhere( + (l) => l.name == rawLevel, + orElse: () => Level.debug, + ) + : null, + stackTrace: rawStackTrace != null + ? _StringStackTrace(rawStackTrace as String) + : null, + extra: json["extra"].fromJsonValue(), + ); + } + + /// Serializes this [LogEvent] to a JSON-compatible map. + /// + /// `metadata` and `extra` are encoded recursively so nested maps and lists + /// are preserved. Non-encodable leaf values fall back to [toString]. + /// `level` is stored as its enum name; `stackTrace` as its string form. + Map toJson() { + return { + "message": message, + if (metadata != null) "metadata": metadata!.toJsonMap(), + if (level != null) "level": level!.name, + if (stackTrace != null) "stackTrace": stackTrace!.toString(), + if (extra != null) "extra": extra.toJsonValue(), + }; + } + + /// Returns a copy of this event with selected fields replaced. + /// + /// Nullable fields use sentinel values to distinguish "keep existing value" + /// from "explicitly set to null". LogEvent copyWith({ String? message, Object? metadata = _sentinel, @@ -37,3 +98,14 @@ class LogEvent { ); } } + +/// A [StackTrace] backed by a plain string, used to round-trip stack trace +/// text through JSON without losing the original content. +class _StringStackTrace implements StackTrace { + const _StringStackTrace(this._trace); + + final String _trace; + + @override + String toString() => _trace; +} diff --git a/lib/src/services/logging/log_interceptor.dart b/lib/src/services/logging/log_interceptor.dart index 4ceb82e..1a2500f 100644 --- a/lib/src/services/logging/log_interceptor.dart +++ b/lib/src/services/logging/log_interceptor.dart @@ -39,7 +39,7 @@ final class LogInterceptorContext { /// /// Example usage: /// ```dart -/// final interceptor = LogInterceptor((event, context) { +/// final interceptor = CallbackLogInterceptor((event, context) { /// // Filter out debug-level logs /// if (event.level == Level.debug) return null; /// return event; @@ -49,26 +49,37 @@ final class LogInterceptorContext { /// See also: /// - [LogEvent], which represents a log entry. /// - [LogInterceptorContext], which provides context for the interception. -class LogInterceptor { - /// Creates a [LogInterceptor] with the given callback. +abstract class LogInterceptor { + /// Creates a callback-backed [LogInterceptor]. /// - /// The [_callback] function will be invoked for each log event, with the - /// event and its context. Return a [LogEvent] to continue processing, or - /// `null` to suppress the event. - const LogInterceptor(this._callback); - - /// The callback function that processes each log event. - /// - /// The function receives the [event] and its [context], and should return - /// either a (possibly modified) [LogEvent], or `null` to suppress the event. - final LogEvent? Function( - LogEvent event, - LogInterceptorContext context, - ) _callback; + /// This keeps inline interceptor usage ergonomic. + const factory LogInterceptor( + LogEvent? Function( + LogEvent event, + LogInterceptorContext context, + ) callback, + ) = CallbackLogInterceptor; /// Invokes the interceptor on the given [event] and [context]. /// /// Returns the (possibly modified) [LogEvent], or `null` to suppress the event. + LogEvent? call( + LogEvent event, { + required LogInterceptorContext context, + }); +} + +/// A callback-backed implementation of [LogInterceptor]. +final class CallbackLogInterceptor implements LogInterceptor { + /// Creates a callback-backed interceptor. + const CallbackLogInterceptor(this._callback); + + final LogEvent? Function( + LogEvent event, + LogInterceptorContext context, + ) _callback; + + @override LogEvent? call( LogEvent event, { required LogInterceptorContext context, diff --git a/pubspec.yaml b/pubspec.yaml index 09c6a33..48a4c53 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: arcane_framework description: "Agnostic Reusable Component Architecture for New Ecosystems: a modern framework for bootstrapping new applications" -version: 2.0.2 +version: 2.0.3 repository: https://github.com/hanskokx/arcane_framework issue_tracker: https://github.com/hanskokx/arcane_framework/issues @@ -13,7 +13,7 @@ environment: flutter: ">=1.17.0" dependencies: - arcane_helper_utils: ^1.4.7 + arcane_helper_utils: ^1.4.8 collection: ^1.19.0 flutter: sdk: flutter