From 4804f411688a5d8a72dca71769ac2b6988ab8488 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Sun, 5 Apr 2026 22:03:31 +0300 Subject: [PATCH 1/2] feat(logging): add LogManager.extend() for custom driver registration (#33) Enables custom LoggerDriver registration via static extend() pattern (mirrors AuthManager.extend). Custom drivers checked before built-in switch in _resolveChannel(), config-driven resolution with override support. Includes resetDrivers() for test cleanup. --- CHANGELOG.md | 1 + doc/digging-deeper/logging.md | 39 ++++---- lib/src/logging/log_manager.dart | 20 ++++ .../references/secondary-systems.md | 19 +++- test/logging/logging_test.dart | 97 +++++++++++++++++++ 5 files changed, 156 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 543461b..5a0f927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### ✨ Features - **Router Observers**: `MagicRouter.instance.addObserver()` enables NavigatorObserver integration for analytics/monitoring (Sentry, Firebase Analytics, custom observers). Observers are passed to GoRouter automatically. (#31) - **Network Driver Plugin Hook**: `DioNetworkDriver.configureDriver()` exposes the underlying Dio instance for SDK integrations (sentry_dio, certificate pinning, custom adapters). (#32) +- **Custom Log Drivers**: `LogManager.extend()` enables custom LoggerDriver registration (Sentry, file, Slack). Config-driven resolution with built-in override support. (#33) ## [1.0.0-alpha.6] - 2026-04-05 diff --git a/doc/digging-deeper/logging.md b/doc/digging-deeper/logging.md index 72e3d08..5344212 100644 --- a/doc/digging-deeper/logging.md +++ b/doc/digging-deeper/logging.md @@ -219,33 +219,24 @@ class SentryLoggerDriver extends LoggerDriver { ### Registering the Custom Driver -Register your driver in a Service Provider: +Register your driver via `LogManager.extend()` in a Service Provider's `boot()` phase: ```dart // lib/app/providers/logging_service_provider.dart class LoggingServiceProvider extends ServiceProvider { + LoggingServiceProvider(super.app); + @override - void register() { - // Extend LogManager to support custom drivers - app.extend('log', (manager) { - final logManager = manager as LogManager; - - // Register custom driver factory - logManager.registerDriver('sentry', (config) { - return SentryLoggerDriver( - dsn: config['dsn'] ?? '', - minLevel: config['level'] ?? 'error', - ); - }); - - return logManager; - }); + Future boot() async { + LogManager.extend('sentry', (config) => SentryLoggerDriver( + dsn: config['dsn'] ?? '', + minLevel: config['level'] ?? 'error', + )); } } ``` -> [!NOTE] -> The `registerDriver` method is a convention. If `LogManager` doesn't support it natively, you can extend it or customize resolution logic. +This follows the same `extend()` pattern as `AuthManager.extend()` for custom auth guards. The factory receives the channel's config map and returns a `LoggerDriver` instance. Then use it in your config: @@ -271,7 +262,17 @@ Log.channel('sentry').error('Something went wrong', { ## Example: Slack Notifications -Here's a complete example for Slack webhook logging: +Register the driver, then use it via config: + +```dart +// In ServiceProvider boot() +LogManager.extend('slack', (config) => SlackLoggerDriver( + webhookUrl: config['webhook_url'], + channel: config['channel'] ?? '#alerts', +)); +``` + +Here's the driver implementation: ```dart class SlackLoggerDriver extends LoggerDriver { diff --git a/lib/src/logging/log_manager.dart b/lib/src/logging/log_manager.dart index 0ca6211..b10e935 100644 --- a/lib/src/logging/log_manager.dart +++ b/lib/src/logging/log_manager.dart @@ -9,6 +9,22 @@ import 'drivers/stack_logger_driver.dart'; class LogManager { LoggerDriver? _cachedDriver; + static final Map)> + _customDrivers = {}; + + /// Register a custom log driver factory. + static void extend( + String driver, + LoggerDriver Function(Map config) factory, + ) { + _customDrivers[driver] = factory; + } + + /// Reset all custom drivers (for testing). + static void resetDrivers() { + _customDrivers.clear(); + } + /// Get the default logger driver based on configuration. LoggerDriver driver([String? channel]) { if (_cachedDriver != null && channel == null) { @@ -32,6 +48,10 @@ class LogManager { final channelConfig = channels[name] as Map? ?? {}; final driverName = channelConfig['driver'] ?? 'console'; + if (_customDrivers.containsKey(driverName)) { + return _customDrivers[driverName]!(channelConfig); + } + switch (driverName) { case 'stack': return _createStackDriver(channelConfig); diff --git a/skills/magic-framework/references/secondary-systems.md b/skills/magic-framework/references/secondary-systems.md index 702b337..1fca8c5 100644 --- a/skills/magic-framework/references/secondary-systems.md +++ b/skills/magic-framework/references/secondary-systems.md @@ -169,6 +169,19 @@ Log.channel('slack').error('Critical server issue'); - **`console`**: Outputs to standard output with configurable log level. - **`stack`**: Aggregates multiple drivers (e.g., console + file simultaneously). +### Custom Drivers + +Register custom log drivers via `LogManager.extend()` — follows the same pattern as `AuthManager.extend()`: + +```dart +// In a ServiceProvider boot(): +LogManager.extend('sentry', (config) => SentryLoggerDriver( + minLevel: config['level'] ?? 'warning', +)); +``` + +Custom drivers implement the `LoggerDriver` abstract class. They can be referenced in config by name and included in stack channels. + ### Configuration ```dart @@ -178,12 +191,16 @@ Log.channel('slack').error('Critical server issue'); 'channels': { 'stack': { 'driver': 'stack', - 'channels': ['console'], + 'channels': ['console', 'sentry'], }, 'console': { 'driver': 'console', 'level': 'debug', }, + 'sentry': { + 'driver': 'sentry', + 'level': 'warning', + }, }, } ``` diff --git a/test/logging/logging_test.dart b/test/logging/logging_test.dart index 6148786..9a4fe72 100644 --- a/test/logging/logging_test.dart +++ b/test/logging/logging_test.dart @@ -13,6 +13,19 @@ class MockLoggerDriver extends LoggerDriver { void clear() => logs.clear(); } +/// A custom logger driver for testing extend() registration. +class _CustomLoggerDriver extends LoggerDriver { + final List> logs = []; + final Map config; + + _CustomLoggerDriver(this.config); + + @override + void log(String level, String message, [dynamic context]) { + logs.add({'level': level, 'message': message, 'context': context}); + } +} + void main() { group('LoggerDriver', () { late MockLoggerDriver driver; @@ -114,4 +127,88 @@ void main() { driver.error('Should be logged'); }); }); + + group('Custom Driver Registration', () { + setUp(() { + MagicApp.reset(); + Magic.flush(); + LogManager.resetDrivers(); + }); + + test('extend() registers custom driver factory', () { + LogManager.extend( + 'custom', + (Map config) => _CustomLoggerDriver(config), + ); + + Config.set('logging.channels', { + 'custom_channel': {'driver': 'custom', 'level': 'debug'}, + }); + + final manager = LogManager(); + final resolved = manager.driver('custom_channel'); + + expect(resolved, isA<_CustomLoggerDriver>()); + }); + + test('custom driver receives channel config', () { + Map? capturedConfig; + + LogManager.extend('custom', (Map config) { + capturedConfig = config; + return _CustomLoggerDriver(config); + }); + + Config.set('logging.channels', { + 'custom_channel': { + 'driver': 'custom', + 'level': 'warning', + 'tag': 'my-app', + }, + }); + + final manager = LogManager(); + manager.driver('custom_channel'); + + expect(capturedConfig, isNotNull); + expect(capturedConfig!['level'], equals('warning')); + expect(capturedConfig!['tag'], equals('my-app')); + }); + + test('custom driver overrides built-in', () { + LogManager.extend( + 'console', + (Map config) => _CustomLoggerDriver(config), + ); + + Config.set('logging.channels', { + 'custom_channel': {'driver': 'console'}, + }); + + final manager = LogManager(); + final resolved = manager.driver('custom_channel'); + + expect(resolved, isA<_CustomLoggerDriver>()); + }); + + test('stack driver can include custom channels', () { + LogManager.extend( + 'custom', + (Map config) => _CustomLoggerDriver(config), + ); + + Config.set('logging.channels', { + 'custom_channel': {'driver': 'custom', 'level': 'debug'}, + 'stack_channel': { + 'driver': 'stack', + 'channels': ['custom_channel'], + }, + }); + + final manager = LogManager(); + final resolved = manager.driver('stack_channel'); + + expect(resolved, isA()); + }); + }); } From 8b3d69928db2592c1ff26caf2bf4c59a436df8d2 Mon Sep 17 00:00:00 2001 From: Anilcan Cakir Date: Sun, 5 Apr 2026 22:09:52 +0300 Subject: [PATCH 2/2] =?UTF-8?q?fix(docs):=20AuthManager.extend()=20?= =?UTF-8?q?=E2=86=92=20Auth.manager.extend(...)=20for=20accuracy=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AuthManager.extend() is an instance method accessed via the facade, not a static call. Updated docs and skill references to match. --- doc/digging-deeper/logging.md | 2 +- skills/magic-framework/references/secondary-systems.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/digging-deeper/logging.md b/doc/digging-deeper/logging.md index 5344212..1ab7463 100644 --- a/doc/digging-deeper/logging.md +++ b/doc/digging-deeper/logging.md @@ -236,7 +236,7 @@ class LoggingServiceProvider extends ServiceProvider { } ``` -This follows the same `extend()` pattern as `AuthManager.extend()` for custom auth guards. The factory receives the channel's config map and returns a `LoggerDriver` instance. +This follows the same `extend()` pattern as `Auth.manager.extend(...)` for custom auth guards. The factory receives the channel's config map and returns a `LoggerDriver` instance. Then use it in your config: diff --git a/skills/magic-framework/references/secondary-systems.md b/skills/magic-framework/references/secondary-systems.md index 1fca8c5..4dd4c6f 100644 --- a/skills/magic-framework/references/secondary-systems.md +++ b/skills/magic-framework/references/secondary-systems.md @@ -171,7 +171,7 @@ Log.channel('slack').error('Critical server issue'); ### Custom Drivers -Register custom log drivers via `LogManager.extend()` — follows the same pattern as `AuthManager.extend()`: +Register custom log drivers via `LogManager.extend()` — follows the same pattern as `Auth.manager.extend(...)`: ```dart // In a ServiceProvider boot():