flutter 日志输出

1,580 阅读2分钟

日志输出工具梳理,支持输出到控制台、本地等,支持颜色分级显示,效果如下:
控制台效果: 32C66F79-F711-4aa2-B8D8-D1107F16F554.png
输出到文件效果:

AA9BF0C3-4855-4dc7-A017-C8457AF4E5E6.png

所需依赖库:

logger: ^1.1.0
path_provider: ^2.0.10

所有代码:

import 'package:basic_frame/util/logger/my_log_output.dart';
import 'package:logger/logger.dart';
///log输出:输出到控制台和本地缓存文件
///输出形式如下:
/// ┌──────────────────────────
/// │ Error info
/// ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
/// │ Method stack history
/// ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
/// │ Log message
/// └──────────────────────────
class LoggerUtil {
  final String defaultTag = 'daemon_li';
  ///栈方法追溯数量
  final int _methodCount = 5;
  ///显示的日志级别
  final Level _level = Level.verbose;

  LoggerUtil._internal();

  factory LoggerUtil() => _instance;

  static late final LoggerUtil _instance = LoggerUtil._internal();

  Logger? logger;

  ///初始化log输出设置
  void _init() {
    logger ??= Logger(
        //输出该级别以上的日志
        level: _level,
        filter: _MyLogFilter(),
        //log输出到控制台和本地缓存文件,
        output: MyLogOutput(),
        printer: PrettyPrinter(
          methodCount: _methodCount, //方法调用追溯数量
          printTime: true,
        ));
  }

  ///最低级log:1
  void v(message, {String? tag}) {
    _init();
    tag = tag == null ? defaultTag: '${defaultTag}_$tag';
    logger!.v('$tag: $message');
  }
  ///debug级:2
  void d(message, {String? tag}) {
    _init();
    tag = tag == null ? defaultTag: '${defaultTag}_$tag';
    logger!.d('$tag: $message');
  }
  ///普通级信息输出:3
  void i(message, {String? tag}) {
    _init();
    tag = tag == null ? defaultTag: '${defaultTag}_$tag';
    logger!.i('$tag: $message');
  }
  ///警告级:3
  void w(message, {String? tag}) {
    _init();
    tag = tag == null ? defaultTag: '${defaultTag}_$tag';
    logger!.w('$tag: $message');
  }
  ///严重级:5
  void e(message, {String? tag}) {
    _init();
    tag = tag == null ? defaultTag: '${defaultTag}_$tag';
    logger!.e('$tag: $message');
  }

  ///超级严重级:6
  void wtf(message, {String? tag}) {
    _init();
    tag = tag == null ? defaultTag: '${defaultTag}_$tag';
    logger!.wtf('$tag: $message');
  }
}

///日志输出过滤器,可用于控制输出仅仅符合指定级别条件的log
class _MyLogFilter extends LogFilter {
  @override
  bool shouldLog(LogEvent event) {
    //仅仅输出某一级别日志
    // if (event.level == Level.debug) {
    //   return true;
    // } else {
    //   return false;
    // }
    //默认输出所有级别log
    return true;
  }
}

//////////////////自定义输出工具////////////////////////////////
import 'dart:io';
import 'package:logger/logger.dart';
import 'package:path_provider/path_provider.dart';
///日志输出到控制台和文件中
///log输出方法在异步方法中被调用,会造成方法调用无法回溯
///输出到文件需要初始化获取文件路径,获取文件路径是异步方法,这里移到内部初始化,避免影响到方法栈回溯
class MyLogOutput extends LogOutput {
  IOSink? _sink;
  //log文件父级目录
  Directory? _directory;
  //log文件后缀
  final fileSuffix = '.txt';
  @override
  void output(OutputEvent event) {
    // ignore: avoid_print
    event.lines.forEach(print);
    _logToFile(event);
  }

  void _logToFile(OutputEvent event) {
    //输出到文件的log,为避免部分颜色相关字符乱码,需要处理
    OutputEvent newEvent = event;
    switch(event.level) {
      case Level.info:
        var lines = event.lines.map((e) => e.substring(10, e.length -4)).toList();
        newEvent = OutputEvent(event.level, lines);
        break;
      case Level.debug:
      //debug不含特殊编码字符,无需处理
        break;
      default:
        var lines = event.lines.map((e) => e.substring(11, e.length -4)).toList();
        newEvent = OutputEvent(event.level, lines);
    }

    //错误级别以上log输出到文件显示方法栈回溯
    //一般性log输出到文件不显示方法栈回溯;
    OutputEvent lastEvent;
    switch(newEvent.level) {
      case Level.verbose:
      case Level.debug:
      case Level.info:
      case Level.warning:
        final length = newEvent.lines.length;
        final date = newEvent.lines.elementAt(length - 4);
        final content = newEvent.lines.elementAt(length - 2);
        lastEvent = OutputEvent(newEvent.level, ['$date$content']);
        break;
      default:
        {
          lastEvent = newEvent;
        }
    }
    //初始化输出文件路径
   if (_directory == null || !_directory!.existsSync()) {
     getExternalStorageDirectory().then((direct) {
       _directory = Directory('${direct!.path}/log');
       if (!_directory!.existsSync()) {
         _directory!.createSync(recursive: true);
       }
       _write(lastEvent);
     });
   } else {
     _write(lastEvent);
   }

  }

  ///此方法调用需保证[_directory]已初始化
  void _write(OutputEvent event) {
    //以日期作为log文件名
    var file = File(_getFileLogPath(DateTime.now()));
    if (!file.existsSync()) {
      //文件不存在后,重写获取指向新文件的句柄
      _sink = file.openWrite(
        mode: FileMode.writeOnlyAppend,
      );
    }

    //这里避免_sink未初始化
    _sink ??= file.openWrite(
      mode: FileMode.writeOnlyAppend,
    );
    _sink?.writeAll(event.lines, '\n');
    _sink?.writeln();
    _deleteLogFile(_directory!.path);
  }

  @override
  void destroy() async {
    await _sink?.flush();
    await _sink?.close();
  }

  ///输出log文件缓存,仅仅保留两天的log
  void _deleteLogFile(String directory) {

    //获取最近两天log文件名称
    final now = DateTime.now();
    final nowLogPath = _getFileLogPath(now);
    final yesterdayLogPath = _getFileLogPath(DateTime.fromMillisecondsSinceEpoch(now.millisecondsSinceEpoch - 24 * 60 * 60 * 1000));

    final target = Directory(directory);
    //筛选log目录下所有文件
    if (target.existsSync()) {
      target.listSync().forEach((element) {
        //不是最近两天的文件都删除
        if (element.path != nowLogPath && element.path != yesterdayLogPath) {
          element.deleteSync();
        }
      });
    }
  }

  String _getFileLogPath(DateTime time) {
    final nowLogName = '${time.year}-${time.month}-${time.day}$fileSuffix';
    return '${_directory?.path}/$nowLogName';
  }
}

release版应用包获取debug运行时log

  1. 手机连接电脑;
  2. 终端输入命令flutter logs;