手把手带你写flutter:2、日志框架(下)

95 阅读3分钟

1. 设计目标

1.1 核心目标

1.2 功能特性

graph TD
    A[DSLogger功能] --> B1[基础功能]
    A --> B2[存储功能]
    A --> B3[扩展功能]
    
    B1 --> C1[日志分级]
    B1 --> C2[标签支持]
    B1 --> C3[异常追踪]
    
    B2 --> D1[文件存储]
    B2 --> D2[自动分片]
    B2 --> D3[定期清理]
    
    B3 --> E1[长文本处理]
    B3 --> E2[Base64预览]
    B3 --> E3[控制台美化]

2. 系统设计

2.1 整体架构

graph TD
    A[应用层] --> B[DSLogger]
    B --> C1[日志处理]
    B --> C2[文件管理]
    B --> C3[缓存管理]
    C1 --> D1[格式化]
    C1 --> D2[过滤]
    C2 --> D3[写入]
    C2 --> D4[清理]

2.2 包结构

lib/
├── core/
│   ├── ds_logger.dart          // 核心Logger类
│   ├── log_config.dart         // 配置类
│   └── log_level.dart          // 日志级别定义
├── formatter/
│   ├── base_formatter.dart     // 格式化基类
│   ├── console_formatter.dart  // 控制台格式化
│   └── file_formatter.dart     // 文件格式化
├── writer/
│   ├── base_writer.dart        // 写入器基类
│   ├── console_writer.dart     // 控制台写入
│   └── file_writer.dart        // 文件写入
├── buffer/
│   ├── log_buffer.dart         // 日志缓冲
│   └── buffer_manager.dart     // 缓冲管理
└── utils/
    ├── file_utils.dart         // 文件工具
    └── format_utils.dart       // 格式化工具

2.3 核心类实现

2.3.1 日志记录器

class DSLogger {
 static final DSLogger _instance = DSLogger._internal();
 factory DSLogger() => _instance;
 
 late final LogConfig _config;
 late final LogBuffer _buffer;
 late final List<LogWriter> _writers;
 late final LogFormatter _formatter;
 
 Future<void> init(LogConfig config) async {
   _config = config;
   _buffer = LogBuffer(
     maxSize: config.bufferSize,
     flushInterval: config.flushInterval,
   );
   
   _writers = [
     if (config.enableConsole) ConsoleWriter(),
     if (config.enableFile) 
       await FileWriter.initialize(
         directory: config.logDir,
         maxFileSize: config.maxFileSize,
         maxFiles: config.maxFiles,
       ),
   ];
   
   _formatter = config.formatter ?? DefaultFormatter();
 }
 
 void log(
   LogLevel level,
   String message, {
   String? tag,
   dynamic error,
   StackTrace? stackTrace,
 }) {
   if (level.index < _config.minLevel.index) return;
   
   final record = LogRecord(
     level: level,
     message: message,
     tag: tag,
     error: error,
     stackTrace: stackTrace ?? StackTrace.current,
     timestamp: DateTime.now(),
   );
   
   _buffer.add(record);
 }
 
 // 便捷方法
 void v(String message, {String? tag, dynamic error}) =>
     log(LogLevel.verbose, message, tag: tag, error: error);
     
 void d(String message, {String? tag, dynamic error}) =>
     log(LogLevel.debug, message, tag: tag, error: error);
 
 // ... 其他日志级别方法

2.3.2 文件写入器

class FileWriter implements LogWriter {
 final String directory;
 final int maxFileSize;
 final int maxFiles;
 
 File? _currentFile;
 int _currentFileSize = 0;
 
 static Future<FileWriter> initialize({
   required String directory,
   required int maxFileSize,
   required int maxFiles,
 }) async {
   final writer = FileWriter._(
     directory: directory,
     maxFileSize: maxFileSize,
     maxFiles: maxFiles,
   );
   
   await writer._initializeDirectory();
   await writer._setupCurrentFile();
   
   return writer;
 }
 
 Future<void> write(List<LogRecord> records) async {
   final content = records.map(_formatter.format).join('\n');
   
   if (_shouldRotateFile(_currentFileSize + content.length)) {
     await _rotateFiles();
   }
   
   await _currentFile?.writeAsString(
     '$content\n',
     mode: FileMode.append,
     flush: true,
   );
   
   _currentFileSize += content.length;
 }
 
 Future<void> _rotateFiles() async {
   // 文件轮转逻辑
 }

2.3.3 缓冲管理器

class LogBuffer {
  final int maxSize
 final Duration flushInterval;
 final Queue<LogRecord> _buffer = Queue();
 Timer? _flushTimer;
 
 LogBuffer({
   required this.maxSize,
   required this.flushInterval,
 }) {
   _startFlushTimer();
 }
 
 void add(LogRecord record) {
   _buffer.add(record);
   if (_buffer.length >= maxSize) {
     flush();
   }
 }
 
