Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -91,6 +91,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 @@ -163,8 +163,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 @@ -175,7 +175,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 @@ -16,7 +16,7 @@ import 'package:mewe_maps/utils/logger.dart';
const String _TAG = "shareMyLocationWithSessions";

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

final userId = StorageRepository.user?.userId;
if (userId != null) {
Expand All @@ -27,16 +27,16 @@ Future<bool> shareMyLocationWithSessions() 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");
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;
}
8 changes: 4 additions & 4 deletions lib/services/workmanager/workmanager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ 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();
} else if (task == _STOP_TRACKING_NO_SESSIONS_TASK) {
await stopPreciseTrackingOnNoSessions();
}

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

Expand All @@ -45,10 +45,10 @@ Future<void> registerPeriodicShareMyLocationWithSessions() async {
_PERIODIC_SHARE_LOCATION_TASK,
frequency: const Duration(minutes: 15),
);
Logger.log(_TAG, "registerPeriodicShareMyLocationWithSessions success");
await Logger.saveOnlineLog(_TAG, "registerPeriodicShareMyLocationWithSessions success");
}

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");
}
29 changes: 26 additions & 3 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 = false;
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