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。

全部源码

/* license: https://mit-license.org**  LNC : Log & Notification Center**                               Written in 2023 by Moky <albert.moky@gmail.com>** =============================================================================* The MIT License (MIT)** Copyright (c) 2023 Albert Moky** Permission is hereby granted, free of charge, to any person obtaining a copy* of this software and associated documentation files (the "Software"), to deal* in the Software without restriction, including without limitation the rights* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell* copies of the Software, and to permit persons to whom the Software is* furnished to do so, subject to the following conditions:** The above copyright notice and this permission notice shall be included in all* copies or substantial portions of the Software.** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE* SOFTWARE.* =============================================================================*//// 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";}}// #0      LogMixin.caller (package:lnc/src/log.dart:85:55)
// #1      LogMixin.debug (package:lnc/src/log.dart:105:41)
// #2      Log.debug (package:lnc/src/log.dart:50:45)
// #3      main.<anonymous closure>.<anonymous closure> (file:///Users/moky/client/test/client_test.dart:14:11)
// #4      Declarer.test.<anonymous closure>.<anonymous closure> (package:test_api/src/backend/declarer.dart:215:19)
// <asynchronous suspension>
// #5      Declarer.test.<anonymous closure> (package:test_api/src/backend/declarer.dart:213:7)
// <asynchronous suspension>
// #6      Invoker._waitForOutstandingCallbacks.<anonymous closure> (package:test_api/src/backend/invoker.dart:258:9)
// <asynchronous suspension>// #?      function (path:1:2)
// #?      function (path:1)
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.rhkb.cn/news/354886.html

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

相关文章

YOLOv10改进 | Conv篇 |YOLOv10引入SPD-Conv卷积

1. SPD-Conv介绍 1.1 摘要:卷积神经网络(CNN)在图像分类和目标检测等许多计算机视觉任务中取得了巨大的成功。 然而,在图像分辨率较低或物体较小的更艰巨的任务中,它们的性能会迅速下降。 在本文中,我们指出,这源于现有 CNN 架构中一个有缺陷但常见的设计,即使用跨步卷…

【github】项目的代码仓库重命名

问题 有时候&#xff0c;我们先创建了远端项目仓库&#xff0c;然后就把相关code上传到远端项目仓库。 可能需要结合实际情况对远端项目仓库进行重命名。 当前仓库名称v_ttc&#xff0c;如何将他修改成v_datejs 操作步骤 1、在 GitHub.com 上&#xff0c;导航到存储库的主页…

【云原生】Kubernetes----Metrics-Server组件与HPA资源

目录 引言 一、概述 &#xff08;一&#xff09;Metrics-Server简介 &#xff08;二&#xff09;Metrics-Server的工作原理 &#xff08;三&#xff09;HPA与Metrics-Server的作用 &#xff08;四&#xff09;HPA与Metrics-Server的关系 &#xff08;五&#xff09;HPA与…

java面向对象(上)

