Logging
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 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:fluttersdk_magic/fluttersdk_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 in a Service Provider:
// lib/app/providers/logging_service_provider.dart
class LoggingServiceProvider extends ServiceProvider {
@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;
});
}
}
[!NOTE] The
registerDrivermethod is a convention. IfLogManagerdoesn't support it natively, you can extend it or customize resolution logic.
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,
});
Example: Slack Notifications
Here's a complete example for Slack webhook logging:
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:';
}
}
}