Flutter/Dart:使用日志模块Logger Easier

Flutter笔记
Flutter/Dart:使用日志模块Logger Easier

Logger Easier 是一个为 DartFlutter 应用程序量身定制的功能强大的现代化日志管理解决方案。它提供了一个高度灵活、功能丰富的日志记录系统,旨在简化开发者的日志管理工作,同时提供一定的定制能力。
【注】本文对模块的功能、基本用法、架构思想做了介绍。但一切API以当前版本我实际提供的API为准。

- 文章信息 -
Author: 李俊才 (jcLee95)
Visit me at CSDN: https://jclee95.blog.csdn.net
My WebSitehttp://thispage.tech/
Email: 291148484@163.com.
Shenzhen China
Address of this article:https://blog.csdn.net/qq_28550263/article/details/144636200
HuaWei:https://bbs.huaweicloud.com/blogs/443063

在这里插入图片描述

组件库地址

在这里插入图片描述
在这里插入图片描述

目 录

1. 介绍

1.1 特性概述

为了能使用类似于其它语言那样的日志,比如 Logger for Java(l4j),logger for JavaScript(l4js)等日志库,但又希望尽可能简化配置,同时参考Dart其它的日志库实现,也结合Dart语言本身的特点,完成了flutter_logger_easier。

这个日志插件可以通过自定义日志 中间件 来对 日志 进行加工或处理,比如 控制台输出、文件保存、日志文件轮转和压缩,这都可以使用一个或者多个中间件来处理。但是这些不是必须,如果你需要使用它,最简单的方式只需要直接实例化一个Logger()。

Logger Easier模块具有如下特点:

  1. 多级日志管理

    • 支持 7 个日志级别:Trace, Debug, Info, Warn, Error, Critical, Fatal;
    • 细粒度的日志级别控制;
    • 可配置的最小日志记录级别;
  2. 灵活的日志输出

    • 控制台输出(支持彩色日志);
    • 文件日志记录;
    • 自定义输出目标;
    • 日志文件轮转和压缩;
  3. 高级日志格式化

    • 可定制的日志格式;
    • 支持多种日志格式模板;
    • 丰富的日志记录元数据(时间戳、来源、错误信息等);
  4. 性能监控(开发中)

    • 内置性能度量方法;
    • 异步和同步操作性能追踪;
    • 性能指标自动记录和报告;
  5. 错误处理

    • 自动错误报告
    • 堆栈跟踪记录
    • 可插拔的错误报告器
  6. 单例模式

    • 全局统一的日志管理
    • 简单的初始化和使用
  7. 日志轮转

    • 内置高可扩展性的日志轮转架构;
    • 开箱即用的日志轮转和日志压缩;
    • 可以轻松实现自定义轮转策略;
    • 可以实现任何自定义日志压缩和解压算法;

最后一点就是可扩展性,正如之前所述,由于采用中间件的方式,你可以将日志看作生产线上的产品,可以在流水线上通过多个中间件执行相应操作。

1.2 整体架构简介

Logger Easier 采用了模块化和中间件的设计理念,将日志处理流程解耦为多个独立的组件。整体架构遵循单一职责原则和开闭原则,使得系统具有高度的可扩展性和灵活性。

1.2.1 核心架构

整个日志系统的核心是 Logger 类,它作为日志处理的入口点,管理着一系列中间件。每个中间件都可以包含自己的输出器、格式化器和过滤器,形成一个完整的日志处理单元。

Logger
Middleware 1
Middleware 2
Middleware n...
Outputer
Formatter
Filter
Outputer
Formatter
Filter
Outputer
Formatter
Filter

1.2.2 中间件架构

中间件是整个系统的核心概念,每个中间件都实现了 AbstractLogMiddleware 接口,包含三个主要组件:

  1. 输出器(Outputer):负责将日志写入具体的目标,如控制台或文件
  2. 格式化器(Formatter):负责将日志记录转换为特定格式的字符串
  3. 过滤器(Filter):负责决定是否处理特定的日志记录
AbstractLogMiddleware
+AbstractOutputer outputer
+AbstractLogFormatter formatter
+AbstractLogFilter filter
+handle(LogRecord) : Future
+close() : Future
AbstractOutputer
+printf(LogRecord) : String
+init() : Future
+close() : Future
AbstractLogFormatter
+String name
+config
+format(LogRecord) : String
AbstractLogFilter
+shouldLog(LogRecord) : bool

1.2.3 日志轮转架构

日志轮转系统采用了策略模式,通过 LogRotateManager 统一管理不同的轮转策略和压缩处理:

LogRotateManager
+RotateStrategy strategy
+CompressionHandler compressionHandler
+checkAndRotate() : Future
RotateStrategy
+shouldRotate() : bool
+getRotatedFileName() : String
+cleanupOldLogs() : Future
CompressionHandler
+compress() : Future
+decompress() : Future

1.2.4 异步处理架构

为了提高性能,系统实现了异步日志处理机制。AsyncOutputer 类提供了一个基于队列的异步处理实现:

abstract class AsyncOutputer implements AbstractOutputer {
  final Queue<LogRecord> _buffer = Queue<LogRecord>();
  final StreamController<LogRecord> _recordController;
  
  Future<void> processRecord(LogRecord record);
  
  void _startProcessor() {
    _recordController.stream.listen((record) {
      _buffer.add(record);
      if (_buffer.length >= maxQueueSize) {
        _flush();
      }
    });
  }
}

1.2.5 扩展性

系统的每个组件都基于接口设计,允许用户通过实现相应接口来扩展功能:

// 自定义输出器示例
class CustomOutputer extends AbstractOutputer {
  
  String printf(LogRecord record) {
    // 自定义输出逻辑
  }
}

// 自定义格式化器示例
class CustomFormatter implements AbstractLogFormatter {
  
  String format(LogRecord record) {
    // 自定义格式化逻辑
  }
}

// 自定义过滤器示例
class CustomFilter implements AbstractLogFilter {
  
  bool shouldLog(LogRecord record) {
    // 自定义过滤逻辑
  }
}

上述模块化的设计使得系统具有很强的可扩展性,用户可以根据需要替换或扩展任何组件,而不会影响其他部分的功能。同时,异步处理机制确保了日志记录不会影响应用程序的主要性能。

2. 安装

pubspec.yaml 中添加依赖:

dependencies:
  logger_easier: 替换为最新版本

运行 dart pub getflutter pub get 安装依赖。

也可以使用add命令直接安装最新版本:

dart pub add logger_easier
# 或
flutter pub add logger_easier

3. 用法

3.1 入门示例

【注】:从v0.0.3+1版本之前,不需要显示使用控制台输出中间件ConsoleMiddleware。这个中间件是一个内部提供的中间件之一,主要用于控制台打印。关于中间件的概念,将在后面的章节中详细介绍。

一个最简单的完整示例是这样的:

// example_project/bin/main.dart
import 'package:logger_easier/logger_easier.dart';

void main(List<String> arguments) {
  final logger = Logger();

  // 实例化一个控制台中间件
  final consoleMiddleware = ConsoleMiddleware();

  // 安装该中间件实例
  logger.use(consoleMiddleware);

  // 记录不同级别的日志
  logger.trace('这是一条追踪日志');
  logger.debug('这是一条调试日志');
  logger.info('这是一条信息日志');
  logger.warn('这是一条警告日志');
  logger.error('这是一条错误日志');
  logger.critical('这是一条严重错误日志');
  logger.fatal('这是一条致命错误日志');
}

这种最基础的用法会使用默认配置:

  • 最小日志级别为 DEBUG
  • 使用控制台输出
  • 启用默认的颜色支持
  • 使用基础格式化器
  • 输出效果大致如下:

在这里插入图片描述

3.2 日志级别

日志级别是日志系统中非常重要的概念,它帮助开发者控制日志的详细程度和重要性。Logger Easier 提供了 7 个日志级别,从最详细到最严重,每个级别都有其特定的使用场景。

Trace 级别是最详细的日志级别,主要用于非常细粒度的诊断信息。在开发和深度调试阶段,你可能需要记录非常详细的执行路径和状态变化。

logger.trace('进入方法 calculateSum,参数:a = $a, b = $b');

Debug 级别适用于开发和诊断阶段,记录对开发者有帮助的详细信息,但在生产环境中通常会被禁用。

logger.debug('计算中间结果:intermediateResult = $intermediateResult');

Info 级别记录应用程序的重要事件和关键流程,这些信息在生产环境中是有意义的,可以帮助理解系统的运行状态。

logger.info('用户 $userId 成功登录');

Warn 级别用于记录潜在的问题或异常情况,这些情况可能不会立即导致系统失败,但值得关注。

logger.warn('数据库连接池接近最大连接数');

Error 级别表示导致功能异常的问题,这些错误会影响特定功能的正常运行,但不会完全中断系统。

try {
  // 某些可能抛出异常的操作
} catch (e) {
  logger.error('文件读取失败', error: e);
}

Critical 级别表示严重错误,这些错误可能会严重影响系统的运行,需要立即关注和处理。

logger.critical('关键服务不可用', error: serviceUnavailableException);

Fatal 级别是最严重的日志级别,表示系统已经无法继续运行,通常意味着需要立即重启或进行紧急恢复。

logger.fatal('系统核心组件崩溃', error: criticalSystemError);

在实际使用中,你可以通过设置最小日志级别来控制记录哪些日志。例如,在生产环境中,你可能只想记录 Info 及以上级别的日志:

final logger = Logger(
  minLevel: LogLevel.info, // 只记录 Info 及以上级别的日志
);

这种灵活的日志级别设计使得 Logger Easier 能够适应不同的开发和运行环境,帮助开发者更好地管理和分析系统日志。

3.3 日志中间件

3.4.1 中间件的概念

中间件是 Logger Easier 灵活性的关键,它用于允许各位开发者自定义日志处理流程。中间件可以看作是日志处理的流水线,每个中间件都可以对日志进行过滤、格式化、打印或输出。

Logger Easier 的中间件由三个核心组件组成:打印器(Printer)、格式化器(Formatter)和过滤器(Filter)。这种设计提供了极高的灵活性和可扩展性。

最简单的中间件配置是使用默认实现:

final logger = Logger(
  middlewares: [
    LogMiddleware(
      printer: ConsolePrinter(),
      formatter: SimpleFormatter(),
      filter: LevelFilter(LogLevel.debug),
    )
  ]
);

你可以创建自定义的中间件组件来满足特定需求。例如,创建一个自定义的打印器:

class CustomPrinter extends BasePrinter {
  
  String printf(LogRecord record) {
    // 自定义日志打印逻辑
    return '[CUSTOM] ${record.timestamp} - ${record.message}';
  }

  
  String get name => 'CustomPrinter';

  
  Map<String, dynamic> get config => {};
}

自定义格式化器同样简单:

class CustomFormatter implements LogFormatter {
  
  String format(LogRecord record) {
    // 自定义日志格式
    return '${record.level}|${record.timestamp}|${record.message}';
  }

  // 实现其他必需的接口方法
  
  String get name => 'CustomFormatter';
}

过滤器允许更精细的日志控制:

class CustomFilter implements LogFilter {
  
  bool shouldLog(LogRecord record) {
    // 复杂的日志过滤逻辑
    return record.level.value >= LogLevel.warn.value && 
           !record.message.contains('ignore');
  }
}

多个中间件可以串联使用,创建复杂的日志处理管道:

final logger = Logger(
  middlewares: [
    // 控制台输出中间件
    LogMiddleware(
      printer: ConsolePrinter(useColor: true),
      formatter: SimpleFormatter(),
      filter: LevelFilter(LogLevel.debug),
    ),
    // 文件输出中间件
    LogMiddleware(
      printer: FilePrinter(
        logDirectory: '/path/to/logs',
        baseFileName: 'app.log',
      ),
      formatter: JsonFormatter(),
      filter: LevelFilter(LogLevel.info),
    ),
    // 网络日志中间件(假设存在)
    LogMiddleware(
      printer: NetworkPrinter(url: 'https://log-collector.example.com'),
      formatter: CompactFormatter(),
      filter: LevelFilter(LogLevel.error),
    )
  ]
);

这种中间件架构的优势在于其极高的灵活性。你可以轻松地添加、移除或替换日志处理组件,而不需要修改核心日志记录逻辑。每个中间件都专注于单一职责:打印器负责输出,格式化器负责格式,过滤器负责筛选,这种设计遵循了单一职责原则。

通过自定义中间件,你可以实现各种高级日志需求,如敏感信息脱敏、日志加密、实时日志分析等。Logger Easier 为开发者提供了一个强大而灵活的日志解决方案。

4. 核心组件

4.1 日志中间件

4.1.1 中间件的概念

中间件是 Logger Easier 中最核心的概念之一。它采用了类似于管道处理的设计模式,将日志处理过程划分为多个独立的处理单元。每个中间件都可以对日志记录进行检查、修改、过滤或转发等操作。

在 Logger Easier 中,中间件是实现了 AbstractLogMiddleware 接口的组件,它包含三个核心部分:

  1. 输出器(Outputer):负责将日志实际输出到目标位置,如控制台或文件
  2. 格式化器(Formatter):负责将日志记录转换为特定格式的字符串
  3. 过滤器(Filter):负责决定是否处理特定的日志记录

中间件的处理流程如下:

