feat: add centralized error logging system
- Implemented Error Log API to capture and store error logs from various services. - Updated admin_ui to include Error Log Viewer component for inspecting logs. - Enhanced error handling in auth_api, employee_api, and transit_api to report errors to the Error Log API. - Modified existing services to utilize the new error logging functionality. - Updated Docker Compose configuration to include the Error Log API service. - Added error logging service in bus_terminal for Flutter application.
This commit is contained in:
@@ -1,12 +1,48 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'screens/login_screen.dart';
|
||||
import 'screens/schedule_overview_screen.dart';
|
||||
import 'services/auth_service.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
import 'services/app_navigator.dart';
|
||||
import 'services/error_log_service.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MainApp());
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
FlutterError.onError = (details) {
|
||||
FlutterError.presentError(details);
|
||||
void report() => ErrorLogService.report(
|
||||
service: 'bus_terminal',
|
||||
partOfService: 'FlutterError.onError',
|
||||
error: details.exceptionAsString(),
|
||||
);
|
||||
// ignore: unawaited_futures
|
||||
report();
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
// ignore: unawaited_futures
|
||||
ErrorLogService.report(
|
||||
service: 'bus_terminal',
|
||||
partOfService: 'PlatformDispatcher.onError',
|
||||
error: '$error\n$stack',
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
runZonedGuarded(() {
|
||||
runApp(const MainApp());
|
||||
}, (error, stack) {
|
||||
// ignore: unawaited_futures
|
||||
ErrorLogService.report(
|
||||
service: 'bus_terminal',
|
||||
partOfService: 'runZonedGuarded',
|
||||
error: '$error\n$stack',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class MainApp extends StatelessWidget {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../services/auth_service.dart';
|
||||
import '../services/error_log_service.dart';
|
||||
import '../widgets/custom_input_field.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
@@ -37,7 +38,17 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
final input = _userCodeController.text.trim();
|
||||
final parsedUserCode = int.tryParse(input);
|
||||
if (parsedUserCode == null) {
|
||||
throw Exception('User code must be a number');
|
||||
await ErrorLogService.report(
|
||||
service: 'bus_terminal',
|
||||
partOfService: 'LoginScreen._handleLogin',
|
||||
error: 'User code must be a number',
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = 'User code must be a number';
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await _authService.login(
|
||||
@@ -50,6 +61,11 @@ class _LoginScreenState extends State<LoginScreen> {
|
||||
Navigator.of(context).pushReplacementNamed('/schedule');
|
||||
}
|
||||
} catch (e) {
|
||||
await ErrorLogService.report(
|
||||
service: 'bus_terminal',
|
||||
partOfService: 'LoginScreen._handleLogin.catch',
|
||||
error: e.toString(),
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = e.toString().replaceAll('Exception: ', '');
|
||||
|
||||
@@ -226,8 +226,6 @@ class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> with Ti
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Diensten'),
|
||||
@@ -323,7 +321,7 @@ class _ProgressTab extends StatelessWidget {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text('Lijn ${svc.lineNumber} — ${svc.busName}'),
|
||||
subtitle: Text('${_formatTime(svc.startTime)}'),
|
||||
subtitle: Text(_formatTime(svc.startTime)),
|
||||
trailing: TextButton(
|
||||
onPressed: () async {
|
||||
try {
|
||||
|
||||
@@ -2,14 +2,23 @@ import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/user.dart';
|
||||
import 'error_log_service.dart';
|
||||
|
||||
/// Authentication Service
|
||||
class AuthService {
|
||||
static const String _apiBaseUrl = 'http://api.transit.local:5000';
|
||||
static const String _apiBaseUrl = 'https://transit.xtransit.testinstance.nl';
|
||||
static const String _tokenKey = 'auth_token';
|
||||
|
||||
late SharedPreferences _prefs;
|
||||
|
||||
Future<void> _reportError(String partOfService, Object error) {
|
||||
return ErrorLogService.report(
|
||||
service: 'bus_terminal',
|
||||
partOfService: partOfService,
|
||||
error: error.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _initPrefs() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
@@ -42,6 +51,7 @@ class AuthService {
|
||||
|
||||
return authResponse;
|
||||
} catch (e) {
|
||||
await _reportError('AuthService.login.parse', e);
|
||||
// Surface the response body to help debug backend field names / formats
|
||||
throw Exception('Login parse error: $e -- response body: $body');
|
||||
}
|
||||
@@ -51,6 +61,7 @@ class AuthService {
|
||||
throw Exception('Login failed: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
await _reportError('AuthService.login', e);
|
||||
throw Exception('Login error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class ErrorLogService {
|
||||
static const String _apiBaseUrl = 'https://transit.xtransit.testinstance.nl';
|
||||
|
||||
static Future<void> report({
|
||||
required String service,
|
||||
required String partOfService,
|
||||
required String error,
|
||||
}) async {
|
||||
try {
|
||||
await http.post(
|
||||
Uri.parse('$_apiBaseUrl/error_log'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'service': service,
|
||||
'partOfService': partOfService,
|
||||
'error': error,
|
||||
}),
|
||||
);
|
||||
} catch (_) {
|
||||
// Intentionally ignore logging failures.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/// Custom exception for authentication errors
|
||||
// Custom exception for authentication errors
|
||||
import 'app_navigator.dart';
|
||||
|
||||
class UnauthorizedException implements Exception {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'error_log_service.dart';
|
||||
import '../models/navigation.dart';
|
||||
|
||||
/// Navigation Service using OpenStreetMap
|
||||
@@ -10,6 +11,13 @@ class NavigationService {
|
||||
static const String _osrmBaseUrl = 'https://router.project-osrm.org/route/v1/driving';
|
||||
|
||||
/// Get current device location
|
||||
Future<void> _reportError(String partOfService, Object error) {
|
||||
return ErrorLogService.report(
|
||||
service: 'bus_terminal',
|
||||
partOfService: partOfService,
|
||||
error: error.toString(),
|
||||
);
|
||||
}
|
||||
Future<NavigationPoint?> getCurrentLocation() async {
|
||||
try {
|
||||
final permission = await Geolocator.checkPermission();
|
||||
@@ -27,6 +35,7 @@ class NavigationService {
|
||||
name: 'Current Location',
|
||||
);
|
||||
} catch (e) {
|
||||
await _reportError('NavigationService.getCurrentLocation', e);
|
||||
throw Exception('Failed to get location: $e');
|
||||
}
|
||||
}
|
||||
@@ -88,6 +97,7 @@ class NavigationService {
|
||||
throw Exception('Failed to get route: ${response.statusCode} -- response body: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
await _reportError('NavigationService.getRoute', e);
|
||||
throw Exception('Navigation error: $e');
|
||||
}
|
||||
}
|
||||
@@ -152,6 +162,7 @@ class NavigationService {
|
||||
end: stops.last,
|
||||
);
|
||||
} catch (e) {
|
||||
await _reportError('NavigationService.getRouteThroughStops', e);
|
||||
throw Exception('Navigation error: $e');
|
||||
}
|
||||
}
|
||||
@@ -252,6 +263,7 @@ class NavigationService {
|
||||
orderedStops: orderedStops.isNotEmpty ? orderedStops : stops,
|
||||
);
|
||||
} catch (e) {
|
||||
await _reportError('NavigationService.getOptimizedRouteThroughStops', e);
|
||||
throw Exception('Navigation error: $e');
|
||||
}
|
||||
}
|
||||
@@ -308,6 +320,7 @@ class NavigationService {
|
||||
throw Exception('Failed to get routes: ${response.statusCode} -- response body: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
await _reportError('NavigationService.getMultipleRoutes', e);
|
||||
throw Exception('Navigation error: $e');
|
||||
}
|
||||
}
|
||||
@@ -358,6 +371,7 @@ class NavigationService {
|
||||
|
||||
return NavigationPoint(latitude: lat, longitude: lon, name: display);
|
||||
} catch (e) {
|
||||
await _reportError('NavigationService.geocodePlace', e);
|
||||
throw Exception('Geocode error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:http/http.dart' as http;
|
||||
import '../models/schedule.dart';
|
||||
import '../models/navigation.dart';
|
||||
import 'auth_service.dart';
|
||||
import 'error_log_service.dart';
|
||||
import 'exceptions.dart';
|
||||
|
||||
/// Schedule/Route Service
|
||||
@@ -10,6 +11,14 @@ class ScheduleService {
|
||||
static const String _apiBaseUrl = 'https://transit.xtransit.testinstance.nl';
|
||||
final _authService = AuthService();
|
||||
|
||||
Future<void> _reportError(String partOfService, Object error) {
|
||||
return ErrorLogService.report(
|
||||
service: 'bus_terminal',
|
||||
partOfService: partOfService,
|
||||
error: error.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get public schedule overview for a specific date.
|
||||
Future<List<ServiceSchedule>> getSchedules(DateTime date, {int? lineId}) async {
|
||||
try {
|
||||
@@ -32,6 +41,7 @@ class ScheduleService {
|
||||
|
||||
throw Exception('Failed to load schedules: ${response.statusCode}');
|
||||
} catch (e) {
|
||||
await _reportError('ScheduleService.getSchedules', e);
|
||||
throw Exception('Schedule error: $e');
|
||||
}
|
||||
}
|
||||
@@ -57,6 +67,7 @@ class ScheduleService {
|
||||
|
||||
throw Exception('Failed to load recurring schedules: ${response.statusCode}');
|
||||
} catch (e) {
|
||||
await _reportError('ScheduleService.getRecurringSchedules', e);
|
||||
throw Exception('Recurring schedule error: $e');
|
||||
}
|
||||
}
|
||||
@@ -76,6 +87,7 @@ class ScheduleService {
|
||||
|
||||
throw Exception('Failed to load schedule stops: ${response.statusCode}');
|
||||
} catch (e) {
|
||||
await _reportError('ScheduleService.getScheduleStops', e);
|
||||
throw Exception('Schedule stops error: $e');
|
||||
}
|
||||
}
|
||||
@@ -130,6 +142,7 @@ class ScheduleService {
|
||||
throw Exception('Failed to load schedule: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
await _reportError('ScheduleService.getDailySchedule', e);
|
||||
throw Exception('Schedule error: $e');
|
||||
}
|
||||
}
|
||||
@@ -152,6 +165,7 @@ class ScheduleService {
|
||||
|
||||
throw Exception('Failed to load progress: ${response.statusCode}');
|
||||
} catch (e) {
|
||||
await _reportError('ScheduleService.getScheduleProgress', e);
|
||||
throw Exception('Schedule progress error: $e');
|
||||
}
|
||||
}
|
||||
@@ -177,6 +191,7 @@ class ScheduleService {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is UnauthorizedException) rethrow;
|
||||
await _reportError('ScheduleService.getRouteDetails', e);
|
||||
throw Exception('Route error: $e');
|
||||
}
|
||||
}
|
||||
@@ -200,6 +215,7 @@ class ScheduleService {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is UnauthorizedException) rethrow;
|
||||
await _reportError('ScheduleService.updateRouteStatus', e);
|
||||
throw Exception('Status update error: $e');
|
||||
}
|
||||
}
|
||||
@@ -228,6 +244,7 @@ class ScheduleService {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is UnauthorizedException) rethrow;
|
||||
await _reportError('ScheduleService.updateScheduleStatus', e);
|
||||
throw Exception('Schedule status update error: $e');
|
||||
}
|
||||
}
|
||||
@@ -266,6 +283,7 @@ class ScheduleService {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is UnauthorizedException) rethrow;
|
||||
await _reportError('ScheduleService.getStops', e);
|
||||
throw Exception('Get stops error: $e');
|
||||
}
|
||||
}
|
||||
@@ -298,6 +316,7 @@ class ScheduleService {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is UnauthorizedException) rethrow;
|
||||
await _reportError('ScheduleService.markScheduleStopPassed', e);
|
||||
throw Exception('Mark stop passed error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user