Logging

Logging

The importance and usage of logging is for Line Of Business apps different than for consumer apps.

Consumer apps

  • Collect application crashes/warnings
  • Narrow down problems to specific manufacturers, hardware, os versions
  • Get insights on how much the app is installed/used across the globe
  • Get insights on how the app is used
  • Get insights on how the monetization of the app can be improved

Line Of Business apps

  • Collect application crashes/warnings
  • Narrow down issues with backend services
  • Narrow down issues reported by individual users
  • Enterprises do not like Line Of Business apps that leak info to Google, Apple, or other Application Monitoring or Tracking Software

On the other hand, logging for Line Of Business apps is quite simple:

  • Logging is written to local persistent storage (text file or database)
  • Add logging to strategic points in the app, so you can follow the usage of the app
  • Each log line has a timestamp, loglevel, message and stacktrace (if available)
  • When the max amount of logging is reached, delete old logging
  • When a problem is reported, download the logging with an EMM and analyze it

Async challenges

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:

  • Logging service
  • Logging service file (persistent part)

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.