abstract class AbstractLogMiddleware {
  late AbstractOutputer outputer;
  late AbstractLogFormatter formatter;
  late AbstractLogFilter filter;

  Future<void> handle(LogRecord record) async {
    // 1. 首先通过过滤器判断是否需要处理该日志
    if (!filter.shouldLog(record)) {
      return;
    }

    // 2. 使用格式化器格式化日志记录
    final formattedRecord = record.copyWith(
      message: formatter.format(record),
    );

    // 3. 通过输出器输出格式化后的日志
    outputer.printf(formattedRecord);
  }
}

举个例子,当你创建一个控制台中间件时,实际上是这样的过程:

final consoleMiddleware = ConsoleMiddleware(
  formatter: BaseFormatter(
    includeTimestamp: true,
    includeLevel: true,
    includeStackTrace: true,
  ),
  filter: LevelFilter(LogLevel.debug),
);

logger.use(consoleMiddleware);

这个中间件会:

  1. 使用 LevelFilter 过滤掉低于 DEBUG 级别的日志
  2. 使用 BaseFormatter 将日志格式化为包含时间戳、日志级别和堆栈跟踪的格式
  3. 使用 ConsolePrinter 将格式化后的日志输出到控制台

中间件的这种设计使得日志处理变得高度可定制化。你可以:

  • 添加多个中间件以实现不同的处理目的
  • 为每个中间件配置不同的过滤规则
  • 使用不同的格式化器来满足不同的输出需求
  • 实现自定义的输出器将日志发送到任何目标

这种灵活性使得 Logger Easier 能够适应各种日志处理场景,从简单的控制台输出到复杂的分布式日志系统都能胜任。

4.1.2 中间件的(3大)组成部分

中间件(Middleware)在 Logger Easier 中由三个核心组件组成:输出器(Outputer)、格式化器(Formatter)和过滤器(Filter)。这三个组件各自负责不同的职责,共同构成了一个完整的日志处理单元。

1. 输出器(Outputer)

输出器实现了 AbstractOutputer 接口,负责将格式化后的日志实际输出到目标位置。它定义了日志的最终输出形式,例如输出到控制台或写入文件:

abstract class AbstractOutputer with ColorSupportMixin {
  /// 打印日志
  String printf(LogRecord record);

  /// 初始化打印器
  Future<void> init() async {}

  /// 关闭打印器
  Future<void> close() async {}

  /// 获取打印器的配置信息
  Map<String, dynamic> get config;

  /// 更新打印器的配置
  Future<void> updateConfig(Map<String, dynamic> newConfig) async {}
}
2. 格式化器(Formatter)

格式化器实现了 AbstractLogFormatter 接口,负责将原始的日志记录转换为特定格式的字符串。它决定了日志的展现形式,包括时间戳、日志级别、消息等信息的排列方式:

abstract class AbstractLogFormatter<T> {
  /// 格式化日志记录
  String format(LogRecord record);

  /// 获取格式化器的名称
  String get name;

  /// 获取格式化器的配置
  T get config;

  /// 更新格式化器的配置
  void updateConfig(Map<String, dynamic> newConfig);

  /// 创建一个新的格式化器实例
  AbstractLogFormatter clone();
}
3. 过滤器(Filter)

过滤器实现了 AbstractLogFilter 接口,负责决定是否处理特定的日志记录。它可以基于日志级别、正则表达式或其他自定义条件来过滤日志:

abstract class AbstractLogFilter {
  /// 判断是否应该记录给定的日志记录
  bool shouldLog(LogRecord record);
}

这三个组件在中间件中的组合方式如下:

abstract class AbstractLogMiddleware {
  /// 日志输出器
  late AbstractOutputer outputer;

  /// 日志格式化器
  late AbstractLogFormatter formatter;

  /// 日志过滤器
  late AbstractLogFilter filter;

  AbstractLogMiddleware({
    AbstractOutputer? outputer,
    AbstractLogFormatter? formatter,
    AbstractLogFilter? filter,
  }) {
    this.formatter = formatter ?? createFormatter();
    this.filter = filter ?? createFilter();
    this.outputer = outputer ?? createOutputer();
  }

  /// 创建输出器
  AbstractOutputer createOutputer();

  /// 创建格式化器
  AbstractLogFormatter createFormatter();

  /// 创建过滤器
  AbstractLogFilter createFilter();
}

当一条日志记录通过中间件处理时,这三个组件会按照以下顺序工作:

  1. 首先,过滤器判断是否需要处理该日志记录
  2. 如果通过过滤,格式化器会将日志记录转换为指定格式
  3. 最后,输出器将格式化后的日志输出到目标位置

这种模块化的设计使得每个组件都可以独立配置和替换,为日志处理提供了极大的灵活性。例如,你可以在一个中间件中组合不同的组件:

final consoleMiddleware = ConsoleMiddleware(
  outputer: ConsolePrinter(useColor: true),
  formatter: BaseFormatter(includeTimestamp: true),
  filter: LevelFilter(LogLevel.debug)
);

这种组件化的设计不仅使得中间件的功能清晰可控,也为自定义扩展提供了便利。你可以通过实现这些接口来创建自己的输出器、格式化器或过滤器,从而满足特定的日志处理需求。

4.1.3 AbstractLogMiddleware 接口

【参考连接-接口源码】:AbstractLogMiddleware

AbstractLogMiddleware 是 Logger Easier 中所有日志中间件的基础接口,它定义了中间件的基本结构和行为。这个接口位于 lib/src/interfaces/abstract_log_middleware.dart 文件中,是实现自定义中间件的起点。

中间件接口的核心定义如下:

// lib/src/interfaces/abstract_log_middleware.dart
abstract class AbstractLogMiddleware {
  /// 日志输出器
  late AbstractOutputer outputer;

  /// 日志格式化器
  late AbstractLogFormatter formatter;

  /// 日志过滤器
  late AbstractLogFilter filter;

  AbstractLogMiddleware({
    AbstractOutputer? outputer,
    AbstractLogFormatter? formatter,
    AbstractLogFilter? filter,
  }) {
    this.formatter = formatter ?? createFormatter();
    this.filter = filter ?? createFilter();
    this.outputer = outputer ?? createOutputer();
  }

  /// 创建一个日志输出器
  AbstractOutputer createOutputer();

  /// 创建一个日志格式化器
  AbstractLogFormatter createFormatter();

  /// 创建一个日志过滤器
  AbstractLogFilter createFilter();

  /// 处理一条日志记录
  Future<void> handle(LogRecord record);

  /// 关闭中间件
  Future<void> close();
}

这个接口定义了中间件的三个核心组件:

  1. outputer:负责实际的日志输出
  2. formatter:负责日志格式化
  3. filter:负责日志过滤

中间件的构造函数采用了命名可选参数的方式,允许在创建中间件时传入这三个组件的具体实现。如果没有提供具体实现,中间件会通过 createOutputer()createFormatter()createFilter() 方法创建默认实现。

接口定义了两个关键的异步方法:

  • handle(LogRecord record):处理单条日志记录的核心方法
  • close():用于清理资源的关闭方法

一个典型的中间件实现(以 ConsoleMiddleware 为例)是这样的:

【参考连接-实现源码】:ConsoleMiddleware

// lib/src/expands/middlewares/console_middleware.dart
class ConsoleMiddleware extends AbstractLogMiddleware {
  ConsoleMiddleware({
    AbstractOutputer? outputer,
    AbstractLogFormatter? formatter,
    AbstractLogFilter? filter,
  }) {
    outputer = outputer ?? createOutputer();
    formatter = formatter ?? createFormatter();
    filter = filter ?? createFilter();
  }

  
  AbstractOutputer createOutputer() {
    return ConsolePrinter(
      useColor: true,
      maxLineLength: 160,
    );
  }

  
  AbstractLogFormatter createFormatter() {
    return BaseFormatter();
  }

  
  AbstractLogFilter createFilter() {
    return LevelFilter(LogLevel.debug);
  }

  
  Future<void> handle(LogRecord record) async {
    if (!filter.shouldLog(record)) {
      return;
    }

    final formattedRecord = record.copyWith(
      message: formatter.format(record),
    );
    outputer.printf(formattedRecord);
  }

  
  Future<void> close() async {
    await outputer.close();
  }
}

通过实现这个接口,你可以创建自己的中间件,完全控制日志的处理流程。中间件可以执行如下操作:

  • 过滤特定的日志记录
  • 修改或增强日志内容
  • 将日志转发到不同的目标
  • 实现自定义的日志处理逻辑

这种设计使得日志系统具有高度的可扩展性和灵活性,能够适应各种复杂的日志处理需求。

4.1.4 内置中间件

为了使用更方便,我在 Logger Easier 中提供了两个开箱即用的中间件实现:ConsoleMiddleware 和 FileMiddleware。这两个中间件分别用于控制台输出和文件输出,能够满足大多数基础的日志记录需求。

1 ConsoleMiddleware(基本控制台中间件)

ConsoleMiddleware 是一个专门用于控制台输出的中间件,它支持彩色输出、自定义格式化和日志级别过滤等功能。这个中间件默认使用:

  • ConsolePrinter 作为输出器
  • BaseFormatter 作为格式化器
  • LevelFilter 作为过滤器

以下是 ConsoleMiddleware 的基本使用示例:

final consoleMiddleware = ConsoleMiddleware(
  formatter: BaseFormatter(
    includeTimestamp: true,
    includeLevel: true,
    includeStackTrace: true,
  ),
  filter: LevelFilter(LogLevel.debug),
);

logger.use(consoleMiddleware);

ConsoleMiddleware 的实现如下:

// packages/logger_easier/lib/src/expands/middlewares/console_middleware.dart
class ConsoleMiddleware extends AbstractLogMiddleware {
  ConsoleMiddleware({
    AbstractOutputer? outputer,
    AbstractLogFormatter? formatter,
    AbstractLogFilter? filter,
  }) {
    outputer = outputer ?? createOutputer();
    formatter = formatter ?? createFormatter();
    filter = filter ?? createFilter();
  }

  
  AbstractOutputer createOutputer() {
    return ConsolePrinter(
      useColor: true,
      maxLineLength: 160,
    );
  }

  
  AbstractLogFormatter createFormatter() {
    return BaseFormatter();
  }

  
  AbstractLogFilter createFilter() {
    return LevelFilter(LogLevel.debug);
  }
}
2 FileMiddleware(基本文件台中间件)

FileMiddleware 是用于文件输出的中间件,它提供了强大的日志文件管理功能,包括:

  • 日志文件轮转
  • 文件压缩
  • 存储空间监控
  • 异步写入

以下是 FileMiddleware 的典型配置示例:

final fileMiddleware = FileMiddleware(
  logDirectory: logDirectory,
  baseFileName: 'app.log',
  rotateConfig: LogRotateConfig(
    strategy: SizeBasedStrategy(
      maxSize: 10 * 1024 * 1024, // 10MB
      maxBackups: 5,
    ),
    compressionHandler: GzipCompressionHandler(),
    delayCompress: true,
    enableStorageMonitoring: true,
  ),
  formatter: BaseFormatter(
    includeTimestamp: true,
    includeLevel: true,
  ),
  filter: LevelFilter(LogLevel.info),
);

FileMiddleware 提供了两个便捷的工厂方法来创建常用配置:

// packages/logger_easier/lib/src/expands/middlewares/file_middleware.dart
static FileMiddleware createTimeBasedMiddleware({
  required String logDirectory,
  required String baseFileName,
  Duration rotateInterval = const Duration(days: 1),
  int maxBackups = 7,
  bool compress = true,
  AbstractLogFormatter? formatter,
  AbstractLogFilter? filter,
}) {
  final config = LogRotateConfig(
    strategy: TimeBasedStrategy(
      rotateInterval: rotateInterval,
      maxBackups: maxBackups,
    ),
    compressionHandler: compress ? GzipCompressionHandler() : null,
  );

  return FileMiddleware(
    logDirectory: logDirectory,
    baseFileName: baseFileName,
    rotateConfig: config,
    formatter: formatter,
    filter: filter,
  );
}

static FileMiddleware createSizeBasedMiddleware({
  required String logDirectory,
  required String baseFileName,
  int maxSize = 10 * 1024 * 1024,
  int maxBackups = 5,
  bool compress = true,
  AbstractLogFormatter? formatter,
  AbstractLogFilter? filter,
}) {
  final config = LogRotateConfig(
    strategy: SizeBasedStrategy(
      maxSize: maxSize,
      maxBackups: maxBackups,
    ),
    compressionHandler: compress ? GzipCompressionHandler() : null,
  );

  return FileMiddleware(
    logDirectory: logDirectory,
    baseFileName: baseFileName,
    rotateConfig: config,
    formatter: formatter,
    filter: filter,
  );
}

这两个内置中间件提供了完整的日志处理解决方案,可以单独使用,也可以组合使用以满足不同的日志记录需求。它们都支持完全的自定义配置,包括输出器、格式化器和过滤器的替换,为用户提供了极大的灵活性。

4.1.5 关于自定义中间件

自定义中间件需要继承 AbstractLogMiddleware 类并实现其所需的方法。通过自定义中间件,你可以实现特定的日志处理逻辑,比如将日志发送到远程服务器、写入数据库或进行特殊的格式转换等。

以下是一个自定义中间件的基本结构:

