Logging

https://bsky.app/profile/sander-roest.bsky.social
Search for a command to run...

https://bsky.app/profile/sander-roest.bsky.social
No comments yet. Be the first to comment.
In this series I will tell my adventures with Flutter and Line Of Business Apps
Some pitfalls with versionCode and keystores
Running flutter upgrade is not enough...

A necessity or a rip-off?

Both are used by humans, but they are not the same.

Recap part 1. In the previous article about this subject, I explained the limitations that I have ran into, with the default onDidRemovePage implementation. In this article I will show how I’ve overcome or better said, circumvented these limitations....
During the development of my RubigoRouter package I ran into some limitations of the Navigator object that I explain here below. The Flutter Navigator has a mechanism to inform the app about a back navigation event. Historically the onPopPage callbac...
The importance and usage of logging is for Line Of Business apps different than for consumer apps.
On the other hand, logging for Line Of Business apps is quite simple:
The async nature of Flutter does make logging a bit more complicated than it was in the synchronous world.
We want to log things as soon as the application starts, but to store the logging, we are dependant on an async filesystem/database that needs time to initialize.
To be able to log events before it can be written, I split the logging service into two parts:
The logging service is the first service that is started. This means that all other services can log messages. As long as the ‘Logging service file’ is not started, the ‘Logging service’ keeps the logging in memory.
As soon as the ‘Logging service file’ is started, it registers its ‘writer’ function in the Logging service. As soon as this is registered, the cached logging is purged to this ‘writer’ function. From now on, the ‘logging service’ uses this ‘writer’ function directly instead of its memory cache.
Initialization of the LoggingServiceFile and registering its writer function:
Future<void> onStart() async {
await Provider.of<VersionService>(context, listen: false).init();
await Provider.of<SoundService>(context, listen: false).init();
await Provider.of<ConfigurationBoxService>(context, listen: false).init();
await Provider.of<ConfigurationLoader>(context, listen: false).init();
await Provider.of<LoggingServiceFile>(context, listen: false).init();
Provider.of<LoggingServiceFile>(context, listen: false).connectLogWriter();
await Provider.of<DatawedgeService>(context, listen: false).init();
await Navigator.pushReplacement(context, TimeRegistrationPage.Route);
}
The implementation of LoggingService:
import 'package:enum_to_string/enum_to_string.dart';
import 'package:flutter/foundation.dart';
enum LogLevel {
debug,
information,
warning,
error,
critical,
}
class LoggingService {
LoggingService() {
_start();
}
final int _maxCacheRows = 1000;
var _cacheLog = List<Logging>();
void _start() {
log(LogLevel.information, "$runtimeType started");
}
void Function(Logging logging) _writer;
void setWriter(void Function(Logging logging) writer) {
_writer = writer;
_cacheLog.forEach((a) => _writer(a));
}
void log(LogLevel logLevel, String message, {String stackTrace = ''}) {
var logging = Logging(
id: null,
timestamp: DateTime.now().toUtc().toIso8601String(),
logLevel: logLevel,
message: message,
stacktrace: stackTrace,
);
debugPrint(logging.toString());
if (_writer == null) {
_cacheLog.add(logging);
if (_cacheLog.length > _maxCacheRows) {
_cacheLog.removeRange(0, _cacheLog.length - _maxCacheRows);
}
} else {
_writer(logging);
}
}
}
class Logging {
Logging({
this.id,
this.timestamp,
this.logLevel,
this.message,
this.stacktrace = '',
});
final int id;
final String timestamp;
final LogLevel logLevel;
final String message;
final String stacktrace;
@override
String toString() {
var result = List<String>();
result.add(
'[LOGGING] ${EnumToString.parse(logLevel)} $timestamp Message: $message');
if (stacktrace.isNotEmpty) {
result.add('[LOGGING StackTrace: $stacktrace]');
}
return result.join('\n');
}
}
The implementation of LoggingServiceFile:
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import 'package:app/services/hive/configuration_box_service.dart';
import 'package:app/services/logging_service.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart';
const String externalLogFilename = "cc600_log.txt";
const String externalBackupLogFilename = "cc600_log_1.txt";
class LoggingServiceFile {
LoggingServiceFile(this._log, this._configurationBoxService) {
_start();
}
final LoggingService _log;
final ConfigurationBoxService _configurationBoxService;
File _logFile;
String _logFilename;
File _backupLogFile;
String _backupLogFilename;
void _start() {
_log.log(LogLevel.information, "$runtimeType started");
}
Future<void> init() async {
var directory = await getExternalStorageDirectory();
_log.log(LogLevel.information, 'External path reported: ${directory.path}');
_logFilename = join(directory.path, externalLogFilename);
_logFile = File(_logFilename);
_backupLogFilename = join(directory.path, externalBackupLogFilename);
_backupLogFile = File(_backupLogFilename);
}
void connectLogWriter() {
_log.setWriter(_writer);
}
void _writer(Logging logging) {
if (!_configurationBoxService.logging) {
return;
}
try {
if (_logFile.existsSync() && _logFile.lengthSync() > 1000000) {
if (_backupLogFile.existsSync()) {
_backupLogFile.deleteSync();
}
_backupLogFile = _logFile.renameSync(_backupLogFilename);
_logFile = File(_logFilename);
}
_logFile.writeAsStringSync(
'${DateFormat('dd-MM-yyyy HH:mm:ss').format(DateTime.now())} ${logging.message}\r\n',
mode: FileMode.append,
flush: true,
);
} catch (e) {
debugPrint('error while writing to log ${e.toString()}');
}
}
}
Note that the LoggingServiceFile could be easily exchanged with a LoggingServiceDatabase. Only the signature of the writer function has to be the same.