Files
eryao/.trellis/spec/frontend/logging-guidelines.md
T

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',
);
}
}
```