class CustomMiddleware extends AbstractLogMiddleware {
  CustomMiddleware({
    AbstractOutputer? outputer,
    AbstractLogFormatter? formatter,
    AbstractLogFilter? filter,
  }) : super(
          outputer: outputer,
          formatter: formatter,
          filter: filter,
        );

  
  AbstractOutputer createOutputer() {
    // 创建默认的输出器
    return CustomOutputer();
  }

  
  AbstractLogFormatter createFormatter() {
    // 创建默认的格式化器
    return BaseFormatter();
  }

  
  AbstractLogFilter createFilter() {
    // 创建默认的过滤器
    return LevelFilter(LogLevel.debug);
  }

  
  Future<void> handle(LogRecord record) async {
    if (!filter.shouldLog(record)) {
      return;
    }

    final formattedRecord = record.copyWith(
      message: formatter.format(record),
    );
    outputer.printf(formattedRecord);
  }

  
  Future<void> close() async {
    await outputer.close();
  }
}

让我们看一个更具体的例子,假设我们要创建一个将日志发送到远程服务器的中间件:

class RemoteLoggerMiddleware extends AbstractLogMiddleware {
  final String serverUrl;
  final Map<String, String> headers;

  RemoteLoggerMiddleware({
    required this.serverUrl,
    this.headers = const {},
    AbstractOutputer? outputer,
    AbstractLogFormatter? formatter,
    AbstractLogFilter? filter,
  }) : super(
          outputer: outputer,
          formatter: formatter,
          filter: filter,
        );

  
  AbstractOutputer createOutputer() {
    return RemoteOutputer(
      serverUrl: serverUrl,
      headers: headers,
    );
  }

  
  AbstractLogFormatter createFormatter() {
    return JsonFormatter(); // 假设我们需要 JSON 格式的输出
  }

  
  AbstractLogFilter createFilter() {
    return LevelFilter(LogLevel.info); // 只发送 INFO 及以上级别的日志
  }
}

// 自定义的远程输出器
class RemoteOutputer extends AsyncOutputer {
  final String serverUrl;
  final Map<String, String> headers;

  RemoteOutputer({
    required this.serverUrl,
    required this.headers,
  });

  
  Future<void> processRecord(LogRecord record) async {
    try {
      // 实现发送日志到远程服务器的逻辑
      final response = await http.post(
        Uri.parse(serverUrl),
        headers: {
          'Content-Type': 'application/json',
          ...headers,
        },
        body: jsonEncode(record.toMap()),
      );

      if (response.statusCode != 200) {
        throw Exception('Failed to send log: ${response.statusCode}');
      }
    } catch (e) {
      print('Error sending log: $e');
    }
  }
}

使用这个自定义中间件的方式如下:

final remoteLogger = RemoteLoggerMiddleware(
  serverUrl: 'https://api.example.com/logs',
  headers: {'Authorization': 'Bearer token123'},
);

logger.use(remoteLogger);

// 现在所有符合过滤条件的日志都会被发送到远程服务器
logger.info('This log will be sent to the remote server');

自定义中间件时需要注意以下几点:

  1. 继承关系:必须继承 AbstractLogMiddleware 并实现所有必需的方法。

  2. 组件创建:createOutputer()createFormatter()createFilter() 方法应该返回合适的默认实现。

  3. 异步处理:如果中间件涉及 I/O 操作(如网络请求或文件操作),应该使用 AsyncOutputer 作为输出器的基类,以确保不会阻塞主线程。

  4. 错误处理:应该妥善处理可能出现的异常,避免影响应用程序的正常运行。

  5. 资源管理:在 close() 方法中正确清理所有使用的资源。

通过自定义中间件,你可以扩展 Logger Easier 的功能,使其适应各种特定的日志处理需求。无论是简单的日志转发还是复杂的分布式日志系统,都可以通过自定义中间件来实现。

4.2 日志输出器

4.2.1 输出器的概念

输出器(Outputer)是 Logger Easier 中负责将日志实际写入到目标位置的组件。它是中间件(见上一小节)三大核心组件之一,主要负责日志的最终输出操作。在 Logger Easier 的设计中,输出器通过实现 AbstractOutputer 接口来定义统一的输出行为。

输出器的主要职责包括:

  1. 将格式化后的日志写入特定目标(如控制台、文件、网络等)
  2. 管理输出资源(如文件句柄、网络连接等)
  3. 处理输出过程中的异常情况
  4. 提供配置更新机制

一个基本的输出器实现需要包含以下核心功能:

abstract class AbstractOutputer with ColorSupportMixin {
  /// 打印日志记录
  String printf(LogRecord record);

  /// 初始化输出器
  Future<void> init() async {}

  /// 关闭输出器并清理资源
  Future<void> close() async {}

  /// 获取输出器的配置信息
  Map<String, dynamic> get config;

  /// 更新输出器的配置
  Future<void> updateConfig(Map<String, dynamic> newConfig) async {}
}

Logger Easier 提供了两种基本的输出器类型:

  1. 同步输出器:直接执行输出操作,适用于简单的输出场景,如控制台输出。例如 ConsolePrinter
class ConsolePrinter extends AbstractOutputer {
  bool useColor;
  int? maxLineLength;
  
  
  String printf(LogRecord record) {
    final message = _formatLogRecord(record);
    stdout.writeln(message);
    return message;
  }
}
  1. 异步输出器:通过队列和异步处理机制执行输出操作,适用于 I/O 密集型场景,如文件写入。例如 FilePrinter
class FilePrinter extends AsyncOutputer {
  final Queue<LogRecord> _buffer = Queue<LogRecord>();
  final StreamController<LogRecord> _recordController;
  
  
  Future<void> processRecord(LogRecord record) async {
    // 异步写入文件
    await _currentLogFile.writeAsString(
      formatter.format(record),
      mode: FileMode.append,
    );
  }
}

输出器的设计遵循以下几个重要原则:

  1. 资源管理:输出器负责管理其使用的资源,确保在不再需要时正确释放:

Future<void> close() async {
  if (_isClosed) return;
  await _flush();  // 确保所有待处理的日志都被输出
  _isClosed = true;
}
  1. 错误处理:输出器应该妥善处理可能出现的异常,避免影响应用程序的正常运行:

String printf(LogRecord record) {
  try {
    final message = _formatLogRecord(record);
    _outputToTarget(message);
    return message;
  } catch (e) {
    print('Error in outputer: $e');
    return record.message;
  }
}
  1. 可配置性:输出器应该支持运行时配置更新,以适应不同的输出需求:

Future<void> updateConfig(Map<String, dynamic> newConfig) async {
  if (newConfig.containsKey('maxLineLength')) {
    maxLineLength = newConfig['maxLineLength'] as int?;
  }
  // 其他配置更新...
}

通过这种设计,输出器为 Logger Easier 提供了灵活且可靠的日志输出能力,能够适应从简单的控制台输出到复杂的分布式日志系统等各种场景。

4.2.2 AbstractOutputer 接口

AbstractOutputer 是 Logger Easier 中所有日志输出器的基础接口,它定义了输出器的基本行为和功能。这个接口通过 Mixin 的方式混入了 ColorSupportMixin,为输出器提供了颜色支持能力。

接口的核心定义如下:

abstract class AbstractOutputer with ColorSupportMixin {
  /// 打印日志
  String printf(LogRecord record);

  /// 初始化打印器
  Future<void> init() async {}

  /// 关闭打印器
  Future<void> close() async {}

  /// 获取打印器的名称
  String get name;

  /// 获取打印器的配置信息
  Map<String, dynamic> get config;

  /// 更新打印器的配置
  Future<void> updateConfig(Map<String, dynamic> newConfig) async {}

  /// 检查打印器是否已关闭
  bool get isClosed => false;

  /// 获取打印器支持的日志级别
  List<String> get supportedLevels =>
      ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICLE', 'FATAL'];

  /// 设置打印器的颜色支持
  void setColorSupport(bool enabled) {}

  /// 获取打印器的统计信息
  Map<String, dynamic> getStats() => {};

  /// 重置打印器的统计信息
  void resetStats() {}

  /// 格式化异常信息
  String formatError(dynamic error, StackTrace? stackTrace);

  /// 格式化日志消息
  String formatMessage(dynamic message) => message.toString();
}

这个接口定义了以下关键方法和属性:

  1. 核心输出方法

    • printf(LogRecord record):输出器的主要方法,负责将日志记录输出到目标位置
    • formatMessage(dynamic message):格式化日志消息
    • formatError(dynamic error, StackTrace? stackTrace):格式化错误信息和堆栈跟踪
  2. 生命周期管理

    • init():初始化输出器,比如打开文件或建立连接
    • close():关闭输出器,清理资源
    • isClosed:表示输出器的关闭状态
  3. 配置管理

    • name:获取输出器的名称
    • config:获取当前配置
    • updateConfig(Map<String, dynamic> newConfig):更新输出器配置
  4. 统计和监控

    • getStats():获取统计信息
    • resetStats():重置统计数据
    • supportedLevels:获取支持的日志级别列表
  5. 颜色支持

    • setColorSupport(bool enabled):设置是否启用颜色输出
    • 通过 ColorSupportMixin 混入,提供了颜色相关的功能

这个接口的设计遵循了以下几个原则:

  1. 可扩展性:通过抽象方法定义核心功能,允许不同的实现采用不同的输出策略

  2. 资源管理:提供了完整的生命周期管理方法,确保资源的正确初始化和清理

  3. 可配置性:支持运行时配置更新,使输出器能够动态调整其行为

  4. 可监控性:通过统计信息接口,支持对输出器的性能和行为进行监控

通过实现这个接口,你可以创建自己的输出器,完全控制日志的输出行为。例如,一个最基本的自定义输出器实现可能如下:

class CustomOutputer extends AbstractOutputer {
  
  String printf(LogRecord record) {
    final message = formatMessage(record.message);
    // 执行自定义的输出逻辑
    return message;
  }

  
  String formatError(dynamic error, StackTrace? stackTrace) {
    return 'Error: $error\nStack: $stackTrace';
  }

  
  String get name => 'CustomOutputer';

  
  Map<String, dynamic> get config => {
        'customConfig': 'value',
      };
}

4.2.3 内置输出器

Logger Easier 提供了两个内置的输出器实现:ConsolePrinter 和 FilePrinter,它们分别用于控制台输出和文件输出。

1. ConsolePrinter

ConsolePrinter 是一个专门用于控制台输出的实现,支持彩色输出和行长度控制等特性。

final consolePrinter = ConsolePrinter(
  useColor: true,  // 启用彩色输出
  maxLineLength: 160,  // 设置最大行长度
);

ConsolePrinter 的主要特性包括:

  • 彩色输出支持:不同日志级别使用不同的颜色
  • 行长度控制:自动换行过长的日志
  • 格式化错误信息:美化错误和堆栈跟踪的输出
2. FilePrinter

FilePrinter 是一个异步文件输出器,继承自 AsyncOutputer,提供了文件写入和日志轮转功能。

final filePrinter = FilePrinter(
  logDirectory: 'logs',
  baseFileName: 'app.log',
  rotateConfig: LogRotateConfig(...),
  maxQueueSize: 1000,  // 异步队列大小
  flushInterval: Duration(seconds: 1),  // 刷新间隔
);

FilePrinter 的主要特性包括:

  • 异步写入:使用队列缓冲日志记录,避免 I/O 阻塞
  • 自动轮转:支持基于大小或时间的日志轮转
  • 批量写入:优化多条日志的写入性能
  • 错误恢复:自动重试失败的写入操作

4.2.4 异步输出器

AsyncOutputerLogger Easier 中专门用于处理异步日志输出的基类,它继承自 AbstractOutputer 并提供了队列缓冲、批处理和错误重试等机制。这个类特别适合用于文件写入、网络传输等 I/O 密集型的日志输出场景。

AsyncOutputer 的核心特性包括:

  1. 队列缓冲:使用队列存储待处理的日志记录
  2. 批量处理:支持批量处理日志以提高性能
  3. 错误重试:内置重试机制处理临时性故障
  4. 定时刷新:自动定期刷新缓冲区

以下是 AsyncOutputer 的基本使用方式:

class NetworkOutputer extends AsyncOutputer {
  final String serverUrl;
  
  NetworkOutputer({
    required this.serverUrl,
    super.maxQueueSize = 1000,
    super.flushInterval = const Duration(seconds: 1),
    super.maxRetries = 3,
    super.retryDelay = const Duration(milliseconds: 100),
  });

  
  Future<void> processRecord(LogRecord record) async {
    // 实现单条日志记录的处理逻辑
    await sendToServer(record);
  }

  
  Future<void> processBatch(List<LogRecord> records) async {
    // 实现批量日志记录的处理逻辑
    await sendBatchToServer(records);
  }
}

AsyncOutputer 提供了以下可配置参数:

AsyncOutputer({
  this.maxQueueSize = 1000,           // 最大队列大小
  this.flushInterval = const Duration(seconds: 1),  // 刷新间隔
  this.maxRetries = 3,                // 最大重试次数
  this.retryDelay = const Duration(milliseconds: 100), // 重试延迟
})

4.2.5 自定义输出器

自定义输出器可以通过继承 AbstractOutputerAsyncOutputer 来实现。根据输出目标的特性,选择合适的基类可以大大简化实现过程。

