Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/isolates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:mewe_maps/repositories/storage/storage_repository.dart';
import 'package:mewe_maps/services/firebase/firebase_initialization.dart';
import 'package:mewe_maps/utils/loggly_logger.dart';

Future<void> initializeIsolate() async {
WidgetsFlutterBinding.ensureInitialized();
Expand All @@ -25,6 +26,7 @@ Future<void> initializeIsolate() async {
exit(1);
};

initializeLogglyLogger();
await initializeFirebase();
await StorageRepository.initialize();
}
42 changes: 42 additions & 0 deletions lib/models/loggly_log_entry.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright MeWe 2025.
//
// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.

import 'package:json_annotation/json_annotation.dart';

part 'loggly_log_entry.g.dart';

@JsonSerializable()
class LogglyLogEntry {
@JsonKey(name: "timestamp")
final String timestamp;
@JsonKey(name: "level")
final String level;
@JsonKey(name: "message")
final String message;
@JsonKey(name: "userId")
final String? userId;
@JsonKey(name: "tags")
final List<String> tags;
@JsonKey(name: "params")
final Map<String, dynamic>? params;

const LogglyLogEntry({
required this.timestamp,
required this.level,
required this.message,
this.userId,
required this.tags,
this.params,
});

factory LogglyLogEntry.fromJson(Map<String, dynamic> json) => _$LogglyLogEntryFromJson(json);

Map<String, dynamic> toJson() => _$LogglyLogEntryToJson(this);
}
1 change: 1 addition & 0 deletions lib/modules/map/bloc/map_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class MapBloc extends Bloc<MapEvent, MapState> {
_observeLocationRequests();
_startObservingFcmToken();
emit(state.copyWith(mapInitialized: true));
Logger.sendOnlineLogs();
}

