added cargo files
This commit is contained in:
488
PinePods-0.8.2/mobile/lib/services/logging/app_logger.dart
Normal file
488
PinePods-0.8.2/mobile/lib/services/logging/app_logger.dart
Normal file
@@ -0,0 +1,488 @@
|
||||
// lib/services/logging/app_logger.dart
|
||||
import 'dart:io';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as path_helper;
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
enum LogLevel {
|
||||
debug,
|
||||
info,
|
||||
warning,
|
||||
error,
|
||||
critical,
|
||||
}
|
||||
|
||||
class LogEntry {
|
||||
final DateTime timestamp;
|
||||
final LogLevel level;
|
||||
final String tag;
|
||||
final String message;
|
||||
final String? stackTrace;
|
||||
|
||||
LogEntry({
|
||||
required this.timestamp,
|
||||
required this.level,
|
||||
required this.tag,
|
||||
required this.message,
|
||||
this.stackTrace,
|
||||
});
|
||||
|
||||
String get levelString {
|
||||
switch (level) {
|
||||
case LogLevel.debug:
|
||||
return 'DEBUG';
|
||||
case LogLevel.info:
|
||||
return 'INFO';
|
||||
case LogLevel.warning:
|
||||
return 'WARN';
|
||||
case LogLevel.error:
|
||||
return 'ERROR';
|
||||
case LogLevel.critical:
|
||||
return 'CRITICAL';
|
||||
}
|
||||
}
|
||||
|
||||
String get formattedMessage {
|
||||
final timeStr = timestamp.toString().substring(0, 19); // Remove milliseconds for readability
|
||||
var result = '[$timeStr] [$levelString] [$tag] $message';
|
||||
if (stackTrace != null && stackTrace!.isNotEmpty) {
|
||||
result += '\nStackTrace: $stackTrace';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class DeviceInfo {
|
||||
final String platform;
|
||||
final String osVersion;
|
||||
final String model;
|
||||
final String manufacturer;
|
||||
final String appVersion;
|
||||
final String buildNumber;
|
||||
|
||||
DeviceInfo({
|
||||
required this.platform,
|
||||
required this.osVersion,
|
||||
required this.model,
|
||||
required this.manufacturer,
|
||||
required this.appVersion,
|
||||
required this.buildNumber,
|
||||
});
|
||||
|
||||
String get formattedInfo {
|
||||
return '''
|
||||
Device Information:
|
||||
- Platform: $platform
|
||||
- OS Version: $osVersion
|
||||
- Model: $model
|
||||
- Manufacturer: $manufacturer
|
||||
- App Version: $appVersion
|
||||
- Build Number: $buildNumber
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
||||
class AppLogger {
|
||||
static final AppLogger _instance = AppLogger._internal();
|
||||
factory AppLogger() => _instance;
|
||||
AppLogger._internal();
|
||||
|
||||
static const int maxLogEntries = 1000; // Keep last 1000 log entries in memory
|
||||
static const int maxSessionFiles = 5; // Keep last 5 session log files
|
||||
static const String crashLogFileName = 'pinepods_last_crash.txt';
|
||||
|
||||
final Queue<LogEntry> _logs = Queue<LogEntry>();
|
||||
DeviceInfo? _deviceInfo;
|
||||
File? _currentSessionFile;
|
||||
File? _crashLogFile;
|
||||
Directory? _logsDirectory;
|
||||
String? _sessionId;
|
||||
bool _isInitialized = false;
|
||||
|
||||
// Initialize the logger and collect device info
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
await _collectDeviceInfo();
|
||||
await _initializeLogFiles();
|
||||
await _setupCrashHandler();
|
||||
await _loadPreviousCrash();
|
||||
|
||||
_isInitialized = true;
|
||||
|
||||
// Log initialization
|
||||
log(LogLevel.info, 'AppLogger', 'Logger initialized successfully');
|
||||
}
|
||||
|
||||
Future<void> _collectDeviceInfo() async {
|
||||
try {
|
||||
final deviceInfoPlugin = DeviceInfoPlugin();
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await deviceInfoPlugin.androidInfo;
|
||||
_deviceInfo = DeviceInfo(
|
||||
platform: 'Android',
|
||||
osVersion: 'Android ${androidInfo.version.release} (API ${androidInfo.version.sdkInt})',
|
||||
model: '${androidInfo.manufacturer} ${androidInfo.model}',
|
||||
manufacturer: androidInfo.manufacturer,
|
||||
appVersion: packageInfo.version,
|
||||
buildNumber: packageInfo.buildNumber,
|
||||
);
|
||||
} else if (Platform.isIOS) {
|
||||
final iosInfo = await deviceInfoPlugin.iosInfo;
|
||||
_deviceInfo = DeviceInfo(
|
||||
platform: 'iOS',
|
||||
osVersion: '${iosInfo.systemName} ${iosInfo.systemVersion}',
|
||||
model: iosInfo.model,
|
||||
manufacturer: 'Apple',
|
||||
appVersion: packageInfo.version,
|
||||
buildNumber: packageInfo.buildNumber,
|
||||
);
|
||||
} else {
|
||||
_deviceInfo = DeviceInfo(
|
||||
platform: Platform.operatingSystem,
|
||||
osVersion: Platform.operatingSystemVersion,
|
||||
model: 'Unknown',
|
||||
manufacturer: 'Unknown',
|
||||
appVersion: packageInfo.version,
|
||||
buildNumber: packageInfo.buildNumber,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// If device info collection fails, create a basic info object
|
||||
try {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
_deviceInfo = DeviceInfo(
|
||||
platform: Platform.operatingSystem,
|
||||
osVersion: Platform.operatingSystemVersion,
|
||||
model: 'Unknown',
|
||||
manufacturer: 'Unknown',
|
||||
appVersion: packageInfo.version,
|
||||
buildNumber: packageInfo.buildNumber,
|
||||
);
|
||||
} catch (e2) {
|
||||
_deviceInfo = DeviceInfo(
|
||||
platform: 'Unknown',
|
||||
osVersion: 'Unknown',
|
||||
model: 'Unknown',
|
||||
manufacturer: 'Unknown',
|
||||
appVersion: 'Unknown',
|
||||
buildNumber: 'Unknown',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void log(LogLevel level, String tag, String message, [String? stackTrace]) {
|
||||
final entry = LogEntry(
|
||||
timestamp: DateTime.now(),
|
||||
level: level,
|
||||
tag: tag,
|
||||
message: message,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
_logs.add(entry);
|
||||
|
||||
// Keep only the last maxLogEntries in memory
|
||||
while (_logs.length > maxLogEntries) {
|
||||
_logs.removeFirst();
|
||||
}
|
||||
|
||||
// Write to current session file asynchronously (don't await to avoid blocking)
|
||||
_writeToSessionFile(entry);
|
||||
|
||||
// Also print to console in debug mode
|
||||
if (kDebugMode) {
|
||||
print(entry.formattedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods for different log levels
|
||||
void debug(String tag, String message) => log(LogLevel.debug, tag, message);
|
||||
void info(String tag, String message) => log(LogLevel.info, tag, message);
|
||||
void warning(String tag, String message) => log(LogLevel.warning, tag, message);
|
||||
void error(String tag, String message, [String? stackTrace]) => log(LogLevel.error, tag, message, stackTrace);
|
||||
void critical(String tag, String message, [String? stackTrace]) => log(LogLevel.critical, tag, message, stackTrace);
|
||||
|
||||
// Log an exception with automatic stack trace
|
||||
void logException(String tag, String message, dynamic exception, [StackTrace? stackTrace]) {
|
||||
final stackTraceStr = stackTrace?.toString() ?? exception.toString();
|
||||
error(tag, '$message: $exception', stackTraceStr);
|
||||
}
|
||||
|
||||
// Get all logs
|
||||
List<LogEntry> get logs => _logs.toList();
|
||||
|
||||
// Get logs filtered by level
|
||||
List<LogEntry> getLogsByLevel(LogLevel level) {
|
||||
return _logs.where((log) => log.level == level).toList();
|
||||
}
|
||||
|
||||
// Get logs from a specific time period
|
||||
List<LogEntry> getLogsInTimeRange(DateTime start, DateTime end) {
|
||||
return _logs.where((log) =>
|
||||
log.timestamp.isAfter(start) && log.timestamp.isBefore(end)
|
||||
).toList();
|
||||
}
|
||||
|
||||
// Get formatted log string for copying
|
||||
String getFormattedLogs() {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// Add device info
|
||||
if (_deviceInfo != null) {
|
||||
buffer.writeln(_deviceInfo!.formattedInfo);
|
||||
}
|
||||
|
||||
// Add separator
|
||||
buffer.writeln('=' * 50);
|
||||
buffer.writeln('Application Logs:');
|
||||
buffer.writeln('=' * 50);
|
||||
|
||||
// Add all logs
|
||||
for (final log in _logs) {
|
||||
buffer.writeln(log.formattedMessage);
|
||||
}
|
||||
|
||||
// Add footer
|
||||
buffer.writeln();
|
||||
buffer.writeln('=' * 50);
|
||||
buffer.writeln('End of logs - Total entries: ${_logs.length}');
|
||||
buffer.writeln('Bug reports: https://github.com/madeofpendletonwool/pinepods/issues');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
// Clear all logs
|
||||
void clearLogs() {
|
||||
_logs.clear();
|
||||
log(LogLevel.info, 'AppLogger', 'Logs cleared by user');
|
||||
}
|
||||
|
||||
// Initialize log files and directory structure
|
||||
Future<void> _initializeLogFiles() async {
|
||||
try {
|
||||
final appDocDir = await getApplicationDocumentsDirectory();
|
||||
_logsDirectory = Directory(path_helper.join(appDocDir.path, 'logs'));
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
if (!await _logsDirectory!.exists()) {
|
||||
await _logsDirectory!.create(recursive: true);
|
||||
}
|
||||
|
||||
// Clean up old session files (keep only last 5)
|
||||
await _cleanupOldSessionFiles();
|
||||
|
||||
// Create new session file
|
||||
_sessionId = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
|
||||
_currentSessionFile = File(path_helper.join(_logsDirectory!.path, 'session_$_sessionId.log'));
|
||||
await _currentSessionFile!.create();
|
||||
|
||||
// Initialize crash log file
|
||||
_crashLogFile = File(path_helper.join(_logsDirectory!.path, crashLogFileName));
|
||||
if (!await _crashLogFile!.exists()) {
|
||||
await _crashLogFile!.create();
|
||||
}
|
||||
|
||||
log(LogLevel.info, 'AppLogger', 'Session log files initialized at ${_logsDirectory!.path}');
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Failed to initialize log files: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up old session files, keeping only the most recent ones
|
||||
Future<void> _cleanupOldSessionFiles() async {
|
||||
try {
|
||||
final files = await _logsDirectory!.list().toList();
|
||||
final sessionFiles = files
|
||||
.whereType<File>()
|
||||
.where((f) => path_helper.basename(f.path).startsWith('session_'))
|
||||
.toList();
|
||||
|
||||
// Sort by last modified date (newest first)
|
||||
sessionFiles.sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()));
|
||||
|
||||
// Delete files beyond the limit
|
||||
if (sessionFiles.length > maxSessionFiles) {
|
||||
for (int i = maxSessionFiles; i < sessionFiles.length; i++) {
|
||||
await sessionFiles[i].delete();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Failed to cleanup old session files: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write log entry to current session file
|
||||
Future<void> _writeToSessionFile(LogEntry entry) async {
|
||||
if (_currentSessionFile == null) return;
|
||||
|
||||
try {
|
||||
await _currentSessionFile!.writeAsString(
|
||||
'${entry.formattedMessage}\n',
|
||||
mode: FileMode.append,
|
||||
);
|
||||
} catch (e) {
|
||||
// Silently fail to avoid logging loops
|
||||
if (kDebugMode) {
|
||||
print('Failed to write log to session file: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup crash handler
|
||||
Future<void> _setupCrashHandler() async {
|
||||
FlutterError.onError = (FlutterErrorDetails details) {
|
||||
_logCrash('Flutter Error', details.exception.toString(), details.stack);
|
||||
// Still call the default error handler
|
||||
FlutterError.presentError(details);
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
_logCrash('Platform Error', error.toString(), stack);
|
||||
return true; // Mark as handled
|
||||
};
|
||||
}
|
||||
|
||||
// Log crash to persistent storage
|
||||
Future<void> _logCrash(String type, String error, StackTrace? stackTrace) async {
|
||||
try {
|
||||
final crashInfo = {
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
'sessionId': _sessionId,
|
||||
'type': type,
|
||||
'error': error,
|
||||
'stackTrace': stackTrace?.toString(),
|
||||
'deviceInfo': _deviceInfo?.formattedInfo,
|
||||
'recentLogs': _logs.length > 20 ? _logs.skip(_logs.length - 20).map((e) => e.formattedMessage).toList() : _logs.map((e) => e.formattedMessage).toList(), // Only last 20 entries
|
||||
};
|
||||
|
||||
if (_crashLogFile != null) {
|
||||
await _crashLogFile!.writeAsString(jsonEncode(crashInfo));
|
||||
}
|
||||
|
||||
// Also log through normal logging
|
||||
critical('CrashHandler', '$type: $error', stackTrace?.toString());
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Failed to log crash: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load and log previous crash if exists
|
||||
Future<void> _loadPreviousCrash() async {
|
||||
if (_crashLogFile == null || !await _crashLogFile!.exists()) return;
|
||||
|
||||
try {
|
||||
final crashData = await _crashLogFile!.readAsString();
|
||||
if (crashData.isNotEmpty) {
|
||||
final crash = jsonDecode(crashData);
|
||||
warning('PreviousCrash', 'Previous crash detected: ${crash['type']} at ${crash['timestamp']}');
|
||||
warning('PreviousCrash', 'Session: ${crash['sessionId'] ?? 'unknown'}');
|
||||
warning('PreviousCrash', 'Error: ${crash['error']}');
|
||||
if (crash['stackTrace'] != null) {
|
||||
warning('PreviousCrash', 'Stack trace available in crash log file');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
warning('AppLogger', 'Failed to load previous crash info: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Get list of available session files
|
||||
Future<List<File>> getSessionFiles() async {
|
||||
if (_logsDirectory == null) return [];
|
||||
|
||||
try {
|
||||
final files = await _logsDirectory!.list().toList();
|
||||
final sessionFiles = files
|
||||
.whereType<File>()
|
||||
.where((f) => path_helper.basename(f.path).startsWith('session_'))
|
||||
.toList();
|
||||
|
||||
// Sort by last modified date (newest first)
|
||||
sessionFiles.sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()));
|
||||
return sessionFiles;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Get current session file path
|
||||
String? get currentSessionPath => _currentSessionFile?.path;
|
||||
|
||||
// Get crash log file path
|
||||
String? get crashLogPath => _crashLogFile?.path;
|
||||
|
||||
// Get logs directory path
|
||||
String? get logsDirectoryPath => _logsDirectory?.path;
|
||||
|
||||
// Check if previous crash exists
|
||||
Future<bool> hasPreviousCrash() async {
|
||||
if (_crashLogFile == null) return false;
|
||||
try {
|
||||
final exists = await _crashLogFile!.exists();
|
||||
if (!exists) return false;
|
||||
final content = await _crashLogFile!.readAsString();
|
||||
return content.isNotEmpty;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear crash log
|
||||
Future<void> clearCrashLog() async {
|
||||
if (_crashLogFile != null && await _crashLogFile!.exists()) {
|
||||
await _crashLogFile!.writeAsString('');
|
||||
}
|
||||
}
|
||||
|
||||
// Get formatted logs with session info
|
||||
String getFormattedLogsWithSessionInfo() {
|
||||
final buffer = StringBuffer();
|
||||
|
||||
// Add session info
|
||||
buffer.writeln('Session ID: $_sessionId');
|
||||
buffer.writeln('Session started: ${DateTime.now().toString()}');
|
||||
|
||||
// Add device info
|
||||
if (_deviceInfo != null) {
|
||||
buffer.writeln(_deviceInfo!.formattedInfo);
|
||||
}
|
||||
|
||||
// Add separator
|
||||
buffer.writeln('=' * 50);
|
||||
buffer.writeln('Application Logs (Current Session):');
|
||||
buffer.writeln('=' * 50);
|
||||
|
||||
// Add all logs
|
||||
for (final log in _logs) {
|
||||
buffer.writeln(log.formattedMessage);
|
||||
}
|
||||
|
||||
// Add footer
|
||||
buffer.writeln();
|
||||
buffer.writeln('=' * 50);
|
||||
buffer.writeln('End of logs - Total entries: ${_logs.length}');
|
||||
buffer.writeln('Session file: ${_currentSessionFile?.path}');
|
||||
buffer.writeln('Bug reports: https://github.com/madeofpendletonwool/pinepods/issues');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
// Get device info
|
||||
DeviceInfo? get deviceInfo => _deviceInfo;
|
||||
}
|
||||
Reference in New Issue
Block a user