以下是几个自定义输出器的示例:

  1. 简单的同步输出器
class DatabaseOutputer extends AbstractOutputer {
  final Database db;
  
  DatabaseOutputer(this.db);

  
  String printf(LogRecord record) {
    final formattedMessage = formatMessage(record.message);
    db.insert('logs', {
      'timestamp': record.timestamp.toIso8601String(),
      'level': record.level.toString(),
      'message': formattedMessage,
    });
    return formattedMessage;
  }

  
  String get name => 'DatabaseOutputer';

  
  Map<String, dynamic> get config => {
    'database': db.path,
  };
}
  1. 异步网络输出器
class RemoteLogOutputer extends AsyncOutputer {
  final String apiEndpoint;
  final Map<String, String> headers;
  
  RemoteLogOutputer({
    required this.apiEndpoint,
    this.headers = const {},
  });

  
  Future<void> processRecord(LogRecord record) async {
    try {
      final response = await http.post(
        Uri.parse(apiEndpoint),
        headers: {
          'Content-Type': 'application/json',
          ...headers,
        },
        body: jsonEncode(record.toMap()),
      );

      if (response.statusCode != 200) {
        throw Exception('Failed to send log: ${response.statusCode}');
      }
    } catch (e) {
      print('Error sending log: $e');
      rethrow;
    }
  }

  
  Future<void> processBatch(List<LogRecord> records) async {
    try {
      final response = await http.post(
        Uri.parse('$apiEndpoint/batch'),
        headers: {
          'Content-Type': 'application/json',
          ...headers,
        },
        body: jsonEncode(records.map((r) => r.toMap()).toList()),
      );

      if (response.statusCode != 200) {
        throw Exception('Failed to send batch logs: ${response.statusCode}');
      }
    } catch (e) {
      print('Error sending batch logs: $e');
      rethrow;
    }
  }
}
  1. 带缓存的文件输出器
class CachedFileOutputer extends AsyncOutputer {
  final String filePath;
  final int maxCacheSize;
  late final File _file;
  final List<String> _cache = [];
  
  CachedFileOutputer({
    required this.filePath,
    this.maxCacheSize = 100,
  });

  
  Future<void> init() async {
    _file = File(filePath);
    if (!await _file.exists()) {
      await _file.create(recursive: true);
    }
  }

  
  Future<void> processRecord(LogRecord record) async {
    final formattedMessage = formatMessage(record.message);
    _cache.add(formattedMessage);
    
    if (_cache.length >= maxCacheSize) {
      await _flushCache();
    }
  }

  Future<void> _flushCache() async {
    if (_cache.isEmpty) return;
    
    final content = _cache.join('\n');
    await _file.writeAsString(
      '$content\n',
      mode: FileMode.append,
    );
    _cache.clear();
  }

  
  Future<void> close() async {
    await _flushCache();
    await super.close();
  }
}

在实现自定义输出器时,需要注意以下几点:

  1. 资源管理

    • init() 中初始化资源
    • close() 中清理资源
    • 确保所有异常都被适当处理
  2. 性能考虑

    • 对于 I/O 操作,优先使用 AsyncOutputer
    • 实现批处理以提高性能
    • 考虑使用缓存减少 I/O 操作
  3. 错误处理

    • 实现适当的错误重试机制
    • 记录错误信息以便调试
    • 避免因错误导致应用程序崩溃
  4. 配置管理

    • 提供合理的默认配置
    • 实现配置更新机制
    • 记录关键的配置信息

通过这种方式,你可以创建适合特定需求的自定义输出器,扩展 Logger Easier 的功能。

4.3 格式化器

4.3.1 格式化器的概念

格式化器(Formatter)是 Logger Easier 中负责将原始日志记录(LogRecord)转换为特定格式字符串的组件。它也是中间件三大核心组件之一,主要负责决定日志的展现形式,包括时间戳、日志级别、消息内容等信息的排列和格式化方式。

Logger Easier 中,所有的格式化器都实现了 AbstractLogFormatter 接口:

abstract class AbstractLogFormatter<T> {
  /// 格式化日志记录
  String format(LogRecord record);

  /// 获取格式化器的名称
  String get name;

  /// 获取格式化器的配置
  T get config;

  /// 更新格式化器的配置
  void updateConfig(Map<String, dynamic> newConfig);

  /// 创建一个新的格式化器实例
  AbstractLogFormatter clone();
}

格式化器的主要职责是处理 LogRecord 对象中的各种信息,包括:

  • 时间戳(timestamp)
  • 日志级别(level)
  • 日志消息(message)
  • 错误信息(error)
  • 堆栈跟踪(stackTrace)
  • 来源信息(source)
  • 额外数据(extra)

以内置的 BaseFormatter 为例,它的格式化逻辑如下:

class BaseFormatter implements AbstractLogFormatter {
  final bool includeTimestamp;
  final bool includeLevel;
  final bool includeStackTrace;

  
  String format(LogRecord record) {
    final buffer = StringBuffer();

    if (includeTimestamp) {
      buffer.write('[${record.timestamp}] ');
    }

    if (includeLevel) {
      buffer.write('${LogUtils.getLevelString(record.level)} ');
    }

    buffer.write(record.message);

    if (record.error != null) {
      buffer.write('\nError: ${record.error}');
    }

    if (includeStackTrace && record.stackTrace != null) {
      buffer.write('\nStack Trace:\n${record.stackTrace}');
    }

    return buffer.toString();
  }
}

格式化器的设计遵循以下几个重要原则:

  1. 可配置性:格式化器应该支持通过配置来调整输出格式,例如是否包含时间戳、日志级别等。

  2. 一致性:同一个格式化器的输出格式应该保持一致,便于日志的解析和分析。

  3. 可扩展性:通过实现 AbstractLogFormatter 接口,可以创建自定义的格式化器以满足特定需求。

  4. 性能考虑:格式化操作应该高效,避免不必要的字符串拼接和对象创建。

格式化器在日志处理流程中的位置如下:

LogRecord
Formatter
格式化字符串
Outputer

通过这种设计,Logger Easier 能够灵活地支持各种日志格式,从简单的单行文本到复杂的结构化数据都可以通过相应的格式化器来实现。

4.3.2 AbstractLogFormatter 接口

AbstractLogFormatterLogger Easier 中所有日志格式化器的基础接口,它定义了格式化器的核心功能和行为。这个接口是实现自定义格式化器的起点。

接口的核心定义如下:

// lib/src/interfaces/abstract_log_formatter.dart
abstract class AbstractLogFormatter<T> {
  /// 格式化日志记录
  String format(LogRecord record);

  /// 获取格式化器的名称
  String get name;

  /// 获取格式化器的配置
  T get config;

  /// 更新格式化器的配置
  void updateConfig(Map<String, dynamic> newConfig);

  /// 创建一个新的格式化器实例,但保持相同的配置
  AbstractLogFormatter clone();

  /// 检查两个格式化器是否相等
  
  bool operator ==(Object other);

  
  int get hashCode;

  /// 返回格式化器的字符串表示
  
  String toString();

  /// 获取格式化器支持的占位符列表
  List<String> get supportedPlaceholders;

  /// 检查给定的格式字符串是否有效
  bool isValidFormatString(String formatString);
}

这个接口定义了格式化器的几个关键方法和属性:

  1. 核心格式化方法

    • format(LogRecord record):将日志记录转换为格式化的字符串
    • isValidFormatString(String formatString):验证格式字符串的有效性
  2. 配置管理

    • name:获取格式化器的名称
    • config:获取当前配置
    • updateConfig(Map<String, dynamic> newConfig):更新格式化器配置
  3. 实例管理

    • clone():创建格式化器的副本,用于在不同的中间件中复用相同的格式化配置
  4. 格式支持

    • supportedPlaceholders:获取支持的占位符列表,用于模板替换

接口还包含了标准的 Dart 对象方法重写:

  • operator ==:用于比较两个格式化器是否相等
  • hashCode:配合相等运算符使用
  • toString:提供格式化器的字符串表示

为了方便使用,Logger Easier 提供了一个简单的格式化器实现作为参考:

// lib/src/interfaces/abstract_log_formatter.dart
class SimpleLogFormatter implements AbstractLogFormatter {
  
  String format(LogRecord record) {
    return '${record.timestamp} [${record.level}] ${record.message}';
  }

  
  String get name => 'SimpleLogFormatter';

  
  Map<String, dynamic> get config => {};

  
  void updateConfig(Map<String, dynamic> newConfig) {
    // 这个简单实现不需要配置,所以这个方法是空的
  }

  
  AbstractLogFormatter clone() => SimpleLogFormatter();

  
  List<String> get supportedPlaceholders => ['timestamp', 'level', 'message'];

  
  bool isValidFormatString(String formatString) {
    // 这个简单实现总是返回true
    return true;
  }
}

通过实现这个接口,你可以创建自己的格式化器,完全控制日志记录的格式化过程。格式化器可以执行如下操作:

  • 自定义日志记录的展现形式
  • 添加或移除特定的日志字段
  • 实现特定的格式化规则
  • 支持模板化的格式字符串

这种设计使得日志系统能够适应各种不同的格式化需求,从简单的文本输出到复杂的结构化数据格式都可以通过相应的格式化器来实现。

4.3.3 BaseFormatter

BaseFormatter 是 Logger Easier 提供的一个基础日志格式化器实现,它提供了灵活的日志格式化配置选项,可以控制日志输出中包含的各种元素。

BaseFormatter 的核心功能是将 LogRecord 对象转换为格式化的字符串,支持配置是否包含时间戳、日志级别和堆栈跟踪信息。

以下是 BaseFormatter 的基本实现:

// lib/src/expands/formatters/base_formatter.dart
class BaseFormatter implements AbstractLogFormatter {
  final bool includeTimestamp;
  final bool includeLevel;
  final bool includeStackTrace;

  BaseFormatter({
    this.includeTimestamp = true,
    this.includeLevel = true,
    this.includeStackTrace = false,
  });

  
  String format(LogRecord record) {
    final buffer = StringBuffer();

    if (includeTimestamp) {
      buffer.write('[${record.timestamp}] ');
    }

    if (includeLevel) {
      buffer.write('${LogUtils.getLevelString(record.level)} ');
    }

    buffer.write(record.message);

    if (record.error != null) {
      buffer.write('\nError: ${record.error}');
    }

    if (includeStackTrace && record.stackTrace != null) {
      buffer.write('\nStack Trace:\n${record.stackTrace}');
    }

    return buffer.toString();
  }
}

BaseFormatter 的配置选项包括:

  1. includeTimestamp:是否在日志中包含时间戳,默认为 true
  2. includeLevel:是否在日志中包含日志级别,默认为 true
  3. includeStackTrace:是否在日志中包含堆栈跟踪信息,默认为 false

使用 BaseFormatter 的典型示例如下:

final formatter = BaseFormatter(
  includeTimestamp: true,
  includeLevel: true,
  includeStackTrace: true,
);

// 在中间件中使用
final consoleMiddleware = ConsoleMiddleware(
  formatter: formatter,
);

logger.use(consoleMiddleware);

使用上述配置,日志输出的格式将类似于:

[2024-01-20 10:30:45.123456] [INFO] This is a log message
Error: Some error occurred
Stack Trace:
#0 main (file:///path/to/main.dart:10:5)
#1 _startIsolate (dart:isolate-patch/isolate_patch.dart:299:32)

BaseFormatter 还实现了 AbstractLogFormatter 接口的其他必要方法:

// lib/src/expands/formatters/base_formatter.dart

String get name => 'BaseFormatter';


Map<String, dynamic> get config => {
      'includeTimestamp': includeTimestamp,
      'includeLevel': includeLevel,
      'includeStackTrace': includeStackTrace,
    };


void updateConfig(Map<String, dynamic> newConfig) {
  // 由于字段是 final 的,配置更新需要创建新实例
}


AbstractLogFormatter clone() => BaseFormatter(
      includeTimestamp: includeTimestamp,
      includeLevel: includeLevel,
      includeStackTrace: includeStackTrace,
    );

BaseFormatter 使用 StringBuffer 来构建输出字符串,这种方式比直接的字符串连接更高效,特别是在处理大量日志时。它还通过条件判断来控制各个组件的包含与否,使得输出格式可以根据需要灵活调整。

4.3.4 自定义格式化器

自定义格式化器需要实现 AbstractLogFormatter 接口,这使得你可以创建完全符合特定需求的日志格式。通过自定义格式化器,你可以实现任何想要的日志格式,比如 JSON 格式、XML 格式,或者其他特定的结构化格式。

让我们通过几个实例来说明如何创建自定义格式化器:

  1. 首先,这是一个简单的 JSON 格式化器实现:
class JsonFormatter implements AbstractLogFormatter {
  
  String format(LogRecord record) {
    return jsonEncode({
      'timestamp': record.timestamp.toIso8601String(),
      'level': record.level.name,
      'message': record.message,
      'error': record.error?.toString(),
      'stackTrace': record.stackTrace?.toString(),
      'source': record.source,
      'extra': record.extra,
    });
  }

  
  String get name => 'JsonFormatter';

  
  Map<String, dynamic> get config => {};

  
  void updateConfig(Map<String, dynamic> newConfig) {
    // 这个简单实现不需要配置
  }

  
  AbstractLogFormatter clone() => JsonFormatter();

  
  List<String> get supportedPlaceholders => [
        'timestamp',
        'level',
        'message',
        'error',
        'stackTrace',
        'source',
        'extra'
      ];

  
  bool isValidFormatString(String formatString) => true;
}
  1. 下面是一个更复杂的自定义格式化器,它支持模板配置:
