Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
39 changes: 20 additions & 19 deletions doc/digging-deeper/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,33 +219,24 @@ class SentryLoggerDriver extends LoggerDriver {
<a name="registering-the-custom-driver"></a>
### 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<void> 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:

Expand All @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions lib/src/logging/log_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ import 'drivers/stack_logger_driver.dart';
class LogManager {
LoggerDriver? _cachedDriver;

static final Map<String, LoggerDriver Function(Map<String, dynamic>)>
_customDrivers = {};

/// Register a custom log driver factory.
static void extend(
String driver,
LoggerDriver Function(Map<String, dynamic> 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) {
Expand All @@ -32,6 +48,10 @@ class LogManager {
final channelConfig = channels[name] as Map<String, dynamic>? ?? {};
final driverName = channelConfig['driver'] ?? 'console';

if (_customDrivers.containsKey(driverName)) {
return _customDrivers[driverName]!(channelConfig);
}

switch (driverName) {
case 'stack':
return _createStackDriver(channelConfig);
Expand Down
19 changes: 18 additions & 1 deletion skills/magic-framework/references/secondary-systems.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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',
},
},
}
```
Expand Down
97 changes: 97 additions & 0 deletions test/logging/logging_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Map<String, dynamic>> logs = [];
final Map<String, dynamic> 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;
Expand Down Expand Up @@ -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<String, dynamic> 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<String, dynamic>? capturedConfig;

LogManager.extend('custom', (Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<StackLoggerDriver>());
});
});
}