Flutter 自定义日志模块设计

前言

村里的老人常说:“工程未动,日志先行。

有效的利用日志,能够显著提高开发/debug效率,否则程序运行出现问题时可能需要花费大量的时间去定位错误位置和出错原因。

然而一个复杂的项目往往需要打印日志的地方比较多,除了控制日志数量之外,
如何做到有效区分重要信息,以及帮助快速定位代码位置,也是衡量一个工程日志质量的重要标准。

效果图

废话不多说,先看看我们的日志长啥样儿:

(图1)

通常日志信息中,除了包含需要显示的文本,同时应该包含执行时间、代码调用位置信息等。
在我这套系统中,还允许通过颜色区分显示不同日志等级的信息,这个在日志过多时可以让你迅速找到重要信息。

由上面的图1可以看到,4种级别的日志分别采用了不同的颜色显示,并且调用位置显示为程序路径文件名,可以直接点击蓝色的文件名跳转到相应的代码行。
是不是十分方便? :D

而下面的 HomePage 则展示了该日志模块的另一种用法:

(图2)

接口设计

我们先来看一下接口代码:


/// Simple Log
class Log {

  static const int kDebugFlag   = 1 << 0;
  static const int kInfoFlag    = 1 << 1;
  static const int kWarningFlag = 1 << 2;
  static const int kErrorFlag   = 1 << 3;

  static const int kDebug   = kDebugFlag|kInfoFlag|kWarningFlag|kErrorFlag;
  static const int kDevelop =            kInfoFlag|kWarningFlag|kErrorFlag;
  static const int kRelease =                      kWarningFlag|kErrorFlag;

  static int level = kRelease;

  static bool colorful = false;  // colored printer
  static bool showTime = true;
  static bool showCaller = false;

  static Logger logger = DefaultLogger();

  static void   debug(String msg) => logger.debug(msg);
  static void    info(String msg) => logger.info(msg);
  static void warning(String msg) => logger.warning(msg);
  static void   error(String msg) => logger.error(msg);

}

根据多年的项目经验,一般的项目需求中日志可以分为4个等级:

  1. 调试信息 (仅 debug 模式下显示)
  2. 普通信息
  3. 警告信息
  4. 错误信息 (严重错误,应收集后定时上报)

其中“调试信息”通常是当我们需要仔细观察每一个关键变量的值时才会打印的信息,这种信息由于太过冗余,通常情况下应该关闭;
而“告警信息”和“错误信息”则是程序出现超出预期范围或错误时需要打印的信息,这两类信息一般不应该关闭,在正式发布的版本中,错误信息甚至可能需要打包上传到日志服务器,以便程序员远程定位问题。

考虑到项目环境等因素,这里将具体的打印功能代理给 Log 类对象 logger 执行(后面会介绍)。

另外,根据 Dart 语言特性,这里还提供了 MixIn(混入)方式调用日志的接口,
通过 MixIn,还可以在打印日志的时候额外输出当前类信息:


/// Log with class name
mixin Logging {
  
  void logDebug(String msg) {
    Type clazz = runtimeType;
    Log.debug('$clazz >\t$msg');
  }

  void logInfo(String msg) {
    Type clazz = runtimeType;
    Log.info('$clazz >\t$msg');
  }

  void logWarning(String msg) {
    Type clazz = runtimeType;
    Log.warning('$clazz >\t$msg');
  }

  void logError(String msg) {
    Type clazz = runtimeType;
    Log.error('$clazz >\t$msg');
  }

}

使用方法也很简单(如上图2所示),先在需要打印日志的类定义中增加 ```with Logging```,
然后使用上面定义的 4 个接口 logXxxx() 打印即可:

import 'package:lnc/log.dart';


// Logging Demo
class MyClass with Logging {

  int _counter = 0;

  void _incrementCounter() {
    logInfo('counter = $_counter');
  }

  //...

}

开发应用

首先以你项目需求所期望的方式实现 ```Logger``` 接口:

import 'package:lnc/log.dart';


class MyLogger implements Logger {

  @override
  void debug(String msg) {
    // 打印调试信息
  }

  @override
  void info(String msg) {
    // 打印普通日志信息
  }

