Skip to content

Commit 6ba8993

Browse files
authored
feat(logging): add LogManager.extend() for custom driver registration (#36)
* 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. * fix(docs): AuthManager.extend() → Auth.manager.extend(...) for accuracy (#36) AuthManager.extend() is an instance method accessed via the facade, not a static call. Updated docs and skill references to match.
1 parent 0cba3ef commit 6ba8993

5 files changed

Lines changed: 156 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
77
### ✨ Features
88
- **Router Observers**: `MagicRouter.instance.addObserver()` enables NavigatorObserver integration for analytics/monitoring (Sentry, Firebase Analytics, custom observers). Observers are passed to GoRouter automatically. (#31)
99
- **Network Driver Plugin Hook**: `DioNetworkDriver.configureDriver()` exposes the underlying Dio instance for SDK integrations (sentry_dio, certificate pinning, custom adapters). (#32)
10+
- **Custom Log Drivers**: `LogManager.extend()` enables custom LoggerDriver registration (Sentry, file, Slack). Config-driven resolution with built-in override support. (#33)
1011

1112
## [1.0.0-alpha.6] - 2026-04-05
1213

doc/digging-deeper/logging.md

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -219,33 +219,24 @@ class SentryLoggerDriver extends LoggerDriver {
219219
<a name="registering-the-custom-driver"></a>
220220
### Registering the Custom Driver
221221

222-
Register your driver in a Service Provider:
222+
Register your driver via `LogManager.extend()` in a Service Provider's `boot()` phase:
223223

224224
```dart
225225
// lib/app/providers/logging_service_provider.dart
226226
class LoggingServiceProvider extends ServiceProvider {
227+
LoggingServiceProvider(super.app);
228+
227229
@override
228-
void register() {
229-
// Extend LogManager to support custom drivers
230-
app.extend('log', (manager) {
231-
final logManager = manager as LogManager;
232-
233-
// Register custom driver factory
234-
logManager.registerDriver('sentry', (config) {
235-
return SentryLoggerDriver(
236-
dsn: config['dsn'] ?? '',
237-
minLevel: config['level'] ?? 'error',
238-
);
239-
});
240-
241-
return logManager;
242-
});
230+
Future<void> boot() async {
231+
LogManager.extend('sentry', (config) => SentryLoggerDriver(
232+
dsn: config['dsn'] ?? '',
233+
minLevel: config['level'] ?? 'error',
234+
));
243235
}
244236
}
245237
```
246238

247-
> [!NOTE]
248-
> The `registerDriver` method is a convention. If `LogManager` doesn't support it natively, you can extend it or customize resolution logic.
239+
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.
249240

250241
Then use it in your config:
251242

@@ -271,7 +262,17 @@ Log.channel('sentry').error('Something went wrong', {
271262

272263
## Example: Slack Notifications
273264

274-
Here's a complete example for Slack webhook logging:
265+
Register the driver, then use it via config:
266+
267+
```dart
268+
// In ServiceProvider boot()
269+
LogManager.extend('slack', (config) => SlackLoggerDriver(
270+
webhookUrl: config['webhook_url'],
271+
channel: config['channel'] ?? '#alerts',
272+
));
273+
```
274+
275+
Here's the driver implementation:
275276

276277
```dart
277278
class SlackLoggerDriver extends LoggerDriver {

lib/src/logging/log_manager.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ import 'drivers/stack_logger_driver.dart';
99
class LogManager {
1010
LoggerDriver? _cachedDriver;
1111

12+
static final Map<String, LoggerDriver Function(Map<String, dynamic>)>
13+
_customDrivers = {};
14+
15+
/// Register a custom log driver factory.
16+
static void extend(
17+
String driver,
18+
LoggerDriver Function(Map<String, dynamic> config) factory,
19+
) {
20+
_customDrivers[driver] = factory;
21+
}
22+
23+
/// Reset all custom drivers (for testing).
24+
static void resetDrivers() {
25+
_customDrivers.clear();
26+
}
27+
1228
/// Get the default logger driver based on configuration.
1329
LoggerDriver driver([String? channel]) {
1430
if (_cachedDriver != null && channel == null) {
@@ -32,6 +48,10 @@ class LogManager {
3248
final channelConfig = channels[name] as Map<String, dynamic>? ?? {};
3349
final driverName = channelConfig['driver'] ?? 'console';
3450

51+
if (_customDrivers.containsKey(driverName)) {
52+
return _customDrivers[driverName]!(channelConfig);
53+
}
54+
3555
switch (driverName) {
3656
case 'stack':
3757
return _createStackDriver(channelConfig);

skills/magic-framework/references/secondary-systems.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,19 @@ Log.channel('slack').error('Critical server issue');
169169
- **`console`**: Outputs to standard output with configurable log level.
170170
- **`stack`**: Aggregates multiple drivers (e.g., console + file simultaneously).
171171

172+
### Custom Drivers
173+
174+
Register custom log drivers via `LogManager.extend()` — follows the same pattern as `Auth.manager.extend(...)`:
175+
176+
```dart
177+
// In a ServiceProvider boot():
178+
LogManager.extend('sentry', (config) => SentryLoggerDriver(
179+
minLevel: config['level'] ?? 'warning',
180+
));
181+
```
182+
183+
Custom drivers implement the `LoggerDriver` abstract class. They can be referenced in config by name and included in stack channels.
184+
172185
### Configuration
173186

174187
```dart
@@ -178,12 +191,16 @@ Log.channel('slack').error('Critical server issue');
178191
'channels': {
179192
'stack': {
180193
'driver': 'stack',
181-
'channels': ['console'],
194+
'channels': ['console', 'sentry'],
182195
},
183196
'console': {
184197
'driver': 'console',
185198
'level': 'debug',
186199
},
200+
'sentry': {
201+
'driver': 'sentry',
202+
'level': 'warning',
203+
},
187204
},
188205
}
189206
```

test/logging/logging_test.dart

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@ class MockLoggerDriver extends LoggerDriver {
1313
void clear() => logs.clear();
1414
}
1515

16+
/// A custom logger driver for testing extend() registration.
17+
class _CustomLoggerDriver extends LoggerDriver {
18+
final List<Map<String, dynamic>> logs = [];
19+
final Map<String, dynamic> config;
20+
21+
_CustomLoggerDriver(this.config);
22+
23+
@override
24+
void log(String level, String message, [dynamic context]) {
25+
logs.add({'level': level, 'message': message, 'context': context});
26+
}
27+
}
28+
1629
void main() {
1730
group('LoggerDriver', () {
1831
late MockLoggerDriver driver;
@@ -114,4 +127,88 @@ void main() {
114127
driver.error('Should be logged');
115128
});
116129
});
130+
131+
group('Custom Driver Registration', () {
132+
setUp(() {
133+
MagicApp.reset();
134+
Magic.flush();
135+
LogManager.resetDrivers();
136+
});
137+
138+
test('extend() registers custom driver factory', () {
139+
LogManager.extend(
140+
'custom',
141+
(Map<String, dynamic> config) => _CustomLoggerDriver(config),
142+
);
143+
144+
Config.set('logging.channels', {
145+
'custom_channel': {'driver': 'custom', 'level': 'debug'},
146+
});
147+
148+
final manager = LogManager();
149+
final resolved = manager.driver('custom_channel');
150+
151+
expect(resolved, isA<_CustomLoggerDriver>());
152+
});
153+
154+
test('custom driver receives channel config', () {
155+
Map<String, dynamic>? capturedConfig;
156+
157+
LogManager.extend('custom', (Map<String, dynamic> config) {
158+
capturedConfig = config;
159+
return _CustomLoggerDriver(config);
160+
});
161+
162+
Config.set('logging.channels', {
163+
'custom_channel': {
164+
'driver': 'custom',
165+
'level': 'warning',
166+
'tag': 'my-app',
167+
},
168+
});
169+
170+
final manager = LogManager();
171+
manager.driver('custom_channel');
172+
173+
expect(capturedConfig, isNotNull);
174+
expect(capturedConfig!['level'], equals('warning'));
175+
expect(capturedConfig!['tag'], equals('my-app'));
176+
});
177+
178+
test('custom driver overrides built-in', () {
179+
LogManager.extend(
180+
'console',
181+
(Map<String, dynamic> config) => _CustomLoggerDriver(config),
182+
);
183+
184+
Config.set('logging.channels', {
185+
'custom_channel': {'driver': 'console'},
186+
});
187+
188+
final manager = LogManager();
189+
final resolved = manager.driver('custom_channel');
190+
191+
expect(resolved, isA<_CustomLoggerDriver>());
192+
});
193+
194+
test('stack driver can include custom channels', () {
195+
LogManager.extend(
196+
'custom',
197+
(Map<String, dynamic> config) => _CustomLoggerDriver(config),
198+
);
199+
200+
Config.set('logging.channels', {
201+
'custom_channel': {'driver': 'custom', 'level': 'debug'},
202+
'stack_channel': {
203+
'driver': 'stack',
204+
'channels': ['custom_channel'],
205+
},
206+
});
207+
208+
final manager = LogManager();
209+
final resolved = manager.driver('stack_channel');
210+
211+
expect(resolved, isA<StackLoggerDriver>());
212+
});
213+
});
117214
}

0 commit comments

Comments
 (0)