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.