  @override
  void warning(String msg) {
    // 打印告警信息
  }

  @override
  void error(String msg) {
    // 打印 or 收集需要上报的错误信息
  }

}

然后在 app 启动之前初始化替换 ```Log.logger```:


void main() {

  Log.logger = MyLogger();  // 替换 logger

  Log.level = Log.kDebug;
  Log.colorful = true;
  Log.showTime = true;
  Log.showCaller = true;

  Log.debug('starting MyApp');
  // ...

}

代码引用

由于我已提交了一个完整的模块代码到 pub.dev,所以在实际应用中,你只需要在项目工程文件 ```pubspec.yaml``` 中添加:

dependencies:

  lnc: ^0.1.2

然后在需要使用的 dart 文件头引入即可:

import 'package:lnc/log.dart';

只有当你需要修改日志行为(例如上报数据)的时候,才需要编写你的 MyLogger。

全部源码



/// Simple Log
class Log {

  static const int kDebugFlag   = 1 << 0;
  static const int kInfoFlag    = 1 << 1;
  static const int kWarningFlag = 1 << 2;
  static const int kErrorFlag   = 1 << 3;

  static const int kDebug   = kDebugFlag|kInfoFlag|kWarningFlag|kErrorFlag;
  static const int kDevelop =            kInfoFlag|kWarningFlag|kErrorFlag;
  static const int kRelease =                      kWarningFlag|kErrorFlag;

  static int level = kRelease;

  static bool colorful = false;  // colored printer
  static bool showTime = true;
  static bool showCaller = false;

  static Logger logger = DefaultLogger();

  static void   debug(String msg) => logger.debug(msg);
  static void    info(String msg) => logger.info(msg);
  static void warning(String msg) => logger.warning(msg);
  static void   error(String msg) => logger.error(msg);

}


/// Log with class name
mixin Logging {
  
  void logDebug(String msg) {
    Type clazz = runtimeType;
    Log.debug('$clazz >\t$msg');
  }

  void logInfo(String msg) {
    Type clazz = runtimeType;
    Log.info('$clazz >\t$msg');
  }

  void logWarning(String msg) {
    Type clazz = runtimeType;
    Log.warning('$clazz >\t$msg');
  }

  void logError(String msg) {
    Type clazz = runtimeType;
    Log.error('$clazz >\t$msg');
  }

}


class DefaultLogger with LogMixin {
  // override for customized logger

  final LogPrinter _printer = LogPrinter();

  @override
  LogPrinter get printer => _printer;

}

abstract class Logger {

  LogPrinter get printer;

  void   debug(String msg);
  void    info(String msg);
  void warning(String msg);
  void   error(String msg);

}

mixin LogMixin implements Logger {

  static String colorRed    = '\x1B[95m';  // error
  static String colorYellow = '\x1B[93m';  // warning
  static String colorGreen  = '\x1B[92m';  // debug
  static String colorClear  = '\x1B[0m';

  String? get now =>
      Log.showTime ? LogTimer().now : null;

  LogCaller? get caller =>
      Log.showCaller ? LogCaller.parse(StackTrace.current) : null;

  int output(String msg, {LogCaller? caller, String? tag, String color = ''}) {
    String body;
    // insert caller
    if (caller == null) {
      body = msg;
    } else {
      body = '$caller >\t$msg';
    }
    // insert tag
    if (tag != null) {
      body = '$tag | $body';
    }
    // insert time
    String? time = now;
    if (time != null) {
      body = '[$time] $body';
    }
    // colored print
    if (Log.colorful && color.isNotEmpty) {
      printer.output(body, head: color, tail: colorClear);
    } else {
      printer.output(body);
    }
    return body.length;
  }

  @override
  void debug(String msg) => (Log.level & Log.kDebugFlag) > 0 &&
      output(msg, caller: caller, tag: ' DEBUG ', color: colorGreen) > 0;

  @override
  void info(String msg) => (Log.level & Log.kInfoFlag) > 0 &&
      output(msg, caller: caller, tag: '       ', color: '') > 0;

  @override
  void warning(String msg) => (Log.level & Log.kWarningFlag) > 0 &&
      output(msg, caller: caller, tag: 'WARNING', color: colorYellow) > 0;

  @override
  void error(String msg) => (Log.level & Log.kErrorFlag) > 0 &&
      output(msg, caller: caller, tag: ' ERROR ', color: colorRed) > 0;

}

