-
Notifications
You must be signed in to change notification settings - Fork 15
Logging screen implementation #163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,6 +73,14 @@ class I18n { | |
| "try_again_later": "An error occurred. Please try again later.", | ||
| "was_empty": "was empty", | ||
| "words_of_the_buddha": "Daily Words of the Buddha", | ||
| "logs": "Logs", | ||
| "logs_empty": "No logs available", | ||
| "refresh_logs": "Refresh logs", | ||
| "clear_logs": "Clear logs", | ||
| "share_logs": "Share logs", | ||
| "clear_logs_confirm": "Are you sure you want to clear all logs?", | ||
| "cancel": "Cancel", | ||
| "clear": "Clear", | ||
| }, | ||
|
|
||
| Language.fra: { | ||
|
|
@@ -115,6 +123,14 @@ class I18n { | |
| "try_again_later": "Une erreur s'est produite. Veuillez réessayer plus tard.", | ||
| "was_empty": "était vide", | ||
| "words_of_the_buddha": "Paroles quotidiennes de Bouddha", | ||
| "logs": "Journaux", | ||
| "logs_empty": "Aucun journal disponible", | ||
| "refresh_logs": "Actualiser les journaux", | ||
| "clear_logs": "Effacer les journaux", | ||
| "share_logs": "Partager les journaux", | ||
| "clear_logs_confirm": "Voulez-vous vraiment effacer tous les journaux ?", | ||
| "cancel": "Annuler", | ||
| "clear": "Effacer", | ||
| }, | ||
|
|
||
| Language.ita: { | ||
|
|
@@ -157,6 +173,14 @@ class I18n { | |
| "try_again_later": "Si è verificato un errore. Riprova più tardi.", | ||
| "was_empty": "era vuoto", | ||
| "words_of_the_buddha": "Parole quotidiane del Buddha", | ||
| "logs": "Registri", | ||
| "logs_empty": "Nessun registro disponibile", | ||
| "refresh_logs": "Aggiorna registri", | ||
| "clear_logs": "Cancella registri", | ||
| "share_logs": "Condividi registri", | ||
| "clear_logs_confirm": "Sei sicuro di voler cancellare tutti i registri?", | ||
| "cancel": "Annulla", | ||
| "clear": "Cancella", | ||
| }, | ||
|
|
||
| Language.lit: { | ||
|
|
@@ -199,6 +223,14 @@ class I18n { | |
| "try_again_later": "Įvyko klaida. Bandykite vėliau.", | ||
| "was_empty": "buvo tuščias", | ||
| "words_of_the_buddha": "Budos žodžiai dienai", | ||
| "logs": "Žurnalai", | ||
| "logs_empty": "Nėra žurnalų", | ||
| "refresh_logs": "Atnaujinti žurnalus", | ||
| "clear_logs": "Išvalyti žurnalus", | ||
| "share_logs": "Dalintis žurnalus", | ||
| "clear_logs_confirm": "Ar tikrai norite išvalyti visus žurnalus?", | ||
| "cancel": "Atšaukti", | ||
| "clear": "Išvalyti", | ||
| }, | ||
|
|
||
| Language.por: { | ||
|
|
@@ -240,7 +272,15 @@ class I18n { | |
| "translation": "Tradução", | ||
| "try_again_later": "Ocorreu um erro. Tente novamente mais tarde.", | ||
| "was_empty": "estava vazio", | ||
| "words_of_the_buddha": "Palavras diárias do Buda" | ||
| "words_of_the_buddha": "Palavras diárias do Buda", | ||
| "logs": "Registros", | ||
| "logs_empty": "Nenhum registro disponível", | ||
| "refresh_logs": "Atualizar registros", | ||
| "clear_logs": "Limpar registros", | ||
| "share_logs": "Compartilhar registros", | ||
| "clear_logs_confirm": "Tem certeza de que deseja limpar todos os registros?", | ||
| "cancel": "Cancelar", | ||
| "clear": "Limpar", | ||
| }, | ||
|
|
||
| Language.spa: { | ||
|
|
@@ -282,7 +322,15 @@ class I18n { | |
| "translation": "Traducción", | ||
| "try_again_later": "Ha ocurrido un error. Por favor, inténtelo de nuevo más tarde.", | ||
| "was_empty": "estaba vacío", | ||
| "words_of_the_buddha": "Palabras de Buda diarias" | ||
| "words_of_the_buddha": "Palabras de Buda diarias", | ||
| "logs": "Registros", | ||
| "logs_empty": "No hay registros disponibles", | ||
| "refresh_logs": "Actualizar registros", | ||
| "clear_logs": "Borrar registros", | ||
| "share_logs": "Compartir registros", | ||
| "clear_logs_confirm": "¿Estás seguro de que quieres borrar todos los registros?", | ||
| "cancel": "Cancelar", | ||
| "clear": "Borrar", | ||
| }, | ||
|
|
||
| Language.srp: { | ||
|
|
@@ -325,6 +373,14 @@ class I18n { | |
| "try_again_later": "Одбијање. Покушајте поново касније.", | ||
| "was_empty": "било је празно", | ||
| "words_of_the_buddha": "Дневне речи Буде", | ||
| "logs": "Дневници", | ||
| "logs_empty": "Нема доступних дневника", | ||
| "refresh_logs": "Освежи дневнике", | ||
| "clear_logs": "Обриши дневнике", | ||
| "share_logs": "Дели дневника", | ||
| "clear_logs_confirm": "Да ли сте сигурни да желите да обришете све дневнике?", | ||
| "cancel": "Откажи", | ||
| "clear": "Обриши", | ||
| }, | ||
|
|
||
| Language.zho_hant: { | ||
|
|
@@ -351,7 +407,7 @@ class I18n { | |
| "light_theme": "明亮主題", | ||
| "nothing_bookmarked": "您還沒有書籤", | ||
| "only_english": "目前僅提供英文作為替代語言", | ||
| "pali_word": "每日一个巴利语单词", | ||
| "pali_word": "每日一个巴利語單詞", | ||
| "pali_word_of_the_day": "Pāli詞語之日", | ||
| "security_and_privacy": "安全與隱私", | ||
| "settings": "設定", | ||
|
|
@@ -366,7 +422,15 @@ class I18n { | |
| "translation": "翻譯", | ||
| "try_again_later": "發生錯誤,請稍後再試。", | ||
| "was_empty": "沒有內容", | ||
| "words_of_the_buddha": "佛陀每日法语" | ||
| "words_of_the_buddha": "佛陀每日法語", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same question as with Pali Word: why has this Chinese translation changed in this PR? This PR should only make changes related to logging. |
||
| "logs": "日誌", | ||
| "logs_empty": "沒有可用的日誌", | ||
| "refresh_logs": "刷新日誌", | ||
| "clear_logs": "清除日誌", | ||
| "share_logs": "分享日誌", | ||
| "clear_logs_confirm": "您確定要清除所有日誌嗎?", | ||
| "cancel": "取消", | ||
| "clear": "清除", | ||
| }, | ||
|
|
||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,17 @@ | ||
| import 'dart:developer'; | ||
| import 'package:patta/app/log_manager.dart'; | ||
|
|
||
| /// Logs a message to both the console and the LogManager | ||
| /// | ||
| /// This function maintains backward compatibility with existing code | ||
| /// while integrating with the new logging system | ||
| void log2(String msg) { | ||
| // lazy local debugging: | ||
| print(msg); | ||
|
|
||
| // add to log manager | ||
| logManager.addLog(msg, "INFO"); | ||
|
|
||
| // the actual log message: | ||
| log(msg, level: 1, name: "app"); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import 'dart:convert'; | ||
|
|
||
| /// Represents a single log entry with timestamp, message, and level information | ||
| class LogEntry { | ||
| final DateTime timestamp; | ||
| final String message; | ||
| final String level; | ||
|
|
||
| LogEntry(this.message, this.level) : timestamp = DateTime.now(); | ||
|
|
||
| @override | ||
| String toString() { | ||
| return "${timestamp.toIso8601String()}: $level: $message"; | ||
| } | ||
|
|
||
| /// Estimates the memory size of this log entry in bytes | ||
| int get estimatedSize { | ||
| // Rough estimate: | ||
| // - DateTime ~= 8 bytes | ||
| // - Each character in strings ~= 2 bytes (UTF-16) | ||
| // - Object overhead ~= 16 bytes | ||
| return 8 + (message.length * 2) + (level.length * 2) + 16; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import 'dart:collection'; | ||
| import 'dart:developer' as developer; | ||
| import 'package:patta/app/log_entry.dart'; | ||
|
|
||
| /// A manager for application logs that maintains a queue of log entries | ||
| /// with constraints on both count and memory usage | ||
| class LogManager { | ||
| // Singleton pattern | ||
| static final LogManager _instance = LogManager._internal(); | ||
|
|
||
| factory LogManager() { | ||
| return _instance; | ||
| } | ||
|
|
||
| LogManager._internal(); | ||
|
|
||
| // Queue with size and memory constraints | ||
| final Queue<LogEntry> _logs = Queue<LogEntry>(); | ||
|
|
||
| // Constraints | ||
| static const int _maxLogCount = 1000; | ||
| static const int _maxMemoryBytes = 10 * 1024 * 1024; // 10 MB | ||
|
|
||
| // Track current memory usage | ||
| int _currentMemoryUsage = 0; | ||
|
|
||
| /// Adds a new log entry to the queue, respecting size and memory constraints | ||
| void addLog(String message, String level) { | ||
| final entry = LogEntry(message, level); | ||
| final entrySize = entry.estimatedSize; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's not do this. The following Feel free to push back if you think this is essential for low-power devices. |
||
|
|
||
| // Remove oldest entries if constraints would be exceeded | ||
| while ((_logs.length >= _maxLogCount) || | ||
| (_currentMemoryUsage + entrySize > _maxMemoryBytes && _logs.isNotEmpty)) { | ||
| if (_logs.isNotEmpty) { | ||
| final oldestEntry = _logs.removeFirst(); | ||
| _currentMemoryUsage -= oldestEntry.estimatedSize; | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // Add new log entry | ||
| _logs.add(entry); | ||
| _currentMemoryUsage += entrySize; | ||
|
|
||
| // Also log to developer console | ||
| developer.log(message, name: "app"); | ||
| } | ||
|
|
||
| /// Returns all logs in reverse chronological order (newest first) | ||
| List<LogEntry> getLogs() { | ||
| return _logs.toList().reversed.toList(); | ||
| } | ||
|
|
||
| /// Clears all logs | ||
| void clearLogs() { | ||
| _logs.clear(); | ||
| _currentMemoryUsage = 0; | ||
| } | ||
|
|
||
| /// Returns the current memory usage of the log queue in bytes | ||
| int get memoryUsage => _currentMemoryUsage; | ||
|
|
||
| /// Returns the current number of log entries | ||
| int get logCount => _logs.length; | ||
| } | ||
|
|
||
| // Global instance | ||
| final logManager = LogManager(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import 'dart:async'; | ||
| import 'dart:developer' as developer; | ||
| import 'package:logging/logging.dart'; | ||
| import 'package:path_provider/path_provider.dart'; | ||
| import 'dart:io'; | ||
|
|
||
| /// A comprehensive logging system for the Pariyatti app that provides: | ||
| /// - In-memory circular buffer for recent logs | ||
| /// - File-based persistent logging with rotation | ||
| /// - Console logging for debugging | ||
| /// - Multiple severity levels (debug, info, warning, error) | ||
| class AppLogger { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's please name the file and class the same: |
||
| // Core logging components | ||
| static final Logger _logger = Logger('PariyattiApp'); | ||
| static bool _initialized = false; | ||
| static File? _logFile; | ||
|
|
||
| // In-memory circular buffer | ||
| static final List<String> _memoryLogs = []; | ||
| static const int _maxMemoryLogs = 1000; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's important that this 1000 log entry limit is the same as the other 1000 from |
||
|
|
||
| /// Initializes the logging system. Must be called before any logging operations. | ||
| static Future<void> init() async { | ||
| if (_initialized) return; | ||
|
|
||
| Logger.root.level = Level.ALL; | ||
| Logger.root.onRecord.listen((record) { | ||
| String logMessage = '${record.time}: ${record.level.name}: ${record.message}'; | ||
| if (record.error != null) { | ||
| logMessage += '\nError: ${record.error}'; | ||
| } | ||
| if (record.stackTrace != null) { | ||
| logMessage += '\nStack Trace:\n${record.stackTrace}'; | ||
| } | ||
|
|
||
| // Store in circular buffer | ||
| _memoryLogs.add(logMessage); | ||
| if (_memoryLogs.length > _maxMemoryLogs) { | ||
| _memoryLogs.removeAt(0); | ||
| } | ||
|
|
||
| // Write to persistent storage | ||
| _writeToFile(logMessage); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the in-memory log is the only one used in the UI (as far as I can tell), why do we bother writing the logs to disk at all? Preferably, we would only use the disk-based log for all our purposes, since I don't understand the value of the in-memory logs, and the on-disk logs could be shared in the event of a crash. At the moment, if the app crashes and then the user tries to share the logs, they are sharing the empty in-memory log, not the on-disk log which would contain the events before the crash. Let me know if this question makes sense? |
||
|
|
||
| // Output to debug console | ||
| developer.log( | ||
| record.message, | ||
| time: record.time, | ||
| level: record.level.value, | ||
| name: record.loggerName, | ||
| error: record.error, | ||
| stackTrace: record.stackTrace, | ||
| ); | ||
| }); | ||
|
|
||
| await _initLogFile(); | ||
| _initialized = true; | ||
| } | ||
|
|
||
| /// Sets up the log file with rotation support | ||
| static Future<void> _initLogFile() async { | ||
| final directory = await getApplicationDocumentsDirectory(); | ||
| _logFile = File('${directory.path}/pariyatti.log'); | ||
|
|
||
| if (!await _logFile!.exists()) { | ||
| await _logFile!.create(); | ||
| } | ||
|
|
||
| // Implement log rotation | ||
| final fileSize = await _logFile!.length(); | ||
| if (fileSize > 5 * 1024 * 1024) { | ||
| final backupFile = File('${directory.path}/pariyatti.log.bak'); | ||
| if (await backupFile.exists()) { | ||
| await backupFile.delete(); | ||
| } | ||
| await _logFile!.rename('${directory.path}/pariyatti.log.bak'); | ||
| _logFile = File('${directory.path}/pariyatti.log'); | ||
| await _logFile!.create(); | ||
| } | ||
| } | ||
|
|
||
| /// Writes a log message to the persistent log file | ||
| static Future<void> _writeToFile(String message) async { | ||
| if (_logFile != null) { | ||
| await _logFile!.writeAsString('$message\n', mode: FileMode.append); | ||
| } | ||
| } | ||
|
|
||
| // Public logging methods | ||
| static void info(String message) { | ||
| _logger.info(message); | ||
| } | ||
|
|
||
| static void warning(String message) { | ||
| _logger.warning(message); | ||
| } | ||
|
|
||
| static void error(String message, [Object? error, StackTrace? stackTrace]) { | ||
| _logger.severe(message, error, stackTrace); | ||
| } | ||
|
|
||
| static void debug(String message) { | ||
| _logger.fine(message); | ||
| } | ||
|
|
||
| /// Returns all logs in reverse chronological order | ||
| static List<String> getLogs() { | ||
| return List.from(_memoryLogs.reversed); | ||
| } | ||
|
|
||
| /// Clears both in-memory and file-based logs | ||
| static Future<void> clearLogs() async { | ||
| _memoryLogs.clear(); | ||
| if (_logFile != null && await _logFile!.exists()) { | ||
| await _logFile!.delete(); | ||
| await _initLogFile(); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why has this line changed? This PR shouldn't be changing translations for Pali Word, since that is unrelated.