# Logging
- [Introduction](#introduction)
- [Configuration](#configuration)
- [Available Channels](#available-channels)
- [Minimum Log Level](#minimum-log-level)
- [Stack Channel](#stack-channel)
- [Writing Log Messages](#writing-log-messages)
- [Log Levels](#log-levels)
- [Contextual Information](#contextual-information)
- [Creating Custom Channels](#creating-custom-channels)
- [Implementing a Custom Driver](#implementing-a-custom-driver)
- [Registering the Custom Driver](#registering-the-custom-driver)
## 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`:
```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:
```dart
'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:
```dart
'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:
```dart
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:
```dart
Log.log('info', 'User logged in');
```
### Contextual Information
Pass additional data as the second argument:
```dart
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`:
```dart
// 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:
```dart
// lib/app/providers/logging_service_provider.dart
class LoggingServiceProvider extends ServiceProvider {
LoggingServiceProvider(super.app);
@override
Future 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:
```dart
'channels': {
'sentry': {
'driver': 'sentry',
'dsn': 'https://your-sentry-dsn',
'level': 'error',
},
}
```
And log to it:
```dart
Log.channel('sentry').error('Something went wrong', {
'user_id': user.id,
});
```
---
## Example: Slack Notifications
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 {
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:';
}
}
}
```