Files
xp_nix/xp_server/lib/src/logging/logger.dart
T

255 lines
7.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}