Improved Logging

qlline_design_update
Faiz Hashmi 2 months ago
parent d9f58bd09a
commit 781e52aae0

@ -2,7 +2,6 @@
// import 'package:flutter/material.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/services.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:get_it/get_it.dart';
import 'package:hmg_qline/api/api_client.dart';
@ -13,6 +12,7 @@ import 'package:hmg_qline/services/cache_service.dart';
import 'package:hmg_qline/services/connectivity_service.dart';
import 'package:hmg_qline/services/logger_service.dart';
import 'package:hmg_qline/services/text_to_speech_service.dart';
import 'package:hmg_qline/services/crash_handler_service.dart';
import 'package:hmg_qline/utilities/native_method_handler.dart';
import 'package:hmg_qline/view_models/queuing_view_model.dart';
import 'package:hmg_qline/view_models/screen_config_view_model.dart';
@ -35,6 +35,9 @@ class AppDependencies {
//api client
getIt.registerSingleton<LoggerService>(LoggerServiceImp(logger: logger));
// register crash handler after logger so it can resolve logger
getIt.registerSingleton<CrashHandlerService>(CrashHandlerServiceImp(loggerService: getIt.get<LoggerService>()));
getIt.registerSingleton<ApiClient>(ApiClientImp(loggerService: getIt.get<LoggerService>()));
//repos

@ -169,6 +169,8 @@ class AppConstants {
static int thresholdForListUI = 5;
static double currentBuildVersion = 9.1;
static double clearLogsHoursThreshold = 48;
// Maximum log file size in bytes before rotation/clearing. Default 2 MB.
static int maxLogFileSizeBytes = 2 * 1024 * 1024;
}
class ApiConstants {

@ -1,4 +1,5 @@
import 'dart:developer';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -10,16 +11,35 @@ import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/view_models/queuing_view_model.dart';
import 'package:hmg_qline/views/view_helpers/size_config.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:hmg_qline/services/crash_handler_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
await AppDependencies.addDependencies();
await WakelockPlus.enable();
} catch (e) {
log('Initialization error: $e');
}
runApp(const MyApp());
void main() {
// Run all initialization inside the same Zone used by runApp to avoid zone mismatch warnings.
runZonedGuarded(() async {
WidgetsFlutterBinding.ensureInitialized();
try {
await AppDependencies.addDependencies();
// initialize crash handler after DI setup
if (getIt.isRegistered<CrashHandlerService>()) {
getIt.get<CrashHandlerService>().setupGlobalErrorHandlers();
}
await WakelockPlus.enable();
} catch (e) {
log('Initialization error: $e');
}
runApp(const MyApp());
}, (error, stack) async {
try {
if (getIt.isRegistered<CrashHandlerService>()) {
await getIt.get<CrashHandlerService>().handleCrash(error: error, stackTrace: stack, context: 'runZonedGuarded');
} else {
log('Uncaught error before DI initialized: $error');
}
} catch (e) {
log('Error in runZonedGuarded handler: $e');
}
});
}
class MyApp extends StatelessWidget {

@ -0,0 +1,58 @@
// filepath: /Volumes/Data/Projects/Flutter/HMG_QLine/lib/services/crash_handler_service.dart
import 'dart:async';
import 'dart:developer';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:hmg_qline/services/logger_service.dart';
// Abstract interface for crash handling (matches style of ConnectivityService)
abstract class CrashHandlerService {
/// Install global error handlers for Flutter and platform errors.
void setupGlobalErrorHandlers();
/// Central crash handler for logging/reporting crashes.
Future<void> handleCrash({required Object error, StackTrace? stackTrace, String? context});
}
class CrashHandlerServiceImp implements CrashHandlerService {
final LoggerService loggerService;
CrashHandlerServiceImp({required this.loggerService});
/// Sets up global error handlers for Flutter framework errors and platform errors.
@override
void setupGlobalErrorHandlers() {
// Flutter framework errors
FlutterError.onError = (FlutterErrorDetails details) async {
try {
final error = details.exception;
final stack = details.stack;
await handleCrash(error: error, stackTrace: stack, context: 'FlutterError.onError');
} catch (e, st) {
// include stack trace in the log to avoid unused variable warning
log('Error while handling FlutterError.onError: $e\nStack: $st');
loggerService.logError('Error while handling FlutterError.onError: $e\nStack: $st');
}
};
// PlatformDispatcher (engine) errors
PlatformDispatcher.instance.onError = (Object error, StackTrace stack) {
handleCrash(error: error, stackTrace: stack, context: 'PlatformDispatcher.onError');
return true; // we handled it
};
// note: Zone errors are handled by runZonedGuarded where runApp is invoked
}
/// Central crash handler. Logs crash info and can be extended to report to remote services.
@override
Future<void> handleCrash({required Object error, StackTrace? stackTrace, String? context}) async {
try {
await loggerService.logCrash(error: error, stackTrace: stackTrace, context: context);
} catch (e) {
// ensure we don't throw while handling crashes
log('CrashHandlerService failed to log crash: $e');
loggerService.logError('CrashHandlerService failed to log crash: $e');
}
}
}

@ -14,6 +14,8 @@ abstract class LoggerService {
void logError(String message);
void logInfo(String message);
Future<void> logCrash({required Object error, StackTrace? stackTrace, String? context, String source = "CRASH"});
}
class LoggerServiceImp implements LoggerService {
@ -21,6 +23,39 @@ class LoggerServiceImp implements LoggerService {
LoggerServiceImp({required this.logger});
Future<void> _ensureLogFileClearedIfNeeded({required File file, required DateTime now, required DateTime? lastLogsClearedAt, required String source, required String typeName}) async {
try {
CacheService cacheService = getIt.get<CacheService>();
// Clear by time threshold
if (lastLogsClearedAt != null && now.difference(lastLogsClearedAt).inHours >= AppConstants.clearLogsHoursThreshold) {
final clearingMessage = "[${DateFormat('yyyy-MM-dd hh:mm:ss a').format(now)}] [SOURCE: $source] $typeName: LOGS HAVE BEEN CLEARED\n";
await file.writeAsString(clearingMessage, mode: FileMode.write);
// persist
final updated = DateTime.now();
getIt.get<ScreenConfigViewModel>().lastTimeLogsCleared = updated;
await cacheService.setLastTimeLogsCleared(lastTimeCleared: updated.millisecondsSinceEpoch);
return;
}
// Clear by size threshold
if (await file.exists()) {
final length = await file.length();
if (length >= AppConstants.maxLogFileSizeBytes) {
final clearingMessage = "[${DateFormat('yyyy-MM-dd hh:mm:ss a').format(now)}] [SOURCE: $source] $typeName: LOGS ROTATED DUE_TO_SIZE\n";
// Overwrite file with clearing message
await file.writeAsString(clearingMessage, mode: FileMode.write);
final updated = DateTime.now();
getIt.get<ScreenConfigViewModel>().lastTimeLogsCleared = updated;
await cacheService.setLastTimeLogsCleared(lastTimeCleared: updated.millisecondsSinceEpoch);
return;
}
}
} catch (e) {
logger.e('Failed to check/clear log file: $e');
}
}
@override
Future<void> logToFile({required String message, required LogTypeEnum type, required String source}) async {
try {
@ -46,23 +81,46 @@ class LoggerServiceImp implements LoggerService {
final file = File('${logDir.path}/${screenConfigViewModel.currentScreenIP}_$logFileName');
CacheService cacheService = getIt.get<CacheService>();
final lastLogsClearedAt = screenConfigViewModel.lastTimeLogsCleared;
final now = DateTime.now();
await _ensureLogFileClearedIfNeeded(file: file, now: now, lastLogsClearedAt: screenConfigViewModel.lastTimeLogsCleared, source: source, typeName: type.name.toUpperCase());
if (lastLogsClearedAt != null && now.difference(lastLogsClearedAt).inHours >= AppConstants.clearLogsHoursThreshold) {
final clearingMessage = "[$timestamp] [SOURCE: $source] ${type.name.toUpperCase()}: LOGS HAVE BEEN CLEARED\n";
await file.writeAsString(clearingMessage);
screenConfigViewModel.lastTimeLogsCleared = now;
cacheService.setLastTimeLogsCleared(lastTimeCleared: now.millisecondsSinceEpoch);
}
await file.writeAsString("$formattedMessage\n", mode: FileMode.append);
} catch (e) {
logger.e('Logging failed: $e');
}
}
@override
Future<void> logCrash({required Object error, StackTrace? stackTrace, String? context, String source = "CRASH"}) async {
try {
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
final timestamp = DateFormat('yyyy-MM-dd hh:mm:ss a').format(DateTime.now());
final sep = '==================== CRASH ====================';
final contextStr = context ?? 'No context provided';
final errorMsg = error.toString();
final stack = stackTrace?.toString() ?? 'No stack trace available';
final crashMessage =
'$sep\n[$timestamp]\nCONTEXT: $contextStr\nERROR: $errorMsg\nSTACKTRACE:\n$stack\n$sep\n';
final baseDir = await getExternalStorageDirectory();
final logDir = Directory('${baseDir!.path}/logs');
if (!await logDir.exists()) {
await logDir.create(recursive: true);
}
final file = File('${logDir.path}/${screenConfigViewModel.currentScreenIP}_${AppStrings.errorLogsFileName}');
final now = DateTime.now();
await _ensureLogFileClearedIfNeeded(file: file, now: now, lastLogsClearedAt: screenConfigViewModel.lastTimeLogsCleared, source: source, typeName: 'ERROR');
await file.writeAsString(crashMessage, mode: FileMode.append);
logger.e('Crash logged: $errorMsg');
} catch (e) {
logger.e('Crash logging failed: $e');
}
}
@override
void logError(String message) {
logger.e(message);

@ -11,6 +11,7 @@ import 'package:hmg_qline/services/audio_service.dart';
import 'package:hmg_qline/services/cache_service.dart';
import 'package:hmg_qline/services/logger_service.dart';
import 'package:hmg_qline/services/text_to_speech_service.dart';
import 'package:hmg_qline/services/crash_handler_service.dart';
import 'package:hmg_qline/utilities/enums.dart';
import 'package:hmg_qline/view_models/screen_config_view_model.dart';
@ -32,21 +33,30 @@ class QueuingViewModel extends ChangeNotifier {
});
Future<void> initializeQueueingVM() async {
await startHubConnection();
initializeAudioPlayer();
initializeTextToSpeech();
try {
await startHubConnection();
initializeAudioPlayer();
initializeTextToSpeech();
} catch (e, st) {
await getIt.get<CrashHandlerService>().handleCrash(error: e, stackTrace: st, context: 'initializeQueueingVM -> queuing_view_model.dart');
}
}
Future<bool?> startHubConnection() async {
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
return await signalrRepo.startHubConnection(
deviceIp: screenConfigViewModel.currentScreenIP,
onHubTicketCall: onHubTicketCall,
onHubConfigCall: onHubConfigCall,
onHubReconnected: onHubReconnected,
onHubDisconnected: onHubDisconnected,
onHubConnecting: onHubConnecting,
);
try {
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
return await signalrRepo.startHubConnection(
deviceIp: screenConfigViewModel.currentScreenIP,
onHubTicketCall: onHubTicketCall,
onHubConfigCall: onHubConfigCall,
onHubReconnected: onHubReconnected,
onHubDisconnected: onHubDisconnected,
onHubConnecting: onHubConnecting,
);
} catch (e, st) {
await getIt.get<CrashHandlerService>().handleCrash(error: e, stackTrace: st, context: 'startHubConnection -> queuing_view_model.dart');
return false;
}
}
Future<void> stopHubConnection() async {
@ -80,14 +90,18 @@ class QueuingViewModel extends ChangeNotifier {
}
Future<void> onHubTicketCall(List<Object?>? response) async {
loggerService.logToFile(message: response.toString(), source: "onHubTicketCall -> queueing_view_model.dart ", type: LogTypeEnum.data);
try {
loggerService.logToFile(message: response.toString(), source: "onHubTicketCall -> queueing_view_model.dart ", type: LogTypeEnum.data);
log("onHubTicketCall: $response");
log("onHubTicketCall: $response");
log("isCallingInProgress: $isCallingInProgress");
if (response != null && response.isNotEmpty) {
TicketDetailsModel ticketDetailsModel = TicketDetailsModel.fromJson(response.first as Map<String, dynamic>);
addNewTicket(ticketDetailsModel);
log("isCallingInProgress: $isCallingInProgress");
if (response != null && response.isNotEmpty) {
TicketDetailsModel ticketDetailsModel = TicketDetailsModel.fromJson(response.first as Map<String, dynamic>);
addNewTicket(ticketDetailsModel);
}
} catch (e, st) {
await getIt.get<CrashHandlerService>().handleCrash(error: e, stackTrace: st, context: 'onHubTicketCall -> queuing_view_model.dart');
}
}
@ -103,7 +117,6 @@ class QueuingViewModel extends ChangeNotifier {
Future<void> onHubDisconnected(var response) async {
log("onHubDisconnected: $response");
loggerService.logToFile(message: response.toString(), source: "onHubDisconnected -> queueing_view_model.dart ", type: LogTypeEnum.connectivity);
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
screenConfigViewModel.updateIsHubConnected(false);
screenConfigViewModel.notifyListeners();
@ -209,31 +222,27 @@ class QueuingViewModel extends ChangeNotifier {
isCallingInProgress = true;
await audioService.playTone(path: AppAssets.callTone, isMute: !(globalConfigurationsModel.isToneReq));
}
}
// Future<void> callTicketOnScreen({required TicketData? ticketData}) async {
// if (ticketData == null) return;
// ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
// GlobalConfigurationsModel globalConfigurationsModel = screenConfigViewModel.globalConfigurationsModel;
// if (globalConfigurationsModel.qTypeEnum == QTypeEnum.appointment) {
// screenConfigViewModel.acknowledgeTicketForAppointmentOnly(
// ticketQueueID: ticketData.id ?? 0,
// ipAddress: screenConfigViewModel.currentScreenIP,
// callTypeEnum: ticketData.callTypeEnum,
// );
// } else {
// screenConfigViewModel.acknowledgeTicket(ticketQueueID: ticketData.id ?? 0);
// }
// if (globalConfigurationsModel.isToneReq) {
// isCallingInProgress = true;
// await audioService.playTone(path: AppAssets.callTone, isMute: !(globalConfigurationsModel.isToneReq));
// } else if (globalConfigurationsModel.isVoiceReq) {
// isCallingInProgress = true;
// await textToSpeechService.speechText(globalConfigurationsModel: globalConfigurationsModel, ticket: currentTickets.first, isMute: !(globalConfigurationsModel.isVoiceReq));
// } else {
// waitAndCallNextTicketIfAvailable();
// }
// }
// jQ
//ucq
List<List<int>> memoryHog = [];
void triggerOOM() {
while (true) {
// Repeatedly add large chunks (e.g., 10 million integers) to a list
// This prevents the Garbage Collector (GC) from reclaiming the memory
memoryHog.add(List.filled(10000000, 9));
}
}
/// Throws an exception from an async context (await) to test runZonedGuarded handling.
Future<void> triggerAsyncCrash() async {
await Future.delayed(const Duration(milliseconds: 50));
throw Exception('Test asynchronous crash from QueuingViewModel.triggerAsyncCrash');
}
/// Creates an unawaited Future that throws to simulate an unhandled async error.
void triggerUnhandledFutureCrash() {
Future.delayed(const Duration(milliseconds: 50), () {
throw Exception('Test unhandled future crash from QueuingViewModel.triggerUnhandledFutureCrash');
});
}
}

@ -6,6 +6,7 @@ import 'package:hmg_qline/config/dependency_injection.dart';
import 'package:hmg_qline/models/global_config_model.dart';
import 'package:hmg_qline/utilities/enums.dart';
import 'package:hmg_qline/utilities/native_method_handler.dart';
import 'package:hmg_qline/view_models/queuing_view_model.dart';
import 'package:hmg_qline/view_models/screen_config_view_model.dart';
import 'package:hmg_qline/views/common_widgets/app_general_widgets.dart';
import 'package:provider/provider.dart';
@ -33,8 +34,8 @@ class AppHeader extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () async {
await getIt.get<NativeMethodChannelService>().smartRestart();
onTap: () {
// getIt.get<QueuingViewModel>().triggerOOM();
},
child: engArabicTextWithSeparatorWidget(
englishText: globalConfigurationsModel.currentServeTextEng ?? "",

@ -1,14 +1,6 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
animated_flip_counter:
dependency: "direct main"
description:
name: animated_flip_counter
sha256: "73f852d84c461c3e4c1ddf320bee334dde8dba89441922ab11a8013be0b2fad1"
url: "https://pub.dev"
source: hosted
version: "0.3.4"
args:
dependency: transitive
description:

@ -56,7 +56,7 @@ dependencies:
flutter_foreground_task: ^9.1.0
# restart_app: ^1.3.2
zo_animated_border: ^1.0.1
animated_flip_counter: ^0.3.4
# animated_flip_counter: ^0.3.4
# smooth_corner: ^1.1.1

Loading…
Cancel
Save