11 KiB
11 KiB
Logging Guidelines
How logging is done in Flutter app.
Overview
This app uses structured logging with custom Logger class:
- Library: Custom
Loggerclass incore/logging/ - Interface:
getLogger(module)fromcore/logging/logger.dart - Log levels:
debug,info,warning,error - Sensitive fields: Never log passwords, tokens, PII
Logger Setup
Import and Initialize
import 'core/logging/logger.dart';
class SomeBloc extends ChangeNotifier {
final Logger _logger = getLogger('features.auth.bloc');
}
Module Naming Convention
| Feature | Module Path |
|---|---|
| auth | features.auth |
| home | features.home |
| divination | features.divination |
| settings | features.settings |
Examples:
// features/auth/presentation/bloc/auth_bloc.dart
final Logger _logger = getLogger('features.auth.bloc');
// features/home/data/repositories/home_repository.dart
final Logger _logger = getLogger('features.home.repository');
// core/network/http_client.dart
final Logger _logger = getLogger('core.network.http_client');
Log Levels
| Level | When to Use | Noise Level | Required |
|---|---|---|---|
| error | All exceptions and failures | Required | Never skip |
| warning | Degraded behavior, retry, fallback | Minimal | Only when action taken |
| info | Key business events | Minimal | Only milestones |
| debug | Detailed flow tracing (dev only) | High | Avoid in release |
Error Logging Requirements
Every try-catch MUST log the exception:
try {
await _repository.someOperation();
} catch (e, stackTrace) {
_logger.error(
message: 'Operation failed: $operationName',
error: e,
stackTrace: stackTrace,
extra: {'context': 'relevant_data'},
);
// handle error
}
Error Logging Pattern
// features/auth/presentation/bloc/auth_bloc.dart
Future<void> start() async {
try {
final user = await _repository.recoverSession();
// ...
} catch (error, stackTrace) {
_logger.error(
message: 'Session recovery failed: ${error.runtimeType}',
error: error.runtimeType.toString(),
stackTrace: stackTrace,
);
await _repository.clearLocalSession();
_state = AuthState(
status: AuthStatus.unauthenticated,
errorMessage: _toSafeMessage(error),
);
notifyListeners();
}
}
Info Logging Requirements
Only log these milestone events:
- User login/logout
- Message sent/received
- Data sync completed
- Important state transitions
Info Logging Pattern
// Login success
_logger.info(
message: 'User logged in',
extra: {'user_id': user.id},
);
// Run success
_logger.info(
message: 'Run completed',
extra: {
'run_id': runId,
'thread_id': threadId,
'duration_ms': duration.inMilliseconds,
},
);
❌ DO NOT log for every operation:
// WRONG: Logging every keystroke
onChanged: (value) {
_logger.info('Input changed: $value'); // Too noisy
}
Warning Logging Requirements
Only log when taking corrective action:
- Retrying after failure
- Using fallback data
- Skipping malformed data
- Deprecation warnings
Warning Logging Pattern
// Cache miss with fallback
_logger.warning(
message: 'Cache miss, loading from remote',
extra: {'key': cacheKey},
);
// Retry attempt
_logger.warning(
message: 'Retry attempt',
extra: {'attempt': 2, 'max_attempts': 3},
);
// Fallback data
_logger.warning(
message: 'Using fallback data due to network timeout',
extra: {'timeout_ms': 5000},
);
Debug Logging
Use sparingly, only in debug builds:
if (kDebugMode) {
_logger.debug(
message: 'Variable value',
extra: {'variable': expensiveObject.toString()},
);
}
Note: Debug logs are automatically filtered in release builds.
Prohibited Practices
❌ Never log sensitive data
// WRONG: Logging password
_logger.info(message: 'User login', extra: {'password': password});
// WRONG: Logging token
_logger.debug(message: 'API call', extra: {'token': accessToken});
// WRONG: Logging PII
_logger.info(message: 'User profile', extra: {'email': userEmail});
❌ Never log at debug level in production
// WRONG: Will log in release build
_logger.debug(message: 'Debug info: $sensitiveData');
Right way: Use kDebugMode guard:
if (kDebugMode) {
_logger.debug(message: 'Variable value', extra: {'var': value});
}
❌ Never skip error logging
// WRONG: Exception is caught but not logged
try {
await operation();
} catch (e) {
state = ErrorState();
// Missing error log!
}
❌ Never log in every iteration
// WRONG: Log every iteration
for (item in items) {
_logger.debug('Processing item: ${item.id}'); // Too noisy
process(item);
}
Right: Log only failures:
for (item in items) {
try {
process(item);
} catch (e, stackTrace) {
_logger.error(
message: 'Item processing failed',
error: e,
stackTrace: stackTrace,
extra: {'item_id': item.id},
);
}
}
Logger Implementation
Core Logger Class
// core/logging/logger.dart
import 'log_entry.dart';
import 'log_service.dart';
class Logger {
final String module;
final LogService? _service;
final bool _isNoOp;
Logger(this.module, this._service) : _isNoOp = _service == null;
factory Logger.get(String module) {
return Logger(module, _globalLogService);
}
void error({
required String message,
required Object error,
required StackTrace stackTrace,
Map<String, dynamic>? extra,
}) {
if (_isNoOp) {
debugPrint(LogEntry(
message: message,
module: module,
errorType: error.runtimeType.toString(),
).toConsoleString());
return;
}
_service!.error(
message: message,
error: error,
stackTrace: stackTrace,
module: module,
extra: extra ?? {},
);
}
void info({
required String message,
Map<String, dynamic>? extra,
}) {
if (_isNoOp) return;
_service!.info(
message: message,
module: module,
extra: extra ?? {},
);
}
void warning({
required String message,
Map<String, dynamic>? extra,
}) {
if (_isNoOp) return;
_service!.warning(
message: message,
module: module,
extra: extra ?? {},
);
}
void debug({
required String message,
Map<String, dynamic>? extra,
}) {
if (_isNoOp || !kDebugMode) return;
_service!.debug(
message: message,
module: module,
extra: extra ?? {},
);
}
}
Logger getLogger(String module) => Logger.get(module);
Log Entry Structure
LogEntry Fields
// core/logging/log_entry.dart
class LogEntry {
final DateTime timestamp;
final LogLevel level;
final String message;
final String module;
final String? errorType;
final String? errorMessage;
final String? stackTrace;
final Map<String, dynamic>? extra;
}
enum LogLevel { debug, info, warning, error }
Log Format
{
"timestamp": "2026-04-10T12:34:56.789Z",
"level": "error",
"module": "features.auth.bloc",
"message": "Session recovery failed: SocketException",
"error_type": "SocketException",
"error_message": "Connection refused",
"stack_trace": "...",
"extra": {
"user_id": "123e4567-e89b-12d3-a456-426614174000"
}
}
Common Mistakes
❌ Using print() instead of Logger
// WRONG: Never use print in production code
print('User logged in: $userId');
Right: Use Logger:
_logger.info(message: 'User logged in', extra: {'user_id': userId});
❌ Logging sensitive data
// WRONG: Logging PII
_logger.info(message: 'User profile', extra: {
'email': userEmail,
'phone': userPhone,
});
Right: Exclude sensitive fields:
_logger.info(message: 'User profile loaded', extra: {'user_id': userId});
❌ Catching without logging
// WRONG: Silent failure
try {
await service.doSomething();
} catch (e) {
// No logging, no re-raise
return null;
}
Right: Log and re-raise:
try {
await service.doSomething();
} catch (e, stackTrace) {
_logger.error(
message: 'Operation failed',
error: e,
stackTrace: stackTrace,
);
rethrow;
}
Examples from Codebase
AuthBloc Error Handling
// features/auth/presentation/bloc/auth_bloc.dart
class AuthBloc extends ChangeNotifier {
final Logger _logger = getLogger('features.auth.bloc');
Future<void> start() async {
try {
final user = await _repository.recoverSession();
if (user == null) {
_state = const AuthState(status: AuthStatus.unauthenticated);
} else {
_state = AuthState(status: AuthStatus.authenticated, user: user);
}
notifyListeners();
} catch (error, stackTrace) {
_logger.error(
message: 'Session recovery failed: ${error.runtimeType}',
error: error.runtimeType.toString(),
stackTrace: stackTrace,
);
await _repository.clearLocalSession();
_state = AuthState(
status: AuthStatus.unauthenticated,
errorMessage: _toSafeMessage(error),
);
notifyListeners();
}
}
Future<void> logout() async {
_logger.info(message: 'User logged out');
_state = const AuthState(status: AuthStatus.unauthenticated);
notifyListeners();
unawaited(
_repository.logout().catchError((Object error, StackTrace stackTrace) {
_logger.error(
message: 'User logout failed: ${error.runtimeType}',
error: error.runtimeType.toString(),
stackTrace: stackTrace,
);
}),
);
}
}
Network Error Handling
// core/network/http_client.dart
final Logger _logger = getLogger('core.network.http_client');
Future<Response> get(String path) async {
try {
final response = await _innerClient.get(path);
final problem = ApiProblemMapper.tryParse(response);
if (problem != null && response.statusCode != 401) {
_logger.warning(
message: 'HTTP error response',
extra: {
'status': response.statusCode,
'path': path,
'code': problem.code,
},
);
}
return response;
} on SocketException catch (e, stackTrace) {
_logger.error(
message: 'Network error',
error: e,
stackTrace: stackTrace,
extra: {'path': path},
);
throw ApiProblem(
status: 0,
title: 'Network Error',
detail: 'No internet connection',
);
}
}