 Future<void> flush() async {
   if (_buffer.isEmpty) return;
   
   final records = List<LogRecord>.from(_buffer);
   _buffer.clear();
   
   for (final writer in DSLogger().writers) {
     try {
       await writer.write(records);
     } catch (e, stack) {
       print('Error writing logs: $e\n$stack');
     }
   }
 }
 
 void _startFlushTimer() {
   _flushTimer?.cancel();
   _flushTimer = Timer.periodic(flushInterval, (_) => flush());
 }

3. 关键实现

3.1 日志格式化

class ConsoleFormatter implements LogFormatter {
  static const _levelColors = {
    LogLevel.verbose: '\x1B[37m', // white
    LogLevel.debug: '\x16m',   // cyan
   LogLevel.info: '\x1B[32m',    // green
   LogLevel.warning: '\x1B[33m', // yellow
   LogLevel.error: '\x1B[31m',   // red
 };
 
 @override
 String format(LogRecord record) {
   final time = _formatTime(record.timestamp);
   final level = _formatLevel(record.level);
   final tag = record.tag != null ? '[${record.tag}] ' : '';
   
   var message = '$time $level $tag${record.message}';
   
   if (record.error != null) {
     message += '\nError: ${record.error}';
   }
   
   if (record.stackTrace != null) {
     message += '\nStack trace:\n${record.stackTrace}';
   }
   
   return message;
 }
 
 String _formatTime(DateTime time) =>
     '${time.hour.toString().padLeft(2, '0')}:'
     '${time.minute.toString().padLeft(2, '0')}:'
     '${time.second.toString().padLeft(2, '0')}.'
     '${time.millisecond.toString().padLeft(3, '0')}';
     
 String _formatLevel(LogLevel level) {
   final color = _levelColors[level] ?? '';
   final reset = '\x1B[0m';
   return '$color[${level.name.toUpperCase()}]$reset';
 }

3.2 文件管理

class LogFileManager {
  static Future<List<File>> getLogiles(String directory) async {
   final dir = Directory(directory);
   if (!await dir.exists()) return [];
   
   final files = await dir
       .list()
       .where((entity) => entity is File && entity.path.endsWith('.log'))
       .cast<File>()
       .toList();
   
   // 按修改时间排序
   files.sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()));
   
   return files;
 }
 
 static Future<void> cleanOldFiles(
   String directory,
   int maxFiles,
 ) async {
   final files = await getLogFiles(directory);
   
   if (files.length > maxFiles) {
     final filesToDelete = files.sublist(maxFiles);
     for (final file in filesToDelete) {
       await file.delete();
     }
   }
 }
 
 static String generateFileName() {
   final now = DateTime.now();
   return 'log_${now.year}${now.month}${now.day}_'
          '${now.hour}${now.minute}${now.second}.log';
 }

4. 性能优化

4.1 写入优化

class BatchWriter {
  staticconst int _maxBatchSize = 100;
 static const Duration _maxDelay = Duration(seconds: 1);
 
 final Queue<LogRecord> _batch = Queue();
 Timer? _batchTimer;
 
 void add(LogRecord record) {
   _batch.add(record);
   _scheduleBatchProcess();
 }
 
 void _scheduleBatchProcess() {
   if (_batch.length >= _maxBatchSize) {
     _processBatch();
   } else if (_batchTimer == null) {
     _batchTimer = Timer(_maxDelay, _processBatch);
   }
 }
 
 Future<void> _processBatch() async {
   _batchTimer?.cancel();
   _batchTimer = null;
   
   if (_batch.isEmpty) return;
   
   final records = List<LogRecord>.from(_batch);
   _batch.clear();
   
   await _writeBatch(records);
 }

5. 应用集成

5.1 初始化配置

void main() async {
  // 确保 Flutter 绑定初始化
  WidgetsFlutterBinding.ensureInitialized();
  
  // 初始化日志系统
  await DSLogger.init(
    LogConfig(
      minLevel: LogLevel.debug,  // 开发环境使用 DEBUG,生产环境建议 INFO
      logDir: await _getLogDirectory(),
      maxFileSize: 10 * 1024 * 1024,  // 10MB
      maxFiles: 5,
      enableConsole: true,  // 开发环境打开,生产环境建议关闭
      bufferSize: 100,
      flushInterval: Duration(seconds: 1),
    ),
  );
  
  runApp(MyApp());
}

Future<String> _getLogDirectory() async {
  final appDir = await getApplicationDocumentsDirectory();
  return path.join(appDir.path, 'logs');
}

5.2 常见使用场景

5.2.1 网络请求日志

class NetworkLogger {
  static final _logger = DSLogger();
  
  static void logRequest(
    String url, 
    String method, 
    Map<String, dynamic>? headers,
    dynamic body,
  ) {
    _logger.d('API Request',
      tag: 'Network',
      error: {
        'url': url,
        'method': method,
        'headers': headers,
        'body': body,
      },
    );
  }
  
