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 printerstatic 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 {@overridevoid debug(String msg) {// 打印调试信息}@overridevoid info(String msg) {// 打印普通日志信息}@overridevoid warning(String msg) {// 打印告警信息}@overridevoid error(String msg) {// 打印 or 收集需要上报的错误信息}}

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


void main() {Log.logger = MyLogger();  // 替换 loggerLog.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 printerstatic 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 loggerfinal LogPrinter _printer = LogPrinter();@overrideLogPrinter 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';  // errorstatic String colorYellow = '\x1B[93m';  // warningstatic String colorGreen  = '\x1B[92m';  // debugstatic 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 callerif (caller == null) {body = msg;} else {body = '$caller >\t$msg';}// insert tagif (tag != null) {body = '$tag | $body';}// insert timeString? time = now;if (time != null) {body = '[$time] $body';}// colored printif (Log.colorful && color.isNotEmpty) {printer.output(body, head: color, tail: colorClear);} else {printer.output(body);}return body.length;}@overridevoid debug(String msg) => (Log.level & Log.kDebugFlag) > 0 &&output(msg, caller: caller, tag: ' DEBUG ', color: colorGreen) > 0;@overridevoid info(String msg) => (Log.level & Log.kInfoFlag) > 0 &&output(msg, caller: caller, tag: '       ', color: '') > 0;@overridevoid warning(String msg) => (Log.level & Log.kWarningFlag) > 0 &&output(msg, caller: caller, tag: 'WARNING', color: colorYellow) > 0;@overridevoid 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 longint limitLength = -1;    // max output length, -1 means unlimitedString carriageReturn = '↩️';/// colorful printvoid 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 printedassert(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 outputsvoid _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;@overrideString 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 Logcontinue;}// assert(line.startsWith('#3      '), 'unknown stack trace: $current');if (line.startsWith('#')) {return line;}}// unknown formatreturn null;}/// parse caller info from tracestatic LogCaller? parse(StackTrace current) {String? text = locate(current);if (text == null) {// unknown formatreturn 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.mzph.cn/diannao/27890.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

web错题(1)

action属性是form标签的必须属性&#xff0c;用于指定表单提交时表单数据将被发往哪里 dir能够指定文本显示方向的属性 可以产生下拉列表的标记时<select> multiple属性设为true&#xff0c;表示输入字段可以选择多个值 lable标签的for属性可以把lable绑定到另一个元…

vagrant putty错误的解决

使用Vagrant projects for Oracle products and other examples 新创建的虚机&#xff0c;例如vagrant-projects/OracleLinux/8。 用vagrant ssh可以登录&#xff1a; $ vagrant ssh > vagrant: Getting Proxy Configuration from Host...Welcome to Oracle Linux Server …

网络协议,OSI,简单通信,IP和mac地址

认识协议 1.讲故事 2004年&#xff0c;小明因为给他爹打电话&#xff08;座机&#xff09;费用太贵&#xff0c;所以约定一种信号&#xff0c;响一次是报平安&#xff0c;响两次是要钱&#xff0c;响三次才需要接通。 2.概念 协议&#xff1a;是一种约定&#xff0c;这种约…

【Android面试八股文】请描述new一个对象的流程

文章目录 请描述new一个对象的流程JVM创建对象的过程检查加载分配内存内存空间初始化设置对象初始化请描述new一个对象的流程 JVM创建对象的过程 当JVM遇到一条new指令时,它需要完成以下几个步骤: 类加载与检查内存分配 并发安全性内存空间初始化设置对象信息对象初始化下图…

10W大奖等你瓜分,OpenTiny CCF开源创新大赛报名火热启动!

OpenTiny CCF开源创新大赛正式启幕&#xff01; &#x1f31f;10万奖金&#xff0c;等你来战&#xff01; &#x1f31f; &#x1f465;无论你是独行侠还是团队英雄&#x1f465; 只要你对前端技术充满热情&#xff0c; 渴望在实战中磨砺技能&#xff0c; 那么&#xff0c…

抢占人工智能行业红利,前阿里巴巴产品专家带你15天入门AI产品经理

前言 当互联网行业巨头纷纷布局人工智能&#xff0c;国家将人工智能上升为国家战略&#xff0c;藤校核心课程涉足人工智能…人工智能领域蕴含着巨大潜力&#xff0c;早已成为业内共识。 面对极大的行业空缺&#xff0c;不少人都希望能抢占行业红利期&#xff0c;进入AI领域。…

文件系统小册(FusePosixK8s csi)【3 K8s csi】

文件系统小册&#xff08;Fuse&Posix&K8s csi&#xff09;【3 K8s csi】 往期文章&#xff1a; 文件系统小册&#xff08;Fuse&Posix&K8s csi&#xff09;【1 Fuse】文件系统小册&#xff08;Fuse&Posix&K8s csi&#xff09;【2 Posix标准】 0 核心知识…

liunx常见指令

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 二、安装环境 1.租借服务器 2.下载安装 XShell 3.使用xshll登录服务器 三、Linux基础命令 一、文件和命令 ​编辑1、cd 命令 2、pwd 命令 3、ls 命令 4、cp 命令 …

邮件钓鱼--前置-攻击防范 7 看

目录 1、什么是 SPF&#xff1a; 2、如何判断 SPF&#xff1a; 3.邮件钓鱼防范&#xff1a;7 看 1、什么是 SPF&#xff1a; SPF 记录&#xff1a;原理、语法及配置方法简介 (zhetao.com) SPF记录详解_spf写法-CSDN博客 发件人策略框架&#xff08;Sender Policy Frame…

【多线程】Thread类及其基本用法

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 文章目录 1. Java中多线程编程1.1 操作系统线程与Java线程1.2 简单使用多线程1.2.1 初步创建新线程代码1.2.2 理解每个…

springboot与flowable(8):候选人

一、流程绘制和部署 创建流程图 绘制如下流程图 给人事审批添加候选人 给经理审批添加两个候选人 保存导出流程图 部署流程定义 Testvoid contextLoads() {DeploymentBuilder deployment repositoryService.createDeployment();deployment.addClasspathResource("process…

《大道平渊》· 拾肆 —— 不要为不属于你负责的事情负责

《平渊》 拾肆 "客观世界如是观照&#xff0c;控制自己&#xff0c;不要介入因果。" 美国开国总统华盛顿说过, 不要干涉欧洲事务。 可是他的后任都不听, 于是纷纷卷入了无穷的麻烦之中。 不要为不属于你负责的事情负责。 别人的行为和你有什么关系&#xff1f; 就…

Linux-Https协议

文章目录 前言一、Https协议二、常见的加密方式对称加密非对称加密数据摘要&&数据指纹中间人攻击 三、Https的加密历程方案1-只使用对称加密方案2-只使用非对称加密方案3-双方都使用非对称加密方案4-非对称加密对称加密 前言 之前我们学习了Http协议&#xff0c;也试着…

官方文档 搬运 MAXMIND IP定位 mysql导入 简单使用

官方文档地址&#xff1a; 官方文档 文件下载 1. 导入mysql可能报错 Error Code: 1290. The MySQL server is running with the --secure-file-priv option so it cannot execute this statement 查看配置 SHOW GLOBAL VARIABLES LIKE %secure%;secure_file_priv 原来…

laravel版本≥ 8.1

laravel10 php ≥ 8.1 且 ≤ 8.3&#xff1f; 8.1 < php < 8.3PHP版本要求在 8.1 到 8.3 之间&#xff0c;包括这两个版本。具体来说&#xff1a;"≥ 8.1" 表示 PHP 的版本至少是 8.1&#xff0c;也就是说 8.1 及以上的版本都可以。 "≤ 8.3" 表示 P…

计算机组成原理学习 Part 1

计算机系统 组成 计算机系统 { 硬件 计算机的实体&#xff0c;如主机、外设等 软件 由具有各类特殊功能的信息&#xff08;程序&#xff09;组成 计算机系统 \begin{cases} 硬件 &\text 计算机的实体&#xff0c;如主机、外设等\\ 软件 &\text 由具有各类特殊功能的信…

【报错】无法找到模块“element-plus/es/locale/index.mjs”的声明文件。

报错&#xff1a; 无法找到模块“element-plus/es/locale/index.mjs”的声明文件。“E:/codeAll/work/test1/test2/HealinLikeMe-ui/node_modules/.pnpm/element-plus2.7.3_vue3.4.27_typescript5.4.5_/node_modules/element-plus/es/locale/index.mjs”隐式拥有 "any&quo…

Linux笔记--vi编辑器

vi编辑器 基本操作 对于vi编辑器有这几种模式 移动 当编辑一个过大的文件时通过方向键移动光标过慢所以可以使用快捷键进行移动 编辑 dw指令只能在单词第一个字母处使用 D指令删除的是当前行 查找替换 pattern指代想要搜索的内容

056、PyCharm 快速代码重构的方法

在实际的编程过程中&#xff0c;如果有一段代码需要在多个地方重复使用&#xff0c;我们应该将这段代码封装成一个函数。这样可以提高代码的可重用性和可维护性。 在PyCharm编辑器里&#xff0c;可以使用以下操作对代码块进行快速的重构。 &#xff08;1&#xff09;、选中一…

【Photoshop】PS修改文字内容

Photoshop(PS)修改图片上文字内容&#xff0c;网上教材不少&#xff0c;本人整理实践过的方法&#xff0c;分享给各位。本人实践方法&#xff1a; 内容识别填充&#xff1a;适用于背景色复杂的图片内容修补工具&#xff1a;适用于背景色为纯色的图片 方式一&#xff1a;内容识…