import 'package:logger/logger.dart'; import 'log_redaction_manager.dart'; /// Redacts sensitive information from log messages based on known values. String _redactSensitiveData(String message) { var redacted = LogRedactionManager.redact(message); // Fallbacks for sensitive fields we cannot track ahead of time. redacted = redacted.replaceAllMapped( RegExp(r'([Aa]uthorization[=:]\s*)([^\s,]+)'), (match) => '${match.group(1)}[REDACTED]', ); redacted = redacted.replaceAllMapped( RegExp(r'([Pp]assword[=:]\s*)([^\s&,;]+)'), (match) => '${match.group(1)}[REDACTED]', ); return redacted; } /// Represents a single log entry stored in memory class LogEntry { final DateTime timestamp; final Level level; final String message; final Object? error; final StackTrace? stackTrace; LogEntry({required this.timestamp, required this.level, required this.message, this.error, this.stackTrace}); /// Estimate the memory size of this log entry in bytes int get estimatedSize { int size = 0; // DateTime: ~8 bytes size += 8; // Level enum: ~4 bytes size += 4; // Message string: 2 bytes per character (UTF-16) size += message.length * 2; // Error string: 2 bytes per character if present if (error != null) { size += error.toString().length * 2; } // Stack trace string: 2 bytes per character if present if (stackTrace != null) { size += stackTrace.toString().length * 2; } return size; } } /// Custom log output that stores logs in memory with a circular buffer class MemoryLogOutput extends LogOutput { static const int maxLogSizeBytes = 5 * 1024 * 1024; // 5 MB static final List _logs = []; static int _currentSize = 0; /// Get all stored logs (newest first) static List getLogs() => List.unmodifiable(_logs.reversed); /// Clear all stored logs static void clearLogs() { _logs.clear(); _currentSize = 0; } /// Get current log buffer size in bytes static int getCurrentSize() => _currentSize; /// Get current log buffer size in MB static double getCurrentSizeMB() => _currentSize / (1024 * 1024); @override void output(OutputEvent event) { // Extract relevant information from the log event for (var line in event.lines) { final logEntry = LogEntry(timestamp: DateTime.now(), level: event.level, message: _redactSensitiveData(line)); _logs.add(logEntry); _currentSize += logEntry.estimatedSize; // Maintain buffer size limit (remove oldest entries) while (_currentSize > maxLogSizeBytes && _logs.isNotEmpty) { final removed = _logs.removeAt(0); _currentSize -= removed.estimatedSize; } } } } /// Custom log printer that also stores error and stack trace information class MemoryAwareLogPrinter extends LogPrinter { final LogPrinter _wrappedPrinter; MemoryAwareLogPrinter(this._wrappedPrinter); @override List log(LogEvent event) { // Store the log with error and stack trace if available final message = _redactSensitiveData(event.message.toString()); final error = event.error != null ? _redactSensitiveData(event.error.toString()) : null; final logEntry = LogEntry( timestamp: DateTime.now(), level: event.level, message: message, error: error, stackTrace: event.stackTrace, ); MemoryLogOutput._logs.add(logEntry); MemoryLogOutput._currentSize += logEntry.estimatedSize; // Maintain buffer size limit (remove oldest entries) while (MemoryLogOutput._currentSize > MemoryLogOutput.maxLogSizeBytes && MemoryLogOutput._logs.isNotEmpty) { final removed = MemoryLogOutput._logs.removeAt(0); MemoryLogOutput._currentSize -= removed.estimatedSize; } // Delegate to wrapped printer for console output return _wrappedPrinter.log(event); } } /// Custom production filter that respects our level setting even in release mode class ProductionFilter extends LogFilter { Level _currentLevel = Level.debug; void setLevel(Level level) { _currentLevel = level; } @override bool shouldLog(LogEvent event) { return event.level.value >= _currentLevel.value; } } /// Global filter instance final _productionFilter = ProductionFilter(); /// Centralized logger instance for the application. /// /// Usage: /// ```dart /// import 'package:plezy/utils/app_logger.dart'; /// /// appLogger.d('Debug message'); /// appLogger.i('Info message'); /// appLogger.w('Warning message'); /// appLogger.e('Error message', error: e, stackTrace: stackTrace); /// ``` Logger appLogger = Logger( printer: MemoryAwareLogPrinter(SimplePrinter()), filter: _productionFilter, level: Level.debug, ); /// Update the logger's level dynamically based on debug setting /// Recreates the logger instance to ensure it works in release mode void setLoggerLevel(bool debugEnabled) { final newLevel = debugEnabled ? Level.debug : Level.info; // Update the filter level _productionFilter.setLevel(newLevel); // Recreate the logger instance with the new level // This ensures it works in release mode where Logger.level might be optimized away appLogger = Logger(printer: MemoryAwareLogPrinter(SimplePrinter()), filter: _productionFilter, level: newLevel); // Also set the static level for consistency Logger.level = newLevel; }