  static void logResponse(
    String url,
    int statusCode,
    dynamic response,
    Duration duration,
  ) {
    _logger.d('API Response',
      tag: 'Network',
      error: {
        'url': url,
        'statusCode': statusCode,
        'duration': '${duration.inMilliseconds}ms',
        'response': response,
      },
    );
  }
}

5.2.2 错误边界处理

class LoggingErrorBoundary extends StatelessWidget {
  final Widget child;
  final DSLogger _logger = DSLogger();

  LoggingErrorBoundary({required this.child});

  @override
  Widget build(BuildContext context) {
    return ErrorWidget.builder = (FlutterErrorDetails details) {
      _logger.e('UI Render Error',
        error: details.exception,
        stackTrace: details.stack,
      );
      
      // 在开发环境显示错误信息,生产环境显示友好提示
      return kDebugMode
          ? ErrorWidget(details.exception)
          : Container(
              alignment: Alignment.center,
              child: Text('Something went wrong'),
            );
    };
  }
}

6. 最佳实践

6.1 日志分级使用

级别 使用场景 示例
VERBOSE详细的调试信息 界面生命周期、细节状态变化
DEBUG开发调试信息API 请求响应、关键流程节点
INFO重要业务信息 用户操作、业务流程完成
WARNING潜在问题警告非致命错误、性能警告
ERROR错误信息 异常捕获、业务错误

6.2 性能优化建议

graph TD
    A[性能优化] --> B1[合理分级]
    A --> B2[批量写入]
    A --> B3[及时清理]
    
    B1 --> C1[生产环境使用INFO级别]
    B2 --> C2[使用缓冲区]
    B3 --> C3[定期清理旧日志]

7. 使用指南

7.1 基础用法

void main() async {
  // 初始化
  await DSLogger.init(
    LogConfig(
      minLevel: LogLevel.debug,
      logDir: 'app_logs',
      maxFileSize: 10 * 1024 * 1024, // 10MB
      maxFiles: 5,
    ),
  );

  final logger = DSLogger();

  // 基础日志记录
  logger.d('Debug message');
  logger.i('Info message', tag: 'Network');

  // 长文本日志处理
  final longText = 'Very long text ' * 1000;
  logger.d('Long text content', 
    tag: 'Content',
    // 自动截断和分行处理
    error: LogContent.text(longText, maxLength: 1000), 
  );

  // Base64图片日志
  final base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgA...';
  logger.i('Image uploaded', 
    tag: 'Upload',
    // 智能处理Base64数据
    error: LogContent.base64(
      base64Image,
      preview: true, // 显示前100个字符
      type: 'image',
    ),
  );

  // JSON数据美化
  final jsonData = {
    'user': {'id': 1, 'name': 'Test'},
    'data': List.generate(100, (i) => 'item $i'),
  };
  logger.d('API Response', 
    tag: 'Network',
    // 自动格式化JSON
    error: LogContent.json(jsonData),
  );

  // 二进制数据处理
  final bytes = Uint8List.fromList([0x68, 0x65, 0x6C, 0x6C, 0x6F]);
  logger.d('Binary data',
    tag: 'Data',
    // 转换为可读格式
    error: LogContent.binary(bytes, format: BinaryFormat.hex),
  );
}

7.2 数据处理工具

/// 日志内容处理工具
class LogContent {
  // 长文本处理
  static String text(String content, {
    int maxLength = 1000,
    bool showEllipsis = true,
  }) {
    if (content.length <= maxLength) return content;
    return '${content.substring(0, maxLength)}'
           '${showEllipsis ? '...(${content.length - maxLength} more chars)' : ''}';
  }

  // Base64处理
  static String base64(String content, {
    bool preview = true,
    String? type,
    int previewLength = 100,
  }) {
    final typeInfo = type != null ? '[$type] ' : '';
    final size = content.length;
    if (!preview) return '$typeInfo(Base64 size: $size)\n$content';
    
    return '$typeInfo(Base64 size: $size)\n'
           '${content.substring(0, math.min(previewLength, content.length))}'
           '${content.length > previewLength ? '...' : ''}';
  }

  // JSON美化
  static String json(dynamic content) {
    const encoder = JsonEncoder.withIndent('  ');
    try {
      return encoder.convert(content);
    } catch (e) {
      return content.toString();
    }
  }

  // 二进制数据处理
  static String binary(Uint8List bytes, {
    BinaryFormat format = BinaryFormat.hex,
    int maxLength = 100,
  }) {
    switch (format) {
      case BinaryFormat.hex:
        return bytes.map((b) => b.toRadixString(16).padLeft(2, '0'))
                   .join(' ');
      case BinaryFormat.base64:
        return base64Encode(bytes);
      default:
        return bytes.toString();
    }
  }
}

8.END