542 lines
11 KiB
Markdown
542 lines
11 KiB
Markdown
|
|
# Logging Guidelines
|
||
|
|
|
||
|
|
> How logging is done in Flutter app.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
This app uses **structured logging** with custom `Logger` class:
|
||
|
|
|
||
|
|
- **Library**: Custom `Logger` class in `core/logging/`
|
||
|
|
- **Interface**: `getLogger(module)` from `core/logging/logger.dart`
|
||
|
|
- **Log levels**: `debug`, `info`, `warning`, `error`
|
||
|
|
- **Sensitive fields**: Never log passwords, tokens, PII
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Logger Setup
|
||
|
|
|
||
|
|
### Import and Initialize
|
||
|
|
|
||
|
|
```dart
|
||
|
|
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:**
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// 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:
|
||
|
|
|
||
|
|
```dart
|
||
|
|
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
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// 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:
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// 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:
|
||
|
|
|
||
|
|
```dart
|
||
|
|
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
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// WRONG: Will log in release build
|
||
|
|
_logger.debug(message: 'Debug info: $sensitiveData');
|
||
|
|
```
|
||
|
|
|
||
|
|
**Right way:** Use `kDebugMode` guard:
|
||
|
|
|
||
|
|
```dart
|
||
|
|
if (kDebugMode) {
|
||
|
|
_logger.debug(message: 'Variable value', extra: {'var': value});
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### ❌ Never skip error logging
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// WRONG: Exception is caught but not logged
|
||
|
|
try {
|
||
|
|
await operation();
|
||
|
|
} catch (e) {
|
||
|
|
state = ErrorState();
|
||
|
|
// Missing error log!
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### ❌ Never log in every iteration
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// WRONG: Log every iteration
|
||
|
|
for (item in items) {
|
||
|
|
_logger.debug('Processing item: ${item.id}'); // Too noisy
|
||
|
|
process(item);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Right: Log only failures:**
|
||
|
|
|
||
|
|
```dart
|
||
|
|
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
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// WRONG: Never use print in production code
|
||
|
|
print('User logged in: $userId');
|
||
|
|
```
|
||
|
|
|
||
|
|
**Right:** Use `Logger`:
|
||
|
|
|
||
|
|
```dart
|
||
|
|
_logger.info(message: 'User logged in', extra: {'user_id': userId});
|
||
|
|
```
|
||
|
|
|
||
|
|
### ❌ Logging sensitive data
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// WRONG: Logging PII
|
||
|
|
_logger.info(message: 'User profile', extra: {
|
||
|
|
'email': userEmail,
|
||
|
|
'phone': userPhone,
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**Right:** Exclude sensitive fields:
|
||
|
|
|
||
|
|
```dart
|
||
|
|
_logger.info(message: 'User profile loaded', extra: {'user_id': userId});
|
||
|
|
```
|
||
|
|
|
||
|
|
### ❌ Catching without logging
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// WRONG: Silent failure
|
||
|
|
try {
|
||
|
|
await service.doSomething();
|
||
|
|
} catch (e) {
|
||
|
|
// No logging, no re-raise
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Right:** Log and re-raise:
|
||
|
|
|
||
|
|
```dart
|
||
|
|
try {
|
||
|
|
await service.doSomething();
|
||
|
|
} catch (e, stackTrace) {
|
||
|
|
_logger.error(
|
||
|
|
message: 'Operation failed',
|
||
|
|
error: e,
|
||
|
|
stackTrace: stackTrace,
|
||
|
|
);
|
||
|
|
rethrow;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Examples from Codebase
|
||
|
|
|
||
|
|
### AuthBloc Error Handling
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```dart
|
||
|
|
// 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',
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|