void _observeMyPosition() async {
Expand Down
6 changes: 3 additions & 3 deletions lib/repositories/location/sharing_location_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ class FirestoreSharingLocationRepository implements SharingLocationRepository {
}

@override
Future<void> uploadPosition(Position position, List<SharingSession> sessions) {
Logger.log(_TAG, 'Uploading position to ${sessions.length} sessions');
Future<void> uploadPosition(Position position, List<SharingSession> sessions) async {
await Logger.saveOnlineLog(_TAG, 'Uploading position to ${sessions.length} sessions');
List<Future> futures = [];
for (var session in sessions) {
final data = ShareData(
Expand All @@ -180,7 +180,7 @@ class FirestoreSharingLocationRepository implements SharingLocationRepository {
);
futures.add(_firestore.collection(FirestoreConstants.COLLECTION_SHARING_DATA).doc(data.sessionId).set(data.toJson()));
}
return Future.value();
await Future.wait(futures);
}

@override
Expand Down
24 changes: 24 additions & 0 deletions lib/services/http/loggly_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright MeWe 2025.
//
// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.

import 'package:dio/dio.dart';
import 'package:retrofit/retrofit.dart';

part 'loggly_service.g.dart';

@RestApi()
abstract class LogglyService {
factory LogglyService(Dio dio, {String baseUrl}) = _LogglyService;

/// Send logs in bulk to Loggly
/// Logs in jsonl format
@POST("bulk/{token}/tag/batch/")
Future<void> sendLogsInBulk(@Path("token") String token, @Body() String logData);
}
10 changes: 5 additions & 5 deletions lib/services/location/location_sharing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import 'package:mewe_maps/utils/logger.dart';
const String _TAG = "shareMyLocationWithSessions";

Future<bool> shareMyLocationWithSessions({VoidCallback? onFinish}) async {
Logger.log(_TAG, "shareMyLocationWithSessions");
await Logger.saveOnlineLog(_TAG, "shareMyLocationWithSessions");

final userId = StorageRepository.user?.userId;
if (userId != null) {
Expand All @@ -29,17 +29,17 @@ Future<bool> shareMyLocationWithSessions({VoidCallback? onFinish}) async {
final sessions = await sharingRepository.getSharingSessionsAsOwner(userId);
if (sessions != null && sessions.isNotEmpty) {
await sharingRepository.uploadPosition(lastPosition, sessions);
Logger.log(_TAG, "success");
await Logger.saveOnlineLog(_TAG, "success");
onFinish?.call();
return true;
} else {
Logger.log(_TAG, "failed (no sessions)");
await Logger.saveOnlineLog(_TAG, "failed (no sessions)");
}
} else {
Logger.log(_TAG, "failed (no last position)");
await Logger.saveOnlineLog(_TAG, "failed (no last position)");
}
} else {
Logger.log(_TAG, "failed (no current user)");
await Logger.saveOnlineLog(_TAG, "failed (no current user)");
}
return false;
}
6 changes: 3 additions & 3 deletions lib/services/workmanager/workmanager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ void workmanagerCallback() async {
await initializeIsolate();

Workmanager().executeTask((task, inputData) async {
Logger.log(_TAG, "executeTask $task");
await Logger.saveOnlineLog(_TAG, "executeTask $task");

if (task == _PERIODIC_SHARE_LOCATION_TASK) {
await shareMyLocationWithSessions();
Expand All @@ -38,7 +38,7 @@ void workmanagerCallback() async {
Logger.log(_TAG, "Unknown task: $task");
}

return Future.value(true);
return Logger.sendOnlineLogs().then((_) => true);
});
}

Expand Down Expand Up @@ -79,5 +79,5 @@ Future<void> registerPeriodicShareTaskOnAndroid() async {

Future<void> registerStopPreciseTrackingOnNoSessions() async {
await Workmanager().registerOneOffTask(_STOP_TRACKING_NO_SESSIONS_TASK, _STOP_TRACKING_NO_SESSIONS_TASK);
Logger.log(_TAG, "registerStopPreciseTrackingOnNoSessions success");
await Logger.saveOnlineLog(_TAG, "registerStopPreciseTrackingOnNoSessions success");
}
27 changes: 25 additions & 2 deletions lib/utils/logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,34 @@
//
// You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.

import 'package:flutter/foundation.dart';

import 'loggly_logger.dart';

class Logger {
static bool LOG_DIO = true;

static void log(String tag, String text) {
final pattern = RegExp('.{1,800}'); // 800 is the size of each chunk
pattern.allMatches(text).forEach((match) => print("MEWE_MAPS: $tag: ${match.group(0)}"));
if (kDebugMode) {
final pattern = RegExp('.{1,800}'); // 800 is the size of each chunk
pattern.allMatches(text).forEach((match) => print("MEWE_MAPS: $tag: ${match.group(0)}"));
}
}

static Future<void> saveOnlineLog(String tag, String text, {Map<String, dynamic>? params}) async {
try {
await LogglyLogger.instance.log(text, tag: tag, params: params);
} catch (e) {
log(tag, "LogglyLogger error: $e");
}
log(tag, text);
}

static Future<void> sendOnlineLogs() async {
try {
await LogglyLogger.instance.sendPendingLogs();
} catch (e) {
log("Logger", "Error sending logs to Loggly: $e");
}
}
}
54 changes: 54 additions & 0 deletions lib/utils/loggly_cache.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright MeWe 2025.
//
// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.

import 'dart:convert';
import 'dart:io';
import 'package:mewe_maps/models/loggly_log_entry.dart';
import 'package:path_provider/path_provider.dart';
import 'package:synchronized/synchronized.dart';

class LogglyCache {
static const _fileName = 'loggly_cache.jsonl';
File? _logFile;
final Lock _lock = Lock();

Future<File> _getLogFile() async {
final dir = await getApplicationDocumentsDirectory();
_logFile = File('${dir.path}/$_fileName');
return _logFile!;
}

Future<void> cacheLog(LogglyLogEntry log) async {
await _lock.synchronized(() async {
final file = _logFile ?? await _getLogFile();
final logLine = '${jsonEncode(log.toJson())}\n';
await file.writeAsString(logLine, mode: FileMode.append, flush: true);
});
}

Future<List<String>> readCachedLogs() async {
return await _lock.synchronized(() async {
final file = await _getLogFile();
if (!await file.exists()) return [];

final lines = await file.readAsLines();
return lines.toList();
});
}

Future<void> clearCache() async {
await _lock.synchronized(() async {
final file = _logFile ?? await _getLogFile();
if (await file.exists()) {
await file.delete();
}
});
}
}
90 changes: 90 additions & 0 deletions lib/utils/loggly_logger.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright MeWe 2025.
//
// This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.

import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:mewe_maps/models/loggly_log_entry.dart';
import 'package:mewe_maps/repositories/storage/storage_repository.dart';
import 'package:mewe_maps/services/http/loggly_service.dart';
import 'package:mewe_maps/utils/logger.dart';
import 'package:mewe_maps/utils/loggly_cache.dart';

const _TAG = "LogglyLogger";

void initializeLogglyLogger() {
LogglyLogger.instance.configure(token: dotenv.env["LOGGLY_TOKEN"], baseUrl: "http://logs-01.loggly.com/");
}

class LogglyLogger {
static final LogglyLogger instance = LogglyLogger._internal();

LogglyService? _logglyService;
String? _token;
final _logglyCache = LogglyCache();

LogglyLogger._internal();

void configure({required String? token, required String baseUrl, String? tag}) {
_token = token;
final dio = Dio();
_logglyService = LogglyService(dio, baseUrl: baseUrl);
}

Future<void> log(String message, {String? tag, String level = 'info', Map<String, dynamic>? params}) {
final log = LogglyLogEntry(
timestamp: DateTime.now().toUtc().toIso8601String(),
level: level,
message: message,
userId: StorageRepository.user?.userId,
tags: _createTags(tag),
params: params,
);

return _logglyCache.cacheLog(log);
}

Future<void> sendPendingLogs() async {
try {
final logs = await _logglyCache.readCachedLogs();
if (logs.isNotEmpty) {
// send logs in bulk (for 100 logs)
for (int i = 0; i < logs.length; i += 100) {
final bulk = logs.sublist(i, i + 100 > logs.length ? logs.length : i + 100).join("\n");
await _logglyService?.sendLogsInBulk(_token!, bulk);
}
await _logglyCache.clearCache();
}
Logger.log(_TAG, "Sent ${logs.length} logs to Loggly");
} catch (e) {
Logger.log(_TAG, "Error sending logs to Loggly: $e");
}
}

List<String> _createTags(String? tag) {
List<String> tags = [];
if (tag != null) {
tags.add(tag);
}
if (Platform.isIOS) {
tags.add("ios");
} else if (Platform.isAndroid) {
tags.add("android");
}
if (kDebugMode) {
tags.add("debug");
} else {
tags.add("release");
}
return tags;
}
}
24 changes: 24 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
url: "https://pub.dev"
source: hosted
version: "2.2.16"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
path_provider_linux:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies:
firebase_core: ^3.12.0
cloud_firestore: ^5.6.4
firebase_messaging: ^15.2.4
path_provider: ^2.1.1

dev_dependencies:
flutter_test:
Expand Down