Skip to content

Logging

View raw .md

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:

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:

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<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:

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

Testing#

Replace the real LogManager with FakeLogManager during tests using Log.fake(). The fake captures all log calls in memory and provides assertion helpers.

dart
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:

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

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.