class LogPrinter {

  int chunkLength = 1000;  // split output when it's too long
  int limitLength = -1;    // max output length, -1 means unlimited

  String carriageReturn = '↩️';

  /// colorful print
  void output(String body, {String head = '', String tail = ''}) {
    int size = body.length;
    if (0 < limitLength && limitLength < size) {
      body = '${body.substring(0, limitLength - 3)}...';
      size = limitLength;
    }
    int start = 0, end = chunkLength;
    for (; end < size; start = end, end += chunkLength) {
      _print(head + body.substring(start, end) + tail + carriageReturn);
    }
    if (start >= size) {
      // all chunks printed
      assert(start == size, 'should not happen');
    } else if (start == 0) {
      // body too short
      _print(head + body + tail);
    } else {
      // print last chunk
      _print(head + body.substring(start) + tail);
    }
  }

  /// override for redirecting outputs
  void _print(Object? object) => print(object);

}

class LogTimer {

  /// full string for current time: 'yyyy-mm-dd HH:MM:SS'
  String get now {
    DateTime time = DateTime.now();
    String m = _twoDigits(time.month);
    String d = _twoDigits(time.day);
    String h = _twoDigits(time.hour);
    String min = _twoDigits(time.minute);
    String sec = _twoDigits(time.second);
    return '${time.year}-$m-$d $h:$min:$sec';
  }

  static String _twoDigits(int n) {
    if (n >= 10) return "$n";
    return "0$n";
  }

}

class LogCaller {
  LogCaller(this.name, this.path, this.line);

  final String name;
  final String path;
  final int line;

  @override
  String toString() => '$path:$line';

  /// locate the real caller: '#3      ...'
  static String? locate(StackTrace current) {
    List<String> array = current.toString().split('\n');
    for (String line in array) {
      if (line.contains('lnc/src/log.dart:')) {
        // skip for Log
        continue;
      }
      // assert(line.startsWith('#3      '), 'unknown stack trace: $current');
      if (line.startsWith('#')) {
        return line;
      }
    }
    // unknown format
    return null;
  }

  /// parse caller info from trace
  static LogCaller? parse(StackTrace current) {
    String? text = locate(current);
    if (text == null) {
      // unknown format
      return null;
    }
    // skip '#0      '
    int pos = text.indexOf(' ');
    text = text.substring(pos + 1).trimLeft();
    // split 'name' & '(path:line:column)'
    pos = text.lastIndexOf(' ');
    String name = text.substring(0, pos);
    String tail = text.substring(pos + 1);
    String path = 'unknown.file';
    String line = '-1';
    int pos1 = tail.indexOf(':');
    if (pos1 > 0) {
      pos = tail.indexOf(':', pos1 + 1);
      if (pos > 0) {
        path = tail.substring(1, pos);
        pos1 = pos + 1;
        pos = tail.indexOf(':', pos1);
        if (pos > 0) {
          line = tail.substring(pos1, pos);
        } else if (pos1 < tail.length) {
          line = tail.substring(pos1, tail.length - 1);
        }
      }
    }
    return LogCaller(name, path, int.parse(line));
  }

}

GitHub 地址:

https://github.com/dimchat/sdk-dart/blob/main/lnc/lib/src/log.dart

结语

这里展示了一个高效简洁美观的 Flutter 日志模块,其中包含了“接口驱动”、“代理模式”、“混入模式”等设计思想。

在这里重点推介“接口驱动”这种设计思想,就是当你准备开发一个功能模块的时候,
首先要充分理解需求,然后根据需求定义接口(这时千万不要过多的考虑具体怎么实现,而是重点关注需求);然后再将具体的实现放到别的地方,从而达到接口与内部执行代码完全分离。
而使用者则无需关心你的内部实现,只需要了解接口定义即可。

这种设计思想,村里的老人们更喜欢称之为“干湿分离”,希望对你有所帮助。 ^_^

