diff --git a/das_client/ios/Podfile.lock b/das_client/ios/Podfile.lock index e1ca177f..d9c08fbd 100644 --- a/das_client/ios/Podfile.lock +++ b/das_client/ios/Podfile.lock @@ -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 diff --git a/das_client/ios/Runner/AppDelegate.swift b/das_client/ios/Runner/AppDelegate.swift index 70693e4a..b6363034 100644 --- a/das_client/ios/Runner/AppDelegate.swift +++ b/das_client/ios/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import UIKit import Flutter -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, diff --git a/das_client/lib/di.dart b/das_client/lib/di.dart index 60d58812..b8b95e58 100644 --- a/das_client/lib/di.dart +++ b/das_client/lib/di.dart @@ -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'; @@ -41,6 +42,19 @@ class DI { param2: param2, ); } + + static Future readyGet({ + String? instanceName, + dynamic param1, + dynamic param2, + }) async { + await GetIt.I.isReady(); + return GetIt.I.get( + instanceName: instanceName, + param1: param1, + param2: param2, + ); + } } // Internal @@ -56,6 +70,7 @@ extension GetItX on GetIt { registerMqttService(useTms: useTms); registerRepositories(); registerSferaService(); + registerBackendService(); await allReady(); } @@ -139,4 +154,10 @@ extension GetItX on GetIt { registerSingletonWithDependencies(() => SferaService(mqttService: get(), sferaRepository: get()), dependsOn: [MqttService, SferaRepository]); } + + void registerBackendService() { + final flavor = get(); + registerSingletonWithDependencies(() => BackendService(authenticator: DI.get(), baseUrl: flavor.backendUrl), + dependsOn: [Authenticator]); + } } diff --git a/das_client/lib/flavor.dart b/das_client/lib/flavor.dart index c81d9122..8897d636 100644 --- a/das_client/lib/flavor.dart +++ b/das_client/lib/flavor.dart @@ -6,23 +6,27 @@ 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', ), 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', ), 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', ); const Flavor({ @@ -33,7 +37,8 @@ enum Flavor { this.tmsMqttUrl, required this.authenticatorConfig, this.tmsAuthenticatorConfig, - this.mqttTopicPrefix = '' + this.mqttTopicPrefix = '', + required this.backendUrl, }); final String displayName; @@ -44,11 +49,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([ diff --git a/das_client/lib/logging/logging_component.dart b/das_client/lib/logging/logging_component.dart new file mode 100644 index 00000000..aaa370cc --- /dev/null +++ b/das_client/lib/logging/logging_component.dart @@ -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()); + } +} \ No newline at end of file diff --git a/das_client/lib/logging/src/das_log_tree.dart b/das_client/lib/logging/src/das_log_tree.dart new file mode 100644 index 00000000..747b3fd0 --- /dev/null +++ b/das_client/lib/logging/src/das_log_tree.dart @@ -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 _initialized; + final Map metadata = {}; + + DasLogTree({required LogService logService}) : _logService = logService { + _initialized = _init(); + } + + Future _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 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; + } + } +} diff --git a/das_client/lib/logging/src/log_entry.dart b/das_client/lib/logging/src/log_entry.dart new file mode 100644 index 00000000..79fc7172 --- /dev/null +++ b/das_client/lib/logging/src/log_entry.dart @@ -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 metadata; + + Map toJson() => { + 'time': time, + 'source': source, + 'message': message, + 'level': level.name, + 'metadata': metadata, + }; + + LogEntry.fromJson(Map json) + : time = json['time'], + source = json['source'], + message = json['message'], + level = LogLevel.values.firstWhere((element) => element.name == json['level']), + metadata = json['metadata']; +} diff --git a/das_client/lib/logging/src/log_level.dart b/das_client/lib/logging/src/log_level.dart new file mode 100644 index 00000000..d932c508 --- /dev/null +++ b/das_client/lib/logging/src/log_level.dart @@ -0,0 +1 @@ +enum LogLevel { trace, debug, info, warning, error, fatal } diff --git a/das_client/lib/logging/src/log_service.dart b/das_client/lib/logging/src/log_service.dart new file mode 100644 index 00000000..a175c096 --- /dev/null +++ b/das_client/lib/logging/src/log_service.dart @@ -0,0 +1,84 @@ +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 _initialized; + late String _logPath; + + DateTime _nextRolloverTimeStamp = DateTime.now().add(const Duration(minutes: _rolloverTimeMinutes)); + + LogService() { + _initialized = _init(); + } + + Future _init() async { + Fimber.i('Initializing LogService...'); + _logPath = await _getLogPath(); + } + + Future _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 logEntries = List.from(iterable.map((json) => LogEntry.fromJson(json))); + + final backendService = await DI.readyGet(); + if (await backendService.sendLogs(logEntries)) { + file.deleteSync(); + } + } + } + }); + } +} diff --git a/das_client/lib/main.dart b/das_client/lib/main.dart index e2a3f068..51a3b8fe 100644 --- a/das_client/lib/main.dart +++ b/das_client/lib/main.dart @@ -1,15 +1,15 @@ import 'package:das_client/app.dart'; import 'package:das_client/auth/auth_cubit.dart'; -import 'package:das_client/bloc/fahrbild_cubit.dart'; import 'package:das_client/di.dart'; import 'package:das_client/flavor.dart'; +import 'package:das_client/logging/logging_component.dart'; import 'package:fimber/fimber.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; Future start(Flavor flavor) async { WidgetsFlutterBinding.ensureInitialized(); - await _initFimber(); + await _initLogging(); await _initDependencyInjection(flavor); runDasApp(); } @@ -23,9 +23,9 @@ Future runDasApp() async { )); } -Future _initFimber() async { - final tree = DebugTree(useColors: true); - Fimber.plantTree(tree); +Future _initLogging() async { + Fimber.plantTree(DebugTree(useColors: false)); + Fimber.plantTree(LoggingComponent.createDasLogTree()); } Future _initDependencyInjection(Flavor flavor) async { diff --git a/das_client/lib/service/backend_service.dart b/das_client/lib/service/backend_service.dart new file mode 100644 index 00000000..2ded59b9 --- /dev/null +++ b/das_client/lib/service/backend_service.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; +import 'dart:core'; + +import 'package:das_client/auth/authenticator.dart'; +import 'package:das_client/logging/src/log_entry.dart'; +import 'package:fimber/fimber.dart'; +import 'package:http/http.dart' as http; + +class BackendService { + final Authenticator _authenticator; + final String _baseUrl; + + BackendService({required Authenticator authenticator, required String baseUrl}) + : _authenticator = authenticator, + _baseUrl = baseUrl; + + Future sendLogs(List logs) async { + Fimber.i("Trying to send logs to backend..."); + final url = Uri.parse('$_baseUrl/api/v1/logging/logs'); + + var authToken = await _authenticator.token(); + + var response = await http.post(url, + headers: { + 'Content-Type': 'application/json', + 'Authorization': '${authToken.tokenType} ${authToken.accessToken.value}', + }, + body: jsonEncode(logs)); + + var statusCode = response.statusCode; + if (statusCode >= 200 && statusCode < 300) { + Fimber.i("Successfully sent logs to backend"); + return true; + } else { + Fimber.w("Failed to send logs to backend. StatusCode=$statusCode"); + return false; + } + } +} diff --git a/das_client/pubspec.lock b/das_client/pubspec.lock index 77437f8a..7814848f 100644 --- a/das_client/pubspec.lock +++ b/das_client/pubspec.lock @@ -553,18 +553,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -601,18 +601,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: "direct main" description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -745,10 +745,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -926,6 +926,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + synchronized: + dependency: "direct main" + description: + name: synchronized + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.dev" + source: hosted + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -938,10 +946,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" timing: dependency: transitive description: @@ -994,10 +1002,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" watcher: dependency: transitive description: @@ -1071,5 +1079,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.3 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.22.0" diff --git a/das_client/pubspec.yaml b/das_client/pubspec.yaml index 8788b99d..ba1210bd 100644 --- a/das_client/pubspec.yaml +++ b/das_client/pubspec.yaml @@ -80,6 +80,8 @@ dependencies: path_provider: ^2.1.3 # https://pub.dev/packages/package_info_plus package_info_plus: ^8.0.3 + # https://pub.dev/packages/synchronized + synchronized: ^3.3.0 dev_dependencies: integration_test: diff --git a/das_client/test/service/sfera/sfera_handshake_task_test.dart b/das_client/test/service/sfera/sfera_handshake_task_test.dart index e3d669a9..bc5e845c 100644 --- a/das_client/test/service/sfera/sfera_handshake_task_test.dart +++ b/das_client/test/service/sfera/sfera_handshake_task_test.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:das_client/model/sfera/otn_id.dart';