← Articles

Structured logging in Flutter for production

By Charlin Joe · 26 October 2025

Structured logging means emitting machine-readable log records with consistent fields — level, timestamp, message, context — rather than free-form print statements. In production Flutter apps, this lets you search, filter, and alert on logs in tools like Sentry, Firebase Crashlytics, and Datadog.

Why structured logging

Free-form print() doesn't scale:

// What does this tell you in production?
print('Error creating order');

// This is infinitely more useful:
logger.error('Failed to create order', {'order_id': orderId, 'user_id': userId, 'error': e.toString()});

With structured logs, you can filter level:error service:orders in your log aggregator to see all order failures.

logger package

dependencies:
  logger: ^2.4.0
import 'package:logger/logger.dart';

final logger = Logger(
  printer: PrettyPrinter(
    methodCount: 2,
    errorMethodCount: 8,
    lineLength: 120,
    colors: true,
    printEmojis: true,
  ),
  level: kDebugMode ? Level.debug : Level.warning,
);

// Usage
logger.d('Loading orders', error: null, stackTrace: null);
logger.i('Order created', error: {'orderId': orderId});
logger.w('Rate limit approaching', error: {'remaining': 5});
logger.e('Payment failed', error: e, stackTrace: stackTrace);

Custom output for Sentry/Crashlytics

Route log output to crash reporters:

class ProductionOutput extends LogOutput {
  @override
  void output(OutputEvent event) {
    for (final line in event.lines) {
      // Add as breadcrumb in Sentry
      Sentry.addBreadcrumb(Breadcrumb(
        message: line,
        level: _mapLevel(event.level),
        timestamp: DateTime.now(),
      ));

      // Log to Crashlytics
      FirebaseCrashlytics.instance.log(line);
    }

    if (event.level >= Level.error) {
      // Send errors immediately
      final error = event.origin.error;
      final stack = event.origin.stackTrace;
      if (error != null) {
        Sentry.captureException(error, stackTrace: stack);
      }
    }
  }

  SentryLevel _mapLevel(Level level) => switch (level) {
    Level.debug => SentryLevel.debug,
    Level.info => SentryLevel.info,
    Level.warning => SentryLevel.warning,
    Level.error || Level.fatal => SentryLevel.error,
    _ => SentryLevel.info,
  };
}

// In main.dart:
Logger(
  output: kReleaseMode ? ProductionOutput() : ConsoleOutput(),
  level: kReleaseMode ? Level.info : Level.debug,
)

Context-aware logging

Enrich logs with contextual information:

class AppLogger {
  AppLogger({required this.service});
  final String service;

  final _logger = Logger();

  void info(String message, {Map<String, dynamic>? extra}) {
    _logger.i('[$service] $message', error: extra);
  }

  void error(String message, Object error, StackTrace stackTrace, {Map<String, dynamic>? extra}) {
    _logger.e(
      '[$service] $message',
      error: error,
      stackTrace: stackTrace,
    );
    Sentry.captureException(
      error,
      stackTrace: stackTrace,
      hint: Hint.withMap({
        'service': service,
        'message': message,
        ...?extra,
      }),
    );
  }
}

// Per-service loggers
final ordersLogger = AppLogger(service: 'orders');
final authLogger = AppLogger(service: 'auth');
final paymentsLogger = AppLogger(service: 'payments');

// Usage
ordersLogger.info('Order created', extra: {'orderId': orderId, 'total': total});
ordersLogger.error('Order creation failed', e, stackTrace, extra: {'orderId': orderId});

Log levels in practice

// DEBUG: detailed diagnostic information (dev only)
logger.d('Fetching orders with cursor: $cursor, limit: $limit');

// INFO: significant lifecycle events
logger.i('User signed in', error: {'userId': user.id, 'method': 'google'});
logger.i('Order created', error: {'orderId': orderId, 'total': total});

// WARNING: unexpected but handled situations
logger.w('Order not found, creating new one', error: {'orderId': orderId});
logger.w('Rate limit: $remaining requests remaining');

// ERROR: operation failed (log and report)
logger.e('Payment processing failed', error: e, stackTrace: stackTrace);

// FATAL: app cannot continue
logger.f('Database initialization failed — app cannot start', error: e);

Disabling verbose logs in release

// In release builds, suppress debug and info logs
final logger = Logger(
  level: kReleaseMode ? Level.warning : Level.debug,
);

// Or disable print entirely
if (kReleaseMode) {
  debugPrint = (String? message, {int? wrapWidth}) {};
}

Structured fields for cloud logging

For cloud log aggregators (Google Cloud Logging, Datadog), emit JSON:

class CloudLogger {
  void log(Level level, String message, Map<String, dynamic> fields) {
    final entry = jsonEncode({
      'timestamp': DateTime.now().toIso8601String(),
      'severity': level.name.toUpperCase(),
      'message': message,
      'service': 'mobile',
      'version': AppConfig.version,
      ...fields,
    });
    print(entry); // Cloud log agents pick this up
  }
}

Common pitfalls

Using print() in production. Print is synchronous and slow in release builds (where it's usually stripped). Use debugPrint() in debug code and a proper logger in production paths.

Logging PII. Email addresses, names, order details, and health data are PII. Never log them, even at debug level — logs are often stored and indexed. Log IDs (user ID, order ID) instead of personal details.

No log levels. If everything is logged at INFO, you can't filter for important events. Use DEBUG for diagnostic detail, INFO for significant events, WARNING for handled anomalies, ERROR for failures.

Sign in to like, dislike, or report.

Structured logging in Flutter for production — ANN Tech