# 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? 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 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 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 flush() async { await _sink?.flush(); } Future close() async { await _sink?.close(); _sink = null; _file = null; } Future> 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 = []; static const _maxBufferSize = 50; LogService._({required LogConfig config}) : _config = config; static Future 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? 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 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 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? 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> 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? extra, StackTrace? stackTrace, }) => _service.debug( message: message, module: module, extra: extra ?? {}, stackTrace: stackTrace, ); void info({ required String message, required Map extra, StackTrace? stackTrace, }) => _service.info( message: message, module: module, extra: extra, stackTrace: stackTrace, ); void warning({ required String message, required Map 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? 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 { 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 **选择哪个?**