28 File Logging
Great! Let’s extend the AppLogger
to support file logging. This is particularly useful for debugging production issues or when you need persistent logs.
28.1 Enhanced AppLogger with File Logging
// lib/utils/app_logger.dart
import 'package:logging/logging.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:developer' as developer;
import 'dart:io';
class AppLogger {
._();
AppLogger
// Logger instances
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');
static final app = Logger('MyApp');
// File logging configuration
static File? _logFile;
static bool _fileLoggingEnabled = false;
static int _maxLogFileSizeBytes = 5 * 1024 * 1024; // 5MB
static int _maxLogFiles = 3; // Keep 3 log files max
/// Initialize logging with optional file logging
static Future<void> initialize({
bool enableFileLogging = true,
int maxLogFileSizeBytes = 5 * 1024 * 1024,
int maxLogFiles = 3,
}) async {
// Set configuration
= maxLogFileSizeBytes;
_maxLogFileSizeBytes = maxLogFiles;
_maxLogFiles
// Set log level
.root.level = kDebugMode ? Level.ALL : Level.INFO;
Logger
// Initialize file logging if requested
if (enableFileLogging) {
await _initializeFileLogging();
}
// Set up log listener
.root.onRecord.listen(_handleLogRecord);
Logger
.info('AppLogger initialized (file logging: $_fileLoggingEnabled)');
app}
/// Initialize file logging
static Future<void> _initializeFileLogging() async {
try {
final directory = await getApplicationDocumentsDirectory();
final logDir = Directory('${directory.path}/logs');
// Create logs directory if it doesn't exist
if (!await logDir.exists()) {
await logDir.create(recursive: true);
}
// Create log file with timestamp
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-');
= File('${logDir.path}/app_log_$timestamp.txt');
_logFile
// Clean up old log files
await _cleanupOldLogFiles(logDir);
= true;
_fileLoggingEnabled
} catch (e) {
.log('Failed to initialize file logging: $e', name: 'AppLogger');
developer= false;
_fileLoggingEnabled }
}
/// Clean up old log files to maintain maximum count
static Future<void> _cleanupOldLogFiles(Directory logDir) async {
try {
final logFiles = logDir
.listSync()
.whereType<File>()
.where((file) => file.path.contains('app_log_'))
.toList();
// Sort by modification time (newest first)
.sort((a, b) =>
logFiles.lastModifiedSync().compareTo(a.lastModifiedSync()));
b
// Delete excess files
if (logFiles.length >= _maxLogFiles) {
final filesToDelete = logFiles.skip(_maxLogFiles - 1);
for (final file in filesToDelete) {
await file.delete();
.log('Deleted old log file: ${file.path}', name: 'AppLogger');
developer}
}
} catch (e) {
.log('Error cleaning up log files: $e', name: 'AppLogger');
developer}
}
/// Handle log records
static void _handleLogRecord(LogRecord record) {
final formattedMessage = _formatLogMessage(record);
if (kDebugMode) {
// Console output in debug mode
.log(
developer.message,
record: record.time,
time: record.level.value,
level: record.loggerName,
name: record.error,
error: record.stackTrace,
stackTrace
);}
// File output (if enabled)
if (_fileLoggingEnabled) {
_writeToFile(formattedMessage);}
// Production logging
if (kReleaseMode) {
_handleProductionLog(record);}
}
/// Format log message for file output
static String _formatLogMessage(LogRecord record) {
final timestamp = record.time.toIso8601String();
final level = record.level.name.padRight(7);
final logger = record.loggerName.padRight(20);
var message = '[$timestamp] [$level] [$logger] ${record.message}';
if (record.error != null) {
+= '\nError: ${record.error}';
message }
if (record.stackTrace != null) {
+= '\nStack trace:\n${record.stackTrace}';
message }
return message;
}
/// Write message to log file
static void _writeToFile(String message) {
if (_logFile == null) return;
try {
// Check file size and rotate if necessary
if (_logFile!.existsSync() &&
!.lengthSync() > _maxLogFileSizeBytes) {
_logFile
_rotateLogFile();}
// Append to file
!.writeAsStringSync('$message\n', mode: FileMode.append);
_logFile
} catch (e) {
.log('Error writing to log file: $e', name: 'AppLogger');
developer}
}
/// Rotate log file when it gets too large
static Future<void> _rotateLogFile() async {
try {
if (_logFile == null) return;
final directory = _logFile!.parent;
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-');
final newLogFile = File('${directory.path}/app_log_$timestamp.txt');
= newLogFile;
_logFile
// Clean up old files
await _cleanupOldLogFiles(directory);
} catch (e) {
.log('Error rotating log file: $e', name: 'AppLogger');
developer}
}
/// Handle production logging
static void _handleProductionLog(LogRecord record) {
// Send to crash reporting services
// FirebaseCrashlytics.instance.log('${record.level.name}: ${record.message}');
if (record.level >= Level.SEVERE) {
// Send severe errors to monitoring
// Sentry.captureException(record.error ?? record.message);
}
}
/// Get current log file path
static String? get currentLogFilePath => _logFile?.path;
/// Check if file logging is enabled
static bool get isFileLoggingEnabled => _fileLoggingEnabled;
/// Get all log files
static Future<List<File>> getLogFiles() async {
try {
final directory = await getApplicationDocumentsDirectory();
final logDir = Directory('${directory.path}/logs');
if (!await logDir.exists()) return [];
return logDir
.listSync()
.whereType<File>()
.where((file) => file.path.contains('app_log_'))
.toList();
} catch (e) {
.severe('Error getting log files', e);
appreturn [];
}
}
/// Read log file content
static Future<String> readLogFile(File logFile) async {
try {
return await logFile.readAsString();
} catch (e) {
.severe('Error reading log file: ${logFile.path}', e);
appreturn 'Error reading log file: $e';
}
}
/// Export logs (for sharing or debugging)
static Future<File?> exportLogs() async {
try {
final logFiles = await getLogFiles();
if (logFiles.isEmpty) return null;
final directory = await getApplicationDocumentsDirectory();
final exportFile = File('${directory.path}/exported_logs.txt');
final buffer = StringBuffer();
.writeln('=== APP LOGS EXPORT ===');
buffer.writeln('Export time: ${DateTime.now().toIso8601String()}');
buffer.writeln('');
buffer
for (final logFile in logFiles) {
.writeln('=== ${logFile.path.split('/').last} ===');
bufferfinal content = await readLogFile(logFile);
.writeln(content);
buffer.writeln('');
buffer}
await exportFile.writeAsString(buffer.toString());
.info('Logs exported to: ${exportFile.path}');
app
return exportFile;
} catch (e) {
.severe('Error exporting logs', e);
appreturn null;
}
}
/// Clear all log files
static Future<void> clearLogs() async {
try {
final logFiles = await getLogFiles();
for (final file in logFiles) {
await file.delete();
}
.info('All log files cleared');
app} catch (e) {
.severe('Error clearing logs', e);
app}
}
/// Convenience methods
static void setLogLevel(String loggerName, Level level) {
.level = level;
Logger(loggerName)}
static void disableLogging() {
.root.level = Level.OFF;
Logger}
static void enableFileLogging() async {
if (!_fileLoggingEnabled) {
await _initializeFileLogging();
}
}
static void disableFileLogging() {
= false;
_fileLoggingEnabled }
}
28.2 Add Dependencies
Add this to your pubspec.yaml
:
dependencies:
flutter:
sdk: flutter
logging: ^1.2.0
path_provider: ^2.1.1
28.3 Usage Examples
1. Initialize with file logging:
// lib/main.dart
import 'package:flutter/material.dart';
import 'utils/app_logger.dart';
void main() async {
.ensureInitialized();
WidgetsFlutterBinding
// Initialize with file logging
await AppLogger.initialize(
: true,
enableFileLogging: 10 * 1024 * 1024, // 10MB
maxLogFileSizeBytes: 5, // Keep 5 files
maxLogFiles
);
runApp(MyApp());}
2. Use normally (logs go to both console and file):
// lib/services/api_service.dart
import '../utils/app_logger.dart';
class ApiService {
static Future<List<User>> fetchUsers() async {
.network.info('Fetching users from API');
AppLogger
try {
final response = await http.get(Uri.parse('/api/users'));
.network.info('Users fetched successfully', {
AppLogger'count': response.data.length,
'statusCode': response.statusCode,
});
return response.data;
} catch (e, stackTrace) {
.network.severe('Failed to fetch users', e, stackTrace);
AppLoggerrethrow;
}
}
}
3. Create a debug screen to view logs:
// lib/screens/debug_screen.dart
import 'package:flutter/material.dart';
import '../utils/app_logger.dart';
class DebugScreen extends StatefulWidget {
@override
=> _DebugScreenState();
_DebugScreenState createState() }
class _DebugScreenState extends State<DebugScreen> {
List<File> logFiles = [];
@override
void initState() {
super.initState();
_loadLogFiles();}
Future<void> _loadLogFiles() async {
final files = await AppLogger.getLogFiles();
{
setState(() = files;
logFiles });
}
@override
{
Widget build(BuildContext context) return Scaffold(
: AppBar(
appBar: Text('Debug Logs'),
title: [
actions
IconButton(: Icon(Icons.share),
icon: _exportLogs,
onPressed,
)
IconButton(: Icon(Icons.delete),
icon: _clearLogs,
onPressed,
),
],
): Column(
body: [
children// Log file info
Padding(: EdgeInsets.all(16),
padding: Column(
child: CrossAxisAlignment.start,
crossAxisAlignment: [
children'File Logging: ${AppLogger.isFileLoggingEnabled ? "Enabled" : "Disabled"}'),
Text('Current Log: ${AppLogger.currentLogFilePath ?? "None"}'),
Text('Log Files: ${logFiles.length}'),
Text(,
],
),
)
// Log files list
Expanded(: ListView.builder(
child: logFiles.length,
itemCount: (context, index) {
itemBuilderfinal file = logFiles[index];
return ListTile(
: Text(file.path.split('/').last),
title: Text('Size: ${file.lengthSync()} bytes'),
subtitle: () => _viewLogFile(file),
onTap
);},
,
),
),
],
)
);}
Future<void> _exportLogs() async {
final file = await AppLogger.exportLogs();
if (file != null) {
.of(context).showSnackBar(
ScaffoldMessenger: Text('Logs exported to: ${file.path}')),
SnackBar(content
);}
}
Future<void> _clearLogs() async {
await AppLogger.clearLogs();
await _loadLogFiles();
.of(context).showSnackBar(
ScaffoldMessenger: Text('Logs cleared')),
SnackBar(content
);}
void _viewLogFile(File file) {
.push(
Navigator,
context
MaterialPageRoute(: (context) => LogFileViewer(file: file),
builder,
)
);}
}
28.4 Key Features
Automatic File Management: - Creates timestamped log files - Rotates when files get too large - Keeps only specified number of files - Cleans up old files automatically
Flexible Configuration: - Enable/disable file logging - Set maximum file size - Set maximum number of files - Export logs for debugging
Production Ready: - Works in both debug and release modes - Handles file system errors gracefully - Minimal performance impact
This file logging system will help you debug production issues and maintain a persistent record of your app’s behavior. The logs are stored locally and can be easily exported for analysis or sharing with your team.
Would you like me to show you how to integrate this with cloud storage or how to create filters for viewing specific log levels?