Logging
The Log facade provides a Laravel-style logging API with configurable channels, minimum log levels, and a stack channel for fanning output to multiple drivers simultaneously.
On this page
Introduction#
Magic provides a robust, Laravel-style logging system based on RFC 5424 severity levels. The Log facade provides a simple interface for writing log messages to various destinations (console, files, remote services).
The logging system is:
- Channel-Based: Route logs to specific destinations using named channels.
- Extensible: Create custom drivers to send logs anywhere (Sentry, Slack, Firebase, etc.).
- Level-Aware: Filter messages based on severity threshold.
Configuration#
Configure logging in lib/config/logging.dart:
// lib/config/logging.dart
final Map<String, dynamic> logging = {
// Default channel used by Log facade
'default': 'console',
// Channel configurations
'channels': {
'console': {
'driver': 'console',
'level': 'debug',
},
'production': {
'driver': 'console',
'level': 'warning', // Only warning and above in production
},
'stack': {
'driver': 'stack',
'channels': ['console', 'sentry'],
},
'sentry': {
'driver': 'sentry',
'dsn': Config.get('SENTRY_DSN'),
'level': 'error',
},
},
};
Available Channels#
Magic includes these built-in drivers:
| Driver | Description |
|---|---|
console |
Pretty-prints to debug console with colors and emojis |
stack |
Broadcasts to multiple channels simultaneously |
Minimum Log Level#
Each channel can specify a minimum level. Messages below this threshold are ignored:
'channels': {
'production': {
'driver': 'console',
'level': 'warning', // Ignores debug, info, notice
},
}
Levels by priority (RFC 5424):
| Level | Priority | Description |
|---|---|---|
emergency |
0 | System is unusable |
alert |
1 | Action must be taken immediately |
critical |
2 | Critical conditions |
error |
3 | Runtime errors |
warning |
4 | Warning conditions |
notice |
5 | Normal but significant |
info |
6 | Informational |
debug |
7 | Detailed debug information |
Stack Channel#
Use the stack driver to send logs to multiple channels at once:
'stack': {
'driver': 'stack',
'channels': ['console', 'sentry', 'slack'],
}
When you log to the stack channel, the message is forwarded to console, sentry, and slack simultaneously.
Writing Log Messages#
Log Levels#
Use the Log facade to write messages at any RFC 5424 level:
Log.emergency('System is unusable');
Log.alert('Action must be taken immediately');
Log.critical('Critical condition');
Log.error('Runtime error');
Log.warning('Warning condition');
Log.notice('Normal but significant condition');
Log.info('Informational message');
Log.debug('Debug-level message');
For dynamic level selection:
Log.log('info', 'User logged in');
Contextual Information#
Pass additional data as the second argument:
Log.error('Payment failed', {
'user_id': user.id,
'amount': 50.00,
'error': e.toString(),
'stack_trace': stackTrace.toString(),
});
This context is formatted and displayed alongside the message.
Creating Custom Channels#
Magic's logging system is fully extensible. You can create custom drivers to send logs to any destination.
Implementing a Custom Driver#
Create a class that extends LoggerDriver:
// lib/app/logging/sentry_logger_driver.dart
import 'package:magic/magic.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
class SentryLoggerDriver extends LoggerDriver {
final String dsn;
final String minLevel;
SentryLoggerDriver({
required this.dsn,
this.minLevel = 'error',
});
@override
void log(String level, String message, [dynamic context]) {
// Only log if level is at or above minLevel
if (!_shouldLog(level)) return;
Sentry.captureMessage(
message,
level: _sentryLevel(level),
withScope: (scope) {
if (context is Map) {
context.forEach((key, value) {
scope.setExtra(key.toString(), value);
});
}
},
);
}
bool _shouldLog(String level) {
const levels = ['emergency', 'alert', 'critical', 'error', 'warning', 'notice', 'info', 'debug'];
final levelIndex = levels.indexOf(level);
final minIndex = levels.indexOf(minLevel);
return levelIndex <= minIndex;
}
SentryLevel _sentryLevel(String level) {
switch (level) {
case 'emergency':
case 'alert':
case 'critical':
return SentryLevel.fatal;
case 'error':
return SentryLevel.error;
case 'warning':
return SentryLevel.warning;
default:
return SentryLevel.info;
}
}
}
Registering the Custom Driver#
Register your driver via LogManager.extend() in a Service Provider's boot() phase:
// lib/app/providers/logging_service_provider.dart
class LoggingServiceProvider extends ServiceProvider {
LoggingServiceProvider(super.app);
@override
Future<void> boot() async {
LogManager.extend('sentry', (config) => SentryLoggerDriver(
dsn: config['dsn'] ?? '',
minLevel: config['level'] ?? 'error',
));
}
}
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:
'channels': {
'sentry': {
'driver': 'sentry',
'dsn': 'https://your-sentry-dsn',
'level': 'error',
},
}
And log to it:
Log.channel('sentry').error('Something went wrong', {
'user_id': user.id,
});
Testing#
Replace the real LogManager with FakeLogManager during tests using Log.fake(). The fake captures all log calls in memory and provides assertion helpers.
import 'package:magic/testing.dart';
test('logs a payment error', () {
final fake = Log.fake();
Log.error('Payment failed', {'amount': 50.00});
fake.assertLoggedError('Payment failed');
fake.assertLoggedCount(1);
Log.unfake();
});
The FakeLogManager exposes:
| Method | Description |
|---|---|
assertLogged(level, message) |
Assert at least one entry matches the given level and message |
assertLoggedError(message) |
Shorthand for assertLogged('error', message) |
assertNothingLogged([level]) |
Assert the log is empty, or empty at a specific level |
assertLoggedCount(n) |
Assert exactly n entries were recorded |
reset() |
Clear all captured entries |
entries |
Read-only list of all captured FakeLogEntry records |
Call Log.unfake() at the end of the test (or in tearDown) to restore the real binding.
Example: Slack Notifications#
Register the driver, then use it via config:
// In ServiceProvider boot()
LogManager.extend('slack', (config) => SlackLoggerDriver(
webhookUrl: config['webhook_url'],
channel: config['channel'] ?? '#alerts',
));
Here's the driver implementation:
class SlackLoggerDriver extends LoggerDriver {
final String webhookUrl;
final String channel;
SlackLoggerDriver({required this.webhookUrl, this.channel = '#alerts'});
@override
void log(String level, String message, [dynamic context]) {
final payload = {
'channel': channel,
'username': 'Magic Bot',
'icon_emoji': _emoji(level),
'text': '[$level] $message',
'attachments': context != null ? [{'text': context.toString()}] : null,
};
Http.post(webhookUrl, data: payload);
}
String _emoji(String level) {
switch (level) {
case 'emergency':
case 'critical': return ':fire:';
case 'error': return ':x:';
case 'warning': return ':warning:';
default: return ':information_source:';
}
}
}