class TemplateFormatter implements AbstractLogFormatter {
  String _template;
  final Map<String, String> _placeholders;

  TemplateFormatter({
    String template = '[{timestamp}] [{level}] {message}',
    Map<String, String>? placeholders,
  })  : _template = template,
        _placeholders = placeholders ?? {};

  
  String format(LogRecord record) {
    var result = _template;
    
    // 替换基本占位符
    result = result.replaceAll('{timestamp}', record.timestamp.toIso8601String());
    result = result.replaceAll('{level}', record.level.name.toUpperCase());
    result = result.replaceAll('{message}', record.message);
    
    // 替换错误信息
    if (record.error != null) {
      result = result.replaceAll('{error}', record.error.toString());
    }
    
    // 替换堆栈信息
    if (record.stackTrace != null) {
      result = result.replaceAll('{stackTrace}', record.stackTrace.toString());
    }
    
    // 替换自定义占位符
    _placeholders.forEach((key, value) {
      result = result.replaceAll('{$key}', value);
    });
    
    return result;
  }

  
  String get name => 'TemplateFormatter';

  
  Map<String, dynamic> get config => {
        'template': _template,
        'placeholders': _placeholders,
      };

  
  void updateConfig(Map<String, dynamic> newConfig) {
    if (newConfig.containsKey('template')) {
      _template = newConfig['template'] as String;
    }
    if (newConfig.containsKey('placeholders')) {
      _placeholders.addAll(
          Map<String, String>.from(newConfig['placeholders'] as Map));
    }
  }

  
  AbstractLogFormatter clone() => TemplateFormatter(
        template: _template,
        placeholders: Map<String, String>.from(_placeholders),
      );

  
  List<String> get supportedPlaceholders =>
      ['timestamp', 'level', 'message', 'error', 'stackTrace'];

  
  bool isValidFormatString(String formatString) {
    final validPlaceholders = supportedPlaceholders
        .map((p) => '{$p}')
        .toList()
      ..addAll(_placeholders.keys.map((k) => '{$k}'));
    
    return validPlaceholders.every((p) => formatString.contains(p));
  }
}

使用这些自定义格式化器的示例:

// 使用 JSON 格式化器
final jsonFormatter = JsonFormatter();
final consoleMiddleware = ConsoleMiddleware(
  formatter: jsonFormatter,
);

// 使用模板格式化器
final templateFormatter = TemplateFormatter(
  template: '[{timestamp}] [{level}] ({custom_field}) {message}',
  placeholders: {'custom_field': 'MyApp'},
);
final fileMiddleware = FileMiddleware(
  formatter: templateFormatter,
  logDirectory: 'logs',
  baseFileName: 'app.log',
);

logger.use(consoleMiddleware);
logger.use(fileMiddleware);

// 输出示例
logger.info('Test message');
// JSON 格式化器输出:{"timestamp":"2024-01-20T10:30:00.000Z","level":"info","message":"Test message"}
// 模板格式化器输出:[2024-01-20T10:30:00.000Z] [INFO] (MyApp) Test message

在创建自定义格式化器时,需要注意以下几点:

  1. 实现所有必需的接口方法,确保格式化器能够正常工作。

  2. 考虑性能影响,避免在格式化过程中进行不必要的字符串操作。

  3. 提供合适的配置机制,使格式化器可以根据需要调整。

  4. 正确处理可能的空值和异常情况,确保格式化过程的稳定性。

  5. 如果格式化器包含状态,确保 clone() 方法正确复制所有状态。

通过自定义格式化器,你可以实现任何所需的日志格式,使日志输出完全符合你的特定需求。

4.4 过滤器

4.4.1 过滤器的概念

过滤器(Filter)是 Logger Easier 中负责决定是否处理特定日志记录的组件。作为中间件的三大核心组件之一,过滤器在日志处理流程中扮演着“守门员”的角色,通过预定义的规则来过滤掉不需要处理的日志记录。

Logger Easier 中,所有的过滤器都实现了 AbstractLogFilter 接口,这个接口非常简洁:

abstract class AbstractLogFilter {
  /// 判断是否应该记录给定的日志记录
  ///
  /// [record] 是要判断的日志记录
  /// 返回 true 如果应该记录该日志,否则返回 false
  bool shouldLog(LogRecord record);
}

过滤器的工作原理是在日志处理流程的早期阶段对日志记录进行筛选。当一条日志记录到达中间件时,首先会通过过滤器的 shouldLog 方法进行判断:

Future<void> handle(LogRecord record) async {
  // 首先通过过滤器判断是否需要处理该日志
  if (!filter.shouldLog(record)) {
    return;
  }
  // 继续处理日志...
}

守门员提供了几种内置的过滤器实现,每种都针对不同的过滤需求:

  1. 级别过滤器(LevelFilter):基于日志级别进行过滤,只允许特定级别及以上的日志通过:
class LevelFilter implements AbstractLogFilter {
  LogLevel _minLevel;

  LevelFilter(this._minLevel);

  
  bool shouldLog(LogRecord record) {
    return record.level.index >= _minLevel.index;
  }
}
  1. 复合过滤器(CompositeFilter):组合多个过滤器,只有当所有过滤器都返回 true 时才允许日志通过:
class CompositeFilter implements AbstractLogFilter {
  final List<AbstractLogFilter> filters;

  CompositeFilter(this.filters);

  
  bool shouldLog(LogRecord record) {
    return filters.every((filter) => filter.shouldLog(record));
  }
}
  1. 正则表达式过滤器(RegexFilter):根据日志消息是否匹配给定的正则表达式来决定是否记录:
class RegexFilter implements AbstractLogFilter {
  final RegExp regex;

  RegexFilter(this.regex);

  
  bool shouldLog(LogRecord record) {
    return regex.hasMatch(record.message);
  }
}

过滤器的设计遵循以下几个重要原则:

  1. 单一职责:每个过滤器只负责一种过滤逻辑,如 LevelFilter 只关注日志级别。

  2. 可组合性:通过 CompositeFilter,可以将多个过滤器组合使用,实现复杂的过滤规则。

  3. 高性能:过滤操作应该快速高效,因为它们会被频繁调用。

  4. 无状态性:过滤器通常应该是无状态的,这样可以安全地在多个地方重用。

通过这种设计,守门员能够灵活地控制日志的输出,避免产生不必要的日志记录,提高日志系统的效率和可用性。

4.4.2 AbstractLogFilter 接口

AbstractLogFilter 是 Logger Easier 中所有日志过滤器的基础接口,它定义在 lib/src/interfaces/abstract_log_filter.dart 文件中。这个接口的设计非常简洁,只包含一个核心方法:

// lib/src/interfaces/abstract_log_filter.dart
abstract class AbstractLogFilter {
  /// 判断是否应该记录给定的日志记录
  ///
  /// [record] 是要判断的日志记录
  /// 返回 true 如果应该记录该日志,否则返回 false
  bool shouldLog(LogRecord record);
}

这个接口采用了单一职责原则,只负责一件事:决定是否应该记录某条日志。通过实现 shouldLog 方法,过滤器可以基于任何条件来决定是否处理一条日志记录,例如:

  • 日志级别
  • 日志来源
  • 日志内容
  • 时间范围
  • 或者这些条件的任意组合

接口的简单性使得它非常灵活,可以轻松实现各种过滤逻辑。例如,一个基于日志级别的过滤器实现可能如下:

class LevelFilter implements AbstractLogFilter {
  final LogLevel minLevel;

  LevelFilter(this.minLevel);

  
  bool shouldLog(LogRecord record) {
    return record.level.index >= minLevel.index;
  }
}

这种设计允许过滤器在日志处理管道中作为独立的组件存在,可以被任何中间件使用。在中间件的 handle 方法中,过滤器通常是第一个被调用的组件:

Future<void> handle(LogRecord record) async {
  // 首先检查过滤器
  if (!filter.shouldLog(record)) {
    return; // 如果不满足过滤条件,直接返回
  }
  
  // 继续处理日志...
}

通过这种设计,Logger Easier 为用户提供了一个清晰的接口来实现自定义的日志过滤逻辑,同时保持了系统的模块化和可扩展性。

4.4.3 内置过滤器

Logger Easier 提供了四种内置的过滤器实现,用于不同的日志过滤场景。

1. LevelFilter

最基础的过滤器,基于日志级别进行过滤:

final levelFilter = LevelFilter(LogLevel.info);  // 只允许 INFO 及以上级别的日志

// 使用静态工厂方法创建
final debugFilter = LevelFilter.debug();  // 允许 DEBUG 及以上级别
final errorFilter = LevelFilter.error();  // 只允许 ERROR 及以上级别
2. CompositeFilter

组合多个过滤器,只有当所有子过滤器都允许时才记录日志:

final compositeFilter = CompositeFilter([
  LevelFilter(LogLevel.info),
  RegexFilter(RegExp(r'database|network')),  // 只记录包含特定关键词的日志
]);
3. RegexFilter

基于正则表达式匹配日志消息:

final regexFilter = RegexFilter(RegExp(r'ERROR|WARN'));  // 只记录包含 ERROR 或 WARN 的日志
4. TimeRangeFilter

在指定的时间范围内记录日志:

final timeRangeFilter = TimeRangeFilter(
  DateTime.now(),  // 开始时间
  DateTime.now().add(Duration(hours: 1)),  // 结束时间
);

这些过滤器可以单独使用,也可以通过 CompositeFilter 组合使用:

// 组合多个过滤器
final filter = CompositeFilter([
  LevelFilter.info(),  // 级别过滤
  RegexFilter(RegExp(r'important')),  // 内容过滤
  TimeRangeFilter(  // 时间范围过滤
    DateTime.now(),
    DateTime.now().add(Duration(hours: 1)),
  ),
]);

// 在中间件中使用
final consoleMiddleware = ConsoleMiddleware(
  filter: filter,
);

所有内置过滤器都实现了 AbstractLogFilter 接口,确保了统一的使用方式和可扩展性。你可以根据需要选择合适的过滤器,或者组合多个过滤器来实现复杂的过滤逻辑。

4.4.4 自定义过滤器

自定义过滤器需要实现 AbstractLogFilter 接口,这使得你可以创建具有特定过滤逻辑的过滤器。通过自定义过滤器,你可以实现任何复杂的日志过滤规则,比如基于特定模式、来源或其他自定义条件来过滤日志。

以下是创建自定义过滤器的基本结构:

class CustomFilter implements AbstractLogFilter {
  
  bool shouldLog(LogRecord record) {
    // 实现自定义的过滤逻辑
    return true;
  }
}

让我们看一些实用的自定义过滤器示例。首先是一个基于日志来源过滤的实现:

class SourceFilter implements AbstractLogFilter {
  final List<String> allowedSources;

  SourceFilter(this.allowedSources);

  
  bool shouldLog(LogRecord record) {
    if (record.source == null) return false;
    return allowedSources.contains(record.source);
  }
}

这个过滤器可以这样使用:

final sourceFilter = SourceFilter(['UserService', 'AuthController']);
final middleware = ConsoleMiddleware(filter: sourceFilter);
logger.use(middleware);

// 只有来自指定源的日志会被记录
logger.info('User logged in', source: 'AuthController'); // 会被记录
logger.info('Cache cleared', source: 'CacheService');    // 不会被记录

另一个有用的示例是基于日志消息模式的过滤器:

class PatternFilter implements AbstractLogFilter {
  final List<RegExp> patterns;
  final bool blacklist;

  PatternFilter({
    required this.patterns,
    this.blacklist = false,
  });

  
  bool shouldLog(LogRecord record) {
    final hasMatch = patterns.any((pattern) => pattern.hasMatch(record.message));
    return blacklist ? !hasMatch : hasMatch;
  }
}

这个过滤器可以用来过滤包含或不包含特定模式的日志:

final sensitiveFilter = PatternFilter(
  patterns: [
    RegExp(r'password', caseSensitive: false),
    RegExp(r'credit.?card', caseSensitive: false),
  ],
  blacklist: true, // 黑名单模式,匹配的消息将被过滤掉
);

logger.use(ConsoleMiddleware(filter: sensitiveFilter));

// 包含敏感信息的日志会被过滤掉
logger.info('User password is ****');        // 不会被记录
logger.info('Credit card number received');  // 不会被记录
logger.info('Payment processed');            // 会被记录

如果你需要更复杂的条件组合,可以创建一个条件过滤器:

class ConditionalFilter implements AbstractLogFilter {
  final bool Function(LogRecord) condition;

  ConditionalFilter(this.condition);

  
  bool shouldLog(LogRecord record) => condition(record);
}

这个过滤器允许你使用任意的条件函数来过滤日志:

final workHoursFilter = ConditionalFilter((record) {
  final hour = record.timestamp.hour;
  return hour >= 9 && hour < 17; // 只在工作时间记录日志
});

final debugModeFilter = ConditionalFilter((record) {
  return const bool.fromEnvironment('DEBUG_MODE') || 
         record.level.index >= LogLevel.warn.index;
});