如有其他问题,可以下载登录 Tarsier 与我交流(默认通讯录里找 Albert Moky)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/711208.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用llama.cpp实现LLM大模型的格式转换、量化、推理、部署

使用llama.cpp实现LLM大模型的格式转换、量化、推理、部署 概述 llama.cpp的主要目标是能够在各种硬件上实现LLM推理&#xff0c;只需最少的设置&#xff0c;并提供最先进的性能。提供1.5位、2位、3位、4位、5位、6位和8位整数量化&#xff0c;以加快推理速度并减少内存使用。…

师彼长技以助己(6)递归思维

师彼长技以助己&#xff08;6&#xff09;递归思维 递归思维-小游戏 思维小游戏 思维 小游戏&#xff1a;1 玩一个从1或2开始往上加的游戏&#xff0c;谁加到20就赢 如何保证一定赢呢&#xff1f;我们倒推&#xff0c;要先到20的话&#xff0c;谁先到17就赢&#xff0c;如此…

固态u盘长期不用会丢数据吗?u盘数据丢失怎么恢复需要多久

在数字化时代&#xff0c;U盘作为便携存储设备&#xff0c;广泛应用于我们的日常生活和工作中。然而&#xff0c;关于固态U盘长期不使用是否会导致数据丢失的问题&#xff0c;以及数据丢失后如何恢复和所需的时间&#xff0c;常常让人感到困惑。本文将针对这些问题进行深入探讨…

汽车IVI中控开发入门及进阶(二十八):视频SERDES芯片

前言: SerDes不是很常见,SerDes是将Ser和Des两种产品组合在一起的名称。Ser是Serializer或“并串转换器”的缩写,Des是Deserializer或“串并转换器”的简写。 Serdes是不是必须的?上一节介绍了camera,上上节也研究了video decoder,那么带摄像头的应用应该具体选哪个方案…

建筑电工精选最新模拟试题(含答案)

一、填空题 1、我国安全生产的基本方针是 安全 第一&#xff0c;预防 为主&#xff0c;综合治理。 2、特种作业人员&#xff0c;必须积极主动参加培训与考核 。既是法律法规的规定&#xff0c;也是自身工作&#xff0c;生产及生命安全 的需要 3、触电急救&#x…

修改SubVI的LabVIEW默认搜索路径

在启动顶级VI后&#xff0c;LabVIEW可能会遇到找不到subVI的情况。这通常是由于subVI的路径发生了变化或没有被正确配置。 LabVIEW默认搜索路径 默认情况下&#xff0c;LabVIEW会按以下顺序搜索文件位置&#xff08;*表示LabVIEW将搜索子目录&#xff09;&#xff1a; <t…

锐尔15 锐尔文档扫描影像处理系统

锐尔文档扫描影像处理系统是一款全中文操作界面的文件、档案扫描及影像优化处理软件&#xff0c;是目前国内档案数字化行业里专业且优秀的影像优化处理软件&#xff0c;在档案数字化业内有广泛的影响且广受好评。 无论是从纸质文件制作高质量的影像文件&#xff0c;或是检查已经…

OpenDevin 环境配置及踩坑指南

不惧怕任何环境配置 首先 clone 项目&#xff0c;然后查看开发者文档&#xff1a;https://github.com/OpenDevin/OpenDevin/blob/main/Development.md make setup-config 自定义 LLM 配置 首先这个 devin 写的是支持自定义的 LLM 配置&#xff0c;并且提供了交互式命令供我们…

【Matlab编程学习】 | matlab语言编程基础:常用图形绘制基础学习

&#x1f3a9; 欢迎来到技术探索的奇幻世界&#x1f468;‍&#x1f4bb; &#x1f4dc; 个人主页&#xff1a;一伦明悦-CSDN博客 ✍&#x1f3fb; 作者简介&#xff1a; C软件开发、Python机器学习爱好者 &#x1f5e3;️ 互动与支持&#xff1a;&#x1f4ac;评论 &…

【仪器仪表/电源专题】浮地信号的测试的四种方案对比

