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..1ab7463 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 `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: @@ -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..4dd4c6f 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 `Auth.manager.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()); + }); + }); }