Flutter 自定义日志模块设计

news2024/11/27 21:39:12

前言

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

有效的利用日志,能够显著提高开发/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.coloradmin.cn/o/1825583.html

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

相关文章

[Python学习篇] Python循环语句

while 循环 语法&#xff1a; while 条件: 条件成立后会重复执行的代码 ...... 示例1&#xff1a;死循环 # 这是一个死循环示例 while True:print("我正在重复执行")示例2&#xff1a;循环指定次数 i 1 while i < 5:print(f"执行次数 {i}")…

Denoising Prior Driven Deep Neural Network for Image Restoration

之所以能够检索到这篇论文是想看看该论文是如何利用多尺度相似性解决图像去噪问题&#xff0c;除了摘要和结论&#xff0c;论文中两次提到这个术语。next section是指section 4。然后整个section 4&#xff0c;根本没有提多尺度的事儿&#xff0c;更别说解决了。又看了一下The …

智慧班牌系统源码,智慧校园云平台系统,基于小程序原生开发的智慧校园小程序源码

智慧班牌系统&#xff0c;也被称为电子班牌系统&#xff0c;是一款专为学校打造的信息化产品&#xff0c;用于加强学校班级文化建设和班级风采展示。该系统通过整合学校对外宣传、日常互动交流、教师教学办公、课外学习延伸、智能硬件接入等各种服务&#xff0c;为老师、家长、…

聊天页面样式

聊天页面样式 代码&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><link rel"styleshee…

深度解析RocketMq源码-持久化组件(一) MappedFile

1. 绪论 rocketmq之所以能够有如此大的吞吐量&#xff0c;离不开两个组件&#xff0c;一个是利用netty实现的高性能网络通信组件&#xff1b;另一个就是利用mmap技术实现的存储组件。而在rocketmq的存储组件中主要有三个组件&#xff0c;分别是持久化文件commitLog&#xff0c…

UDP 协议详解与实战

目录 简介什么是 UDP&#xff1f;UDP 与 TCP 的区别 UDP 数据传输方式单播 - Unicast&#xff08;1:1&#xff09;广播 - Broadcast&#xff08;1:n&#xff09;有限广播 - Limited Broadcast直接广播 - Directed Broadcast 组/多播 - Multicast&#xff08;n:m&#xff09;任播…

Golang——gRPC认证和拦截器

一. OpenSSL 1.1 介绍 OpenSSL是一个开放源代码的软件库包&#xff0c;用于支持网络通讯过程中的加密。这个库提供的功能包含了SSL和TLS协议的实现&#xff0c;并可用于生成密钥、证书、进行密码运算等。 其组成主要包括一下三个组件&#xff1a; openssl&#xff1a;多用途的命…

智能化状态管理:自动状态流转处理模块

目录 基本背景介绍 具体实现 基本数据准备 基本数据表 状态转换常量 状态转换注解 任务处理模版 各任务实现逻辑 开启比对任务进行处理 降噪字段处理任务处理 开启业务数据比对处理 业务数据比对处理 开始核对数据生成最终报告处理 核对数据生成最终报告处理 状…

[渗透测试学习] SolarLab-HackTheBox

SolarLab-HackTheBox 信息搜集 nmap扫描端口 nmap -sV -v 10.10.11.16扫描结果如下 PORT STATE SERVICE VERSION 80/tcp open http nginx 1.24.0 135/tcp open msrpc Microsoft Windows RPC 139/tcp open netbios-ssn Microsoft Windows n…

观光车司机N2精选考试题库(附答案)

一、判断题 1、在使用手电钻、电砂轮等手持电动工具时,为保证安全,应该装设漏电保护器。(√) 2、碳弧气刨的方法设备工具简单.操作使用安全。(√) 3、事故调查组有权向有关单位和个人了解与事故有关的情况。()(√) 4、发射药(动力药)是能产生发射和推进效应的烟火药,有粒状、粉…

SAP BOM项目类别N非库存项目简介

在BOM的项目类别中用的最多的就是L类型的库存管理,还有T类型的文本类型,但是在实际业务中也会存在物料不做库存管理,但是物料需要进行成本的管控,进入对应的工单成本中,比如在电子行业中需要烧录的正版软件,或者是电脑制造行业中需要预装的正版的Windows系统,购买的软件…

【SpringBoot】SpringBoot:简化数据库操作与API开发

文章目录 引言SpringBoot概述数据库操作简化传统数据库操作的挑战使用Spring Data JPA示例&#xff1a;定义Repository接口实现服务层 使用MyBatis示例&#xff1a;配置MyBatis定义Mapper接口 API开发简化RESTful API概述创建RESTful API示例&#xff1a;定义控制器 高级特性与…

【二】【动态规划NEW】91. 解码方法,62. 不同路径,63. 不同路径 II

91. 解码方法 一条包含字母 A-Z 的消息通过以下映射进行了 编码 &#xff1a; ‘A’ -> “1” ‘B’ -> “2” … ‘Z’ -> “26” 要 解码 已编码的消息&#xff0c;所有数字必须基于上述映射的方法&#xff0c;反向映射回字母&#xff08;可能有多种方法&#xff…

小知识点快速总结:Batch Normalization Layer(BN层)的作用

本系列文章只做简要总结&#xff0c;不详细说明原理和公式。 目录 1. 参考文章2. 主要作用3. 具体分析3.1 正则化&#xff0c;降低过拟合3.2 提高模型收敛速度&#xff0c;加速训练3.3 减少梯度爆炸或者梯度消失的情况 4. 补充4.1 BN层做的是标准化不是归一化4.2 BN层的公式4.…

洗地机提升渗透率,降价不是唯一解

作者 | 辰纹 来源 | 洞见新研社 添可2019年开创洗地机赛道时&#xff0c;看好的人不多&#xff0c;在扫地机器人正被风口吹在天上翻滚的那个年代&#xff0c;洗地机被扣上了“智商税”的标签。 洗地机到底有没有用&#xff0c;市场用脚投票。 奥维云网数据显示&#xff0c…

PS通过GTX实现SFP网络通信2

PS 程序设计 LWIP 库修改 修改原因 SDK 2017.4 自带的 LWIP 1.4.1 库的版本为 2.0 &#xff0c;直接使用该库将无法通过 SFP 实现网络通信。 因此需要进行修改。 修改的原因有 2 个&#xff0c;第 1 个原因是由于 2017.4 版本产生的新 bug 。在 2015.4 版本…

Java数据结构之ArrayList(如果想知道Java中有关ArrayList的知识点,那么只看这一篇就足够了!)

前言&#xff1a;ArrayList是Java中最常用的动态数组实现之一&#xff0c;它提供了便捷的操作接口和灵活的扩展能力&#xff0c;使得在处理动态数据集合时非常方便。本文将深入探讨Java中ArrayList的实现原理、常用操作以及一些使用场景。 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨…

Kotlin 语言基础学习

什么是Kotlin ? Kotiln翻译为中文是:靠他灵。它是由JetBrains 这家公司开发的,JetBrains 是一家编译器软件起家的,例如常用的WebStorm、IntelliJ IDEA等软件。 Kotlin官网 JetBrains 官网 Kotlin 语言目前的现状: 目前Android 已将Kotlin 作为官方开发语言。 Spring 框…

Java—读取properties配置文件

编写配置文件 usernameroot password123456 urljdbc:mysql://localhost:3306/myDatabase driverClassNamecom.mysql.cj.jdbc.Driver 编写测试类 import java.io.FileInputStream; import java.io.IOException; import java.util.Enumeration; import java.util.Properties;/*…

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 …