Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 68 additions & 4 deletions lib/app/I18n.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand All @@ -351,7 +407,7 @@ class I18n {
"light_theme": "明亮主題",
"nothing_bookmarked": "您還沒有書籤",
"only_english": "目前僅提供英文作為替代語言",
"pali_word": "每日一个巴利语单词",
"pali_word": "每日一个巴利語單詞",
Copy link
Copy Markdown
Member

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.

"pali_word_of_the_day": "Pāli詞語之日",
"security_and_privacy": "安全與隱私",
"settings": "設定",
Expand All @@ -366,7 +422,15 @@ class I18n {
"translation": "翻譯",
"try_again_later": "發生錯誤,請稍後再試。",
"was_empty": "沒有內容",
"words_of_the_buddha": "佛陀每日法语"
"words_of_the_buddha": "佛陀每日法語",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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": "清除",
},

};
Expand Down
9 changes: 9 additions & 0 deletions lib/app/log.dart
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");
}
24 changes: 24 additions & 0 deletions lib/app/log_entry.dart
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;
}
}
70 changes: 70 additions & 0 deletions lib/app/log_manager.dart
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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not do this. The following while loop is kind of convoluted and I'm not sure the constraint on memory usage is required. I think that having a max count of 1000 entries will be fine. If that size goes over 10MB by a small amount it's not a big deal. I think trying to constrain the memory usage by the estimated size of the entries makes the code too hard to read.

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();
119 changes: 119 additions & 0 deletions lib/app/logger.dart
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 {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's please name the file and class the same: app_logger.dart

// 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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 LogManager, that number should be defined in a single place so it's not repeated.


/// 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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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();
}
}
}
9 changes: 8 additions & 1 deletion lib/main_common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import 'package:patta/app/I18n.dart';
import 'package:patta/ui/screens/HomeScreen.dart';
import 'package:patta/app/app_themes.dart';
import 'package:provider/provider.dart';
import 'package:patta/app/log_manager.dart';
import 'package:patta/model/Language.dart';

import 'app/preferences.dart';

Expand All @@ -27,7 +29,12 @@ Future<void> mainCommon(Environment environment) async {
await Preferences.init();
await I18n.init();
await FeedPreferences.init();


// Add some initial log entries
logManager.addLog('Application started', 'INFO');
logManager.addLog('Environment: ${environment.kosaBaseUrl}', 'DEBUG');
logManager.addLog('Language: ${Preferences.getLanguage(Language.SETTINGS_KEY)}', 'DEBUG');

initFirstRun();
runApp(PariyattiApp(environment, configReader));
}
Expand Down
Loading