// 组合多个过滤器
final compositeFilter = CompositeFilter([
  workHoursFilter,
  debugModeFilter,
]);

logger.use(ConsoleMiddleware(filter: compositeFilter));

在创建自定义过滤器时,需要注意以下几点:

  1. 性能考虑:过滤器的 shouldLog 方法会频繁调用,应确保其实现高效。

  2. 线程安全:如果过滤器维护了状态,需要确保其线程安全。

  3. 可配置性:考虑提供配置选项以增加过滤器的灵活性。

  4. 错误处理:妥善处理可能的异常,避免影响日志系统的正常运行。

通过这些示例,你可以看到自定义过滤器提供了极大的灵活性,允许你根据具体需求实现各种复杂的日志过滤逻辑。无论是简单的条件过滤还是复杂的规则组合,都可以通过实现 AbstractLogFilter 接口来实现。

5. 日志轮转

GIF动图示意的功能:
在这里插入图片描述

5.1 日志轮转和压缩概述

日志轮转(Log Rotation)是一种常见的日志文件管理机制,用于解决日志文件持续增长带来的存储空间问题。Logger Easier 提供了完整的日志轮转和压缩解决方案,包含以下核心功能:

日志轮转机制

当日志文件满足特定条件时(如达到大小限制或时间间隔),系统会:

  1. 将当前日志文件重命名为备份文件
  2. 创建新的空日志文件继续写入
  3. 根据配置决定是否对备份文件进行压缩
  4. 清理过期的备份文件

Logger Easier 支持两种基本的轮转策略:

  • 基于大小的轮转: 当日志文件大小超过指定限制时触发轮转
  • 基于时间的轮转: 按照固定的时间间隔(如每天、每周)进行轮转

压缩处理

为了节省存储空间,Logger Easier 支持对轮转后的日志文件进行压缩。系统:

  • 提供了开箱即用的 Gzip 压缩实现
  • 支持延迟压缩以避免影响性能
  • 允许通过实现压缩处理器接口来支持其他压缩算法

存储监控

为了防止日志写入导致系统存储空间耗尽,Logger Easier 内置了存储监控功能:

  • 定期检查可用存储空间
  • 当可用空间低于阈值时暂停日志写入
  • 支持跨平台的存储空间检测

这些功能通过统一的配置接口进行管理,使得用户可以根据实际需求灵活配置日志文件的管理策略。后续章节将详细介绍每个功能的具体实现和使用方法。

5.2 轮转配置

日志轮转配置在 Logger Easier 中通过 LogRotateConfig 类来管理。这个配置类提供了全面的轮转参数控制,包括轮转策略、压缩处理、存储监控等多个方面。

5.2.1 LogRotateConfig

LogRotateConfig 类是日志轮转配置的核心,它封装了所有与日志轮转相关的配置参数。以下是其基本结构:

// packages/logger_easier/lib/src/log_rotate/rotate_config.dart
class LogRotateConfig {
  /// 日志轮转策略
  final RotateStrategy strategy;

  /// 压缩处理器
  final CompressionHandler? compressionHandler;

  /// 是否延迟压缩
  final bool delayCompress;

  /// 检查间隔
  final Duration checkInterval;

  /// 是否启用异步轮转
  final bool enableAsyncRotation;

  /// 轮转队列大小
  final int rotationQueueSize;

  /// 是否启用存储监控
  final bool enableStorageMonitoring;

  /// 最小剩余空间
  final int minimumFreeSpace;

  /// 是否在文件名中包含日期
  final bool includeDate;

  /// 是否在文件名中包含具体时间
  final bool includeTime;

  /// 日期时间分隔符
  final String separator;

  /// 归档目录
  final String? archiveDir;
}

创建一个基本的轮转配置非常简单:

final config = LogRotateConfig(
  strategy: SizeBasedStrategy(
    maxSize: 10 * 1024 * 1024, // 10MB
    maxBackups: 5,
  ),
  compressionHandler: GzipCompressionHandler(),
  delayCompress: true,
  checkInterval: const Duration(minutes: 5),
);

