You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
279 lines
8.7 KiB
279 lines
8.7 KiB
import 'dart:developer' as developer;
|
|
import 'dart:io';
|
|
import 'dart:async';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
/// 日志级别
|
|
enum LogLevel {
|
|
debug,
|
|
info,
|
|
warning,
|
|
error,
|
|
}
|
|
|
|
/// 日志工具类
|
|
///
|
|
/// 提供统一的日志记录接口,支持控制台输出和文件存储
|
|
class Logger {
|
|
/// 当前日志级别,低于此级别的日志不会被输出
|
|
static LogLevel _currentLevel = LogLevel.debug;
|
|
|
|
/// 默认标签
|
|
static const String _defaultTag = 'App';
|
|
|
|
/// 文件日志相关
|
|
static IOSink? _logSink;
|
|
static File? _currentLogFile;
|
|
static bool _fileLogEnabled = false;
|
|
static String? _logDirPath;
|
|
static final DateFormat _dateFormat = DateFormat('yyyy-MM-dd HH:mm:ss.SSS');
|
|
static final DateFormat _fileDateFormat = DateFormat('yyyy-MM-dd');
|
|
|
|
/// 最大单个日志文件大小 (2MB)
|
|
static const int _maxFileSize = 2 * 1024 * 1024;
|
|
|
|
/// 最多保留日志文件数量
|
|
static const int _maxLogFiles = 7;
|
|
|
|
/// 写入计数器,用于节流轮转检查和刷盘
|
|
static int _writeCount = 0;
|
|
|
|
/// 轮转检查间隔(每 100 次写入检查一次)
|
|
static const int _rotationCheckInterval = 100;
|
|
|
|
/// 刷盘间隔(每 50 次写入刷一次)
|
|
static const int _flushInterval = 50;
|
|
|
|
/// 轮转锁,防止并发轮转
|
|
static bool _isRotating = false;
|
|
|
|
/// 当前日志文件的日期标识,用于快速判断是否需要换天
|
|
static String _currentFileDate = '';
|
|
|
|
/// 初始化文件日志
|
|
static Future<void> initFileLog() async {
|
|
try {
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
_logDirPath = '${dir.path}/logs';
|
|
final logDir = Directory(_logDirPath!);
|
|
if (!await logDir.exists()) {
|
|
await logDir.create(recursive: true);
|
|
}
|
|
await _openLogFile();
|
|
_fileLogEnabled = true;
|
|
Logger.i('Logger', '文件日志初始化成功: $_logDirPath');
|
|
} catch (e) {
|
|
developer.log('[Logger] 文件日志初始化失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 打开/创建当前日志文件
|
|
static Future<void> _openLogFile() async {
|
|
final today = _fileDateFormat.format(DateTime.now());
|
|
final filePath = '$_logDirPath/app_$today.log';
|
|
_currentLogFile = File(filePath);
|
|
// 先关闭旧 sink,必须 await 确保关闭完成
|
|
await _logSink?.flush();
|
|
await _logSink?.close();
|
|
_logSink = _currentLogFile!.openWrite(mode: FileMode.append);
|
|
_currentFileDate = today;
|
|
}
|
|
|
|
/// 检查日志文件是否需要轮转(受节流和锁保护)
|
|
static Future<void> _checkRotation() async {
|
|
if (_isRotating || _currentLogFile == null) return;
|
|
_isRotating = true;
|
|
try {
|
|
// 检查日期是否变更
|
|
final today = _fileDateFormat.format(DateTime.now());
|
|
if (_currentFileDate != today) {
|
|
await _openLogFile();
|
|
}
|
|
// 检查文件大小
|
|
if (await _currentLogFile!.exists()) {
|
|
final size = await _currentLogFile!.length();
|
|
if (size > _maxFileSize) {
|
|
final now = DateTime.now();
|
|
final suffix = DateFormat('HHmmss').format(now);
|
|
final todayStr = _fileDateFormat.format(now);
|
|
final newPath = '$_logDirPath/app_${todayStr}_$suffix.log';
|
|
await _logSink?.flush();
|
|
await _logSink?.close();
|
|
_currentLogFile = File(newPath);
|
|
_logSink = _currentLogFile!.openWrite(mode: FileMode.append);
|
|
}
|
|
}
|
|
// 清理旧日志文件
|
|
await _cleanOldLogs();
|
|
} catch (_) {} finally {
|
|
_isRotating = false;
|
|
}
|
|
}
|
|
|
|
/// 清理旧日志文件,保留最近 _maxLogFiles 个
|
|
static Future<void> _cleanOldLogs() async {
|
|
if (_logDirPath == null) return;
|
|
try {
|
|
final logDir = Directory(_logDirPath!);
|
|
final files = await logDir
|
|
.list()
|
|
.where((e) => e is File && e.path.endsWith('.log'))
|
|
.cast<File>()
|
|
.toList();
|
|
if (files.length > _maxLogFiles) {
|
|
files.sort((a, b) => a.path.compareTo(b.path));
|
|
final toDelete = files.sublist(0, files.length - _maxLogFiles);
|
|
for (final f in toDelete) {
|
|
await f.delete();
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
/// 单行最大字符数,超过则分批写入
|
|
static const int _maxLineLength = 4000;
|
|
|
|
/// 写入日志到文件
|
|
static void _writeToFile(String level, String tag, String message) {
|
|
if (!_fileLogEnabled || _logSink == null) return;
|
|
try {
|
|
final timestamp = _dateFormat.format(DateTime.now());
|
|
if (message.length <= _maxLineLength) {
|
|
_logSink!.writeln('$timestamp [$level][$tag] $message');
|
|
} else {
|
|
final totalParts = (message.length + _maxLineLength - 1) ~/ _maxLineLength;
|
|
for (var i = 0; i < totalParts; i++) {
|
|
final start = i * _maxLineLength;
|
|
final end = start + _maxLineLength > message.length ? message.length : start + _maxLineLength;
|
|
final chunk = message.substring(start, end);
|
|
_logSink!.writeln('$timestamp [$level][$tag] [${i + 1}/$totalParts] $chunk');
|
|
}
|
|
}
|
|
_writeCount++;
|
|
|
|
// 定期刷盘,确保数据写入磁盘
|
|
if (_writeCount % _flushInterval == 0) {
|
|
_logSink!.flush();
|
|
}
|
|
|
|
// 定期检查轮转(节流,不是每次写入都检查)
|
|
if (_writeCount % _rotationCheckInterval == 0) {
|
|
_checkRotation();
|
|
}
|
|
} catch (e) {
|
|
// 文件写入失败不应影响业务逻辑(尤其是 Dio 拦截器链)
|
|
developer.log('[Logger] 文件写入失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 获取日志目录路径
|
|
static String? get logDirPath => _logDirPath;
|
|
|
|
/// 获取所有日志文件列表
|
|
static Future<List<File>> getLogFiles() async {
|
|
if (_logDirPath == null) return [];
|
|
final logDir = Directory(_logDirPath!);
|
|
if (!await logDir.exists()) return [];
|
|
final files = await logDir
|
|
.list()
|
|
.where((e) => e is File && e.path.endsWith('.log'))
|
|
.cast<File>()
|
|
.toList();
|
|
files.sort((a, b) => b.path.compareTo(a.path)); // 最新在前
|
|
return files;
|
|
}
|
|
|
|
/// 刷新缓冲区(导出前调用)
|
|
static Future<void> flush() async {
|
|
await _logSink?.flush();
|
|
}
|
|
|
|
/// 设置日志级别
|
|
static void setLevel(LogLevel level) {
|
|
_currentLevel = level;
|
|
}
|
|
|
|
/// 输出调试日志 (不带标签版本)
|
|
static void debug(String message) {
|
|
if (_currentLevel.index <= LogLevel.debug.index) {
|
|
_log('DEBUG', _defaultTag, message);
|
|
}
|
|
}
|
|
|
|
/// 输出调试日志
|
|
static void d(String tag, String message) {
|
|
if (_currentLevel.index <= LogLevel.debug.index) {
|
|
_log('DEBUG', tag, message);
|
|
}
|
|
}
|
|
|
|
/// 输出信息日志 (不带标签版本)
|
|
static void info(String message) {
|
|
if (_currentLevel.index <= LogLevel.info.index) {
|
|
_log('INFO', _defaultTag, message);
|
|
}
|
|
}
|
|
|
|
/// 输出信息日志
|
|
static void i(String tag, String message) {
|
|
if (_currentLevel.index <= LogLevel.info.index) {
|
|
_log('INFO', tag, message);
|
|
}
|
|
}
|
|
|
|
/// 输出警告日志 (不带标签版本)
|
|
static void warning(String message) {
|
|
if (_currentLevel.index <= LogLevel.warning.index) {
|
|
_log('WARNING', _defaultTag, message);
|
|
}
|
|
}
|
|
|
|
/// 输出警告日志
|
|
static void w(String tag, String message) {
|
|
if (_currentLevel.index <= LogLevel.warning.index) {
|
|
_log('WARNING', tag, message);
|
|
}
|
|
}
|
|
|
|
/// 输出错误日志 (不带标签版本)
|
|
static void error(String message, [dynamic error, StackTrace? stackTrace]) {
|
|
if (_currentLevel.index <= LogLevel.error.index) {
|
|
_log('ERROR', _defaultTag, message);
|
|
if (error != null) {
|
|
_log('ERROR', _defaultTag, 'Original error: $error');
|
|
}
|
|
if (stackTrace != null) {
|
|
_log('ERROR', _defaultTag, 'Stack trace: $stackTrace');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 输出错误日志
|
|
static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) {
|
|
if (_currentLevel.index <= LogLevel.error.index) {
|
|
_log('ERROR', tag, message);
|
|
if (error != null) {
|
|
_log('ERROR', tag, 'Original error: $error');
|
|
}
|
|
if (stackTrace != null) {
|
|
_log('ERROR', tag, 'Stack trace: $stackTrace');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 内部日志输出方法
|
|
static void _log(String level, String tag, String message) {
|
|
if (message.length <= _maxLineLength) {
|
|
developer.log('[$tag]: $message');
|
|
} else {
|
|
final totalParts = (message.length + _maxLineLength - 1) ~/ _maxLineLength;
|
|
for (var i = 0; i < totalParts; i++) {
|
|
final start = i * _maxLineLength;
|
|
final end = start + _maxLineLength > message.length ? message.length : start + _maxLineLength;
|
|
developer.log('[$tag]: [${i + 1}/$totalParts] ${message.substring(start, end)}');
|
|
}
|
|
}
|
|
_writeToFile(level, tag, message);
|
|
}
|
|
}
|
|
|