search ESC

Searching…

No results for "".

Type at least 2 characters to search.

Docs

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: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 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,
});

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:';
    }
  }
}