Flutter 日志模块之参考设计

目录

[1. 📂 简介](#1. 📂 简介)

[2. 🔱 核心架构设计](#2. 🔱 核心架构设计)

[3. 💠 使用步骤](#3. 💠 使用步骤)

[3.1 添加依赖](#3.1 添加依赖)

[3.2 初始化](#3.2 初始化)

[3.3 打印日志](#3.3 打印日志)

[4. ⚛️ 工程化封装](#4. ⚛️ 工程化封装)

[4.1 第一层:优雅的调用入口](#4.1 第一层:优雅的调用入口)

[4.1.1 快捷函数 (log_utils.dart)](#4.1.1 快捷函数 (log_utils.dart))

[4.1,2 日志管理器 (log_manager.dart)](#4.1,2 日志管理器 (log_manager.dart))

[4.2 第二层:配置与中控逻辑](#4.2 第二层:配置与中控逻辑)

[4.2.1 灵活的配置 (log_config.dart)](#4.2.1 灵活的配置 (log_config.dart))

[4.2.2 逻辑中控 (log_core.dart)](#4.2.2 逻辑中控 (log_core.dart))

[4.3 第三层:性能优化与实现](#4.3 第三层:性能优化与实现)

[4.3.1 异步缓冲区机制 (file_logger.dart)](#4.3.1 异步缓冲区机制 (file_logger.dart))

[4.3.2 控制台美化 (console_logger.dart)](#4.3.2 控制台美化 (console_logger.dart))

[5. ✅ 小结](#5. ✅ 小结)


1. 📂 简介

在 Flutter 项目开发中,日志系统是调试和线上排查的"眼睛"。很多开发者习惯直接使用 print 或 debugPrint,但在中大型项目中,这显然不够:

  1. 无法持久化:线上用户报错,没日志等于盲飞。

  2. 性能隐患:高频的 IO 操作会阻塞 UI 线程。

  3. 职责混乱:打印逻辑散落在业务代码各处,难以统一开关。

今天分享一套三层架构 的日志模块设计方案,支持异步缓冲区写入、多处理器分发 以及自动清理机制

2. 🔱 核心架构设计

为了降低维护成本,我们将日志系统分为三层,各司其职:

  • 第一层:门面层 (Facade):提供最简单的全局函数和单例入口,让开发者"无感"调用。

  • 第二层:核心层 (Core):负责配置解析、级别过滤、逻辑分发。

  • 第三层:实现层 (Handler):具体的落地执行者,如控制台着色打印、文件分片存储。

3. 💠 使用步骤

3.1 添加依赖

复制代码
# pubspec.yaml
dependencies:
  #  用于国际化(i18n)和本地化(l10n)操作,如日期、时间、数字格式化等。
  intl: ^0.20.2
  #  用于在 Flutter 应用中进行日志记录。它支持多种输出形式,如控制台、文件、以及彩色日志。
  logger: ^1.1.0
  #  提供了在设备上查找常用文件目录的方法,如获取应用的文档目录、临时目录等。
  path_provider: ^2.1.3

3.2 初始化

复制代码
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // 根据编译模式选择配置
  await LogManager().initialize(
    config: kReleaseMode ? LogConfig.production() : LogConfig.defaultConfig()
  );
  
  runApp(const MyApp());
}

3.3 打印日志

复制代码
logI("App Running...");

4. ⚛️ 工程化封装

4.1 第一层:优雅的调用入口

我们不希望在业务代码里看到复杂的初始化逻辑,因此通过 log_utils.dart 提供一套快捷函数。

4.1.1 快捷函数 (log_utils.dart)

复制代码
///
/// Description:    日志工具类
/// CreateDate:     2025/12/17 11:41
/// Author:         agg
///
void logD(String message, {String? tag = "MoJieGlass"}) {
  LogManager.debug(tag != null ? '[$tag] $message' : message);
}

void logI(String message, {String? tag = "MoJieGlass"}) {
  LogManager.info(tag != null ? '[$tag] $message' : message);
}

void logE(String message, {String? tag = "MoJieGlass"}) {
  LogManager.error(tag != null ? '[$tag] $message' : message);
}

4.1,2 日志管理器 (log_manager.dart)

LogManager 采用单例模式,作为整个系统的"总线",负责生命周期管理。

复制代码
///
/// Description:    日志管理器(单例)
/// CreateDate:     2025/12/17 11:40
/// Author:         agg
///
class LogManager {
  // 声明静态变量
  static final LogManager _instance = LogManager._internal();

  // 工厂构造函数
  factory LogManager() => _instance;

  // 私有命名构造函数
  LogManager._internal();

  LogCore? _logCore;
  LogConfig? _config;

  /// 初始化日志系统
  Future<void> initialize({LogConfig? config}) async {
    if (_logCore != null) {
      await _logCore!.dispose();
    }

    _config = config ?? LogConfig.defaultConfig();
    _logCore = LogCore(config: _config!);
    await _logCore!.initialize();
  }

  /// 获取日志核心实例
  LogCore get logger {
    if (_logCore == null) {
      throw StateError('LogManager not initialized. Call initialize() first.');
    }
    return _logCore!;
  }

  /// 获取配置
  LogConfig? get config => _config;

  /// 是否已初始化
  bool get isInitialized => _logCore != null;

  /// 快捷访问方法
  static void verbose(String message) {
    _instance.logger.verbose(message);
  }

  static void debug(String message) {
    _instance.logger.debug(message);
  }

  static void info(String message) {
    _instance.logger.info(message);
  }

  static void warning(String message) {
    _instance.logger.warning(message);
  }

  static void error(String message) {
    _instance.logger.error(message);
  }

  static void wtf(String message) {
    _instance.logger.wtf(message);
  }

  /// 网络日志
  static void network(
    String method,
    String url, {
    dynamic data,
    Map<String, dynamic>? headers,
    int? statusCode,
    String? response,
    int duration = 0,
  }) {
    _instance.logger.network(
      method,
      url,
      data: data,
      headers: headers,
      statusCode: statusCode,
      response: response,
      duration: duration,
    );
  }

  /// 销毁资源
  Future<void> dispose() async {
    await _logCore?.dispose();
    _logCore = null;
    _config = null;
  }
}

4.2 第二层:配置与中控逻辑

这一层是整个模块的大脑,决定了日志"去哪里"和"存多久"。

4.2.1 灵活的配置 (log_config.dart)

通过 LogConfig,我们可以轻松实现:开发环境看控制台,生产环境关掉控制台只存文件。

复制代码
///
/// Description:    日志配置类
/// CreateDate:     2025/12/17 11:19
/// Author:         agg
///
class LogConfig {
  /// 是否启用文件日志
  final bool enableFileLog;

  /// 是否启用控制台日志
  final bool enableConsoleLog;

  /// 最小日志级别(低于此级别的日志不会被记录)
  final Level minLevel;

  /// 日志目录路径,为空则使用默认目录
  final String? logDirectory;

  /// 单个日志文件最大大小(字节),默认 5MB
  final int maxFileSize;

  /// 内存缓冲区最大日志条数
  final int maxBufferSize;

  /// 日志文件名前缀
  final String filePrefix;

  /// 日志文件扩展名
  final String fileExtension;

  /// 保留日志文件的天数
  final int daysToKeep;

  /// 默认配置
  factory LogConfig.defaultConfig() => LogConfig(
    enableFileLog: true,
    enableConsoleLog: true,
    minLevel: Level.verbose,
    maxFileSize: 5 * 1024 * 1024,
    maxBufferSize: 100,
    daysToKeep: 7,
  );

  /// 生产环境配置
  factory LogConfig.production() => LogConfig(
    enableFileLog: true,
    enableConsoleLog: true,
    // 生产环境关闭控制台日志
    minLevel: Level.info,
    // 生产环境只记录info及以上级别,10MB文件大小
    maxFileSize: 10 * 1024 * 1024,
    maxBufferSize: 200,
    // 生产环境保留更久
    daysToKeep: 30,
  );

  const LogConfig({
    required this.enableFileLog,
    required this.enableConsoleLog,
    this.minLevel = Level.verbose,
    this.logDirectory,
    this.maxFileSize = 5 * 1024 * 1024,
    this.maxBufferSize = 100,
    this.filePrefix = 'app',
    this.fileExtension = 'log',
    this.daysToKeep = 7,
  });

  /// 验证配置
  void validate() {
    if (maxFileSize <= 0) {
      throw ArgumentError('maxFileSize must be greater than 0');
    }
    if (maxBufferSize <= 0) {
      throw ArgumentError('maxBufferSize must be greater than 0');
    }
    if (daysToKeep <= 0) {
      throw ArgumentError('daysToKeep must be greater than 0');
    }
  }
}

4.2.2 逻辑中控 (log_core.dart)

LogCore 持有两个处理器:ConsoleLogger 和 FileLogger。

复制代码
///
/// Description:    核心日志类
/// CreateDate:     2025/12/17 11:39
/// Author:         agg
///
class LogCore {
  final LogConfig config;
  late ConsoleLogger _consoleLogger;
  late FileLogger _fileLogger;
  bool _isInitialized = false;
  bool _isDisposed = false;

  LogCore({required this.config}) {
    config.validate();
    _initLoggers();
  }

  void _initLoggers() {
    if (config.enableConsoleLog) {
      _consoleLogger = ConsoleLogger();
    }

    if (config.enableFileLog) {
      _fileLogger = FileLogger(
        customDirectory: config.logDirectory,
        maxFileSize: config.maxFileSize,
        maxBufferSize: config.maxBufferSize,
        filePrefix: config.filePrefix,
        fileExtension: config.fileExtension,
        daysToKeep: config.daysToKeep,
      );
    }
  }

  /// 初始化日志系统
  Future<void> initialize() async {
    if (_isDisposed) {
      throw StateError('LogCore has been disposed');
    }

    try {
      if (config.enableFileLog) {
        await _fileLogger.initialize();
      }
      _isInitialized = true;
    } catch (e) {
      _isInitialized = false;
      // 文件日志初始化失败不影响控制台日志
      if (config.enableConsoleLog) {
        _consoleLogger.log(
          level: Level.error,
          message: 'Failed to initialize file logger: $e',
        );
      }
    }
  }

  void log({required Level level, required String message}) {
    if (!_isInitialized || _isDisposed) return;
    if (level.index < config.minLevel.index) return;

    try {
      // 控制台日志
      if (config.enableConsoleLog) {
        _consoleLogger.log(level: level, message: message);
      }

      // 文件日志
      if (config.enableFileLog) {
        _fileLogger.log(level: level, message: message);
      }
    } catch (e) {
      // 日志记录失败不应影响应用运行
      print('Logging failed: $e');
    }
  }

  void verbose(String message) {
    log(level: Level.verbose, message: message);
  }

  void debug(String message) {
    log(level: Level.debug, message: message);
  }

  void info(String message) {
    log(level: Level.info, message: message);
  }

  void warning(String message) {
    log(level: Level.warning, message: message);
  }

  void error(String message) {
    log(level: Level.error, message: message);
  }

  void wtf(String message) {
    log(level: Level.wtf, message: message);
  }

  /// 网络请求日志
  void network(
    String method,
    String url, {
    dynamic data,
    Map<String, dynamic>? headers,
    int? statusCode,
    String? response,
    int duration = 0,
  }) {
    final message =
        '''
[NETWORK] $method $url
Duration: ${duration}ms
Status: $statusCode
${headers != null ? 'Headers: $headers' : ''}
${data != null ? 'Request: $data' : ''}
${response != null ? 'Response: $response' : ''}
    '''
            .trim();

    info(message);
  }

  /// 获取日志文件
  Future<List<File>> getLogFiles() async {
    if (!_isInitialized || _isDisposed || !config.enableFileLog) {
      return [];
    }
    return await _fileLogger.getLogFiles();
  }

  /// 获取最近日志
  Future<String> getRecentLogs({int lines = 100, String? filePath}) async {
    if (!_isInitialized || _isDisposed || !config.enableFileLog) {
      return '';
    }
    return await _fileLogger.getRecentLogs(lines: lines, filePath: filePath);
  }

  /// 强制刷新缓冲区
  Future<void> flush() async {
    if (!_isInitialized || _isDisposed || !config.enableFileLog) {
      return;
    }
    await _fileLogger.flush();
  }

  /// 销毁资源
  Future<void> dispose() async {
    if (_isDisposed) return;

    _isDisposed = true;

    try {
      await _fileLogger.dispose();
    } catch (e) {
      // 忽略清理错误
    }

    try {
      _consoleLogger.dispose();
    } catch (e) {
      // 忽略清理错误
    }
  }
}

4.3 第三层:性能优化与实现

最考验功力的地方在于 FileLogger 的实现。频繁打开/关闭文件是非常耗电且影响性能的。

4.3.1 异步缓冲区机制 (file_logger.dart)

为了极致性能,我们引入了 Buffer (缓冲区)。日志产生后不立即写磁盘,而是先存在内存里。

  • 触发条件1:缓冲区达到 100 条日志。

  • 触发条件2:定时器每隔 1s 强制刷新。

    ///
    /// Description: 文件日志处理器
    /// CreateDate: 2025/12/17 11:37
    /// Author: agg
    ///
    class FileLogger {
    final String? customDirectory;
    final int maxFileSize;
    final int maxBufferSize;
    final String filePrefix;
    final String fileExtension;
    final int daysToKeep;

    复制代码
    File? _currentFile;
    final List<String> _buffer = [];
    Timer? _periodicTimer;
    late String _logDirectory;
    final DateFormat _dateFormat = DateFormat('yyyy-MM-dd');
    final DateFormat _timeFormat = DateFormat('HH:mm:ss.SSS');
    bool _isInitialized = false;
    bool _isDisposed = false;
    
    FileLogger({
      this.customDirectory,
      this.maxFileSize = 5 * 1024 * 1024,
      this.maxBufferSize = 100,
      this.filePrefix = 'app',
      this.fileExtension = 'log',
      this.daysToKeep = 7,
    });
    
    /// 初始化文件日志
    Future<void> initialize() async {
      if (_isDisposed) {
        throw StateError('FileLogger has been disposed');
      }
    
      try {
        Directory directory;
        if (customDirectory != null && customDirectory!.isNotEmpty) {
          directory = Directory(customDirectory!);
        } else {
          Directory? appDir;
          if (Platform.isAndroid) {
            appDir = await getExternalStorageDirectory();
          } else {
            appDir = await getApplicationDocumentsDirectory();
          }
          directory = Directory('${appDir?.path}/logs');
          // Android 日志路径: /sdcard/Android/data/<package_name>/files/logs/
          // iOS 日志路径: <AppHome>/Documents/logs
        }
    
        _logDirectory = directory.path;
    
        if (!await directory.exists()) {
          await directory.create(recursive: true);
        }
    
        await _setupLogFile();
        await _cleanOldLogs();
    
        _isInitialized = true;
    
        // 写入初始化日志
        await _writeToBuffer(
          '========== Log System Initialized ==========',
          skipBuffer: true,
        );
    
        // 启动定时器1s定期刷新缓冲区
        _periodicTimer = Timer.periodic(Duration(seconds: 1), (_) {
          if (_buffer.isNotEmpty) {
            _flushBuffer();
          }
        });
      } catch (e) {
        _isInitialized = false;
        rethrow;
      }
    }
    
    /// 设置日志文件
    Future<void> _setupLogFile() async {
      final now = DateTime.now();
      final dateStr = _dateFormat.format(now);
      final baseName = '${_logDirectory}/$filePrefix\_$dateStr';
    
      // 检查是否存在今天的日志文件
      String filePath = '$baseName.$fileExtension';
      File file = File(filePath);
    
      // 如果文件存在且超过大小限制,创建带时间戳的新文件
      if (await file.exists()) {
        final length = await file.length();
        if (length >= maxFileSize) {
          final timeStr = DateFormat('HH-mm-ss').format(now);
          filePath = '${baseName}_$timeStr.$fileExtension';
          file = File(filePath);
        }
      }
    
      _currentFile = file;
    }
    
    /// 写入日志
    Future<void> log({required Level level, required String message}) async {
      if (!_isInitialized || _isDisposed) return;
    
      try {
        final timestamp = _timeFormat.format(DateTime.now());
        var levelStr = "";
        if (level == Level.info) {
          levelStr = "INFOS";
        } else {
          levelStr = level.name.toUpperCase();
        }
    
        await _writeToBuffer('[$timestamp][$levelStr] $message');
      } catch (e) {
        // 文件日志失败时不应影响应用运行
        print('File logger error: $e');
      }
    }
    
    /// 写入缓冲区
    Future<void> _writeToBuffer(
      String logEntry, {
      bool skipBuffer = false,
    }) async {
      _buffer.add('$logEntry\n');
    
      // 如果缓冲区满了或者需要立即写入,则刷新到文件
      if (_buffer.length >= maxBufferSize || skipBuffer) {
        await _flushBuffer();
      }
    }
    
    /// 刷新缓冲区到文件
    Future<void> _flushBuffer() async {
      if (_buffer.isEmpty || _currentFile == null || _isDisposed) return;
    
      try {
        await _currentFile!.writeAsString(
          _buffer.join(),
          mode: FileMode.append,
          flush: true,
        );
        _buffer.clear();
      } catch (e) {
        // 如果写入失败,保留缓冲区内容下次再试
        print('Failed to flush log buffer: $e');
      }
    }
    
    /// 清理旧日志文件
    Future<void> _cleanOldLogs() async {
      try {
        final directory = Directory(_logDirectory);
        if (!await directory.exists()) return;
    
        final files = await directory
            .list()
            .where((entity) => entity is File)
            .cast<File>()
            .where((file) => file.path.endsWith('.$fileExtension'))
            .toList();
    
        final cutoffDate = DateTime.now().subtract(Duration(days: daysToKeep));
    
        for (final file in files) {
          try {
            final stat = await file.stat();
            if (stat.modified.isBefore(cutoffDate)) {
              await file.delete();
            }
          } catch (e) {
            // 忽略单个文件删除失败
          }
        }
      } catch (e) {
        // 清理失败不应影响主要功能
      }
    }
    
    /// 获取所有日志文件
    Future<List<File>> getLogFiles() async {
      if (!_isInitialized || _isDisposed) return [];
    
      final directory = Directory(_logDirectory);
      if (!await directory.exists()) return [];
    
      try {
        final files = await directory
            .list()
            .where((entity) => entity is File)
            .cast<File>()
            .where((file) => file.path.endsWith('.$fileExtension'))
            .toList();
    
        // 按修改时间排序(最新的在前)
        files.sort((a, b) {
          try {
            final statA = a.statSync();
            final statB = b.statSync();
            return statB.modified.compareTo(statA.modified);
          } catch (e) {
            return 0;
          }
        });
    
        return files;
      } catch (e) {
        return [];
      }
    }
    
    /// 获取最近日志
    Future<String> getRecentLogs({int lines = 100, String? filePath}) async {
      if (!_isInitialized || _isDisposed) return '';
    
      try {
        File file;
        if (filePath != null) {
          file = File(filePath);
        } else {
          final files = await getLogFiles();
          if (files.isEmpty) return 'No log files found';
          file = files.first;
        }
    
        if (!await file.exists()) return 'Log file not found';
    
        final content = await file.readAsString();
        final linesList = content.split('\n');
    
        final start = linesList.length > lines ? linesList.length - lines : 0;
        return linesList.sublist(start).join('\n');
      } catch (e) {
        return 'Error reading logs: $e';
      }
    }
    
    /// 强制刷新缓冲区
    Future<void> flush() => _flushBuffer();
    
    /// 销毁资源
    Future<void> dispose() async {
      if (_isDisposed) return;
    
      _isDisposed = true;
      _periodicTimer?.cancel();
      await _flushBuffer();
      _buffer.clear();
      _currentFile = null;
    }

    }

4.3.2 控制台美化 (console_logger.dart)

通过集成 logger 库,我们可以让输出带上边框、时间戳和颜色,方便快速定位问题。

复制代码
///
/// Description:    控制台日志处理器
/// CreateDate:     2025/12/17 11:34
/// Author:         agg
///
class ConsoleLogger {
  late Logger _logger;

  ConsoleLogger() {
    _initLogger();
  }

  void _initLogger() {
    _logger = Logger(
      printer: PrettyPrinter(
        methodCount: 0,
        errorMethodCount: 8,
        lineLength: 120,
        colors: false,
        printEmojis: false,
        printTime: false,
        noBoxingByDefault: true,
      ),
      filter: DevelopmentFilter(),
      level: Level.verbose,
    );
  }

  /// 记录日志到控制台
  void log({
    required Level level,
    required String message,
    dynamic error,
    StackTrace? stackTrace,
  }) {
    try {
      final fullMessage = error != null ? '$message\nError: $error' : message;

      switch (level) {
        case Level.verbose:
          _logger.v(fullMessage);
          break;
        case Level.debug:
          _logger.d(fullMessage);
          break;
        case Level.info:
          _logger.i(fullMessage);
          break;
        case Level.warning:
          _logger.w(fullMessage);
          break;
        case Level.error:
          _logger.e(fullMessage);
          break;
        case Level.wtf:
          _logger.wtf(fullMessage);
          break;
        case Level.nothing:
          break;
      }
    } catch (e) {
      // 防止日志记录本身导致应用崩溃
      print('Console logger error: $e');
    }
  }

  /// 销毁资源
  void dispose() {
    // Logger库没有dispose方法,这里预留接口
  }
}

5. ✅ 小结

这套方案通过三层架构实现了职责分离,通过异步缓冲区解决了性能瓶颈,通过 LogConfig 适配了多环境需求。

另外,由于本人能力有限,如有错误,敬请批评指正,谢谢。

相关推荐
0和1的舞者6 天前
SpringBoot日志框架全解析
java·学习·springboot·日志·打印·lombok
余子桃12 天前
ubutun日志文件自动流转
linux·日志
少云清13 天前
【接口测试】7_代码实现 _日志收集
接口测试·日志·代码实现
Gogo81615 天前
Node.js 生产环境避坑指南:从 PM2“麦当劳理论”到日志全链路治理
node.js·日志·pm2
长安第一美人18 天前
C 语言可变参数(...)实战:从 logger_print 到通用日志函数
c语言·开发语言·嵌入式硬件·日志·工业应用开发
逻极20 天前
Python MySQL监控与日志配置实战:从“盲人摸象”到“明察秋毫”
python·mysql·监控·日志
色空大师21 天前
【linux查看日志】
java·linux·运维·日志
没有bug.的程序员21 天前
SOA、微服务、分布式系统的区别与联系
java·jvm·微服务·架构·wpf·日志·gc
fakerth22 天前
【OpenHarmony】日志服务hilog_lite
操作系统·日志·openharmony