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

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);
}
}