Flutter/Dart:使用日志模块Logger Easier
- 文章信息 - Author: 李俊才 (jcLee95)Logger Easier 是一个为 Dart 和 Flutter 应用程序量身定制的现代化日志管理解决方案。它提供了一个高度灵活、功能丰富的日志记录系统,旨在简化开发者的日志管理工作,同时提供一定的定制能力。
【注】本文对模块的功能、基本用法、架构思想做了介绍。一切API以模块当前版本实际提供的API为准。
Visit me at CSDN: https://jclee95.blog.csdn.net
My WebSite:http://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
组件库地址:
- Pub: https://pub.dev/packages/logger_easier
- GitHub:https://github.com/jacklee1995/flutter_logger_easier
- Gitee: https://gitee.com/jacklee1995/flutter_logger_easier
为了能使用类似于其它语言那样的日志,比如 Logger for Java(l4j),logger for JavaScript(l4js)等日志库,但又希望尽可能简化配置,同时参考Dart其它的日志库实现,也结合Dart语言本身的特点,完成了flutter_logger_easier。
这个日志插件可以通过自定义日志 中间件 来对 日志 进行加工或处理,比如 控制台输出、文件保存、日志文件轮转和压缩,这都可以使用一个或者多个中间件来处理。但是这些不是必须,如果你需要使用它,最简单的方式只需要直接实例化一个Logger()。
Logger Easier模块具有如下特点:
-
多级日志管理
- 支持 7 个日志级别:Trace, Debug, Info, Warn, Error, Critical, Fatal;
- 细粒度的日志级别控制;
- 可配置的最小日志记录级别;
-
灵活的日志输出
- 控制台输出(支持彩色日志);
- 文件日志记录;
- 自定义输出目标;
- 日志文件轮转和压缩;
-
高级日志格式化
- 可定制的日志格式;
- 支持多种日志格式模板;
- 丰富的日志记录元数据(时间戳、来源、错误信息等);
-
性能监控(开发中)
- 内置性能度量方法;
- 异步和同步操作性能追踪;
- 性能指标自动记录和报告;
-
错误处理
- 自动错误报告
- 堆栈跟踪记录
- 可插拔的错误报告器
-
单例模式
- 全局统一的日志管理
- 简单的初始化和使用
最后一点就是可扩展性,正如之前所述,你可以将日志看作生产线上的产品,可以在流水线上通过多个中间件执行相应操作。
2. 安装在 pubspec.yaml
中添加依赖:
dependencies:
logger_easier: 替换为最新版本
运行 dart pub get
或 flutter pub get
安装依赖。
也可以使用add
命令直接安装最新版本:
dart pub add logger_easier
# 或
flutter pub add logger_easier
3. 用法
3.1 最简单的用法
// app/logger.dart
final logger = Logger();
然后你就可以打印各个级别的日志了。比如下面是一个在Flutter中使用的例子(未必是Flutter,完全可以用于纯Dart项目):
// main.dart
import 'package:flutter/material.dart';
import 'app/logger.dart';
void main() {
logger.trace('The logger is ready.');
logger.debug('The logger is ready.');
logger.info('The logger is ready.');
logger.warn('The logger is ready.');
logger.error('The logger is ready.');
logger.critical('The logger is ready.');
logger.fatal('The logger is ready.');
runApp(const LoggerDemoApp());
}
// ...
3.2 文件转存
实际上在内存文件转存也是由插件来实现的,你可以自定义插件来实现你的转存形式。不过基本的文件转存你只需要指定所保存的位置,否则日志记录器也不知道你想要保存到哪里。比如最简单的方式是你直接传入一个绝对路径,比如在Windows上就像这样:
final logger = Logger(
minLevel: LogLevel.trace,
outputFunction: debugPrint,
logDirectory: r'C:\Users\jclee95\Documents\logs',
baseFileName: 'app.log',
maxFileSize: 10 * 1024 * 1024, // 10MB
maxBackupIndex: 5,
compress: true,
);
Windows这里显然就涉及具体的用户名,比如我当前的用户名为jclee95。
但是在其它系统平台上的路径写法可能又是另外一个情况,比如在安卓上目录就可能成了这样:/data/user/0/com.example.example/app_flutter/logs
。这里就涉及一个应用域名,比如这里的“com.example.example”(在andriod子项目里面可以设置,也可以在创建Flutter应用时指定)。
可见在不同用户、不同系统上,如果指定绝对路径是没有通用性的。因此顺便提一句,有客户端开发经验的读者一般是不会直接去拼接一个绝对路径的,即使仅用于几种PC端Electron中就是如此,更不要说全平台开发的Flutter。在Flutter中,目前这个是使用 path_provider 包来获取的。比如:
import 'package:path_provider/path_provider.dart';
对于应用程序可能放置用户生成的数据或应用程序无法重新创建的数据的目录路径,可使用path_provider
的getApplicationDocumentsDirectory
方法。这在Windows上是用户的Documents目录,而在安卓上则为应用目录。
这样,我们可以在getApplicationDocumentsDirectory
获取的目录下使用一个logs
子目录来写入、读取日志。
final appDocDir = await getApplicationDocumentsDirectory();
final logDirectory = path.join(appDocDir.path, 'logs');
不过这个logs
子目录不一定存在,比如一般你没创建它时就一定不存在,并且还需要一些检查目录是否可写入文件的东西。这就可以加入下面这些代码:
// 确保日志目录存在
final directory = Directory(logDirectory);
if (!await directory.exists()) {
await directory.create(recursive: true);
debugPrint('Created log directory: $logDirectory');
}
// 测试目录是否可写
try {
final testFile = File(path.join(logDirectory, 'test.txt'));
await testFile.writeAsString('permission test');
await testFile.delete();
debugPrint('Log directory is writable');
} catch (e, s) {
debugPrint('Cannot write to log directory: $e\n$s');
}
最后,你就可以舒适地创建一个基本的日志器了:
Logger(
minLevel: LogLevel.trace,
outputFunction: debugPrint,
logDirectory: logDirectory,
baseFileName: 'app.log',
maxFileSize: 10 * 1024 * 1024, // 10MB
maxBackupIndex: 5,
compress: true,
);
然后尝试打印一些日志,你就可以在指定的目录看到同时被保存下来了:
日志级别是日志系统中非常重要的概念,它帮助开发者控制日志的详细程度和重要性。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.4 中间件中间件是 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 为开发者提供了一个强大而灵活的日志解决方案。
3.5 打印器 3.5.1 打印器简介打印器是日志中间件的核心组件,负责日志的最终输出和呈现。在日志处理流水线中,打印器处于最后一个环节,它决定了日志信息的最终展现方式和目的地。每个打印器都实现了 BasePrinter
抽象类,提供了灵活且可扩展的日志输出机制。
打印器的主要职责是将格式化后的日志记录转换为可读的字符串,并将其输出到特定的目标。这个目标可以是控制台、文件、网络接口,甚至是自定义的输出通道。不同的打印器可以实现完全不同的输出策略,使得日志系统具有极高的灵活性。
3.5.2 BasePrinter接口BasePrinter
接口定义了打印器的基本行为和约定:
abstract class BasePrinter {
/// 打印日志记录
String printf(LogRecord record);
/// 初始化打印器
Future<void> init() async {}
/// 关闭打印器
Future<void> close() async {}
/// 获取打印器名称
String get name;
/// 获取打印器配置
Map<String, dynamic> get config;
// ...
}
3.5.3 内置打印器
Logger Easier 提供了三种内置打印器,每个都针对不同的使用场景:
控制台打印器(ConsolePrinter,已内置)是最常用的打印器,提供彩色输出和丰富的格式化选项:
final consolePrinter = ConsolePrinter(
useColor: true,
maxLineLength: 120,
);
文件打印器(FilePrinter)支持日志文件的自动管理,包括文件轮转和压缩:
final filePrinter = FilePrinter(
logDirectory: '/path/to/logs',
baseFileName: 'app.log',
maxFileSize: 10 * 1024 * 1024, // 10MB
maxBackupIndex: 5,
compress: true,
);
JSON打印器(JsonPrinter)专门用于生成结构化的日志输出:
final jsonPrinter = JsonPrinter(
prettyPrint: true,
includeStackTrace: true,
);
3.5.4 自定义打印器
自定义打印器可以通过继承 BasePrinter
来实现特殊的日志输出需求,比如:
class NetworkPrinter extends BasePrinter {
final String logServerUrl;
NetworkPrinter(this.logServerUrl);
String printf(LogRecord record) {
// 实现日志通过网络发送到远程服务器
_sendLogToServer(record);
return record.message;
}
Future<void> _sendLogToServer(LogRecord record) async {
// 网络日志发送逻辑
}
// ...
}
打印器不仅仅是输出日志,它们还提供了丰富的配置和统计功能。例如,可以获取打印器的统计信息、重置统计数据,甚至动态更新配置:
// 获取打印器统计信息
final stats = consolePrinter.getStats();
print('已打印日志数量: ${stats['printedLogs']}');
// 重置统计信息
consolePrinter.resetStats();
通过这种设计,Logger Easier 的打印器不仅提供了灵活的日志输出方式,还为开发者提供了强大的日志管理和定制能力。无论是简单的控制台输出,还是复杂的分布式日志系统,开发者都可以轻松实现。
3.6 格式化器格式化器是日志中间件的关键组件,负责将原始日志记录转换为人类可读或机器可解析的字符串。在日志处理流水线中,格式化器位于过滤器之后、打印器之前,它决定了日志信息的最终呈现形式。
格式化器的主要职责是提取日志记录中的关键信息,并按照预定义或自定义的模式进行排列和展示。这种灵活的设计使得开发者可以根据不同的使用场景定制日志输出格式。
Logger Easier 定义了 LogFormatter 接口,为所有格式化器提供了统一的约定:
abstract class LogFormatter {
/// 格式化日志记录
String format(LogRecord record);
/// 获取格式化器的名称
String get name;
/// 获取格式化器的配置
Map<String, dynamic> get config;
}
内置的格式化器提供了多种常用的日志格式化方案。简单格式化器(SimpleFormatter)提供了基础的日志格式:
final simpleFormatter = SimpleFormatter(
includeTimestamp: true,
includeLevel: true,
includeStackTrace: false,
);
JSON格式化器(JsonFormatter)专门用于生成结构化的日志输出:
final jsonFormatter = JsonFormatter(
prettyPrint: true, // 美化JSON输出
);
自定义格式化器可以通过实现 LogFormatter 接口来满足特定需求:
class CustomFormatter implements LogFormatter {
String format(LogRecord record) {
// 自定义日志格式
return '${record.level}|${record.timestamp}|${record.message}';
}
String get name => 'CustomFormatter';
Map<String, dynamic> get config => {};
}
格式化器还支持更高级的功能,如占位符和格式字符串:
class AdvancedFormatter implements LogFormatter {
List<String> get supportedPlaceholders => [
'timestamp', 'level', 'message', 'error', 'source'
];
bool isValidFormatString(String formatString) {
// 验证自定义格式字符串的有效性
return formatString.contains('{timestamp}') &&
formatString.contains('{message}');
}
String format(LogRecord record) {
// 使用自定义格式字符串
return '[{timestamp}] ({level}) {message}';
}
}
在中间件中使用格式化器非常简单:
final logger = Logger(
middlewares: [
LogMiddleware(
printer: ConsolePrinter(),
formatter: JsonFormatter(prettyPrint: true),
filter: LevelFilter(LogLevel.info),
)
]
);
格式化器的设计遵循单一职责原则,专注于将日志记录转换为特定格式的字符串。这种解耦的设计使得日志系统具有极高的灵活性和可扩展性。开发者可以轻松地为不同的使用场景(如调试、监控、审计)定制日志格式。
通过提供丰富的内置格式化器和简单的自定义机制,Logger Easier 为开发者提供了强大且灵活的日志格式化解决方案。无论是简单的控制台输出,还是复杂的结构化日志记录,开发者都可以轻松实现。
3.7 过滤器 3.7.1 过滤器简介过滤器用于负责决定哪些日志记录应该被处理和记录。在日志处理流水线中,过滤器位于最前端,充当日志的"守门员",根据预定义的规则精确控制日志的输出。
过滤器的核心职责是评估每个日志记录是否满足特定条件。这种机制使得开发者能够根据日志级别、内容、时间戳或任何自定义逻辑来控制日志的记录。通过灵活的过滤策略,可以显著减少不必要的日志输出,提高系统性能和日志的可读性。
3.7.2 LevelFilter基础级别过滤器(LevelFilter)是最常用的过滤器,它根据日志级别进行过滤:
// 只记录 INFO 及以上级别的日志
final levelFilter = LevelFilter(LogLevel.info);
3.7.3 CompositeFilter
复合过滤器(CompositeFilter)允许组合多个过滤器,只有当所有过滤器都通过时才记录日志:
final compositeFilter = CompositeFilter([
LevelFilter(LogLevel.debug),
CustomFilter((record) => !record.message.contains('sensitive')),
]);
3.7.4 RegexFilter
正则表达式过滤器(RegexFilter)允许基于消息内容的高级过滤:
final regexFilter = RegexFilter(RegExp(r'error|critical', caseSensitive: false));
3.7.5 TimeRangeFilter
时间范围过滤器(TimeRangeFilter)可以限制特定时间段内的日志:
final timeFilter = TimeRangeFilter(
DateTime.now().subtract(Duration(days: 1)),
DateTime.now()
);
过滤器的真正强大之处在于它们可以在中间件中灵活组合:
final logger = Logger(
middlewares: [
LogMiddleware(
printer: ConsolePrinter(),
formatter: SimpleFormatter(),
filter: CompositeFilter([
LevelFilter(LogLevel.info),
CustomFilter((record) {
// 排除包含特定敏感词的日志
return !record.message.contains('password');
}),
TimeRangeFilter(
DateTime.now().subtract(Duration(hours: 24)),
DateTime.now()
)
]),
)
]
);
3.7.6 自定义过滤器
自定义过滤器(CustomFilter)提供了最大的灵活性,可以实现任意复杂的日志过滤逻辑:
final customFilter = CustomFilter((record) {
// 复杂的过滤逻辑
return record.level.index >= LogLevel.warn.index &&
!record.message.contains('ignore') &&
record.timestamp.isAfter(DateTime.now().subtract(Duration(hours: 1)));
});
开发者还可以通过继承 LogFilter 接口创建完全自定义的过滤器:
class AdvancedCustomFilter implements LogFilter {
final List<String> _ignoredModules;
AdvancedCustomFilter(this._ignoredModules);
bool shouldLog(LogRecord record) {
// 复杂的过滤逻辑:根据模块、级别和其他条件过滤
return !_ignoredModules.contains(record.source) &&
record.level.index >= LogLevel.warn.index;
}
}