27  App Logging

Q

I’d like to group related functionalities in the same class. Can we group these loggers in a one class that can be imported and use in other places in app? Is it a good idea to do so?

Absolutely! Grouping loggers in a centralized class is a great idea and follows good software engineering practices. It provides consistency, easier maintenance, and better organization - exactly the kind of simple, practical solution you prefer.

Here’s how to create a centralized logging class:

27.1 AppLogger Class

// lib/utils/app_logger.dart
import 'package:logging/logging.dart';
import 'package:flutter/foundation.dart';
import 'dart:developer' as developer;

class AppLogger {
  // Private constructor to prevent instantiation
  AppLogger._();
  
  // Logger instances - organized by functionality
  static final auth = Logger('MyApp.Auth');
  static final network = Logger('MyApp.Network');
  static final database = Logger('MyApp.Database');
  static final ui = Logger('MyApp.UI');
  static final navigation = Logger('MyApp.Navigation');
  static final storage = Logger('MyApp.Storage');
  static final payment = Logger('MyApp.Payment');
  static final analytics = Logger('MyApp.Analytics');
  
  // Root logger for app-wide messages
  static final app = Logger('MyApp');
  
  /// Initialize logging configuration
  static void initialize() {
    // Set log level based on build mode
    Logger.root.level = kDebugMode ? Level.ALL : Level.WARNING;
    
    // Set up log listener
    Logger.root.onRecord.listen(_handleLogRecord);
    
    app.info('AppLogger initialized');
  }
  
  /// Handle log records - customize output format and routing
  static void _handleLogRecord(LogRecord record) {
    if (kDebugMode) {
      // Use dart:developer log for better DevTools integration
      developer.log(
        record.message,
        time: record.time,
        level: record.level.value,
        name: record.loggerName,
        error: record.error,
        stackTrace: record.stackTrace,
      );
    } else {
      // In production, route to crash reporting, analytics, etc.
      _handleProductionLog(record);
    }
  }
  
  /// Handle production logging (customize as needed)
  static void _handleProductionLog(LogRecord record) {
    // Example: Send to crash reporting service
    // FirebaseCrashlytics.instance.log('${record.level.name}: ${record.message}');
    
    // Example: Send severe errors to monitoring service
    if (record.level >= Level.SEVERE) {
      // Sentry.captureException(record.error ?? record.message);
    }
  }
  
  /// Convenience method to set log level for specific functionality
  static void setLogLevel(String loggerName, Level level) {
    Logger(loggerName).level = level;
  }
  
  /// Convenience method to disable all logging
  static void disableLogging() {
    Logger.root.level = Level.OFF;
  }
}

27.2 Usage in Your App

1. Initialize in main.dart:

// lib/main.dart
import 'package:flutter/material.dart';
import 'utils/app_logger.dart';

void main() {
  AppLogger.initialize();
  runApp(MyApp());
}

2. Use in your widgets and services:

// lib/pages/home_page.dart
import 'package:flutter/material.dart';
import '../utils/app_logger.dart';

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  void initState() {
    super.initState();
    AppLogger.ui.info('HomePage initialized');
    _loadData();
  }
  
  Future<void> _loadData() async {
    try {
      AppLogger.network.info('Starting data fetch');
      
      final data = await ApiService.fetchUserData();
      
      AppLogger.network.info('Data fetched successfully', data.length);
      AppLogger.ui.fine('UI updated with new data');
      
    } catch (e, stackTrace) {
      AppLogger.network.severe('Failed to fetch data', e, stackTrace);
      
      // Show user-friendly error
      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Failed to load data')),
        );
      }
    }
  }
}

3. Use in services:

// lib/services/auth_service.dart
import '../utils/app_logger.dart';

class AuthService {
  static Future<User?> signIn(String email, String password) async {
    AppLogger.auth.info('Sign in attempt', {'email': email});
    
    try {
      final user = await _performSignIn(email, password);
      
      AppLogger.auth.info('Sign in successful', {'userId': user.id});
      AppLogger.analytics.info('User signed in', {'method': 'email'});
      
      return user;
      
    } catch (e, stackTrace) {
      AppLogger.auth.warning('Sign in failed', e, stackTrace);
      rethrow;
    }
  }
}

27.3 Benefits of This Approach

Organization: - All loggers in one place - Clear functional grouping - Easy to see what areas you’re logging

Consistency: - Same naming convention across the app - Centralized configuration - Uniform log formatting

Maintenance: - Easy to add new loggers - Simple to change log levels for debugging - One place to modify output routing

Flexibility: - Can easily disable specific areas: AppLogger.setLogLevel('MyApp.Network', Level.OFF) - Easy to add new output handlers (files, network, etc.)

27.4 Advanced Features You Can Add

class AppLogger {
  // ... existing code ...
  
  /// Log user actions for analytics
  static void logUserAction(String action, [Map<String, dynamic>? parameters]) {
    analytics.info('User action: $action', parameters);
  }
  
  /// Log performance metrics
  static void logPerformance(String operation, Duration duration) {
    app.info('Performance: $operation took ${duration.inMilliseconds}ms');
  }
  
  /// Log with custom context
  static void logWithContext(Logger logger, Level level, String message, {
    Object? error,
    StackTrace? stackTrace,
    Map<String, dynamic>? context,
  }) {
    final enrichedMessage = context != null 
        ? '$message | Context: $context'
        : message;
        
    logger.log(level, enrichedMessage, error, stackTrace);
  }
}

This approach gives you a clean, maintainable logging system that grows with your app. It’s much better than scattered debugPrint() statements and provides the foundation for professional app monitoring.

Would you like me to show you how to extend this for file logging or integration with Firebase Crashlytics?