5.2.2 配置参数说明

  1. 轮转策略(strategy)

    • 类型:RotateStrategy
    • 必需参数
    • 决定何时进行日志轮转以及如何管理轮转后的文件
    • 支持基于大小(SizeBasedStrategy)和基于时间(TimeBasedStrategy)的策略
  2. 压缩处理器(compressionHandler)

    • 类型:CompressionHandler?
    • 可选参数
    • 用于配置日志文件的压缩方式
    • 默认提供 GzipCompressionHandler 实现
  3. 延迟压缩(delayCompress)

    • 类型:bool
    • 默认值:true
    • 控制是否在轮转后立即压缩日志文件
    • 启用延迟压缩可以减少对应用性能的影响
  4. 检查间隔(checkInterval)

    • 类型:Duration
    • 默认值:Duration(minutes: 5)
    • 定义多久检查一次是否需要轮转
  5. 异步轮转(enableAsyncRotation)

    • 类型:bool
    • 默认值:true
    • 控制轮转操作是否异步执行
    • 异步执行可以避免阻塞主线程
  6. 轮转队列大小(rotationQueueSize)

    • 类型:int
    • 默认值:100
    • 控制异步轮转时的队列大小
    • 防止内存占用过大
  7. 存储监控(enableStorageMonitoring)

    • 类型:bool
    • 默认值:true
    • 是否监控存储空间使用情况
  8. 最小剩余空间(minimumFreeSpace)

    • 类型:int
    • 默认值:100 * 1024 * 1024(100MB)
    • 定义存储空间的最小剩余要求
  9. 文件名格式化选项

    • includeDate:是否在文件名中包含日期(默认:true
    • includeTime:是否在文件名中包含时间(默认:false
    • separator:日期时间分隔符(默认:'_'
  10. 归档目录(archiveDir)

    • 类型:String?
    • 可选参数
    • 指定轮转后的日志文件存储位置

这些配置参数可以组合使用,以满足不同的日志轮转需求。例如,一个完整的配置示例:

final config = LogRotateConfig(
  strategy: SizeBasedStrategy(
    maxSize: 10 * 1024 * 1024,
    maxBackups: 5,
  ),
  compressionHandler: GzipCompressionHandler(
    onProgress: (message) => print('Compression progress: $message'),
  ),
  delayCompress: true,
  checkInterval: const Duration(minutes: 5),
  enableAsyncRotation: true,
  rotationQueueSize: 100,
  enableStorageMonitoring: true,
  minimumFreeSpace: 100 * 1024 * 1024,
  includeDate: true,
  includeTime: false,
  separator: '_',
  archiveDir: 'logs/archives',
);

5.3 轮转策略

5.3.1 基于日期的轮转策略(SizeBasedStrategy)

基于大小的轮转策略(SizeBasedStrategy)是 Logger Easier 提供的一个开箱即用的日志轮转策略,它通过监控日志文件的大小来触发轮转操作。当日志文件的大小超过指定的阈值时,就会创建一个新的日志文件,并将旧的日志文件重命名为备份文件。

// lib/src/log_rotate/strategies/size_based_strategy.dart
class SizeBasedStrategy implements RotateStrategy {
  /// 最大文件大小(字节)
  final int maxSize;

  /// 最大备份数量
  
  final int maxBackups;

  /// 构造函数
  SizeBasedStrategy({
    int? maxSize,
    this.maxBackups = 99,
  }) : maxSize = maxSize ?? (99 * 1024 * 1024); // 默认99MB

该策略提供了两个主要的配置参数:

  1. maxSize:单个日志文件的最大大小(以字节为单位),默认为 99MB
  2. maxBackups:保留的最大备份文件数量,默认为 99 个

轮转判断逻辑实现在 shouldRotate 方法中:

// lib/src/log_rotate/strategies/size_based_strategy.dart

bool shouldRotate(File logFile, int currentSize, DateTime lastRotateTime) {
  // 检查文件大小
  final shouldRotate = currentSize >= maxSize;
  if (shouldRotate) {
    print(
        'Log rotation needed: current size ($currentSize bytes) >= max size ($maxSize bytes)');
  }
  return shouldRotate;
}

当需要进行轮转时,策略会生成新的备份文件名。备份文件的命名采用数字递增的方式,例如:app.log.1app.log.2 等。这个逻辑在 getRotatedFileName 方法中实现:

// lib/src/log_rotate/strategies/size_based_strategy.dart

String getRotatedFileName(File originalFile, int rotateIndex) {
  final dir = path.dirname(originalFile.path);
  final extension = path.extension(originalFile.path);
  final baseNameWithoutExt =
      path.basenameWithoutExtension(originalFile.path);
  return path.join(dir, '$baseNameWithoutExt.$rotateIndex$extension');
}

为了防止备份文件占用过多存储空间,策略会在轮转时清理过期的日志文件。清理逻辑在 cleanupOldLogs 方法中实现:

// lib/src/log_rotate/strategies/size_based_strategy.dart

Future<void> cleanupOldLogs(Directory directory, String pattern) async {
  try {
    final rotatedFiles = await _listRotatedFiles(directory, pattern);
    if (rotatedFiles.length <= maxBackups) return;

    // 按轮转索引排序
    rotatedFiles.sort((a, b) =>
        _getRotationIndex(b).compareTo(_getRotationIndex(a)));

    // 删除超出最大备份数量的文件
    for (var i = maxBackups; i < rotatedFiles.length; i++) {
      final file = rotatedFiles[i];
      try {
        await file.delete();
        print('Deleted old log file: ${file.path}');
      } catch (e) {
        print('Error deleting old log file ${file.path}: $e');
      }
    }
  } catch (e) {
    print('Error cleaning up old logs: $e');
  }
}

使用这个策略的典型配置示例如下:

final fileMiddleware = FileMiddleware.createSizeBasedMiddleware(
  logDirectory: 'logs',
  baseFileName: 'app.log',
  maxSize: 10 * 1024 * 1024, // 10MB
  maxBackups: 5,
  compress: true,
);

这个配置会创建一个基于大小的日志轮转策略,当日志文件超过 10MB 时触发轮转,最多保留 5 个备份文件,并对轮转的文件进行压缩。

基于大小的轮转策略特别适合以下场景:

  1. 需要严格控制单个日志文件大小的场景
  2. 存储空间有限的环境
  3. 需要确保日志文件便于传输和处理的情况

通过合理配置 maxSizemaxBackups 参数,可以有效平衡日志记录的完整性和存储空间的使用。

5.3.2 基于时间的轮转策略(TimeBasedStrategy)

基于时间的轮转策略(TimeBasedStrategy)是 Logger Easier 提供的一个内置轮转策略,它根据时间间隔来触发日志文件的轮转。这种策略特别适合需要按照固定时间周期(如每天、每周)进行日志归档的场景。

// lib/src/log_rotate/strategies/time_based_strategy.dart
class TimeBasedStrategy implements RotateStrategy {
  final Duration rotateInterval;
  final int maxBackups;
  final String dateFormat;
  late final DateFormat _formatter;

  TimeBasedStrategy({
    this.rotateInterval = const Duration(days: 1),
    this.maxBackups = 7,
    this.dateFormat = 'yyyyMMdd',
  }) {
    _formatter = DateFormat(dateFormat);
  }
}

这个策略提供了三个主要的配置选项:

  1. rotateInterval:轮转时间间隔,默认为1天
  2. maxBackups:保留的最大备份数量,默认为7个
  3. dateFormat:日期格式化模式,默认为’yyyyMMdd’

轮转判断逻辑实现在 shouldRotate 方法中:

// lib/src/log_rotate/strategies/time_based_strategy.dart

bool shouldRotate(File logFile, int currentSize, DateTime lastRotateTime) {
  final now = DateTime.now();
  return now.difference(lastRotateTime) >= rotateInterval;
}

当需要进行轮转时,TimeBasedStrategy 会根据配置的日期格式生成新的文件名:

// lib/src/log_rotate/strategies/time_based_strategy.dart

String getRotatedFileName(File originalFile, int rotateIndex) {
  final date = DateTime.now().subtract(Duration(days: rotateIndex - 1));
  return '${originalFile.path}.${_formatter.format(date)}';
}

为了确保不超过最大备份数量限制,策略实现了清理旧日志的逻辑:

// lib/src/log_rotate/strategies/time_based_strategy.dart

Future<void> cleanupOldLogs(Directory directory, String pattern) async {
  try {
    final rotatedFiles = await _listRotatedFiles(directory, pattern);
    if (rotatedFiles.length > maxBackups) {
      // 按日期排序,保留最新的日志
      rotatedFiles.sort((a, b) {
        final dateA = _extractDateFromFileName(a);
        final dateB = _extractDateFromFileName(b);
        return dateB.compareTo(dateA);
      });

      // 删除超出限制的旧日志
      for (var i = maxBackups; i < rotatedFiles.length; i++) {
        await rotatedFiles[i].delete();
      }
    }
  } catch (e) {
    print('Error cleaning up old logs: $e');
  }
}

TimeBasedStrategy 还提供了一些辅助方法来处理日志文件名和日期:

// lib/src/log_rotate/strategies/time_based_strategy.dart
bool _isRotatedLogFile(String fileName, String baseFileName) {
  final datePattern = _getDatePatternFromFormat();
  final pattern = RegExp('^$baseFileName\\.($datePattern)\$');
  return pattern.hasMatch(fileName);
}

DateTime _extractDateFromFileName(File file) {
  try {
    final fileName = path.basename(file.path);
    final datePart = fileName.split('.').last;
    return _formatter.parse(datePart);
  } catch (e) {
    return DateTime.fromMillisecondsSinceEpoch(0);
  }
}

使用 TimeBasedStrategy 的典型配置示例:

final timeBasedMiddleware = FileMiddleware.createTimeBasedMiddleware(
  logDirectory: 'logs',
  baseFileName: 'app.log',
  rotateInterval: Duration(days: 1),  // 每天轮转一次
  maxBackups: 7,                      // 保留最近7天的日志
  compress: true,                     // 启用压缩
);

logger.use(timeBasedMiddleware);

这种基于时间的轮转策略特别适合以下场景:

  • 需要按固定时间周期(如每天、每周)归档日志的系统
  • 审计日志系统,需要按日期保存和查询日志
  • 需要确保日志文件按时间段组织的场景

通过合理配置轮转间隔和最大备份数量,TimeBasedStrategy 可以有效管理日志文件的生命周期,避免日志文件占用过多存储空间。

5.3.3 自定义轮转策略

自定义轮转策略需要实现 RotateStrategy 接口,该接口定义了日志轮转的核心行为。通过实现这个接口,你可以创建适合特定需求的轮转策略,比如基于特定事件、复合条件或自定义算法的轮转策略。

首先让我们回顾 RotateStrategy 接口的定义:

// packages/logger_easier/lib/src/log_rotate/interfaces/rotate_strategy.dart
abstract class RotateStrategy {
  /// 最大备份数量
  int get maxBackups;

  /// 检查是否需要进行日志轮转
  bool shouldRotate(File logFile, int currentSize, DateTime lastRotateTime);

  /// 获取轮转后的文件名
  String getRotatedFileName(File originalFile, int rotateIndex);

  /// 清理过期的日志文件
  Future<void> cleanupOldLogs(Directory directory, String pattern);
}

下面是一个基于自定义条件的轮转策略示例,它结合了文件大小和时间间隔两个条件:

class HybridRotateStrategy implements RotateStrategy {
  final int maxSize;
  final Duration timeThreshold;
  final int _maxBackups;
  
  
  int get maxBackups => _maxBackups;

  HybridRotateStrategy({
    required this.maxSize,
    required this.timeThreshold,
    int maxBackups = 5,
  }) : _maxBackups = maxBackups;

  
  bool shouldRotate(File logFile, int currentSize, DateTime lastRotateTime) {
    final now = DateTime.now();
    final timeSinceLastRotate = now.difference(lastRotateTime);
    
    // 当文件大小超过阈值或时间间隔超过阈值时触发轮转
    return currentSize >= maxSize || timeSinceLastRotate >= timeThreshold;
  }

  
  String getRotatedFileName(File originalFile, int rotateIndex) {
    final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-');
    final dir = path.dirname(originalFile.path);
    final name = path.basenameWithoutExtension(originalFile.path);
    final ext = path.extension(originalFile.path);
    
    return path.join(dir, '${name}_${timestamp}_$rotateIndex$ext');
  }

  
  Future<void> cleanupOldLogs(Directory directory, String pattern) async {
    try {
      final files = await _listRotatedFiles(directory, pattern);
      if (files.length > maxBackups) {
        // 按修改时间排序,保留最新的文件
        files.sort((a, b) => b.lastModifiedSync().compareTo(a.lastModifiedSync()));
        
        // 删除超出数量限制的旧文件
        for (var i = maxBackups; i < files.length; i++) {
          await files[i].delete();
        }
      }
    } catch (e) {
      print('Error cleaning up old logs: $e');
    }
  }

  Future<List<File>> _listRotatedFiles(
    Directory directory,
    String baseFileName,
  ) async {
    final rotatedFiles = <File>[];
    
    try {
      await for (final entity in directory.list()) {
        if (entity is File && _isRotatedLogFile(entity.path, baseFileName)) {
          rotatedFiles.add(entity);
        }
      }
    } catch (e) {
      print('Error listing rotated files: $e');
    }
    
    return rotatedFiles;
  }

  bool _isRotatedLogFile(String filePath, String baseFileName) {
    final fileName = path.basename(filePath);
    // 检查文件名是否匹配轮转模式
    final pattern = RegExp('^${baseFileName}_.*_\\d+\\..*\$');
    return pattern.hasMatch(fileName);
  }
}

使用这个自定义策略的方式如下:

final hybridStrategy = HybridRotateStrategy(
  maxSize: 5 * 1024 * 1024, // 5MB
  timeThreshold: Duration(hours: 12),
  maxBackups: 10,
);

final rotateConfig = LogRotateConfig(
  strategy: hybridStrategy,
  compressionHandler: GzipCompressionHandler(),
  delayCompress: true,
);

final fileMiddleware = FileMiddleware(
  logDirectory: 'logs',
  baseFileName: 'app.log',
  rotateConfig: rotateConfig,
);

创建自定义轮转策略时需要注意以下几点:

  1. 正确实现接口方法:确保所有抽象方法都得到了正确实现,特别是 shouldRotate 方法中的轮转判断逻辑。

  2. 文件名生成:在 getRotatedFileName 方法中生成的文件名应该是唯一的,通常可以结合时间戳和索引来确保唯一性。

  3. 清理策略cleanupOldLogs 方法应该高效且可靠地清理过期日志,同时要注意处理可能的异常情况。

  4. 资源管理:确保文件操作安全,正确处理文件系统异常。

通过实现自定义轮转策略,你可以根据特定需求来控制日志文件的轮转行为,实现更灵活的日志管理方案。

5.4 压缩处理

5.4.1 CompressionHandler接口

CompressionHandlerLogger Easier 中用于处理日志文件压缩和解压缩的核心接口。它定义了日志压缩处理器的基本行为,使得日志系统能够支持不同的压缩算法和处理方式。

这个接口的定义非常简洁但功能完整:

// packages/logger_easier/lib/src/log_rotate/interfaces/compression_handler.dart
abstract class CompressionHandler {
  /// 压缩日志文件
  ///
  /// [sourceFile] 源文件
  /// [targetFile] 目标文件
  Future<void> compress(File sourceFile, File targetFile);

  /// 解压日志文件
  ///
  /// [sourceFile] 源文件
  /// [targetFile] 目标文件
  Future<void> decompress(File sourceFile, File targetFile);

  /// 获取压缩文件扩展名
  String get compressedExtension;
}

接口定义了三个核心成员:

  1. compress 方法:负责将源日志文件压缩并保存到目标文件。这是一个异步操作,返回 Future 以支持非阻塞式压缩处理。

  2. decompress 方法:负责将压缩的日志文件解压缩到指定的目标文件。同样是异步操作,确保解压过程不会阻塞主线程。

  3. compressedExtension 属性:定义压缩文件的扩展名(如 .gz),用于标识压缩文件的格式。

这个接口的设计遵循了以下几个重要原则:

  1. 异步处理:所有的压缩和解压操作都是异步的,避免阻塞主线程。

  2. 文件级别操作:直接处理 File 对象,而不是字节流或字符串,使得实现更加清晰和统一。

  3. 扩展性:通过抽象接口的方式,允许实现不同的压缩算法和策略。

  4. 明确的职责:接口只关注压缩和解压缩操作,不涉及其他日志处理逻辑。

CompressionHandler 在日志轮转系统中的典型使用场景如下:

class LogRotateManager {
  final CompressionHandler? compressionHandler;
  
  Future<void> rotateLog(File currentLog, File rotatedLog) async {
    // 先重命名日志文件
    await currentLog.rename(rotatedLog.path);
    
    // 如果配置了压缩处理器,执行压缩
    if (compressionHandler != null) {
      final compressedFile = File(
        '${rotatedLog.path}${compressionHandler.compressedExtension}'
      );
      await compressionHandler.compress(rotatedLog, compressedFile);
      // 压缩完成后删除原文件
      await rotatedLog.delete();
    }
  }
}

通过这个接口,Logger Easier 能够灵活地支持各种压缩需求,用户可以根据需要实现自己的压缩处理器,或使用内置的 GzipCompressionHandler处理器。

5.4.2 GzipCompressionHandler用法

GzipCompressionHandlerLogger Easier 提供的一个开箱即用的 Gzip 压缩处理器,用于在日志轮转时自动压缩旧的日志文件。以下是其主要使用场景和配置方法:

  1. 基本用法:
// 创建一个基本的 GzipCompressionHandler
final handler = GzipCompressionHandler();

// 在文件中间件配置中使用
final fileMiddleware = FileMiddleware(
  logDirectory: 'logs',
  baseFileName: 'app.log',
  rotateConfig: LogRotateConfig(
    strategy: SizeBasedStrategy(maxSize: 10 * 1024 * 1024),
    compressionHandler: handler,
  ),
);
  1. 带进度回调的用法:
// 创建带进度通知的压缩处理器
final handler = GzipCompressionHandler(
  onProgress: (message) {
    print('Compression status: $message');
    // 可以在这里更新UI或记录压缩进度
  },
);
  1. 在完整的日志配置中使用:
final logger = Logger();

final fileMiddleware = FileMiddleware(
  logDirectory: logDirectory,
  baseFileName: 'server.log',
  rotateConfig: LogRotateConfig(
    strategy: SizeBasedStrategy(
      maxSize: 10 * 1024, // 10KB用于测试
      maxBackups: 5,
    ),
    compressionHandler: GzipCompressionHandler(
      onProgress: (message) => print('Compression progress: $message'),
    ),
    delayCompress: true,  // 延迟压缩以提高性能
    archiveDir: 'archives', // 压缩文件存储目录
  ),
);

logger.use(fileMiddleware);

使用 GzipCompressionHandler 时的注意事项:

  1. 性能考虑:

    • 建议启用 delayCompress 选项,避免在日志写入时立即压缩
    • 压缩操作是 CPU 密集型的,在资源受限的环境中要谨慎使用
  2. 存储管理:

    • 压缩文件会自动添加 .gz 扩展名
    • 可以通过 archiveDir 配置单独的压缩文件存储目录
    • 系统会自动管理压缩文件的数量,遵循 maxBackups 设置
  3. 错误处理:

    • 压缩失败不会影响正常的日志记录
    • 可以通过 onProgress 回调捕获和处理压缩过程中的错误

通过这些配置,你可以轻松地在应用中实现日志文件的自动压缩,有效节省存储空间。

5.4.3 自定义压缩处理器

如果你需要使用其他压缩算法或实现特定的压缩逻辑,可以通过实现 CompressionHandler 接口来创建自定义的压缩处理器。

CompressionHandler 接口定义如下:

abstract class CompressionHandler {
  /// 压缩日志文件
  Future<void> compress(File sourceFile, File targetFile);

  /// 解压日志文件
  Future<void> decompress(File sourceFile, File targetFile);

  /// 获取压缩文件扩展名
  String get compressedExtension;
}

下面是一个使用 ZIP 算法的自定义压缩处理器示例:

import 'dart:io';
import 'package:archive/archive.dart';
import 'package:logger_easier/logger_easier.dart';

class ZipCompressionHandler implements CompressionHandler {
  final void Function(String message)? onProgress;
  
  ZipCompressionHandler({this.onProgress});

  
  Future<void> compress(File sourceFile, File targetFile) async {
    try {
      onProgress?.call('Starting ZIP compression of ${sourceFile.path}');
      
      // 读取源文件
      final bytes = await sourceFile.readAsBytes();
      final filename = sourceFile.path.split(Platform.pathSeparator).last;
      
      // 创建 ZIP 存档
      final archive = Archive();
      final archiveFile = ArchiveFile(filename, bytes.length, bytes);
      archive.addFile(archiveFile);
      
      // 压缩并写入目标文件
      final zipData = ZipEncoder().encode(archive);
      if (zipData != null) {
        await targetFile.writeAsBytes(zipData);
        onProgress?.call('ZIP compression completed: ${targetFile.path}');
      }
    } catch (e) {
      onProgress?.call('ZIP compression failed: $e');
      rethrow;
    }
  }

  
  Future<void> decompress(File sourceFile, File targetFile) async {
    try {
      onProgress?.call('Starting ZIP decompression of ${sourceFile.path}');
      
      // 读取压缩文件
      final bytes = await sourceFile.readAsBytes();
      final archive = ZipDecoder().decodeBytes(bytes);
      
      // 解压第一个文件
      if (archive.isNotEmpty) {
        final firstFile = archive.first;
        await targetFile.writeAsBytes(firstFile.content as List<int>);
        onProgress?.call('ZIP decompression completed: ${targetFile.path}');
      }
    } catch (e) {
      onProgress?.call('ZIP decompression failed: $e');
      rethrow;
    }
  }

  
  String get compressedExtension => '.zip';
}

使用自定义压缩处理器的方式:

final zipHandler = ZipCompressionHandler(
  onProgress: (message) => print('ZIP operation: $message'),
);

final fileMiddleware = FileMiddleware(
  logDirectory: 'logs',
  baseFileName: 'app.log',
  rotateConfig: LogRotateConfig(
    strategy: SizeBasedStrategy(
      maxSize: 10 * 1024 * 1024,
      maxBackups: 5,
    ),
    compressionHandler: zipHandler,
    delayCompress: true,
  ),
);

logger.use(fileMiddleware);

自定义压缩处理器时需要注意以下几点:

  1. 错误处理:应该妥善处理压缩和解压过程中可能出现的异常。
  2. 进度通知:通过回调函数提供操作进度信息。
  3. 资源管理:确保正确处理文件读写操作,避免资源泄漏。
  4. 扩展名:提供适当的压缩文件扩展名,以便系统正确识别文件类型。

通过实现自定义压缩处理器,你可以根据特定需求选择不同的压缩算法或实现特殊的压缩逻辑,使日志文件管理更加灵活和高效。

5.5 存储监控

5.5.1 StorageMonitor

6高级特性. 综合示例

这部分是扩展功能,待日后编写。

6.1 性能监控

6.1.1 PerformanceMonitor

6.1.2 性能指标收集

6.2 错误报告

6.2.1 ErrorReporter

6.2.2 CrashAnalytics

6.3 安全特性

6.3.1 日志加密

Logger Easier 提供了内置的日志加密功能,可以对敏感的日志内容进行加密保护。日志加密功能基于 AES-CBC 加密算法,使用 256 位密钥,能够有效保护日志文件的安全性。

1 基本用法

使用日志加密功能非常简单,只需要创建 LogEncryptor 实例并指定加密密钥:

// 创建加密器实例
final encryptor = LogEncryptor('your-32-byte-encryption-key-here-123');

// 加密日志文件
await encryptor.encrypt(
  File('original.log'),
  File('encrypted.log')
);

// 解密日志文件
await encryptor.decrypt(
  File('encrypted.log'),
  File('decrypted.log')
);
2 在日志系统中使用加密

要在日志系统中启用加密功能,可以在创建文件中间件时配置加密器:

// 创建加密器
final encryptor = LogEncryptor('your-secure-key-here');

// 创建支持加密的文件中间件
final encryptedFileMiddleware = FileMiddleware(
  logDirectory: 'logs',
  baseFileName: 'encrypted.log',
  outputer: EncryptedFileOutputer(encryptor),
);

// 使用加密中间件
logger.use(encryptedFileMiddleware);
3 使用注意事项

在使用日志加密功能时,需要注意以下几点:

  1. 密钥管理

    • 密钥长度必须是 32 字节
    • 密钥需要安全保存,避免泄露
    • 建议使用环境变量或配置文件管理密钥
  2. 性能考虑

    • 加密会带来一定的性能开销
    • 建议只对敏感日志启用加密
    • 可以配合日志级别过滤,只加密高级别日志
  3. 存储空间

    • 加密后的日志文件会比原始文件略大
    • 需要预留足够的存储空间
    • 建议配合日志轮转功能使用

通过合理使用日志加密功能,可以有效保护敏感的日志信息,防止未经授权的访问和篡改,同时保持系统的性能和可用性。

6.3.2 LogEncryptor

7. 综合示例

请在下面的连接查看或者下载一个综合示例项目:

https://gitee.com/jacklee1995/flutter_logger_easier/tree/master/packages/logger_easier/example

setState() or markNeedsBuild() called when widget tree was locked. This BetterPlayerSubtitlesDrawer widget cannot be marked as needing to build because the framework is locked. The widget on which setState() or markNeedsBuild() was called was: BetterPlayerSubtitlesDrawer state: _BetterPlayerSubtitlesDrawerState#42f65 When the exception was thrown, this was the stack: #0 Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:5193:9) #1 Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:5203:6) #2 State.setState (package:flutter/src/widgets/framework.dart:1224:15) #3 _BetterPlayerSubtitlesDrawerState._updateState (package:better_player_plus/src/subtitles/better_player_subtitles_drawer.dart:79:7) #4 ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:437:24) #5 ValueNotifier.value= (package:flutter/src/foundation/change_notifier.dart:559:5) #6 VideoPlayerController.pause (package:better_player_plus/src/video_player/video_player.dart:442:5) #7 BetterPlayerController.pause (package:better_player_plus/src/core/better_player_controller.dart:584:34) #8 BetterPlayerController.dispose (package:better_player_plus/src/core/better_player_controller.dart:1207:9) #9 _BetterPlayerState.dispose (package:better_player_plus/src/core/better_player.dart:115:23) #10 StatefulElement.unmount (package:flutter/src/widgets/framework.dart:5840:11) #11 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2078:13) #12 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #13 MultiChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:7012:16) #14 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #15 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #16 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #17 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #18 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #19 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #20 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #21 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #22 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #23 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #24 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #25 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #26 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #27 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #28 MultiChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:7012:16) #29 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #30 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #31 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #32 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #33 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #34 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #35 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #36 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #37 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #38 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #39 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #40 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #41 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #42 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #43 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #44 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #45 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #46 SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6900:14) #47 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #48 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #49 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #50 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #51 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #52 SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6900:14) #53 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #54 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #55 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #56 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #57 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #58 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #59 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #60 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #61 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #62 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #63 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #64 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #65 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #66 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #67 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #68 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #69 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #70 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #71 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #72 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #73 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #74 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #75 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #76 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #77 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #78 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #79 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #80 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #81 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #82 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #83 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #84 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #85 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #86 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #87 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #88 SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6900:14) #89 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #90 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #91 SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6900:14) #92 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #93 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #94 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #95 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #96 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #97 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #98 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #99 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #100 SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6900:14) #101 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #102 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #103 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #104 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #105 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #106 SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6900:14) #107 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #108 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #109 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #110 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #111 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #112 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #113 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #114 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #115 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #116 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #117 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #118 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #119 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #120 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #121 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #122 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #123 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #124 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #125 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #126 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #127 SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6900:14) #128 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #129 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #130 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #131 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #132 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #133 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #134 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #135 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #136 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #137 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #138 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #139 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #140 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #141 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #142 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #143 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #144 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #145 SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6900:14) #146 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #147 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #148 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #149 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #150 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #151 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #152 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #153 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #154 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #155 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #156 _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2076:7) #157 ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5683:14) #158 _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2074:13) #159 ListIterable.forEach (dart:_internal/iterable.dart:49:13) #160 _InactiveElements._unmountAll (package:flutter/src/widgets/framework.dart:2087:25) #161 BuildOwner.lockState (package:flutter/src/widgets/framework.dart:2954:15) #162 BuildOwner.finalizeTree (package:flutter/src/widgets/framework.dart:3264:7) #163 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:1183:19) #164 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:475:5) #165 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1397:15) #166 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1318:9) #167 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1176:5) #168 _invoke (dart:ui/hooks.dart:312:13) #169 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:427:5) #170 _drawFrame (dart:ui/hooks.dart:283:31) The VideoPlayerController sending notification was: VideoPlayerController#77d27(VideoPlayerValue(duration: 0:51:26.720000, size: Size(1920.0, 960.0), position: 0:00:08.695000, absolutePosition: null, buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:08.695000)], isPlaying: false, isLooping: false, isBuffering: false, volume: 1.0, errorDescription: null)) ==================================================================================================== E/flutter (17057): [ERROR:f. 问题还在
07-18
<think>我们正在解决Flutter中调用setState()或markNeedsBuild()时出现的框架锁定问题。该问题通常发生在Widget树正在构建过程中尝试更新状态,导致异常。以下是系统性的解决方案: ### 问题核心原因 1. **状态更新与构建过程冲突** 在`build`方法执行期间,Flutter框架会锁定Widget树,此时调用`setState()`会触发异常[^1]。 2. **异步操作未检查Widget状态** 异步回调(如网络请求、定时器、动画回调)可能在Widget被释放后触发`setState()`[^2]。 3. **跨组件状态管理不当** 全局状态更新可能触发多个组件的重建,导致局部更新与全局更新冲突[^3]。 --- ### 解决方案 #### 1. 添加`mounted`检查(关键步骤) 在`setState()`前检查Widget是否仍挂载: ```dart if (mounted) { setState(() { // 更新状态 }); } ``` > **适用场景**:所有异步操作(如`Future`、`Stream`、`Timer`)中的状态更新。 #### 2. 使用`addPostFrameCallback`延迟更新 确保状态更新发生在当前帧渲染完成后: ```dart WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { // 更新状态 }); } }); ``` > **适用场景**:需要在`initState`或`didChangeDependencies`中触发但依赖当前布局的状态更新。 #### 3. 优化状态管理流程 - **避免在`build`方法中直接触发状态更新** 将数据准备逻辑移至`initState`或独立方法。 - **使用`StatefulWidget`生命周期控制** 在`dispose`中取消所有订阅: ```dart @override void dispose() { _timer?.cancel(); _streamSubscription?.cancel(); super.dispose(); } ``` #### 4. 全局状态管理分离 使用`Provider`或`Riverpod`将状态与UI分离: ```dart // 创建全局状态 final counterProvider = StateProvider<int>((ref) => 0); // 在UI中消费状态 Consumer(builder: (context, ref, child) { final count = ref.watch(counterProvider); return Text('$count'); }); // 更新状态(自动避免冲突) ref.read(counterProvider.notifier).state++; ``` > **优势**:状态更新由框架自动调度,避开构建锁定期。 #### 5. 错误边界处理 使用`ErrorWidget.builder`捕获并处理异常: ```dart MaterialApp( builder: (context, widget) { ErrorWidget.builder = (FlutterErrorDetails details) { return Center(child: Text('渲染异常')); }; return widget!; }, ); ``` --- ### 验证步骤 1. 模拟异步操作(如按钮点击后延迟更新) 2. 快速切换页面触发多次重建 3. 观察控制台是否输出: - ✅ 无`setState() called after dispose()`警告 - ✅ 无`setState() or markNeedsBuild() called during build`异常 > **注意**:若问题仍存,检查第三方库是否在内部未正确处理状态更新。 --- ### 相关问题 1. 如何检测Flutter中的内存泄漏? 2. Provider状态管理的最佳实践是什么? 3. Flutter的Widget生命周期有哪些关键阶段? 4. 如何优化Flutter应用的渲染性能? 5. Dart的异步编程模型有哪些特点? [^1]: Flutter框架禁止在`build`期间修改状态树 [^2]: 异步操作可能跨越多个生命周期阶段 [^3]: 全局状态更新需避免局部组件树锁定</think>针对 Flutter 中调用 `setState()` 或 `markNeedsBuild()` 时出现框架锁定的异常问题,以下是系统性解决方案。该问题通常发生在 Widget 树构建过程中尝试更新状态,导致框架锁定冲突[^1]。 --- ### 核心原因分析 1. **构建过程冲突** `setState()` 在 `build()` 方法执行期间被调用,此时 Widget 树处于锁定状态[^2] 2. **异步回调未检查状态** 网络请求、定时器等异步操作在组件销毁后仍尝试更新 UI 3. **生命周期管理不当** 未正确处理 `dispose()` 导致释放资源后仍被访问 4. **跨组件状态冲突** 全局状态更新触发多个组件同时重建 --- ### 解决方案与代码实现 #### 1. 添加 `mounted` 检查(关键修复) 在所有异步操作中检查组件是否仍挂载: ```dart Future<void> fetchData() async { final data = await api.getData(); // 关键检查:确保组件未销毁 if (!mounted) return; setState(() => _data = data); } ``` #### 2. 使用 `WidgetsBinding` 延迟更新 在 `initState()` 等可能触发重建的方法中使用帧回调: ```dart @override void initState() { super.initState(); // 等待当前帧渲染完成 WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() => _initializeUI()); } }); } ``` #### 3. 优化状态管理生命周期 正确处理组件销毁流程: ```dart @override void dispose() { // 1. 取消所有订阅和控制器 _timer?.cancel(); _streamSubscription?.cancel(); _animationController?.dispose(); // 2. 标记组件已销毁 _isDisposed = true; super.dispose(); } ``` #### 4. 分离全局状态与本地状态 使用状态管理库避免直接 `setState()`: ```dart // 使用 Riverpod 示例 final counterProvider = StateProvider<int>((ref) => 0); class MyWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final count = ref.watch(counterProvider); return ElevatedButton( onPressed: () { // 框架自动处理状态更新 ref.read(counterProvider.notifier).state++; }, child: Text('Count: $count'), ); } } ``` #### 5. 错误边界处理 添加全局异常捕获: ```dart void main() { FlutterError.onError = (details) { if (details.exception.toString().contains('setState') { // 忽略特定框架锁定错误 return; } // 处理其他异常 }; runApp(MyApp()); } ``` --- ### 验证方法 1. **快速页面切换测试** 在页面跳转时连续触发状态更新 2. **异步操作取消测试** 发起网络请求后立即退出页面 3. **内存泄漏检测** 使用 DevTools 观察 Widget 销毁情况 > **预期结果**: > ✅ 无 `setState() called after dispose()` 警告 > ✅ 无 `setState() or markNeedsBuild() called during build` 异常 > ✅ 页面切换无黑屏/卡顿现象 --- ### 相关问题 1. 如何检测 Flutter 应用中的内存泄漏? 2. Riverpod 和 Provider 在状态管理上有何区别? 3. Flutter 的 Widget 生命周期有哪些关键阶段? 4. 如何优化 Flutter 应用的渲染性能? 5. Dart 的异步编程模型有哪些最佳实践? [^1]: Flutter 框架禁止在 `build()` 期间修改状态树 [^2]: 异步操作可能跨越多个生命周期阶段 [^3]: 全局状态更新需避免局部组件树锁定
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jcLee95

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值