21  Logging

I’ll guide you through logging in Dart and Flutter, starting from the basics and building up to more advanced techniques. Let’s explore each approach systematically.

21.1 Understanding Logging in Dart/Flutter

Logging is essential for debugging, monitoring application behavior, and tracking issues in both development and production environments. Think of logging as leaving breadcrumbs through your code - these breadcrumbs help you understand what happened when something goes wrong or when you need to trace the flow of your application.

21.2 Basic Logging with print()

The print() function is the simplest way to output messages in Dart. It’s like console.log() in JavaScript or print() in Python.

void main() {
  print('Application started');
  
  var user = 'John';
  var age = 25;
  print('User: $user, Age: $age');
  
  // Printing objects
  var userMap = {'name': 'John', 'age': 25};
  print(userMap); // {name: John, age: 25}
}

When to use print():

  • Quick debugging during development
  • Simple scripts or command-line Dart applications
  • When you need immediate, simple output without formatting

Limitations of print():

  • In Flutter release builds, print() statements are removed for performance
  • No timestamp or source information
  • No log levels (info, warning, error)
  • Can’t be filtered or redirected easily

21.3 Developer Tools Logging with developer.log()

The developer.log() function from dart:developer provides more sophisticated logging capabilities designed specifically for development tools.

import 'dart:developer' as developer;

void fetchUserData() {
  developer.log('Fetching user data...', name: 'UserService');
  
  try {
    // Simulate API call
    var userData = {'id': 123, 'name': 'John'};
    
    developer.log(
      'User data fetched successfully',
      name: 'UserService',
      error: null,
      time: DateTime.now(),
    );
  } catch (e, stackTrace) {
    developer.log(
      'Failed to fetch user data',
      name: 'UserService',
      error: e,
      stackTrace: stackTrace,
      level: 1000, // Error level
    );
  }
}

Key features of developer.log():

  • name: Categorizes logs (like a logger name)
  • time: Adds timestamp
  • sequenceNumber: Orders logs
  • level: Sets severity (0-2000, where 1000+ is severe)
  • error and stackTrace: Properly formats exceptions

When to use developer.log():

  • During Flutter development when you need structured logs
  • When debugging with Flutter DevTools
  • When you need to log errors with stack traces
  • For temporary debugging that shouldn’t affect production

21.4 Advanced Logging with the logging Package

The logging package provides a comprehensive logging solution similar to logging frameworks in other languages (like Python’s logging module).

First, add it to your pubspec.yaml:

dependencies:
  logging: ^1.2.0

Here’s a complete example showing various features:

import 'package:logging/logging.dart';

// Create a logger instance
final _logger = Logger('MyApp');

void setupLogging() {
  // Set the root logging level
  Logger.root.level = Level.ALL;
  
  // Configure output formatting
  Logger.root.onRecord.listen((LogRecord record) {
    final time = record.time.toString().substring(11, 19); // Extract time
    final level = record.level.name.padRight(7); // Pad level name
    final logger = record.loggerName.padRight(15); // Pad logger name
    
    print('$time [$level] $logger: ${record.message}');
    
    // Print stack trace if available
    if (record.stackTrace != null) {
      print(record.stackTrace);
    }
  });
}

// Example usage in different parts of your app
class UserService {
  static final _logger = Logger('UserService');
  
  Future<void> loginUser(String username) async {
    _logger.info('Attempting login for user: $username');
    
    try {
      // Simulate API call
      await Future.delayed(Duration(seconds: 1));
      
      if (username.isEmpty) {
        throw ArgumentError('Username cannot be empty');
      }
      
      _logger.fine('Login successful for user: $username');
    } catch (e, stackTrace) {
      _logger.severe('Login failed for user: $username', e, stackTrace);
      rethrow;
    }
  }
}

class DataProcessor {
  static final _logger = Logger('DataProcessor');
  