接地信号和浮地信号区别 所有的电压测量都是差分测量&#xff0c;差分测量定义为两点之间的电压差。所以会分成两类&#xff1a; 1.参考地电平测量&#xff08;有时也叫接地信号&#xff09; 2.非参考地电平测量&#xff08;也称为浮地测量&#xff09; 测试信号可以分为接地信…

leetcode 56合并区间

思路 合并就是首先应该按照left左边界排序&#xff0c;排完序以后&#xff0c;如果i的左边界小于等于i-1的右边界&#xff0c;说明有重合&#xff0c;此时这两个可以合并&#xff0c;右边界应该取最大值。 代码 排序 我是定义了一个类,存储左右边界&#xff0c;先将数组转化…

【计算机视觉】人脸算法之图像处理基础知识(五)

图像的几何变换 3.图像的旋转 图像的旋转就是让图像按照某一点旋转到指定的角度。需要确定3个参数&#xff1a;图像的旋转中心、旋转角度和缩放因子。在openv中通过getRotationMatrix2D()函数来实现图像的旋转。 import cv2 import numpy as npimgpath "images/img1.j…

logTrick

贴一下灵神的题解里面的解释~ 就是一种优化策略&#xff0c;logtrick class Solution { public:int minimumDifference(vector<int>& nums, int k) {int res 0x3f3f3f3f;int n nums.size();for(int i0;i<n;i){res min(res,abs(nums[i]-k));for(int j i-1;j&g…

视频转换器在线哪个好?让视频播放不受格式限制

在日常的视频观看中&#xff0c;我们可能会遇到视频格式与设备不兼容的问题&#xff0c;导致无法顺畅播放。这就像是缺少了播放的钥匙&#xff0c;让人无法享受视频内容。 面对视频格式不兼容的挑战&#xff0c;选择合适的转换工具至关重要。但不用担心&#xff0c;本文将分享…

OpenGL3.3_C++_Windows(10)

最终演示 ​ demo演示 Assimp模型渲染 模型导入库Assimp&#xff1a;导入很多种不同的模型文件格式&#xff0c;加载至Assimp的通用数据结构&#xff08;树形&#xff09;中&#xff0c;不论导入的是什么种类的文件格式&#xff0c;用同一种方式访问我们需要的数据。 Assimp库…

从开源EPR产品Odoo学习

前言 一个先进、敏捷、经济高效、可快速扩展的Odoo免费开源企业信息化解决方案,让企业获得适应未来发展的长期创新和增长能力。 Odoo 的免费开源模式 让我们可利用无数开发人员和业务专家,在短短数年内,打造数百款应用。凭借强大的技术基础,Odoo 的框架是非常独特且优秀的…

0614,表达式,语句

题目一&#xff1a; 许多简单的交互式程序都是基于菜单的&#xff1a;它们向用户显示可供选择的命令列表&#xff1b;一旦用户选择了某条命令&#xff0c;程序就执行相应的操作&#xff0c;然后提示用户输入下一条命令&#xff1b;这个过程一直会持续到用户选择 "退出&qu…

图论(一)之概念介绍与图形#matlab

图论&#xff08;一&#xff09;之概念介绍与图形目录 前言 一、图论介绍 二、基本概念 2.1图的概念 2.2图形分类 2.3邻接矩阵 2.3.1无向图 2.3.2有向图 2.3.3有向赋权图 2.4出度&#xff08;Outdegree&#xff09; 2.5入度&#xff08;Indegree&#xff09; 3.四种…

Apollo9.0 PNC源码学习之Control模块(五)—— 基于LQR的横向控制

前面文章&#xff1a; Apollo9.0 PNC源码学习之Control模块&#xff08;一&#xff09; Apollo9.0 PNC源码学习之Control模块&#xff08;二&#xff09; Apollo9.0 PNC源码学习之Control模块&#xff08;三&#xff09; Apollo9.0 PNC源码学习之Control模块&#xff08;四&…

GenICam标准(六)

系列文章目录 GenICam标准&#xff08;一&#xff09; GenICam标准&#xff08;二&#xff09; GenICam标准&#xff08;三&#xff09; GenICam标准&#xff08;四&#xff09; GenICam标准&#xff08;五&#xff09; GenICam标准&#xff08;六&#xff09; 文章目录 系列文…