Skip to content

Commit

Permalink
feat: implement logging service (#336)
Browse files Browse the repository at this point in the history
  • Loading branch information
Grodien authored Oct 30, 2024
1 parent a4ebf6c commit e23c252
Show file tree
Hide file tree
Showing 23 changed files with 567 additions and 48 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/flutter_ios_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
java-version: '17'
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.22.2'
flutter-version: '3.24.3'
- name: Prepare Flutter Build
run: |
flutter pub get
Expand Down
13 changes: 12 additions & 1 deletion das_client/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,27 @@ linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
always_use_package_imports: true
constant_identifier_names: false
library_prefixes: false
unnecessary_library_name: false
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
analyzer:
exclude:
- lib/**.g.dart
- test/**.mocks.dart
errors:
invalid_annotation_target: ignore
8 changes: 4 additions & 4 deletions das_client/integration_test/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

import 'di.dart';
import 'test/fahrbild_test.dart' as FahrbildTests;
import 'test/navigation_test.dart' as NavigationTests;
import 'test/fahrbild_test.dart' as fahrbild_tests;
import 'test/navigation_test.dart' as navigation_tests;

AppLocalizations l10n = AppLocalizationsDe();

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
Fimber.plantTree(DebugTree());

FahrbildTests.main();
NavigationTests.main();
fahrbild_tests.main();
navigation_tests.main();
}

Future<void> prepareAndStartApp(WidgetTester tester) async {
Expand Down
2 changes: 1 addition & 1 deletion das_client/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_appauth: 1ce438877bc111c5d8f42da47729909290624886
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
Expand Down
2 changes: 1 addition & 1 deletion das_client/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import UIKit
import Flutter

@UIApplicationMain
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
Expand Down
8 changes: 8 additions & 0 deletions das_client/lib/di.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:das_client/auth/azure_authenticator.dart';
import 'package:das_client/auth/token_spec_provider.dart';
import 'package:das_client/flavor.dart';
import 'package:das_client/repo/sfera_repository.dart';
import 'package:das_client/service/backend_service.dart';
import 'package:das_client/service/mqtt/mqtt_client_connector.dart';
import 'package:das_client/service/mqtt/mqtt_client_oauth_connector.dart';
import 'package:das_client/service/mqtt/mqtt_client_tms_oauth_connector.dart';
Expand Down Expand Up @@ -56,6 +57,7 @@ extension GetItX on GetIt {
registerMqttService(useTms: useTms);
registerRepositories();
registerSferaService();
registerBackendService();
await allReady();
}

Expand Down Expand Up @@ -139,4 +141,10 @@ extension GetItX on GetIt {
registerSingletonWithDependencies<SferaService>(() => SferaService(mqttService: get(), sferaRepository: get()),
dependsOn: [MqttService, SferaRepository]);
}

void registerBackendService() {
final flavor = get<Flavor>();
registerSingletonWithDependencies<BackendService>(() => BackendService(authenticator: DI.get(), baseUrl: flavor.backendUrl),
dependsOn: [Authenticator]);
}
}
17 changes: 13 additions & 4 deletions das_client/lib/flavor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,30 @@ enum Flavor {
dev(
displayName: 'Dev',
tokenExchangeUrl: 'https://sfera-mock.app.sbb.ch/customClaim/requestToken',
tmsTokenExchangeUrl: 'https://imts-token-provider-tms-vad-imtrackside-dev.apps.halon-ocp1-1-t.sbb-aws-test.net/token/exchange',
tmsTokenExchangeUrl:
'https://imts-token-provider-tms-vad-imtrackside-dev.apps.halon-ocp1-1-t.sbb-aws-test.net/token/exchange',
mqttUrl: 'wss://das-poc.messaging.solace.cloud',
tmsMqttUrl: 'wss://tms-vad-imtrackside-dev-mobile.messaging.solace.cloud',
authenticatorConfig: _authenticatorConfigMockDev,
tmsAuthenticatorConfig: _authenticatorConfigTmsDev,
backendUrl: 'https://das-backend-dev.app.sbb.ch',
mqttTopicPrefix: '',
),
inte(
displayName: 'Inte',
tokenExchangeUrl: 'https://sfera-mock.app.sbb.ch/customClaim/requestToken',
mqttUrl: 'wss://das-poc.messaging.solace.cloud',
authenticatorConfig: _authenticatorConfigInte,
backendUrl: 'https://das-backend-dev.app.sbb.ch',
mqttTopicPrefix: '',
),
prod(
displayName: 'Prod',
tokenExchangeUrl: 'https://sfera-mock.app.sbb.ch/customClaim/requestToken',
mqttUrl: 'wss://das-poc.messaging.solace.cloud',
authenticatorConfig: _authenticatorConfigProd,
backendUrl: 'https://das-backend-dev.app.sbb.ch',
mqttTopicPrefix: '',
);

const Flavor({
Expand All @@ -33,7 +40,8 @@ enum Flavor {
this.tmsMqttUrl,
required this.authenticatorConfig,
this.tmsAuthenticatorConfig,
this.mqttTopicPrefix = ''
required this.mqttTopicPrefix,
required this.backendUrl,
});

final String displayName;
Expand All @@ -44,11 +52,12 @@ enum Flavor {
final AuthenticatorConfig authenticatorConfig;
final AuthenticatorConfig? tmsAuthenticatorConfig;
final String mqttTopicPrefix;
final String backendUrl;
}


const _authenticatorConfigTmsDev = AuthenticatorConfig(
discoveryUrl: "https://login.microsoftonline.com/2cda5d11-f0ac-46b3-967d-af1b2e1bd01a/v2.0/.well-known/openid-configuration",
discoveryUrl:
"https://login.microsoftonline.com/2cda5d11-f0ac-46b3-967d-af1b2e1bd01a/v2.0/.well-known/openid-configuration",
clientId: '8af8281c-4f1d-47b5-ad77-526b1da61b2b',
redirectUrl: 'ch.sbb.das://sbbauth/redirect',
tokenSpecs: TokenSpecProvider([
Expand Down
13 changes: 13 additions & 0 deletions das_client/lib/logging/logging_component.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
library logging;

import 'package:das_client/logging/src/das_log_tree.dart';
import 'package:das_client/logging/src/log_service.dart';
import 'package:fimber/fimber.dart';

class LoggingComponent {
const LoggingComponent._();

static LogTree createDasLogTree() {
return DasLogTree(logService: LogService());
}
}
90 changes: 90 additions & 0 deletions das_client/lib/logging/src/das_log_tree.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import 'dart:io';

import 'package:das_client/logging/src/log_entry.dart';
import 'package:das_client/logging/src/log_level.dart';
import 'package:das_client/logging/src/log_service.dart';
import 'package:das_client/util/device_id_info.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:fimber/fimber.dart';
import 'package:package_info_plus/package_info_plus.dart';

class DasLogTree extends LogTree {
final LogService _logService;
late Future<void> _initialized;
final Map<String, String> metadata = {};

DasLogTree({required LogService logService}) : _logService = logService {
_initialized = _init();
}

Future<void> _init() async {
Fimber.i('Initializing DasLogTree...');
metadata['deviceId'] = await DeviceIdInfo.getDeviceId();

final info = await PackageInfo.fromPlatform();
metadata['appVersion'] = info.version;

final deviceInfoPlugin = DeviceInfoPlugin();
if (Platform.isAndroid) {
_processAndroidDeviceInfo(await deviceInfoPlugin.androidInfo);
} else if (Platform.isIOS) {
_processIosDeviceInfo(await deviceInfoPlugin.iosInfo);
}
}

void _processAndroidDeviceInfo(AndroidDeviceInfo deviceInfo) {
metadata['systemName'] = "android";
metadata['systemVersion'] = deviceInfo.version.sdkInt.toString();
metadata['model'] = deviceInfo.model;
}

void _processIosDeviceInfo(IosDeviceInfo deviceInfo) {
metadata['systemName'] = deviceInfo.systemName;
metadata['systemVersion'] = deviceInfo.systemVersion;
metadata['model'] = deviceInfo.model;
}

@override
List<String> getLevels() {
return ["I", "W", "E"];
}

@override
void log(String level, String message, {String? tag, ex, StackTrace? stacktrace}) {
logInternal(level, message, tag: tag ?? LogTree.getTag(), ex: ex, stacktrace: stacktrace);
}

void logInternal(String level, String message, {String? tag, ex, StackTrace? stacktrace}) async {
await _initialized;

final messageBuilder = StringBuffer("$tag:\t $message");

if (ex != null) {
messageBuilder.write("\n$ex");
}
if (stacktrace != null) {
final tmpStacktrace = stacktrace.toString().split('\n');
final stackTraceMessage =
tmpStacktrace.map((stackLine) => "\t$stackLine").join("\n");
messageBuilder.write("\n$stackTraceMessage");
}

_logService.save(LogEntry(messageBuilder.toString(), _getLogLevel(level), metadata));
}

LogLevel _getLogLevel(String level) {
switch (level) {
case "D":
return LogLevel.debug;
case "W":
return LogLevel.warning;
case "E":
return LogLevel.error;
case "V":
return LogLevel.trace;
case "I":
default:
return LogLevel.info;
}
}
}
28 changes: 28 additions & 0 deletions das_client/lib/logging/src/log_entry.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:das_client/logging/src/log_level.dart';

class LogEntry {
LogEntry(this.message, this.level, this.metadata)
: time = DateTime.now().millisecondsSinceEpoch / 1000,
source = "das_client";

final double time;
final String source;
final String message;
final LogLevel level;
final Map<String, dynamic> metadata;

Map<String, dynamic> toJson() => {
'time': time,
'source': source,
'message': message,
'level': level.name,
'metadata': metadata,
};

LogEntry.fromJson(Map<String, dynamic> json)
: time = json['time'],
source = json['source'],
message = json['message'],
level = LogLevel.values.firstWhere((element) => element.name == json['level']),
metadata = json['metadata'];
}
1 change: 1 addition & 0 deletions das_client/lib/logging/src/log_level.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enum LogLevel { trace, debug, info, warning, error, fatal }
85 changes: 85 additions & 0 deletions das_client/lib/logging/src/log_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'dart:convert';
import 'dart:io';

import 'package:das_client/di.dart';
import 'package:das_client/logging/src/log_entry.dart';
import 'package:das_client/service/backend_service.dart';
import 'package:fimber/fimber.dart';
import 'package:path_provider/path_provider.dart';
import 'package:synchronized/synchronized.dart';

class LogService {
static const _rolloverTimeMinutes = 1;
static const _maxFileSize = 50 * 1024;
static const _prefix = "das-log";
static const _lastSavedFileName = "$_prefix-lastSavedFile.json";
final _lock = Lock();
final _senderLock = Lock();

late final Future<void> _initialized;
late String _logPath;

DateTime _nextRolloverTimeStamp = DateTime.now().add(const Duration(minutes: _rolloverTimeMinutes));

LogService() {
_initialized = _init();
}

Future<void> _init() async {
Fimber.i('Initializing LogService...');
_logPath = await _getLogPath();
}

Future<String> _getLogPath() async {
return "${(await getApplicationSupportDirectory()).path}/logs";
}

void save(LogEntry log) {
_saveInternal(log);
}

void _saveInternal(LogEntry log) async {
await _initialized;
_lock.synchronized(() {
var lastSavedFile = File("$_logPath/$_lastSavedFileName");
if (!(lastSavedFile.existsSync())) {
lastSavedFile.createSync(recursive: true);
}
lastSavedFile.writeAsStringSync("${jsonEncode(log)},", mode: FileMode.append);

// Check rollover
if (lastSavedFile.lengthSync() > _maxFileSize || _nextRolloverTimeStamp.isBefore(DateTime.now())) {
Fimber.d("Rolling over log file");
lastSavedFile.renameSync("$_logPath/$_prefix-${DateTime.now().millisecondsSinceEpoch}.json");
_nextRolloverTimeStamp = DateTime.now().add(const Duration(minutes: _rolloverTimeMinutes));
_sendLogs();
}
});
}

void _sendLogs() async {
_senderLock.synchronized(() async {
var logDir = Directory(_logPath);
var files = logDir.listSync();
Fimber.d('Found ${files.length} log files in log directory: $_logPath');

for (var file in files) {
if (file is File && file.path.endsWith(".json") && !file.path.contains(_lastSavedFileName)) {
Fimber.d('Sending ${file.path} to backend');

var content = file.readAsStringSync();
content = '[${content.substring(0, content.length - 1)}]'; // Remove trailing comma

Iterable iterable = json.decode(content);
List<LogEntry> logEntries = List<LogEntry>.from(iterable.map((json) => LogEntry.fromJson(json)));

final backendService = DI.get<BackendService>();
if (await backendService.sendLogs(logEntries)) {
Fimber.d('Deleting ${file.path}');
file.deleteSync();
}
}
}
});
}
}
Loading

0 comments on commit e23c252

Please sign in to comment.