681 lines
16 KiB
Markdown
681 lines
16 KiB
Markdown
|
|
# Flutter 日志系统实现计划
|
|||
|
|
|
|||
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|||
|
|
|
|||
|
|
**Goal:** 建立前端日志系统,debug 输出到 console,release 输出到本地文件
|
|||
|
|
|
|||
|
|
**Architecture:**
|
|||
|
|
- 日志模块放在 `apps/lib/core/logging/`
|
|||
|
|
- 提供 `getLogger(module)` 接口
|
|||
|
|
- Debug 模式:`debugPrint` 输出到 console
|
|||
|
|
- Release 模式:输出到 `getApplicationDocumentsDirectory()/logs/app.log`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 1: 日志模块骨架
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `apps/lib/core/logging/logger.dart`
|
|||
|
|
- Create: `apps/lib/core/logging/log_entry.dart`
|
|||
|
|
- Create: `apps/lib/core/logging/log_config.dart`
|
|||
|
|
- Create: `apps/lib/core/logging/log_service.dart`
|
|||
|
|
- Create: `apps/lib/core/logging/log_file_handler.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Create log_entry.dart**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// apps/lib/core/logging/log_entry.dart
|
|||
|
|
|
|||
|
|
enum LogLevel { debug, info, warning, error }
|
|||
|
|
|
|||
|
|
class LogEntry {
|
|||
|
|
final DateTime timestamp;
|
|||
|
|
final LogLevel level;
|
|||
|
|
final String message;
|
|||
|
|
final String module;
|
|||
|
|
final String? funcName;
|
|||
|
|
final int? lineNo;
|
|||
|
|
final String? errorType;
|
|||
|
|
final String? stackTrace;
|
|||
|
|
final Map<String, dynamic>? extra;
|
|||
|
|
|
|||
|
|
LogEntry({
|
|||
|
|
required this.timestamp,
|
|||
|
|
required this.level,
|
|||
|
|
required this.message,
|
|||
|
|
required this.module,
|
|||
|
|
this.funcName,
|
|||
|
|
this.lineNo,
|
|||
|
|
this.errorType,
|
|||
|
|
this.stackTrace,
|
|||
|
|
this.extra,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
Map<String, dynamic> toJson() => {
|
|||
|
|
'timestamp': timestamp.toIso8601String(),
|
|||
|
|
'level': level.name,
|
|||
|
|
'message': message,
|
|||
|
|
'module': module,
|
|||
|
|
if (funcName != null) 'func_name': funcName,
|
|||
|
|
if (lineNo != null) 'line_no': lineNo,
|
|||
|
|
if (errorType != null) 'error_type': errorType,
|
|||
|
|
if (stackTrace != null) 'stack_trace': stackTrace,
|
|||
|
|
if (extra != null && extra!.isNotEmpty) 'extra': extra,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
String toConsoleString() {
|
|||
|
|
final ts = timestamp.toIso8601String();
|
|||
|
|
final location = [
|
|||
|
|
if (funcName != null) funcName,
|
|||
|
|
if (lineNo != null) '@$lineNo',
|
|||
|
|
].join('');
|
|||
|
|
final locationStr = location.isNotEmpty ? ' [$location]' : '';
|
|||
|
|
final errorStr = errorType != null ? ' [$errorType]' : '';
|
|||
|
|
final extraStr = extra != null && extra!.isNotEmpty ? ' $extra' : '';
|
|||
|
|
return '$ts ${level.name.toUpperCase().padRight(7)} [$module$locationStr]$errorStr $message$extraStr';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
String toFileString() {
|
|||
|
|
final sb = StringBuffer();
|
|||
|
|
sb.writeln('[$timestamp] ${level.name.toUpperCase()} [$module]');
|
|||
|
|
if (funcName != null || lineNo != null) {
|
|||
|
|
sb.write(' at $funcName' ?? '');
|
|||
|
|
if (lineNo != null) sb.write(':$lineNo');
|
|||
|
|
sb.writeln();
|
|||
|
|
}
|
|||
|
|
sb.writeln(' $message');
|
|||
|
|
if (errorType != null) {
|
|||
|
|
sb.writeln(' Error: $errorType');
|
|||
|
|
}
|
|||
|
|
if (stackTrace != null) {
|
|||
|
|
sb.writeln(' StackTrace:');
|
|||
|
|
sb.writeln(stackTrace);
|
|||
|
|
}
|
|||
|
|
if (extra != null && extra!.isNotEmpty) {
|
|||
|
|
sb.writeln(' Extra: $extra');
|
|||
|
|
}
|
|||
|
|
return sb.toString();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Create log_config.dart**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// apps/lib/core/logging/log_config.dart
|
|||
|
|
|
|||
|
|
enum LogOutput { console, file }
|
|||
|
|
|
|||
|
|
class LogConfig {
|
|||
|
|
final LogLevel minLevel;
|
|||
|
|
final LogOutput output;
|
|||
|
|
final String logFileName;
|
|||
|
|
final String logDir;
|
|||
|
|
|
|||
|
|
const LogConfig({
|
|||
|
|
this.minLevel = LogLevel.debug,
|
|||
|
|
this.output = LogOutput.console,
|
|||
|
|
this.logFileName = 'app.log',
|
|||
|
|
this.logDir = 'logs',
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
static LogConfig forDebug() => const LogConfig(
|
|||
|
|
minLevel: LogLevel.debug,
|
|||
|
|
output: LogOutput.console,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
static LogConfig forRelease() => const LogConfig(
|
|||
|
|
minLevel: LogLevel.warning,
|
|||
|
|
output: LogOutput.file,
|
|||
|
|
logFileName: 'app.log',
|
|||
|
|
logDir = 'logs',
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 3: Create log_file_handler.dart**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// apps/lib/core/logging/log_file_handler.dart
|
|||
|
|
|
|||
|
|
import 'dart:io';
|
|||
|
|
import 'package:path_provider/path_provider.dart';
|
|||
|
|
|
|||
|
|
class LogFileHandler {
|
|||
|
|
File? _file;
|
|||
|
|
IOSink? _sink;
|
|||
|
|
|
|||
|
|
Future<void> init(String logDir, String logFileName) async {
|
|||
|
|
final dir = await getApplicationDocumentsDirectory();
|
|||
|
|
final logPath = '${dir.path}/$logDir';
|
|||
|
|
await Directory(logPath).create(recursive: true);
|
|||
|
|
_file = File('$logPath/$logFileName');
|
|||
|
|
_sink = _file!.openWrite(mode: FileMode.append);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void write(String content) {
|
|||
|
|
_sink?.writeln(content);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<void> flush() async {
|
|||
|
|
await _sink?.flush();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<void> close() async {
|
|||
|
|
await _sink?.close();
|
|||
|
|
_sink = null;
|
|||
|
|
_file = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<List<String>> readAllLines() async {
|
|||
|
|
if (_file == null || !await _file!.exists()) return [];
|
|||
|
|
return await _file!.readAsLines();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
String? get filePath => _file?.path;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 4: Create log_service.dart**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// apps/lib/core/logging/log_service.dart
|
|||
|
|
|
|||
|
|
import 'package:flutter/foundation.dart';
|
|||
|
|
import 'log_config.dart';
|
|||
|
|
import 'log_entry.dart';
|
|||
|
|
import 'log_file_handler.dart';
|
|||
|
|
|
|||
|
|
class LogService {
|
|||
|
|
final LogConfig _config;
|
|||
|
|
LogFileHandler? _fileHandler;
|
|||
|
|
final _buffer = <String>[];
|
|||
|
|
static const _maxBufferSize = 50;
|
|||
|
|
|
|||
|
|
LogService._({required LogConfig config}) : _config = config;
|
|||
|
|
|
|||
|
|
static Future<LogService> create({LogConfig? config}) async {
|
|||
|
|
final isRelease = kReleaseMode;
|
|||
|
|
|
|||
|
|
final effectiveConfig = config ??
|
|||
|
|
(isRelease ? LogConfig.forRelease() : LogConfig.forDebug());
|
|||
|
|
|
|||
|
|
final service = LogService._(config: effectiveConfig);
|
|||
|
|
|
|||
|
|
if (effectiveConfig.output == LogOutput.file) {
|
|||
|
|
service._fileHandler = LogFileHandler();
|
|||
|
|
await service._fileHandler!.init(
|
|||
|
|
effectiveConfig.logDir,
|
|||
|
|
effectiveConfig.logFileName,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return service;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
String? get logFilePath => _fileHandler?.filePath;
|
|||
|
|
|
|||
|
|
void _log(LogEntry entry) {
|
|||
|
|
if (entry.level.index < _config.minLevel.index) return;
|
|||
|
|
|
|||
|
|
if (_config.output == LogOutput.console) {
|
|||
|
|
debugPrint(entry.toConsoleString());
|
|||
|
|
if (entry.stackTrace != null) {
|
|||
|
|
debugPrint(entry.stackTrace!);
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
_buffer.add(entry.toFileString());
|
|||
|
|
if (_buffer.length >= _maxBufferSize) {
|
|||
|
|
_flushBuffer();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void _flushBuffer() {
|
|||
|
|
for (final line in _buffer) {
|
|||
|
|
_fileHandler?.write(line);
|
|||
|
|
}
|
|||
|
|
_buffer.clear();
|
|||
|
|
_fileHandler?.flush();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
(String?, int?) _extractLocation(StackTrace stackTrace) {
|
|||
|
|
final frames = stackTrace.toString().split('\n');
|
|||
|
|
for (final frame in frames) {
|
|||
|
|
if (frame.contains('.dart')) {
|
|||
|
|
final match = RegExp(r'#\d+\s+(.+?)\s+\((.+?):(\d+)\)').firstMatch(frame);
|
|||
|
|
if (match != null) {
|
|||
|
|
return (match.group(1), int.tryParse(match.group(3) ?? ''));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return (null, null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void debug({
|
|||
|
|
required String message,
|
|||
|
|
required String module,
|
|||
|
|
Map<String, dynamic>? extra,
|
|||
|
|
StackTrace? stackTrace,
|
|||
|
|
}) {
|
|||
|
|
final trace = stackTrace ?? StackTrace.current;
|
|||
|
|
final (funcName, lineNo) = _extractLocation(trace);
|
|||
|
|
_log(LogEntry(
|
|||
|
|
timestamp: DateTime.now(),
|
|||
|
|
level: LogLevel.debug,
|
|||
|
|
message: message,
|
|||
|
|
module: module,
|
|||
|
|
funcName: funcName,
|
|||
|
|
lineNo: lineNo,
|
|||
|
|
extra: extra,
|
|||
|
|
stackTrace: trace.toString(),
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void info({
|
|||
|
|
required String message,
|
|||
|
|
required String module,
|
|||
|
|
required Map<String, dynamic> extra,
|
|||
|
|
StackTrace? stackTrace,
|
|||
|
|
}) {
|
|||
|
|
final trace = stackTrace ?? StackTrace.current;
|
|||
|
|
final (funcName, lineNo) = _extractLocation(trace);
|
|||
|
|
_log(LogEntry(
|
|||
|
|
timestamp: DateTime.now(),
|
|||
|
|
level: LogLevel.info,
|
|||
|
|
message: message,
|
|||
|
|
module: module,
|
|||
|
|
funcName: funcName,
|
|||
|
|
lineNo: lineNo,
|
|||
|
|
extra: extra,
|
|||
|
|
stackTrace: trace.toString(),
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void warning({
|
|||
|
|
required String message,
|
|||
|
|
required String module,
|
|||
|
|
required Map<String, dynamic> extra,
|
|||
|
|
StackTrace? stackTrace,
|
|||
|
|
}) {
|
|||
|
|
final trace = stackTrace ?? StackTrace.current;
|
|||
|
|
final (funcName, lineNo) = _extractLocation(trace);
|
|||
|
|
_log(LogEntry(
|
|||
|
|
timestamp: DateTime.now(),
|
|||
|
|
level: LogLevel.warning,
|
|||
|
|
message: message,
|
|||
|
|
module: module,
|
|||
|
|
funcName: funcName,
|
|||
|
|
lineNo: lineNo,
|
|||
|
|
extra: extra,
|
|||
|
|
stackTrace: trace.toString(),
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void error({
|
|||
|
|
required String message,
|
|||
|
|
required Object error,
|
|||
|
|
required StackTrace stackTrace,
|
|||
|
|
required String module,
|
|||
|
|
Map<String, dynamic>? extra,
|
|||
|
|
}) {
|
|||
|
|
final (funcName, lineNo) = _extractLocation(stackTrace);
|
|||
|
|
_log(LogEntry(
|
|||
|
|
timestamp: DateTime.now(),
|
|||
|
|
level: LogLevel.error,
|
|||
|
|
message: message,
|
|||
|
|
module: module,
|
|||
|
|
funcName: funcName,
|
|||
|
|
lineNo: lineNo,
|
|||
|
|
errorType: error.runtimeType.toString(),
|
|||
|
|
stackTrace: stackTrace.toString(),
|
|||
|
|
extra: extra,
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void flush() {
|
|||
|
|
_flushBuffer();
|
|||
|
|
_fileHandler?.flush();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Future<List<String>> readLogs() async {
|
|||
|
|
return await _fileHandler?.readAllLines() ?? [];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 5: Create logger.dart**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// apps/lib/core/logging/logger.dart
|
|||
|
|
|
|||
|
|
import 'log_service.dart';
|
|||
|
|
|
|||
|
|
LogService? _globalLogService;
|
|||
|
|
|
|||
|
|
void setLogService(LogService service) {
|
|||
|
|
_globalLogService = service;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
LogService _ensureService() {
|
|||
|
|
return _globalLogService ?? (throw StateError('LogService not initialized'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class Logger {
|
|||
|
|
final String module;
|
|||
|
|
final LogService _service;
|
|||
|
|
|
|||
|
|
Logger(this.module, this._service);
|
|||
|
|
|
|||
|
|
factory Logger.get(String module) {
|
|||
|
|
return Logger(module, _ensureService());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
void debug({
|
|||
|
|
required String message,
|
|||
|
|
Map<String, dynamic>? extra,
|
|||
|
|
StackTrace? stackTrace,
|
|||
|
|
}) => _service.debug(
|
|||
|
|
message: message,
|
|||
|
|
module: module,
|
|||
|
|
extra: extra ?? {},
|
|||
|
|
stackTrace: stackTrace,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
void info({
|
|||
|
|
required String message,
|
|||
|
|
required Map<String, dynamic> extra,
|
|||
|
|
StackTrace? stackTrace,
|
|||
|
|
}) => _service.info(
|
|||
|
|
message: message,
|
|||
|
|
module: module,
|
|||
|
|
extra: extra,
|
|||
|
|
stackTrace: stackTrace,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
void warning({
|
|||
|
|
required String message,
|
|||
|
|
required Map<String, dynamic> extra,
|
|||
|
|
StackTrace? stackTrace,
|
|||
|
|
}) => _service.warning(
|
|||
|
|
message: message,
|
|||
|
|
module: module,
|
|||
|
|
extra: extra,
|
|||
|
|
stackTrace: stackTrace,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
void error({
|
|||
|
|
required String message,
|
|||
|
|
required Object error,
|
|||
|
|
required StackTrace stackTrace,
|
|||
|
|
Map<String, dynamic>? extra,
|
|||
|
|
}) => _service.error(
|
|||
|
|
message: message,
|
|||
|
|
error: error,
|
|||
|
|
stackTrace: stackTrace,
|
|||
|
|
module: module,
|
|||
|
|
extra: extra,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Logger getLogger(String module) => Logger.get(module);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 6: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/core/logging/
|
|||
|
|
git commit -m "feat(logging): add logging module skeleton"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 2: 全局错误处理器
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `apps/lib/core/logging/error_handler.dart`
|
|||
|
|
- Modify: `apps/lib/main.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Create error_handler.dart**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// apps/lib/core/logging/error_handler.dart
|
|||
|
|
|
|||
|
|
class AppErrorHandler {
|
|||
|
|
final Logger _logger = getLogger('flutter.error');
|
|||
|
|
|
|||
|
|
void register() {
|
|||
|
|
FlutterError.onError = (details) {
|
|||
|
|
_logger.error(
|
|||
|
|
message: 'FlutterError: ${details.exceptionAsString()}',
|
|||
|
|
error: details.exceptionAsString(),
|
|||
|
|
stackTrace: details.stack ?? StackTrace.current,
|
|||
|
|
extra: {'context': 'FlutterError.onError'},
|
|||
|
|
);
|
|||
|
|
FlutterError.presentError(details);
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Modify main.dart**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// main.dart
|
|||
|
|
|
|||
|
|
import 'core/logging/logger.dart';
|
|||
|
|
import 'core/logging/log_service.dart';
|
|||
|
|
|
|||
|
|
void main() async {
|
|||
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|||
|
|
|
|||
|
|
final logService = await LogService.create();
|
|||
|
|
Logger.setLogService(logService);
|
|||
|
|
|
|||
|
|
AppErrorHandler().register();
|
|||
|
|
|
|||
|
|
await configureDependencies();
|
|||
|
|
await Env.init();
|
|||
|
|
|
|||
|
|
getLogger('app').info(
|
|||
|
|
message: 'App starting...',
|
|||
|
|
extra: {'version': Env.appVersion, 'platform': Env.platformName},
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
runApp(const LinksyApp());
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 3: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/core/logging/error_handler.dart apps/lib/main.dart
|
|||
|
|
git commit -m "feat(logging): add global error handler"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 3: 添加 path_provider 依赖
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `apps/pubspec.yaml`
|
|||
|
|
|
|||
|
|
**Step 1: Add dependency**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
dependencies:
|
|||
|
|
path_provider: ^2.1.2
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/pubspec.yaml
|
|||
|
|
git commit -m "feat(logging): add path_provider dependency"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 4: 迁移 ChatBloc 示例
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Modify: `apps/lib/features/chat/presentation/bloc/chat_bloc.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Add logging to ChatBloc**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
// chat_bloc.dart
|
|||
|
|
|
|||
|
|
import '../../../../core/logging/logger.dart';
|
|||
|
|
|
|||
|
|
class ChatBloc extends Bloc<ChatBlocEvent, ChatBlocState> {
|
|||
|
|
final _logger = getLogger('features.chat.bloc');
|
|||
|
|
|
|||
|
|
void _onChatSend(ChatBlocSend event) {
|
|||
|
|
_logger.info(
|
|||
|
|
message: 'Sending chat message',
|
|||
|
|
extra: {
|
|||
|
|
'message_length': event.message.length,
|
|||
|
|
'attachments_count': event.attachments.length,
|
|||
|
|
},
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// ... 业务逻辑
|
|||
|
|
_logger.info(
|
|||
|
|
message: 'Chat message sent successfully',
|
|||
|
|
extra: {'message_id': result.id},
|
|||
|
|
);
|
|||
|
|
} catch (e, stackTrace) {
|
|||
|
|
_logger.error(
|
|||
|
|
message: 'Failed to send chat message',
|
|||
|
|
error: e,
|
|||
|
|
stackTrace: stackTrace,
|
|||
|
|
extra: {'message': event.message},
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/lib/features/chat/presentation/bloc/chat_bloc.dart
|
|||
|
|
git commit -m "feat(logging): add logging to ChatBloc"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 日志输出示例
|
|||
|
|
|
|||
|
|
### Debug 模式 (console)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
2026-04-01T10:30:00.123 DEBUG [features.chat.bloc@onChatSend:142] Chat message sent successfully {'message_id': 'abc123'}
|
|||
|
|
#0 ChatBloc._onChatSend (package:social_app/features/chat/presentation/bloc/chat_bloc.dart:142)
|
|||
|
|
...
|
|||
|
|
|
|||
|
|
2026-04-01T10:30:01.456 ERROR [features.chat.bloc@onChatSend:156] Failed to send chat message [SocketException]
|
|||
|
|
#0 ChatBloc._onChatSend (package:social_app/features/chat/presentation/bloc/chat_bloc.dart:156)
|
|||
|
|
...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Release 模式 (file)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
[2026-04-01T10:30:00.123] DEBUG [features.chat.bloc]
|
|||
|
|
at onChatSend:142
|
|||
|
|
Chat message sent successfully
|
|||
|
|
Extra: {'message_id': 'abc123'}
|
|||
|
|
|
|||
|
|
[2026-04-01T10:30:01.456] ERROR [features.chat.bloc]
|
|||
|
|
at onChatSend:156
|
|||
|
|
Failed to send chat message
|
|||
|
|
Error: SocketException
|
|||
|
|
StackTrace:
|
|||
|
|
#0 ChatBloc._onChatSend (package:social_app/features/chat/presentation/bloc/chat_bloc.dart:156)
|
|||
|
|
...
|
|||
|
|
Extra: {'message': 'Hello'}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## API 使用规范
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
import 'core/logging/logger.dart';
|
|||
|
|
|
|||
|
|
final logger = getLogger('module.path');
|
|||
|
|
|
|||
|
|
// debug - 可选 extra
|
|||
|
|
logger.debug(message: 'Debug info', extra: {'key': 'value'});
|
|||
|
|
|
|||
|
|
// info - 必须有 extra
|
|||
|
|
logger.info(message: 'Operation completed', extra: {'id': '123'});
|
|||
|
|
|
|||
|
|
// warning - 必须有 extra
|
|||
|
|
logger.warning(message: 'Deprecated API called', extra: {'api': 'old'});
|
|||
|
|
|
|||
|
|
// error - 必须有 error 和 stackTrace
|
|||
|
|
logger.error(
|
|||
|
|
message: 'Operation failed',
|
|||
|
|
error: e,
|
|||
|
|
stackTrace: stackTrace,
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Task 5: 单元测试
|
|||
|
|
|
|||
|
|
**Files:**
|
|||
|
|
- Create: `apps/test/core/logging/log_entry_test.dart`
|
|||
|
|
|
|||
|
|
**Step 1: Write tests**
|
|||
|
|
|
|||
|
|
```dart
|
|||
|
|
void main() {
|
|||
|
|
group('LogEntry', () {
|
|||
|
|
test('toJson should serialize correctly', () {
|
|||
|
|
final entry = LogEntry(
|
|||
|
|
timestamp: DateTime(2026, 4, 1, 10, 0, 0),
|
|||
|
|
level: LogLevel.error,
|
|||
|
|
message: 'Test error',
|
|||
|
|
module: 'test.module',
|
|||
|
|
funcName: 'testFunc',
|
|||
|
|
lineNo: 42,
|
|||
|
|
errorType: 'TestError',
|
|||
|
|
stackTrace: 'test stack',
|
|||
|
|
extra: {'key': 'value'},
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
final json = entry.toJson();
|
|||
|
|
|
|||
|
|
expect(json['level'], 'error');
|
|||
|
|
expect(json['error_type'], 'TestError');
|
|||
|
|
expect(json['extra'], {'key': 'value'});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Step 2: Commit**
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
git add apps/test/core/logging/
|
|||
|
|
git commit -m "test(logging): add logging tests"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 执行方式
|
|||
|
|
|
|||
|
|
**两个选择:**
|
|||
|
|
|
|||
|
|
1. **Subagent-Driven (当前 session)** - 我 dispatch 新的 subagent 逐个执行任务,任务间 review,快速迭代
|
|||
|
|
|
|||
|
|
2. **Parallel Session (新 session)** - 在 worktree 中打开新 session,使用 executing-plans,批量执行带 checkpoint
|
|||
|
|
|
|||
|
|
**选择哪个?**
|