  void processData(List<int> data) {
    _logger.finest('Starting data processing with ${data.length} items');
    
    for (var i = 0; i < data.length; i++) {
      if (i % 100 == 0) {
        _logger.finer('Processed $i items');
      }
      
      // Process item
      if (data[i] < 0) {
        _logger.warning('Negative value found at index $i: ${data[i]}');
      }
    }
    
    _logger.info('Data processing completed');
  }
}

21.5 Log Levels in the logging Package

The logging package provides these levels (from lowest to highest severity):

  • FINEST (300): Most detailed information
  • FINER (400): Fairly detailed information
  • FINE (500): Useful debugging information
  • CONFIG (700): Configuration information
  • INFO (800): General informational messages
  • WARNING (900): Potential problems
  • SEVERE (1000): Serious failures
  • SHOUT (1200): Extra loud severe messages

21.6 Configuring Logging for Different Environments

Here’s a practical setup for different environments:

import 'package:logging/logging.dart';
import 'package:flutter/foundation.dart';

void configureLogging() {
  if (kDebugMode) {
    // Development configuration
    Logger.root.level = Level.ALL;
    Logger.root.onRecord.listen((record) {
      debugPrint('${record.level.name}: ${record.time}: '
          '${record.loggerName}: ${record.message}');
    });
  } else if (kProfileMode) {
    // Profile mode configuration
    Logger.root.level = Level.INFO;
    Logger.root.onRecord.listen((record) {
      if (record.level >= Level.INFO) {
        print('[${record.level.name}] ${record.message}');
      }
    });
  } else {
    // Release mode configuration
    Logger.root.level = Level.WARNING;
    // In production, you might send logs to a crash reporting service
    Logger.root.onRecord.listen((record) {
      if (record.level >= Level.WARNING) {
        // Send to crash analytics service
        _sendToCrashlytics(record);
      }
    });
  }
}

void _sendToCrashlytics(LogRecord record) {
  // Implementation would depend on your crash reporting service
  // Example: FirebaseCrashlytics.instance.log(record.message);
}

21.7 Best Practices and When to Use Each Approach

Use print() when:

  • You’re doing quick, temporary debugging
  • Working on simple Dart scripts
  • You need immediate output during development
  • You don’t care about the output in production

Use developer.log() when:

  • You’re debugging Flutter applications
  • You need structured logs visible in Flutter DevTools
  • You want to include error objects and stack traces
  • You need categorized logs but don’t want a full logging framework

Use the logging package when:

  • Building production applications
  • You need fine-grained control over log levels
  • Different parts of your app need different logging configurations
  • You want to send logs to external services
  • You need consistent, formatted logging across your application

21.8 Practical Example: Combining Approaches

Here’s how you might use different logging approaches in a real Flutter application:

import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'dart:developer' as developer;

final _logger = Logger('MainApp');

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    
    // Quick debug during development
    print('App initializing...');
    
    // Structured development log
    developer.log('Initializing app state', name: 'MyApp');
    
    // Production-ready logging
    _logger.info('Application started at ${DateTime.now()}');
    
    _loadConfiguration();
  }
  
  void _loadConfiguration() {
    try {
      _logger.config('Loading application configuration');
      // Load config...
      _logger.fine('Configuration loaded successfully');
    } catch (e, stackTrace) {
      _logger.severe('Failed to load configuration', e, stackTrace);
      
      // Also log to developer tools for immediate visibility
      developer.log(
        'Config loading failed',
        error: e,
        stackTrace: stackTrace,
        level: 1000,
      );
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Logging Demo')),
        body: Center(
          child: ElevatedButton(
            onPressed: () {
              // Quick debug
              print('Button pressed');
              
              // Detailed logging
              _logger.info('User interaction: Button pressed');
            },
            child: Text('Press Me'),
          ),
        ),
      ),
    );
  }
}

This layered approach gives you the flexibility to use quick debugging when needed while maintaining a robust logging system for production. Remember that effective logging is about finding the right balance - too little and you can’t debug issues, too much and you drown in noise.