ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flutter插件鸿蒙适配:接入CloudWatch日志与监控

Flutter插件鸿蒙适配:接入CloudWatch日志与监控 从 Flutter 项目迁到 OpenHarmony 设备上跑最头疼的往往不是 Dart 侧而是那些看起来人畜无害的三方插件。aws_cloudwatch_api就是这样的角色在 Android 上接 CloudWatch直接挂 AWS SDK 就行全程十几分钟可一旦目标变成「在鸿蒙设备上报云原生监控数据」你会发现原有的原生依赖完全失效包里的android/、ios/目录在 OpenHarmony 上都不认Dart 层调了白调日志埋点一条都发不出去。这篇文章记录的是我最近在一批 OpenHarmony 工控终端上接入 AWS CloudWatch 的完整适配过程。最终目标很具体让 Flutter 应用在鸿蒙环境里通过aws_cloudwatch_api完成两件事——把安全相关日志登录结果、权限变更、异常操作写入 CloudWatch Logs同时把关键业务指标通过 PutMetricData 推到 CloudWatch Metrics。如果你也在折腾这类三方库的鸿蒙适配这篇指南应该能帮你少走不少弯路。1. 适配决策aws_cloudwatch_api 到底要动哪一层1.1 先判断这个包是“纯 Dart”还是“带原生依赖”拿到一个三方包第一步不是急着写代码而是把它拆开看结构。aws_cloudwatch_api对外暴露的 API 通常是putMetricData、putLogEvents、createLogStream、getMetricStatistics这些CloudWatch 的核心操作基本都有。但真正决定适配工作量的是它的实现方式。打开源码之后重点看仓库目录如果根目录只有lib/没有android/和ios/说明它内部大概率用package:http直接请求 AWS 的 REST API是纯 Dart 实现。这种包在 OpenHarmony 上通常能直接跑因为 Dart 代码是跨平台的。如果同时存在android/、ios/目录说明关键功能依赖原生 SDKDart 只是套壳。这种包迁到鸿蒙时问题就大了android/依赖的 Gradle 产物、iOS 依赖的 CocoaPods 在 OpenHarmony 上都不存在MethodChannel 根本没有对端。我实际遇到的是第二种。这个包在构建阶段不报错Dart 编译器也通过但运行后所有 CloudWatch 调用都卡死在MissingPluginException上。这个异常非常典型Dart 层发出了 channel 调用可鸿蒙侧没有对端实现平台通道直接返回「找不到插件」。1.2 OpenHarmony 上 Flutter 插件的生命周期动摇改之前还得把 OpenHarmony 的 Flutter 插件机制说清楚不然很容易在注册环节踩空。OpenHarmony 上的 Flutter 引擎其实是标准的 Flutter 运行时Dart 侧通过BinaryMessenger把消息传到 Native 侧Native 侧由 ArkTS 宿主实现FlutterPlugin接口。主流程分四步Flutter 引擎在 Ability 初始化时启动插件注册器插件实现FlutterPlugin在onAttach回调里创建 MethodChannelDart 侧调用invokeMethod后ArkTS 侧回调方法开始分发处理结果通过result.success()或result.error()回传。理解了这条链路之后再回头看 m所谓的鸿蒙适配本质上就是Dart 层不动ArkTS 侧补齐一个对等的插件实现把原来 Android/iOS 原生 SDK 完成的工作用鸿蒙原生 API 重写一遍。1.3 改造路线换纯 Dart 包还是自建插件模块面对这种带原生依赖的包一般有三条路可以选方案 A换一个纯 Dart 实现的 CloudWatch 客户端。优点是省事缺点是如果团队已经基于aws_cloudwatch_api封装了业务代码替换成本不小而且有些包对 CloudWatch 新特性的支持不一定全。方案 B给现有包补一个ohos/模块。保留 Dart 层 API 不变在包工程里新增 OpenHarmony 插件模块实现同样的 MethodChannel。这样上层业务代码一行不用改。方案 C完全绕开包在业务层用package:http直连 CloudWatch API。最灵活但工作量最大而且签名逻辑要自己维护。我最终选了方案 B。理由是这套包在团队里的调用面已经铺开了与其换 API 不如补齐鸿蒙侧实现上层业务代码和测试用例全部复用能少出很多回归问题。2. 通道层打通把 Dart 的 MethodChannel 接到 ArkTS 侧2.1 先把包的 Dart 侧调用方式摸清楚既然要保留 Dart 层不动就得知道aws_cloudwatch_api到底用了哪些 MethodChannel 方法。翻一下包里的 Dart 源码通常能看到类似下面的定义import package:flutter/services.dart; class AwsCloudwatchApi { static const MethodChannel _channel MethodChannel(aws_cloudwatch_api); Futurevoid putMetricData(MapString, dynamic metricData) async { await _channel.invokeMethod(putMetricData, metricData); } FutureMapString, dynamic putLogEvents(MapString, dynamic logEvents) async { return await _channel.invokeMethod(putLogEvents, logEvents); } }注意两点一是 Channel 的名字要原封不动地抄下来鸿蒙侧注册时必须完全一致否则 Dart 调用会落到MissingPluginException二是invokeMethod传入的 Map 会被编码成标准消息鸿蒙侧收到的是类似 JSON 的键值对象字段类型要仔细核对。2.2 ArkTS 侧插件骨架工程OpenHarmony 的 Flutter 模板工程里插件一般放在ohos/src/main/ets/下。我新建了一个AwsCloudWatchApiPlugin.ets实现FlutterPlugin接口在onAttach里把自己注册到 MethodChannel// AwsCloudWatchApiPlugin.ets import { FlutterPlugin, FlutterPluginBinding, MethodChannel, MethodCall, MethodResult } from ohos/flutter_plugin_bindings; export class AwsCloudWatchApiPlugin implements FlutterPlugin { private channel: MethodChannel | null null; onAttach(binding: FlutterPluginBinding): void { this.channel new MethodChannel(binding.getBinaryMessenger(), aws_cloudwatch_api); this.channel.setMethodCallHandler(this.handleMethodCall.bind(this)); } private async handleMethodCall(call: MethodCall, result: MethodResult): Promisevoid { switch (call.method) { case putMetricData: await this.putMetricData(call.arguments as object, result); break; case putLogEvents: await this.putLogEvents(call.arguments as object, result); break; case createLogStream: await this.createLogStream(call.arguments as object, result); break; default: result.notImplemented(); } } private async putMetricData(args: object, result: MethodResult): Promisevoid { // 调用 CloudWatch 服务具体逻辑见第 3、4 章 } private async putLogEvents(args: object, result: MethodResult): Promisevoid { // 调用 CloudWatch Logs 服务 } private async createLogStream(args: object, result: MethodResult): Promisevoid { // 初始化日志流 } onDetach(): void { this.channel?.setMethodCallHandler(null); this.channel null; } }注册位置也容易踩坑。不同 Flutter Fork 版本对插件注册的入口要求不一样有的直接在 EntryAbility 的onCreate里通过addPlugin注册有的要求在 Flutter Engine 初始化完成后手动绑定。我调的时候最终是在 AbilityStage 中拿到了引擎实例然后把插件挂了进去。2.3 网络权限和模块配置不能漏插件代码写好后还有一个特别容易忽略的点网络权限。aws_cloudwatch_api是要往外发 HTTPS 请求的而很多基于 OpenHarmony 的工程默认并没有给应用开网络权限。在src/main/module.json5的requestPermissions里必须加上{ requestPermissions: [ { name: ohos.permission.INTERNET } ] }如果调试环境用的是自签证书或非标准 HTTP 网关还得检查鸿蒙的安全配置文件里的网络白名单。生产环境连接 CloudWatch 走的是标准 HTTPS一般不需要额外开明文放行但如果你在实验室里自建了代理网关做抓包那就要单独加规则别在正式包上放开。3. 签名不自欺在 ArkTS 里手写 SigV4 的完整过程3.1 为什么不能直接调现成的 AWS SDK很多朋友会问aws_cloudwatch_api在 Android 上是调 AWS 原生 SDK 的鸿蒙上有没有类似的 SDK很遗憾AWS 目前没有提供 OpenHarmony 的官方 SDK也没有发布对应的.har包。这就意味着 CloudWatch 的 HTTP 请求签名得自己算。AWS 签名协议 SigV4 是所有的路都绕不开的核心。3.2 SigV4 签名到底在干什么一句话解释SigV4 就是用你的 AccessKey 和 SecretKey对当前请求的内容、时间、地域、服务名做一系列 HMAC-SHA256 运算生成一个不可伪造的 Authorization 头。AWS 服务端拿同样的信息重算一遍能对上就放行对不上就返回 403。具体来说签名分四步构造 CanonicalRequest规范化请求用 CanonicalRequest 生成 StringToSign用 SecretKey 逐层派生出 SigningKey对 StringToSign 做 HMAC得到 SignatureCanonicalRequest 的形式是这样的HTTPMethod CanonicalURI CanonicalQueryString CanonicalHeaders SignedHeaders HexEncode(SHA256(RequestPayload))以 PutMetricData 为例假设 Region 是us-east-1请求头长这样POST / 空查询串 content-type:application/x-amz-json-1.1 host:monitoring.us-east-1.amazonaws.com x-amz-date:20240216T120000Z content-type;host;x-amz-date hex(sha256(requestPayload))注意这里多了一个x-amz-target头CloudWatch Metrics 服务的 Targets 头是monitoring.PutMetricData。这个头参与签名但要记得在最终发送请求时也要带上漏了服务端一样拒绝。3.3 在 ArkTS 里用 CryptoFramework 实现 HMACArkTS 里做签名最方便的是用鸿蒙的kit.CryptoArchitectureKit。下面是我实际跑通的核心代码片段import { cryptoFramework } from kit.CryptoArchitectureKit; import { util } from kit.ArkTS; function stringToBytes(str: string): Uint8Array { return new util.TextEncoder().encodeInto(str); } function bytesToHex(bytes: Uint8Array): string { let hex ; bytes.forEach((b: number) { hex b.toString(16).padStart(2, 0); }); return hex; } async function sha256Hex(data: Uint8Array): Promisestring { const md cryptoFramework.createMd(SHA256); await md.update({ data }); const digest await md.digest(); return bytesToHex(digest.data); } async function hmacSha256(key: Uint8Array, message: Uint8Array): PromiseUint8Array { const mac cryptoFramework.createMac(SHA256); const keyBlob: cryptoFramework.DataBlob { data: key }; await mac.init(keyBlob); await mac.update({ data: message }); const output await mac.doFinal(); return output.data; } async function getSigningKey(secretKey: string, shortDate: string, region: string, service: string): PromiseUint8Array { const kDate await hmacSha256(stringToBytes(AWS4 secretKey), stringToBytes(shortDate)); const kRegion await hmacSha256(kDate, stringToBytes(region)); const kService await hmacSha256(kRegion, stringToBytes(service)); const kSigning await hmacSha256(kService, stringToBytes(aws4_request)); return kSigning; }然后组装Authorization头AWS4-HMAC-SHA256 CredentialAKIAXXXX/20240216/us-east-1/monitoring/aws4_request, SignedHeaderscontent-type;host;x-amz-date;x-amz-target, Signature计算出来的十六进制串注意如果你用的是 STS 临时凭证还需要加x-amz-security-token头并且这个头也要纳入 SignedHeaders。3.4 设备时间不同步是最隐蔽的坑签名做对了请求还是 403这时候八成是设备时间不对。SigV4 签名里带的是请求时间x-amz-dateAWS 服务端会校验时间窗口前后超过 15 分钟基本直接拒绝。OpenHarmony 工控终端如果没接 NTP 同步设备时间往往慢几分钟甚至差几个小时。我排查过一台设备签名字符串完全正确但就是连不上最后发现是 RTC 电池失效系统时间停留在几天前。解决办法在应用层每次请求前和服务器时间做一次可靠对时可以从后端或任意 HTTPS API 拿到标准时间或者直接把签名有效期设计得非常短配合网络对时服务刷新x-amz-date。4. CloudWatch 对接实战安全日志上报的完整链路4.1 指标和日志先分清楚该走哪个口CloudWatch 里有两套不同的上报通道很多人一开始就混着用| 上报内容 | 通道 | 接口 | 典型用途 | 维度示例 | | 登录次数、失败率、设备存活数等数值型指标 | Metrics | PutMetricData | 告警、趋势图、扩缩容 | userId、deviceId、result | | 审计记录、原始事件、结构化日志 | Logs | PutLogEvents | 检索、追溯、安全分析 | logGroup 按业务划分logStream 按设备划分 |我的做法很直接所有适合聚合的数字走 Metrics所有需要事后查原文的内容走 Logs。登录失败次数这种聚合值放到 Metrics 里做监控告警登录失败的完整记录IP、设备型号、失败原因放进 Logs 做审计。4.2 PutMetricData 的请求构造与限制调用 PutMetricData 时请求体是一个 Namespace 加一个 MetricData 数组{ Namespace: HarmonyApp/Security, MetricData: [ { MetricName: LoginFailureCount, Value: 3, Unit: Count, Timestamp: 1708820580000, Dimensions: [ { Name: DeviceId, Value: harmony-device-001 }, { Name: Result, Value: FAILED } ] } ] }这里有几个容易被忽略的约束单次请求最多 1000 个 MetricDatum超出要分批。同一个请求里的 Metric 不能重名、同维度完全重复会返回 InvalidParameterValue。Timestamp 需要是毫秒时间戳千万不要传字符串。安全场景下我最看重的是LoginFailureCount这个指标配合 CloudWatch 的告警规则当某个设备连续失败超过阈值时能直接触发告警非常适合用来跟踪异常登录。4.3 PutLogEvents 与 sequenceToken一个必须记住顺序的流程Logs 上报比 Metrics 麻烦的地方在于sequenceToken。PutLogEvents 接口要求同一时刻只能有一个写操作而且每次调用必须携带上一次成功返回的nextSequenceToken否则服务端会拒绝。过程是这样的第一次上传前先确认 LogGroup 和 LogStream 存在不存在就调用createLogGroup/createLogStream创建第一次 PutLogEvents 不带 sequenceToken服务端会返回nextSequenceToken第二次以后每次上传都要带上这个 token如果并发冲突服务端返回InvalidSequenceTokenException同时返回一个expectedSequenceToken拿它重试。这个状态必须缓存到本地持久化存储里不能每次启动都从头再来。我的实现是把 token 存到了应用沙箱目录启动时读出来上传成功后立刻更新。4.4 安全日志的典型字段设计给日志设计结构时我是按「谁、何时、何事、什么结果、附加信息」的原则来的。一个登录事件的实际 payload 大概是这样的{ logVersion: 1.0, eventType: LOGIN, eventId: a3f4b8c9-6e2f-4d8f-b8b1-92c4f1a8e6b1, occurredAt: 1708820580321, userId: u_1024, deviceId: harmony-device-001, sourceIp: 172.16.30.12, action: login, result: FAILED, failReason: WRONG_PASSWORD, appVersion: 2.3.1 }强烈建议在 ArkTS 侧把消息做成 JSON 字符串再上传不要在客户端把敏感字段打印到日志里。密码、令牌这类东西永远不要进 logEvents 的 message。如果需要对错误码做告警就把这些字段同时映射成 Metrics 维度两条通道并行既保留现场又方便统计。4.5 指数退避重试不能省端侧设备网络不稳定上报失败是常态。我设计的重试逻辑很简单第一次失败等 1 秒第二次等 2 秒第三次等 4 秒最多重试 5 次超过就落本地缓存等下次启动再补。有个细节PutLogEvents 失败后如果服务端返回了expectedSequenceToken重试时必须用这个新 token否则会一直撞墙。撞墙的时间都花在没意义的重复请求上还会加剧配额消耗。5. 真机调试里被敲醒的五个问题5.1 HTTP 客户端的 CA 证书校验比想象中严格最开始我在 ArkTS 里用kit.BasicServicesKit的http模块发请求连 CloudWatch 一直报错。日志拉出来一看是 TLS 握手失败。OpenHarmony 的 HTTP 客户端默认会做完整的证书链校验和部分 Android 设备上的宽松策略不同。如果你的测试环境用了 Charles 之类的代理抓包代理证书没装进系统信任区域就会在这里翻车。我在代码里临时把校验关掉来排查问题但最终上线前恢复成了默认安全策略。生产环境千万不要设置跳过 SSL 校验安全日志系统如果自己都不安全那整套监控就是笑话。5.2 ArkTS 严格模式下的类型转换Flutter 侧传过来的 Map在 ArkTS 侧实际拿到的是一个键值对象。ArkTS 类型检查很严格不能随便as any我在取Timestamp字段时踩过坑Flutter 侧传的是intArkTS 侧拿到的可能是number但 Map 里的值类型还是得做显式转换不转换直接用会报类型错误。建议在插件入口统一清理参数类型const timestamp Number(args[timestamp]); const metricName args[metricName] as string;只要涉及平台通道传参一律在入口做一次类型收敛后面逻辑就清爽了。5.3 请求超时和并发控制CloudWatch API 对单个账户有并发和吞吐限制端侧十几台设备同时上报问题不大但如果一个设备上多个页面并发调用很容易触发限流。我的做法是在 ArkTS 插件里加了一个简单的串行队列所有 PutLogEvents 请求进入一个队列前一个完成才发下一个。这正好也满足 sequenceToken 的时序要求一举两得。5.4 别把 AccessKey 留在端侧安全日志上报最不该犯的错误是把长期 AccessKey 硬编码在 App 里。CloudWatch 的权限面一旦泄漏攻击者可以直接读取或删除整个日志组。我的方案是后端做一个 STS 换证接口Flutter 端启动时请求后端后端用自己的权限签发临时凭证有效期 30 分钟再把 AccessKey、SecretKey、SessionToken 传给 ArkTS 侧用。即使设备被攻破临时凭证的伤害面也有限得多。5.5 配额的隐形墙请求量超了才知道CloudWatch 有一套默认配额端侧流量不大一般碰不到但一旦日志批量补传很容易瞬间就撞上限制| 接口 | 限制 | | CreateLogStream | 默认每秒 5 个请求 | | PutLogEvents | 单次请求最大 1MB 有效载荷 | | PutMetricData | 单次 1000 个数据点、总载荷不超过 1MB | | GetMetricData | 单次最多 50 个指标 |我撞过的是 CreateLogStream 限流几十台设备同时开机每台都要初始化日志流触发 Rate exceeded。解决方式很简单把创建日志流的操作改成提前统一预建或者加本地缓存同设备只建一次。6. 分享一点做适配时的个人体会最后说几句实在话。这套适配做下来最大的体会是三方包的鸿蒙适配难点从来不在代码量而在你对底层链路的理解程度。Dart 层的包代码你基本碰不到真正决定成败的是 ArkTS 侧的通道注册、SigV4 签名的准确性以及对 CloudWatch 各种接口脾气的掌握程度。我个人现在写这类适配代码习惯把通道注册、签名、网络请求、重试队列这四个模块独立成类方便在真机上单独打日志排查。鸿蒙侧看不到 Android 的 Logcat很多问题只能靠插桩定位模块拆清楚排障效率能高很多。另外安全日志的字段结构一定要提前定死两边Flutter 层和 ArkTS 层同时维护一份相同定义不然改一次字段就要动两层代码后期维护很痛。希望这份指南能帮到正在做同样适配的团队。
返回列表