Skip to content

Commit b81dada

Browse files
committed
feat: Background logs recording
1 parent 49594e1 commit b81dada

File tree

9 files changed

+348
-129
lines changed

9 files changed

+348
-129
lines changed

lib/background/background.dart

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import 'dart:async';
22
import 'dart:ui';
33
import 'package:flutter/material.dart';
44
import 'package:flutter_background_service/flutter_background_service.dart';
5-
import 'package:logger/logger.dart';
65
import 'package:mostro_mobile/data/models/nostr_filter.dart';
76
import 'package:mostro_mobile/data/repositories/event_storage.dart';
7+
import 'package:mostro_mobile/data/repositories/log_storage.dart';
88
import 'package:mostro_mobile/features/settings/settings.dart';
99
import 'package:mostro_mobile/features/notifications/services/background_notification_service.dart' as notification_service;
10+
import 'package:mostro_mobile/services/logger_service.dart';
1011
import 'package:mostro_mobile/services/nostr_service.dart';
1112
import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart';
1213

@@ -15,18 +16,10 @@ String currentLanguage = 'en';
1516

1617
@pragma('vm:entry-point')
1718
Future<void> serviceMain(ServiceInstance service) async {
18-
// Create a local logger for the background isolate
19-
final logger = Logger(
20-
printer: PrettyPrinter(
21-
methodCount: 2,
22-
errorMethodCount: 8,
23-
lineLength: 120,
24-
colors: true,
25-
printEmojis: true,
26-
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
27-
),
28-
level: Level.debug,
29-
);
19+
// Initialize persistent logging for background isolate
20+
final logsDb = await openMostroDatabase('logs.db');
21+
final logStorage = LogStorage(db: logsDb);
22+
initializeLogger(logStorage);
3023

3124
final Map<String, Map<String, dynamic>> activeSubscriptions = {};
3225
final nostrService = NostrService();
@@ -104,6 +97,7 @@ Future<void> serviceMain(ServiceInstance service) async {
10497
service.on("stop").listen((event) async {
10598
nostrService.disconnectFromRelays();
10699
await db.close();
100+
await logsDb.close();
107101
service.stopSelf();
108102
});
109103

lib/background/desktop_background_service.dart

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import 'dart:async';
22
import 'dart:isolate';
33
import 'package:dart_nostr/dart_nostr.dart';
44
import 'package:flutter/services.dart';
5-
import 'package:logger/logger.dart';
65
import 'package:mostro_mobile/data/models/nostr_filter.dart';
6+
import 'package:mostro_mobile/data/repositories/log_storage.dart';
77
import 'package:mostro_mobile/features/settings/settings.dart';
8+
import 'package:mostro_mobile/services/logger_service.dart';
89
import 'package:mostro_mobile/services/nostr_service.dart';
10+
import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart';
911
import 'abstract_background_service.dart';
1012

1113
class DesktopBackgroundService implements BackgroundService {
@@ -19,19 +21,6 @@ class DesktopBackgroundService implements BackgroundService {
1921
Future<void> init() async {}
2022

2123
static void isolateEntry(List<dynamic> args) async {
22-
// Create a local logger for the desktop background isolate
23-
final logger = Logger(
24-
printer: PrettyPrinter(
25-
methodCount: 2,
26-
errorMethodCount: 8,
27-
lineLength: 120,
28-
colors: true,
29-
printEmojis: true,
30-
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
31-
),
32-
level: Level.debug,
33-
);
34-
3524
final isolateReceivePort = ReceivePort();
3625
final mainSendPort = args[0] as SendPort;
3726
final token = args[1] as RootIsolateToken;
@@ -40,6 +29,11 @@ class DesktopBackgroundService implements BackgroundService {
4029

4130
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
4231

32+
// Initialize persistent logging for desktop background isolate
33+
final logsDb = await openMostroDatabase('logs.db');
34+
final logStorage = LogStorage(db: logsDb);
35+
initializeLogger(logStorage);
36+
4337
final nostrService = NostrService();
4438

4539
bool isAppForeground = true;
@@ -84,6 +78,9 @@ class DesktopBackgroundService implements BackgroundService {
8478
}
8579
});
8680
break;
81+
case 'stop':
82+
await logsDb.close();
83+
break;
8784
default:
8885
logger.i('Unknown command: $command');
8986
break;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import 'package:logger/logger.dart';
2+
import 'package:mostro_mobile/data/repositories/base_storage.dart';
3+
import 'package:mostro_mobile/services/logger_service.dart';
4+
import 'package:sembast/sembast.dart';
5+
6+
/// Sembast-based storage for log entries
7+
class LogStorage extends BaseStorage<LogEntry> {
8+
LogStorage({required Database db})
9+
: super(db, stringMapStoreFactory.store('logs'));
10+
11+
@override
12+
Map<String, dynamic> toDbMap(LogEntry entry) {
13+
return {
14+
'timestamp': entry.timestamp.toIso8601String(),
15+
'level': entry.level.name, // 'error', 'warning', 'info', 'debug', 'trace'
16+
'message': entry.message,
17+
'service': entry.service,
18+
'line': entry.line,
19+
};
20+
}
21+
22+
@override
23+
LogEntry fromDbMap(String key, Map<String, dynamic> json) {
24+
return LogEntry(
25+
timestamp: DateTime.parse(json['timestamp'] as String),
26+
level: _levelFromString(json['level'] as String),
27+
message: json['message'] as String,
28+
service: json['service'] as String,
29+
line: json['line'] as String,
30+
);
31+
}
32+
33+
/// Convert level string back to Level enum
34+
Level _levelFromString(String levelStr) {
35+
switch (levelStr.toLowerCase()) {
36+
case 'error':
37+
case 'fatal':
38+
return Level.error;
39+
case 'warning':
40+
case 'warn':
41+
return Level.warning;
42+
case 'info':
43+
return Level.info;
44+
case 'debug':
45+
return Level.debug;
46+
case 'trace':
47+
return Level.trace;
48+
default:
49+
return Level.debug;
50+
}
51+
}
52+
53+
/// Get all logs sorted by timestamp (newest first)
54+
Future<List<LogEntry>> getAllLogs() async {
55+
return await find(
56+
sort: [SortOrder('timestamp', false)], // false = descending
57+
);
58+
}
59+
60+
/// Get log count
61+
Future<int> getLogCount() async {
62+
return await store.count(db);
63+
}
64+
65+
/// Watch logs with real-time updates (newest first)
66+
Stream<List<LogEntry>> watchLogs() {
67+
return watch(
68+
sort: [SortOrder('timestamp', false)],
69+
);
70+
}
71+
72+
/// Perform rotation: delete oldest entries when count exceeds limit
73+
Future<void> rotateIfNeeded(int maxEntries, int batchDeleteSize) async {
74+
final count = await getLogCount();
75+
76+
if (count > maxEntries) {
77+
// Find oldest entries to delete
78+
final oldestEntries = await find(
79+
sort: [SortOrder('timestamp', true)], // true = ascending (oldest first)
80+
limit: batchDeleteSize,
81+
);
82+
83+
if (oldestEntries.isNotEmpty) {
84+
// Get the timestamp of the last entry to delete
85+
final cutoffTimestamp = oldestEntries.last.timestamp;
86+
87+
// Delete all entries with timestamp <= cutoffTimestamp
88+
await deleteWhere(
89+
Filter.lessThanOrEquals('timestamp', cutoffTimestamp.toIso8601String())
90+
);
91+
}
92+
}
93+
}
94+
95+
/// Clear all logs
96+
Future<void> clearAll() async {
97+
await deleteAll();
98+
}
99+
}

lib/features/logs/screens/logs_screen.dart

Lines changed: 71 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'package:mostro_mobile/core/app_theme.dart';
88
import 'package:mostro_mobile/core/config.dart';
99
import 'package:mostro_mobile/features/settings/settings_provider.dart';
1010
import 'package:mostro_mobile/generated/l10n.dart';
11-
import 'package:mostro_mobile/services/logger_service.dart';
11+
import 'package:mostro_mobile/services/logger_service.dart' as logger_service;
1212
import 'package:path_provider/path_provider.dart';
1313
import 'package:share_plus/share_plus.dart';
1414

@@ -57,7 +57,7 @@ class _LogsScreenState extends ConsumerState<LogsScreen> {
5757
super.dispose();
5858
}
5959

60-
List<LogEntry> _filterLogs(List<LogEntry> logs) {
60+
List<logger_service.LogEntry> _filterLogs(List<logger_service.LogEntry> logs) {
6161
var filtered = logs;
6262

6363
if (_selectedLevel != null) {
@@ -80,63 +80,68 @@ class _LogsScreenState extends ConsumerState<LogsScreen> {
8080

8181
@override
8282
Widget build(BuildContext context) {
83-
final allLogs = MemoryLogOutput.instance.getAllLogs();
84-
final logs = _filterLogs(allLogs);
85-
86-
return Stack(
87-
children: [
88-
Scaffold(
89-
backgroundColor: AppTheme.backgroundDark,
90-
appBar: AppBar(
91-
backgroundColor: Colors.transparent,
92-
elevation: 0,
93-
title: Text(
94-
S.of(context)!.logsReport,
95-
style: const TextStyle(
96-
color: AppTheme.textPrimary,
97-
fontSize: 20,
98-
fontWeight: FontWeight.w600,
99-
),
100-
),
101-
iconTheme: const IconThemeData(color: AppTheme.textPrimary),
102-
actions: [
103-
IconButton(
104-
icon: const Icon(Icons.delete_outline),
105-
onPressed: logs.isEmpty ? null : _showClearConfirmation,
106-
tooltip: S.of(context)!.clearLogs,
107-
),
108-
],
109-
),
110-
body: Column(
111-
children: [
112-
_buildStatsHeader(allLogs.length, logs.length),
113-
_buildSearchBar(),
114-
_buildFilterChips(),
115-
Expanded(
116-
child: logs.isEmpty
117-
? _buildEmptyState()
118-
: _buildLogsList(logs),
83+
return StreamBuilder<List<logger_service.LogEntry>>(
84+
stream: logger_service.watchLogs(),
85+
builder: (context, snapshot) {
86+
final allLogs = snapshot.data ?? [];
87+
final logs = _filterLogs(allLogs);
88+
89+
return Stack(
90+
children: [
91+
Scaffold(
92+
backgroundColor: AppTheme.backgroundDark,
93+
appBar: AppBar(
94+
backgroundColor: Colors.transparent,
95+
elevation: 0,
96+
title: Text(
97+
S.of(context)!.logsReport,
98+
style: const TextStyle(
99+
color: AppTheme.textPrimary,
100+
fontSize: 20,
101+
fontWeight: FontWeight.w600,
102+
),
103+
),
104+
iconTheme: const IconThemeData(color: AppTheme.textPrimary),
105+
actions: [
106+
IconButton(
107+
icon: const Icon(Icons.delete_outline),
108+
onPressed: logs.isEmpty ? null : _showClearConfirmation,
109+
tooltip: S.of(context)!.clearLogs,
110+
),
111+
],
119112
),
120-
if (allLogs.isNotEmpty) _buildActionButtons(),
121-
],
122-
),
123-
),
124-
if (_showScrollToTop)
125-
Positioned(
126-
right: 16,
127-
bottom: 100,
128-
child: FloatingActionButton(
129-
mini: true,
130-
backgroundColor: AppTheme.activeColor,
131-
onPressed: _scrollToTop,
132-
child: const Icon(
133-
Icons.arrow_upward,
134-
color: Colors.white,
135-
size: 20,
113+
body: Column(
114+
children: [
115+
_buildStatsHeader(allLogs.length, logs.length),
116+
_buildSearchBar(),
117+
_buildFilterChips(),
118+
Expanded(
119+
child: logs.isEmpty
120+
? _buildEmptyState()
121+
: _buildLogsList(logs),
122+
),
123+
if (allLogs.isNotEmpty) _buildActionButtons(),
124+
],
136125
),
137126
),
138-
),
139-
],
127+
if (_showScrollToTop)
128+
Positioned(
129+
right: 16,
130+
bottom: 100,
131+
child: FloatingActionButton(
132+
mini: true,
133+
backgroundColor: AppTheme.activeColor,
134+
onPressed: _scrollToTop,
135+
child: const Icon(
136+
Icons.arrow_upward,
137+
color: Colors.white,
138+
size: 20,
139+
),
140+
),
141+
),
142+
],
143+
);
144+
},
140145
);
141146
}
142147

@@ -320,18 +325,18 @@ class _LogsScreenState extends ConsumerState<LogsScreen> {
320325
);
321326
}
322327

323-
Widget _buildLogsList(List<LogEntry> logs) {
328+
Widget _buildLogsList(List<logger_service.LogEntry> logs) {
324329
return ListView.builder(
325330
controller: _scrollController,
326331
itemCount: logs.length,
327332
itemBuilder: (context, index) {
328-
final log = logs[logs.length - 1 - index];
333+
final log = logs[index];
329334
return _buildLogItem(log);
330335
},
331336
);
332337
}
333338

334-
Widget _buildLogItem(LogEntry log) {
339+
Widget _buildLogItem(logger_service.LogEntry log) {
335340
final color = _getLogLevelColor(log.level);
336341
final icon = _getLogLevelIcon(log.level);
337342
final levelStr = log.level.toString().split('.').last.toUpperCase();
@@ -419,7 +424,7 @@ class _LogsScreenState extends ConsumerState<LogsScreen> {
419424
);
420425
}
421426

422-
Future<void> _copyLogToClipboard(LogEntry log) async {
427+
Future<void> _copyLogToClipboard(logger_service.LogEntry log) async {
423428
await Clipboard.setData(ClipboardData(text: log.format()));
424429
if (mounted) {
425430
ScaffoldMessenger.of(context).showSnackBar(
@@ -549,7 +554,7 @@ class _LogsScreenState extends ConsumerState<LogsScreen> {
549554
}
550555

551556
Future<File> _createLogFile() async {
552-
final logs = MemoryLogOutput.instance.getAllLogs();
557+
final logs = await logger_service.getAllLogs();
553558
final buffer = StringBuffer();
554559

555560
buffer.writeln('Mostro Mobile - Logs Report');
@@ -640,10 +645,11 @@ class _LogsScreenState extends ConsumerState<LogsScreen> {
640645
),
641646
),
642647
TextButton(
643-
onPressed: () {
644-
MemoryLogOutput.instance.clear();
645-
Navigator.of(context).pop();
646-
setState(() {});
648+
onPressed: () async {
649+
await logger_service.clearAllLogs();
650+
if (context.mounted) {
651+
Navigator.of(context).pop();
652+
}
647653
},
648654
child: Text(
649655
S.of(context)!.clear,

0 commit comments

Comments
 (0)