1. 项目缘起:为什么需要一个鸿蒙上的HTTP日志审计引擎
做 Flutter 开发的人应该都有过这样的经历:联调阶段接口报错了,你先打开控制台看一眼日志,结果 Dio 默认只给你打一行 URL,状态码和响应体全都要自己手动 debug 去看;遇到 500 或者超时,你还得猜请求头里到底带了什么 token、请求体是不是 JSON 序列化错了。当时我是在给一个鸿蒙 App 做网络层改造,Flutter 端用的网络库自然是 Dio,但日志这块一直不太顺手。调研了一圈,发现 dio_http_formatter 这个库在 Android 和 iOS 上体验很好——请求、响应、状态码、耗时、请求头、响应体全都有,而且输出是带颜色的格式化日志。唯一的遗憾是它官方没有支持鸿蒙,在 OpenHarmony 环境下跑不起来。
这里先交代一下背景。鸿蒙生态真正起来之后,Flutter 在鸿蒙上的运行方案已经不再是“能不能跑”的问题了,而是“跑得好不好”的问题。鸿蒙 SDK 提供了 Flutter 引擎的适配层,Dio 这类纯 Dart 实现的三方库基本可以直接用,但凡是涉及到原生能力桥接的插件,比如网络诊断、日志输出到系统日志、证书校验这类,都需要单独做鸿蒙化适配。dio_http_formatter 虽然核心逻辑是纯 Dart 写的,但它对日志输出这块依赖了dart:io和一些平台相关的路径处理,加上鸿蒙的 HTTP 栈走的是自研的网络协议栈,所以直接拿来用会踩一些坑。
这个项目要解决的核心痛点有三个:第一,鸿蒙端 Flutter 应用的网络请求日志不透明,出了问题只能抓瞎;第二,Dio 的 interceptor 机制本身很强大,但日志格式化器的实现细节在不同平台上行为不一致;第三,团队内部需要一套统一的、带颜色标记和敏感信息过滤的 HTTP 审计方案,不能把 token、密码这类信息直接打到日志里。
如果你也是在鸿蒙上做 Flutter 开发,或者你正在给你的 Dio 请求层做日志能力升级,这篇指南会带你完整走一遍 dio_http_formatter 的鸿蒙化适配流程,包括源码分析、编译问题排查、日志格式化增强、以及敏感信息脱敏这些细节。内容偏向实操,我尽量把这次适配过程中踩过的坑都写清楚。
2. 适配前的准备工作:理解 dio_http_formatter 的架构与鸿蒙的差异点
2.1 dio_http_formatter 的工作原理拆解
dio_http_formatter 本质上是 Dio 的一个拦截器(Interceptor),它通过包装 Dio 的请求流程,在请求发出前、响应返回后这两个时机点插入日志打印逻辑。它的核心类叫LogInterceptor,职责就是拦截请求和响应,然后通过HttpLogger把可读性高的日志内容格式化后输出。
这个库的工作流程大概是这样的:
- 请求发起时,
LogInterceptor会拿到RequestOptions,里面包含 method、url、headers、queryParameters、data 这些信息。 - 它会根据配置决定是否打印请求体,如果请求体是 FormData 类型,它会做特殊处理,避免二进制内容直接输出。
- 响应返回时,它会读取
Response对象,把状态码、响应头、响应体、耗时都组装成一段格式化的文本。 - 最后通过
HttpLogger的printLog方法输出到控制台。
大家知道 Dio 的拦截器分三类:Interceptor(普通拦截器)、QueuedInterceptor(队列拦截器)、QueuedInterceptor的子类可以保证请求按顺序处理。dio_http_formatter 使用的是普通拦截器,所以在并发请求比较多的时候,日志顺序可能不是绝对按发起顺序排列的。这个特性在鸿蒙适配的时候要注意,因为鸿蒙的异步调度机制和 Android 有些差异,日志输出的顺序可能会更乱。
关键点在于,这个库的核心价值不只是“打印日志”,而是把日志格式化成了统一的、可读性极强的文本块。比如它会用┌──────这样的字符画出一条分隔线,把请求信息分成几个区块,每个区块有明确的标题。这种格式在终端里看起来非常直观,出问题的时候一眼就能定位到是请求阶段挂了还是响应阶段挂了。
2.2 鸿蒙环境下 Flutter 网络栈的差异分析
鸿蒙的 Flutter 适配和 Android 最大的不同在于网络栈的底层实现。Android 上 Flutter 的dart:io走的是 BoringSSL + socket 那套,而鸿蒙上 Flutter 引擎的网络能力有一部分是桥接到鸿蒙自身的网络框架上的。这就导致了一个问题:如果你在鸿蒙上跑一个纯 Dart 编写的 HTTP 客户端,底层走的其实是被鸿蒙网络框架接管过的链路,而不再是你熟悉的那个 socket 直连。
这个差异带来的直接影响有几个:
- 首先,
HttpOverrides这个类在鸿蒙上的行为可能和 Android 不一致。如果你之前在 Android 上用过HttpOverrides.global来替换 HTTP 客户端实现,到鸿蒙上这套逻辑可能会失效或者表现异常。 - 其次,
dart:io的HttpClient在鸿蒙上对代理设置的支持还没有完全对齐,如果你公司的网络环境需要走代理才能访问测试服务器,那么 Dio 的请求在鸿蒙上可能会绕过代理,直接连接目标地址,导致连接超时。 - 再看日志层面,鸿蒙的日志系统是
hilog,和 Android 的logcat参数格式完全不同。dio_http_formatter 默认走的是debugPrint这路输出,在鸿蒙的 Flutter 引擎里,debugPrint会被路由到 flutter 的日志通道,但如果你想让日志直接打到 hilog 里,就需要自己做一层桥接。
理解了这些差异,你就能明白为什么 dio_http_formatter 不是直接就能在鸿蒙上用的。它的核心逻辑不用改,但输出层、配置层、还有对平台通道的依赖,都需要针对鸿蒙做一次适配。
2.3 适配前的工具链准备
在开始动手之前,先把工具链准备好。我用的是 DevEco Studio 4.0 以上版本,配套的 HarmonyOS SDK API 10,Flutter SDK 这边用的是 3.16 以上的版本,并且安装了鸿蒙的 Flutter 适配分支。
这里有个很重要的点:鸿蒙的 Flutter 支持目前主要靠 OpenHarmony 社区的 flutter_flutter 仓库维护,你需要把 Flutter SDK 切换到openharmony分支或者使用社区发布的鸿蒙版 Flutter SDK。如果你用的是官方 Flutter 稳定版,在鸿蒙设备上是跑不起来的,这一点和 Android/iOS 的情况完全不同。
# 以社区版 Flutter SDK 为例 git clone https://gitee.com/openharmony-sig/flutter_flutter.git git checkout openharmony-3.16Dio 的版本我这边选的是 5.x 系列,dio_http_formatter 用的是 2.x 的最新版。注意 dio_http_formatter 的版本和 Dio 5.x 是兼容的,但如果你项目里还是 Dio 4.x,那需要找对应老版本的 formatter,否则会有 API 不兼容的编译错误。
另外,建议在适配之前先跑通一个最小的鸿蒙 Flutter 工程,确保 Flutter 引擎能在鸿蒙模拟器上正常启动、能发基本的 HTTP 请求。这样后面适配 dio_http_formatter 的时候,排查问题会更干净,不会把环境问题和代码问题混在一起。
3. 鸿蒙化适配的核心改造:从编译修复到日志引擎增强
3.1 源码引入与编译问题排查
第一步,把 dio_http_formatter 的源码引入到项目里。我的做法是直接把源码拷到工程的third_party目录下,而不是通过 pub.dev 依赖拉取,因为鸿蒙适配需要改动库的内部实现,如果用 pub 依赖管理,每次flutter pub get都会把你的改动覆盖掉。
# pubspec.yaml 中的引用方式 dependencies: dio: ^5.3.0 dio_http_formatter: path: ./third_party/dio_http_formatter引入之后先尝试编译,大概率会遇到下面的报错。
第一个常见报错是dart:io的某些类在鸿蒙 SDK 里不可用。具体来说,dio_http_formatter内部会用到HttpHeaders这个类来解析响应头,但鸿蒙的 Flutter SDK 在dart:io的实现上做了一些裁剪,某些常量或者方法没有被实现。解决办法是改用package:http或者直接用字符串解析,把对HttpHeaders的依赖去掉。
// 改造前 import 'dart:io'; final contentType = response.headers[HttpHeaders.contentTypeHeader]; // 改造后 final contentType = response.headers['content-type'] ?? 'unknown';第二个报错和HttpLogger的printLog方法有关。原版实现是直接调用print(),这在鸿蒙的 release 模式下会被系统当成未捕获异常处理,导致日志丢失。需要改成通过debugPrint输出,并且增加一个开关来控制是否输出到 hilog。
第三个报错是FormData的序列化问题。鸿蒙的 Flutter 引擎在处理FormData的readAsBytes时,偶尔会触发一个UnimplementedError。这个问题的根子在于鸿蒙的文件系统接口还没有完全对齐,导致 FormData 里的文件流无法被正常读取。绕过的办法是捕获这个异常,然后降级打印 FormData 的字段名不打印具体文件内容。
if (data is FormData) { try { final bytes = await data.readAsBytes(); logBuffer.write(String.fromCharCodes(bytes)); } catch (e) { logBuffer.write('[FormData: 文件内容读取失败,已跳过]'); } }3.2 日志输出层与 hilog 桥接
编译通过之后,接下来就要解决日志输出去向的问题。在 Android 上,Flutter 的debugPrint最终会走Log.d输出到 logcat。在鸿蒙上,如果你想让日志出现在 DevEco Studio 的日志面板里,最佳实践是走 hilog。
鸿蒙的 hilog 有 domain、tag、level 三个关键参数。domain 用来区分业务模块,一般用 0x0001 这种保留段;tag 是日志标签,可以起一个比较容易过滤的名字,比如NetLogger;level 对应 debug/info/warn/error 四个级别。
我在适配时做了一个HilogBridge,把 Dart 层和鸿蒙原生层之间的通道打通。实现方式是通过 MethodChannel,在 Dart 侧定义一个打印方法,然后在鸿蒙的 ArkTS 侧接收这个调用,最终转发给 hilog。
// Dart 侧的定义 class HilogBridge { static const _channel = MethodChannel('com.example.net_logger/hilog'); static void log(String message, {int level = 3}) { if (!Platform.isOpenHarmony) return; try { _channel.invokeMethod('writeLog', {'message': message, 'level': level}); } catch (e) { // 桥接失败时降级用 debugPrint debugPrint(message); } } }// ArkTS 侧的接受实现 const methodChannel = new MethodChannel('com.example.net_logger/hilog'); methodChannel.setMethodCallHandler((call) => { if (call.method === 'writeLog') { const message = call.arguments['message']; const level = call.arguments['level']; hilog.info(0x0001, 'NetLogger', '%{public}s', message); return Promise.resolve(true); } });这里有个细节要提醒一下:hilog 的隐私保护默认是开启的,%{public}s表示这个参数可以明文显示,如果不加这个标记,日志内容会被自动替换成{private},到时候你会看到一堆{private}不知道原始内容是什么。这个问题我在第一次适配的时候踩过,查了半天才发现是日志格式化占位符的问题。
3.3 全彩日志渲染与终端兼容性处理
dio_http_formatter 在 Android 上输出的彩色日志依赖的是 ANSI 转义序列,比如\x1B[31m代表红色,\x1B[32m代表绿色。这个机制在 Android Studio 的 logcat 里可以正常工作,因为 logcat 支持 ANSI 颜色渲染。
但鸿蒙的 DevEco Studio 日志面板对 ANSI 转义的支持并不完整,尤其是旧版本的 DevEco Studio,会把转义序列原样打印出来,导致日志里出现一堆←[32m这种乱码。如果你用的是新版 DevEco Studio,需要在设置里开启 ANSI 颜色渲染,否则就会看到乱码。
// 颜色控制类,可以按需开启/关闭 class LogColor { static const enable = true; static const red = '\x1B[31m'; static const green = '\x1B[32m'; static const yellow = '\x1B[33m'; static const blue = '\x1B[34m'; static const reset = '\x1B[0m'; static String paint(String text, String color) { if (!enable) return text; return '$color$text$reset'; } }我建议把颜色开关做成可配置的,在 release 模式下自动关闭颜色输出,避免日志文件里存了大量转义序列导致检索困难。同时保留 debug 模式下的全彩输出,毕竟在终端里看彩色的日志确实舒服,请求成功绿色、失败红色、警告黄色,扫一眼就能发现问题。
3.4 敏感信息脱敏机制的实现
日志审计最忌讳的是把 token、密码、验证码这类的敏感信息直接打到日志里。dio_http_formatter 原版没有做脱敏处理,我在这次鸿蒙化适配中把脱敏机制作为重点功能加进去了。
实现思路是在拦截器输出日志之前,对 headers 和 queryParameters 做一次过滤。用正则表达式匹配常见的敏感字段名:token、password、secret、access_key、private_key,匹配到的值统一替换成***。
class SensitiveFilter { static final _regex = RegExp( r'(?i)(token|password|secret|access_key|private_key)', ); static Map<String, String> filterHeaders(Map<String, String> headers) { final filtered = <String, String>{}; headers.forEach((key, value) { if (_regex.hasMatch(key)) { filtered[key] = '***'; } else { filtered[key] = value; } }); return filtered; } }这里要补充一个观点:脱敏不是简单地打码,而是要把“哪些字段需要脱敏”这件事配置化,让不同团队根据自己的安全规范灵活调整。我的做法是把脱敏字段列表放到配置文件里,用enum定义常见的敏感字段,然后允许外部传入自定义的脱敏规则。
class LogConfig { final List<String> sensitiveKeys; final bool enableColor; final bool enableHilog; final bool printRequest; final bool printResponse; const LogConfig({ this.sensitiveKeys = const ['token', 'password', 'secret', 'access_key'], this.enableColor = true, this.enableHilog = false, this.printRequest = true, this.printResponse = true, }); }4. 完整接入流程:从初始化到线上验证
4.1 初始化与拦截器注册的详细步骤
调试接口的时候,我们团队统一的接入方式是:在 Dio 初始化之后,立刻把LogInterceptor注册到拦截器链里。但注册顺序有讲究,如果你还有其他拦截器,比如TokenInterceptor用来做 token 刷新,RetryInterceptor用来做重试,日志拦截器的位置决定了你能看到多少信息。
我的建议是:LogInterceptor放在第一个,也就是最后注册。这样它在请求链路的起点就能记录到完整的 headers(包括其他拦截器添加的 token),在响应链路的终点能记录到最终的状态码和响应体。如果你把它放在中间位置,它会漏掉后面拦截器对请求的修改。
final dio = Dio(BaseOptions( baseUrl: 'https://api.example.com', connectTimeout: Duration(seconds: 10), receiveTimeout: Duration(seconds: 10), )); // 注册顺序:先加业务拦截器,最后加日志拦截器 dio.interceptors.add(TokenInterceptor()); dio.interceptors.add(RetryInterceptor()); dio.interceptors.add(LogInterceptor( config: LogConfig( enableColor: true, enableHilog: true, sensitiveKeys: ['token', 'password'], ), ));初始化完成之后,建议写一个小工具类,把dio实例封装成单例,这样整个应用共享同一个 Dioclient 和日志配置,避免出现不同页面使用不同 Dio 实例导致日志缺失的情况。
class NetManager { static final NetManager _instance = NetManager._internal(); factory NetManager() => _instance; late final Dio dio; NetManager._internal() { dio = Dio(); // 省略 baseOptions 和其他拦截器配置 } }4.2 请求日志的格式化输出示例
完成初始化之后,跑一个最简单的请求,你会在控制台看到类似下面的输出:
┌─────────────────────────────────────────────────────────── │ HTTP REQUEST ▸ POST https://api.example.com/v1/user/login │ Time: 2025-01-15 14:32:08.123 ├─────────────────────────────────────────────────────────── │ Headers: │ • content-type: application/json │ • Authorization: *** │ • User-Agent: Flutter/OpenHarmony ├─────────────────────────────────────────────────────────── │ Query Parameters: │ (none) ├─────────────────────────────────────────────────────────── │ Request Body: │ {"phone":"138****1234","code":"***"} └──────────────────────────────────────────────────────────响应日志长这样:
┌─────────────────────────────────────────────────────────── │ HTTP RESPONSE ▸ 200 OK │ Time: 2025-01-15 14:32:08.223 (耗时: 100ms) ├─────────────────────────────────────────────────────────── │ Headers: │ • content-type: application/json │ • server: OpenHarmony/4.0 ├─────────────────────────────────────────────────────────── │ Response Body: │ {"code":0,"message":"success","data":{"userId":"12345"}} └──────────────────────────────────────────────────────────这样的日志格式,在控制台里扫一眼就能知道请求有没有走到服务器、服务器返回了什么、耗时大不大。如果有问题,直接在日志面板里按HTTP REQUEST关键词过滤,所有请求一目了然。
4.3 耗时统计与性能分析增强
请求耗时是一个非常重要的指标,dio_http_formatter 原版就带耗时统计,但是实现方式比较简单,就是响应回来之后记录一下DateTime.now()和请求发出时的时间差。这个方案在高并发场景下会有误差,因为DateTime.now()受系统时间调整影响,不够精准。
我建议改用Stopwatch,它底层走的是系统单调时钟,不受系统时间跳变的影响。把Stopwatch的实例挂在请求的上下文中,在请求发出前启动,在响应返回后停止,这样拿到的耗时是纯网络耗时加编解码耗时,更加准确。
class LogInterceptor extends Interceptor { final _stopwatch = Stopwatch(); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { _stopwatch ..reset() ..start(); super.onRequest(options, handler); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { _stopwatch.stop(); final elapsedMs = _stopwatch.elapsedMilliseconds; // 组装日志输出 super.onResponse(response, handler); } }补充一个使用场景,当你在鸿蒙真机上调试的时候,如果发现某一个接口的耗时会突然涨到 3 秒以上,多半不是网络问题,而是鸿蒙的 DNS 解析慢或者底层的网络栈在做连接复用策略调整。这时候日志里的耗时数据就很有参考价值,可以先判断是建连慢还是数据下载慢,再对症下药。
4.4 请求重放与断点调试的扩展思路
在完成基础适配之后,我还在这个日志引擎的基础上做了一个小扩展:把每次请求的完整上下文(URL、headers、body)记录下来,缓存到本地文件里。这样当接口报错的时候,可以把这段请求信息直接导入到 Postman 或者 ApiFox 里做重放测试,省去了手动拷贝参数的过程。
这个功能实现起来不复杂,核心就是在onResponse里把请求和响应的关键信息拼接成一段 JSON 字符串,然后写入到应用沙盒目录下。鸿蒙的沙盒路径可以通过path_provider这个插件获取,但要注意 path_provider 在鸿蒙上适配的版本,我用的是社区适配版,可以正常获取到应用缓存路径。
Future<void> saveRequestLog(Map<String, dynamic> logData) async { try { final dir = await getApplicationCacheDirectory(); final file = File('${dir.path}/http_logs/${DateTime.now().millisecondsSinceEpoch}.json'); await file.writeAsString(jsonEncode(logData)); } catch (e) { // 日志存储失败不影响主流程 } }有了这个能力,你在鸿蒙设备上遇到一个偶现的崩溃,就可以通过本地的请求日志来还原现场,而不是靠用户复述“刚才点了什么”。这一点在测试阶段特别有用,大家可以试一下。
5. 鸿蒙适配中的常见问题与排查思路
5.1 日志不输出或者输出乱码
这个问题非常典型,排查的时候按下述顺序逐项检查:
如果日志完全不输出,先确认是不是 release 模式。默认情况下debugPrint在 release 模式会被丢弃,如果你没把debugPrint替换成自己的打印函数,那么 release 包自然没有日志。
如果日志输出但是在 DevEco Studio 里显示乱码,大概率是 ANSI 转义序列没被正确渲染。排查方式是把日志内容拷贝到支持 ANSI 的终端里(比如 VS Code 的终端),如果能正常显示颜色,说明问题出在 DevEco Studio 的设置上;如果还是乱码,说明你的代码在拼接颜色字符串的时候出了问题。
如果只有在部分设备上乱码,那就是不同鸿蒙版本的 Flutter 引擎对 ANSI 的处理不一致。API 9 和 API 10 的表现就有差别,老版本引擎会自动去掉 ANSI 序列,新版本会原样输出。
表格总结一下排查方向:
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 日志完全没输出 | release 模式 debugPrint 被丢弃 | 使用自定义打印函数或走 hilog 输出 |
日志里有←[32m乱码 | DevEco Studio 未开启 ANSI 渲染 | 设置里开启 ANSI 支持,或关闭颜色输出 |
| 日志输出顺序错乱 | 并发请求多导致拦截器唤醒顺序不定 | 使用 QueuedInterceptor 替代普通 Interceptor |
hilog 里全是{private} | 隐私保护占位符错误 | 使用%{public}s格式化参数 |
| 日志内容过长被截断 | hilog 单条消息长度限制 | 分割长文本为多条日志输出 |
5.2 FormData 文件上传时日志崩溃
dio_http_formatter在处理 FormData 时会读取文件的字节内容来打印,这在鸿蒙上会偶发崩溃。崩溃现场是这样的:调用readAsBytes()的时候抛了一个UnimplementedError,然后整个请求就挂掉了。
这个问题我在前面写过一个降级方案,但是这里再补充一个更稳妥的处理方式:直接判断FormData里是否包含文件类型字段,如果包含,那就只打印字段名和文件大小,不打印文件内容。这样既不泄露文件内容,也不会因为读取文件流触发未实现的接口。
void _handleFormData(FormData data, StringBuffer buffer) { buffer.write('[FormData 字段列表]\n'); for (final entry in data.fields) { if (entry.value is MultipartFile) { final file = entry.value as MultipartFile; buffer.write(' • ${entry.key}: [FILE] ${file.filename} (${file.length} bytes)\n'); } else { buffer.write(' • ${entry.key}: ${entry.value}\n'); } } }5.3 连接超时但日志显示请求已发出
鸿蒙上还会遇到一个比较隐蔽的问题:Dio 报连接超时,但是日志拦截器已经打印了请求日志,让你误以为请求真的发出去了,实际上 TCP 连接根本没有建立成功。
这是因为拦截器打印请求日志的时间点是在onRequest阶段,也就是 Dio 刚把请求对象组装出来的时候,此时还没开始建立 socket 连接。如果你想区分“请求已发出”和“连接已建立”,需要在onRequest之后、onResponse之前的某个中间点再打一条日志,这个点不太好抓。
我的做法是在日志里区分两个阶段:请求日志的标题用HTTP REQUEST SENT,响应日志用HTTP RESPONSE RECEIVED,如果有异常则用HTTP ERROR OCCURRED。这样看到SENT却没有RECEIVED,就知道是对端没响应,而不是本地没发出去。
这一条经验在实际调试中真的帮了不少忙,尤其是在排查跨网段访问内网服务的问题时,能快速判断是本地网络不通还是服务端处理超时。
5.4 日志文件无限膨胀的隐患与清理策略
本地日志缓存的机制如果做不好清理策略,时间长了会占用大量应用沙盒空间,两个 G 的占用也可能发生。我实现了一套简单的按天清理机制:每次写入前检查当天日志目录的总大小,如果超过预设阈值(比如 50MB),就删除最早的文件。
Future<void> cleanUpLogs(String dirPath, {int maxSizeMB = 50}) async { final dir = Directory(dirPath); if (!await dir.exists()) return; final files = await dir.list().toList(); var totalSize = 0; for (final file in files) { totalSize += await file.length(); } // 单位换算 final maxBytes = maxSizeMB * 1024 * 1024; if (totalSize > maxBytes) { // 按创建时间排序,先删最老的 files.sort((a, b) => a.statSync().modified.compareTo(b.statSync().modified)); var freed = 0; for (final file in files) { if (freed >= totalSize - maxBytes) break; freed += await file.length(); await file.delete(); } } }其实在实际开发中,日志文件的清理策略非常容易被人忽略,等到测试人员反馈“手机存储空间不足”,才发现是日志文件占了几 G。
6. 进一步扩展:日志审计能力与团队协作的融合
6.1 日志分级与上报策略
日志审计不应该只是开发阶段的自嗨,线上问题的定位同样需要日志支持。我把日志分级做了扩展:DEBUG、INFO、WARN、ERROR四个等级,线上环境默认只记录WARN和ERROR,降低日志写入频率和存储压力。
enum LogLevel { debug, info, warn, error } class LogEntry { final LogLevel level; final String message; final DateTime timestamp; final String tag; LogEntry(this.level, this.message, this.timestamp, this.tag); }对于线上ERROR级别的日志,建议增加一个上报通道,把日志内容加密后上传到你的日志平台。这样即使应用崩溃,崩溃前的最后几条日志也能帮你还原现场。我当时的做法是接入了鸿蒙的崩溃日志采集能力,同时把最近 20 条请求日志保存在内存里,万一发生崩溃,会随崩溃报告一起打包上传。
6.2 多端日志格式统一的意义
这个项目还有个额外的收获:日志格式统一之后,Flutter 端、ArkTS 端、Android 端的日志可以做到“同构”——即不管哪一端打印出来的 HTTP 请求日志,字段顺序和格式都保持一致。这样在后端日志平台上做关联分析的时候,不需要为每个端写不同的解析规则。
具体执行上,我在 ArkTS 端也写了一套类似LogInterceptor的请求包装器,和 Flutter 端的格式化规则保持一致。业务方在做日志检索的时候,只需要搜HTTP REQUEST这个关键词,就能同时搜到两端的请求日志,调试跨端通信问题方便了不少。
6.3 后续可以演进的方向
这轮适配完成之后,其实还留下了几个可以继续打磨的点:
- 性能分析维度可以增加弱网模拟,通过鸿蒙的网络扩展能力做丢包和时延注入,看看弱网下的请求表现。
- 日志检索可以接入全文检索引擎,在本地做一个日志文件的索引,支持按 URL 关键字、状态码、时间段做查询,不用每次都用文本编辑器翻文件。
- 隐私合规方面可以再做一层 DataMasker,对可能涉及个人信息的字段(手机号、身份证号)做正则脱敏。
我个人比较关注的是日志检索这块,因为实际排查线上问题的时候,最耗时的往往不是分析日志,而是在一堆日志文件里找到你说的那条日志。如果后续有时间,我打算基于 sqlite 做一套轻量级的日志索引,让查询效率提升一个量级。
7. 最后的几个建议
整个 dio_http_formatter 鸿蒙化适配项目走下来,我最大的体会是:鸿蒙上的 Flutter 生态虽然还在快速完善中,但大部分纯 Dart 的三方库比想象中要容易适配,真正难的反而是一些细节——hilog 的隐私占位符、ANSI 颜色在日志面板的兼容性、FormData 在鸿蒙上的未实现接口,这些坑如果没人提示你,可能要翻很久源码才能找到原因。
我建议如果你也要做类似的适配,一开始就要建立起“日志输出不是小事”的意识。把脱敏机制、日志分级、存储清理这些问题提前设计好,比等出事了再来补要省力得多。
最后分享一个小技巧:在调试鸿蒙上的 Flutter 网络请求时,可以同时开两个日志面板,一个看 hilog,一个看 Flutter 控制台。因为有些网络层的信息只会在 hilog 里出现,比如底层的 DNS 解析错误、socket 连接失败,这些信息通常不会传到 Flutter 引擎的 debugPrint 通道里。两边对照着看,排查效率会高很多。