255 lines
7.9 KiB
Dart
255 lines
7.9 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
|
||
import 'package:xp_nix/src/models/activity_event.dart';
|
||
|
||
enum LogLevel {
|
||
debug(0, 'DEBUG'),
|
||
info(1, 'INFO'),
|
||
warn(2, 'WARN'),
|
||
error(3, 'ERROR');
|
||
|
||
const LogLevel(this.value, this.name);
|
||
final int value;
|
||
final String name;
|
||
}
|
||
|
||
class Logger {
|
||
static Logger? _instance;
|
||
static Logger get instance => _instance ??= Logger._();
|
||
|
||
Logger._();
|
||
|
||
LogLevel _currentLevel = LogLevel.info;
|
||
String _logDirectory = 'logs';
|
||
int _maxFileSizeMB = 10;
|
||
int _maxFiles = 5;
|
||
File? _currentLogFile;
|
||
final StreamController<String> _logStreamController = StreamController<String>.broadcast();
|
||
|
||
Stream<String> get logStream => _logStreamController.stream;
|
||
|
||
Future<void> initialize({
|
||
LogLevel level = LogLevel.info,
|
||
String logDirectory = 'logs',
|
||
int maxFileSizeMB = 10,
|
||
int maxFiles = 5,
|
||
}) async {
|
||
_currentLevel = level;
|
||
_logDirectory = logDirectory;
|
||
_maxFileSizeMB = maxFileSizeMB;
|
||
_maxFiles = maxFiles;
|
||
|
||
await _setupLogFile();
|
||
_info('Logger initialized with level: ${level.name}');
|
||
}
|
||
|
||
Future<void> _setupLogFile() async {
|
||
final logDir = Directory(_logDirectory);
|
||
if (!await logDir.exists()) {
|
||
await logDir.create(recursive: true);
|
||
}
|
||
|
||
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-').split('.')[0];
|
||
_currentLogFile = File('$_logDirectory/xp_nix_$timestamp.log');
|
||
|
||
// Create the file if it doesn't exist
|
||
if (!await _currentLogFile!.exists()) {
|
||
await _currentLogFile!.create();
|
||
}
|
||
}
|
||
|
||
Future<void> _rotateLogIfNeeded() async {
|
||
if (_currentLogFile == null) return;
|
||
|
||
try {
|
||
final stats = await _currentLogFile!.stat();
|
||
final fileSizeMB = stats.size / (1024 * 1024);
|
||
|
||
if (fileSizeMB > _maxFileSizeMB) {
|
||
await _cleanupOldLogs();
|
||
await _setupLogFile();
|
||
_info('Log file rotated due to size limit');
|
||
}
|
||
} catch (e) {
|
||
// If we can't check file size, just continue
|
||
print('Could not check log file size: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> _cleanupOldLogs() async {
|
||
final logDir = Directory(_logDirectory);
|
||
if (!await logDir.exists()) return;
|
||
|
||
try {
|
||
final logFiles =
|
||
await logDir.list().where((entity) => entity is File && entity.path.endsWith('.log')).cast<File>().toList();
|
||
|
||
logFiles.sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()));
|
||
|
||
if (logFiles.length >= _maxFiles) {
|
||
for (int i = _maxFiles - 1; i < logFiles.length; i++) {
|
||
_info('Deleting old log file ${logFiles[i].toString()}');
|
||
await logFiles[i].delete();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
print('Could not cleanup old logs: $e');
|
||
}
|
||
}
|
||
|
||
void _log(LogLevel level, String message, [Object? error, StackTrace? stackTrace]) {
|
||
if (level.value < _currentLevel.value) return;
|
||
|
||
final timestamp = DateTime.now().toIso8601String();
|
||
final logEntry = '[$timestamp] [${level.name}] $message';
|
||
|
||
// Print to console
|
||
print(logEntry);
|
||
|
||
// Add to stream for real-time monitoring
|
||
_logStreamController.add(logEntry);
|
||
|
||
// Write to file asynchronously
|
||
_writeToFileAsync(logEntry, error, stackTrace);
|
||
}
|
||
|
||
void _writeToFileAsync(String logEntry, [Object? error, StackTrace? stackTrace]) {
|
||
// Use Future.microtask to avoid blocking the current execution
|
||
Future.microtask(() async {
|
||
await _writeToFile(logEntry, error, stackTrace);
|
||
});
|
||
}
|
||
|
||
Future<void> _writeToFile(String logEntry, [Object? error, StackTrace? stackTrace]) async {
|
||
if (_currentLogFile == null) return;
|
||
|
||
try {
|
||
final buffer = StringBuffer();
|
||
buffer.writeln(logEntry);
|
||
|
||
if (error != null) {
|
||
buffer.writeln('Error: $error');
|
||
}
|
||
|
||
if (stackTrace != null) {
|
||
buffer.writeln('Stack trace: $stackTrace');
|
||
}
|
||
|
||
// Write to file using writeAsString with append mode
|
||
await _currentLogFile!.writeAsString(buffer.toString(), mode: FileMode.append, flush: true);
|
||
|
||
// Check if rotation is needed
|
||
await _rotateLogIfNeeded();
|
||
} catch (e) {
|
||
// If writing fails, try to recreate the log file
|
||
print('Log writing failed: $e');
|
||
try {
|
||
await _setupLogFile();
|
||
} catch (setupError) {
|
||
print('Could not recreate log file: $setupError');
|
||
}
|
||
}
|
||
}
|
||
|
||
// Static convenience methods
|
||
static void debug(String message) => instance._log(LogLevel.debug, message);
|
||
static void info(String message) => instance._log(LogLevel.info, message);
|
||
static void warn(String message) => instance._log(LogLevel.warn, message);
|
||
static void error(String message, [Object? error, StackTrace? stackTrace]) =>
|
||
instance._log(LogLevel.error, message, error, stackTrace);
|
||
|
||
// Instance methods for when you have a logger instance
|
||
// ignore: unused_element
|
||
void _debug(String message) => _log(LogLevel.debug, message);
|
||
// ignore: unused_element
|
||
void _info(String message) => _log(LogLevel.info, message);
|
||
// ignore: unused_element
|
||
void _warn(String message) => _log(LogLevel.warn, message);
|
||
// ignore: unused_element
|
||
void _error(String message, [Object? error, StackTrace? stackTrace]) =>
|
||
_log(LogLevel.error, message, error, stackTrace);
|
||
|
||
// Activity-specific logging methods
|
||
static void logActivity(String activityType, String application, int durationSeconds, int xpGained) {
|
||
info('ACTIVITY: $activityType in $application for ${durationSeconds}s (+$xpGained XP)');
|
||
}
|
||
|
||
static void logFocusSession(int minutes, int bonusXP) {
|
||
info('FOCUS_SESSION: ${minutes}min session completed (+$bonusXP XP)');
|
||
}
|
||
|
||
static void logLevelUp(int oldLevel, int newLevel, int totalXP) {
|
||
info('LEVEL_UP: $oldLevel → $newLevel (Total XP: $totalXP)');
|
||
}
|
||
|
||
static void logAchievement(String name, String description, int xpReward) {
|
||
info('ACHIEVEMENT: $name - $description (+$xpReward XP)');
|
||
}
|
||
|
||
static void logConfigChange(String path, dynamic oldValue, dynamic newValue) {
|
||
info('CONFIG_CHANGE: $path changed from $oldValue to $newValue');
|
||
}
|
||
|
||
static void logXPCalculation(ActivityEventType activityType, int baseXP, double multiplier, int finalXP) {
|
||
debug('XP_CALC: ${activityType.displayName} base=$baseXP × $multiplier = $finalXP');
|
||
}
|
||
|
||
static void logIdleStateChange(bool isIdle, Duration? idleDuration) {
|
||
if (isIdle) {
|
||
debug('IDLE: User became idle');
|
||
} else {
|
||
final duration = idleDuration?.inMinutes ?? 0;
|
||
info('ACTIVE: User became active after ${duration}min idle');
|
||
}
|
||
}
|
||
|
||
static void logZoomStatusChange(String oldStatus, String newStatus) {
|
||
info('ZOOM_STATUS: $oldStatus → $newStatus');
|
||
}
|
||
|
||
static void logThemeChange(int level, String themeName) {
|
||
info('THEME_CHANGE: Applied theme "$themeName" for level $level');
|
||
}
|
||
|
||
static void logXPGain(String source, int xpGained, String activity, int currentXP, int currentLevel) {
|
||
info('XP_GAIN: +$xpGained from $source ($activity) - Total: $currentXP XP, Level: $currentLevel');
|
||
}
|
||
|
||
// Performance logging
|
||
static void logPerformanceMetric(String operation, Duration duration) {
|
||
debug('PERFORMANCE: $operation took ${duration.inMilliseconds}ms');
|
||
}
|
||
|
||
// Get recent log entries for dashboard
|
||
Future<List<String>> getRecentLogs([int count = 100]) async {
|
||
if (_currentLogFile == null || !await _currentLogFile!.exists()) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
final lines = await _currentLogFile!.readAsLines();
|
||
return lines.reversed.take(count).toList();
|
||
} catch (e) {
|
||
_error('Failed to read log file: $e');
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// Get logs by level
|
||
Future<List<String>> getLogsByLevel(LogLevel level, [int count = 50]) async {
|
||
final allLogs = await getRecentLogs(count * 2);
|
||
return allLogs.where((log) => log.contains('[${level.name}]')).take(count).toList();
|
||
}
|
||
|
||
// Update log level dynamically
|
||
void setLogLevel(LogLevel level) {
|
||
_currentLevel = level;
|
||
info('Log level changed to: ${level.name}');
|
||
}
|
||
|
||
Future<void> dispose() async {
|
||
await _logStreamController.close();
|
||
}
|
||
}
|