一.面向对象与面向过程 1.面向过程 面向过程(procedure Oriented Programming),简称POP,主要思想就是将问题分解成一个个步骤去解决,把这个步骤称为函数. 典型语言:C语言 优点:可以大大简化代码 缺点:当代码量过大时,不方便维护 2.面向对象 面向对象(Object Oriented Pr…

【C语言】手写学生管理系统丨附源码+教程

最近感觉大家好多在忙C语言课设~ 我来贡献一下&#xff0c;如果对你有帮助的话谢谢大家的点赞收藏喔&#xff01; 1. 项目分析 小白的神级项目&#xff0c;99%的程序员&#xff0c;都做过这个项目&#xff01; 掌握这个项目&#xff0c;就基本掌握 C 语言了&#xff01; 跳…

口袋中有红、黄、蓝、白、黑5种颜色的球若干。每次从口袋中任意取出3个球,问得到3种不同颜色的球的可能取法,输出每种排列的情况

如果一个变量只能有几种可能的值&#xff0c;可以定义为枚举&#xff08;enumeration&#xff09;类型。所谓"枚举"是指将变量的值一一列举出来&#xff0c;变量的值只能在列举出来的值的范围内。 声明枚举类型用enum开头。例如&#xff1a; enum weekday{su…

Matlab个性化绘图第3期—带三维球标记的折线图

前段时间有会员在群里问该如何绘制下面这种带三维球标记的折线图&#xff1a; 本期内容就来分享一下带三维球标记的折线图的Matlab绘制思路。 先来看一下成品效果&#xff1a; 特别提示&#xff1a;本期内容『数据代码』已上传资源群中&#xff0c;加群的朋友请自行下载。有需…

Navicat和SQLynx功能比较三(数据导出:使用MySQL近千万数据测试)

数据导出的功能在数据库管理工具中是最普遍的功能之一。所以数据导出的功能稳定性和性能也是数据库管理工具是否能很好地满足应用需求的一个考虑因素。 目录 1. 整体比较 2. 示例 2.1 前置环境 2.2 Navicat导出 2.3 SQLynx导出 2.4 性能对比结果&#xff08;690万行数据&…

【机器学习】线性回归:从基础到实践的深度解析

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 线性回归&#xff1a;从基础到实践的深度解析引言一、线性回归基础1.1 定义与目…

浸没式液冷服务器的换热效率及节能潜力分析

服务器浸没式液冷的换热效率及节能潜力 摘要&#xff1a;我们针对服务器浸没式液冷实验台进行了深入测试&#xff0c;探究了不同室外温度和服务器发热功率对系统制冷PUE的影响。实验数据显示&#xff0c;该系统的制冷PUE值介于1.05至1.28之间&#xff0c;高效节能特点显著。 在…

Java代码如何运行

通过前面的第一篇文章&#xff0c;对JVM整体脉络有了一个大概了解。第二篇文章我们通过对高级语言低级语言不同特性的探讨引出了Java的编译过程。有了前面的铺垫&#xff0c;咱们今天正式进入Java到底是如何运行起来的探讨。 目前大部分公司都是使用maven作为包管理工具&#x…

大润发超市购物卡怎么用?

收到大润发超市的礼品卡以后&#xff0c;我才发现&#xff0c;最近的大润发也得十来公里 为了100块的大润发打车也太不划算了 叫外送也不在配送范围内 最后没办法&#xff0c;在收卡云上出掉了&#xff0c;还好最近价格不错&#xff0c;也不亏&#xff0c;收卡云的到账速度也…

Jmeter 性能测试步骤是什么?

性能测试是软件开发过程中非常重要的一环。它可以帮助我们评估软件系统在不同负载下的性能表现&#xff0c;找出系统中的性能瓶颈&#xff0c;并提供改进方案。而JMeter作为一款功能强大且广泛使用的性能测试工具&#xff0c;可以帮助我们实现这一目标。 下面&#xff0c;我将…

【机器学习】从理论到实践:决策树算法在机器学习中的应用与实现

&#x1f4dd;个人主页&#xff1a;哈__ 期待您的关注 目录 &#x1f4d5;引言 ⛓决策树的基本原理 1. 决策树的结构 2. 信息增益 熵的计算公式 信息增益的计算公式 3. 基尼指数 4. 决策树的构建 &#x1f916;决策树的代码实现 1. 数据准备 2. 决策树模型训练 3.…

XMind 2024软件最新版下载及详细安装教程

​人所共知的是XMind 在公司和教育领域都有很广泛的应用&#xff0c;在公司中它能够用来进行会议管理、项目管理、信息管理、计划和XMind 被认为是一种新一代演示软件的模式。也就是说XMind不仅能够绘制思维导图&#xff0c;还能够绘制鱼骨图、二维图、树形图、逻辑图、组织结构…

记一次某单位的内网渗透测试

0x01 web打点 访问漏洞url:http://www.xx.xx.com进入某医疗系统 使用越权加文件上传拿到shell 0x02 内网渗透 192.168.xx.x 管理员 通过哥斯拉上线msf 上线后进行信息收集: 网卡信息、补丁信息、杀毒进程、用户在线情况、是否存在域、翻文件查找数据库密码、浏览器保存密码…

【ai】tx2-nx:搭配torch的torchvision

微雪的教程pytorch_version 1.10.0 官方教程安装torch官方教程 依赖项 nvidia@tx2-nx:~/twork/03_yolov5$ $ sudo apt-get install libjpeg-dev zlib1g-dev lib

EtherCAT笔记(三) —— 主站与从站的硬件组成

1. EtherCAT 主站的硬件组成 EtherCAT主站使用标准以太网控制器&#xff0c;也即EtherCAT主站可以使用以太网控制器的任何设备。当我们有一台带网口的笔记本、工控机&#xff0c;甚至是树莓派也可以作为EtherCAT主站。 EtherCAT协议是对Ethernet协议在实时控制等方面的优化&am…

el-table表格变更前后根据数据值改变背景颜色

需求&#xff1a; 1.左侧变更前表格数据不可以编辑&#xff0c;并且背景色加灰 2.右侧变更后表格数据可被编辑&#xff0c;编辑后变更前与变更后行数据不一致&#xff0c;添加背景色区分 3.点击删除的时候&#xff0c;给变更后表格当前行&#xff0c;添加背景色和删除的中横…

功能测试 之 单模块测试----轮播图、登录、注册

单功能怎么测&#xff1f; 需求分析 拆解测试点 编写用例 1.轮播图 &#xff08;1&#xff09;需求分析 位置&#xff1a;后台--页面--广告管理---广告列表(搜索index页面增加广告位2) 操作完成后需要点击admin---更新缓存,前台页面刷新生效 &#xff08;2&#xff09;拆解…