13-日志太多影响交付-用分级标签把排障和发布态分开
第13篇日志太多影响交付用分级标签把排障态和发布态分开摘要HarmonyOS 项目到交付前日志问题通常不是“多几行输出”这么简单。页面里的console.info、服务里的完整 payload、仓储里的原始异常、临时 mock 标记混在一起真机排查时找不到关键错误发布前又担心隐私字段被带出去。更稳的做法是先给日志分 owner业务只记录阶段和摘要Logger 统一处理等级、标签、脱敏和发布态策略发布前再用固定搜索命令收尾。我处理过一个课程类应用的交付包功能主流程能跑但日志里能看到手机号、请求参数、调试开关和一堆“这里进来了”。一旦真机上出现保存失败关键错误反而被普通输出淹没。后来没有靠人工逐行删除而是把日志入口集中起来按 debug、交付排查、release 三种场景留不同的信息发布前才有可复查的边界。这篇文章解决四个实际问题如何区分开发调试日志、业务阶段日志、错误日志和发布态日志。为什么敏感字段要在 Logger 底层脱敏而不是靠调用方自觉。页面、Service、Repository 分别应该记录什么不能记录什么。发布前如何用搜索、真机日志和构建命令确认日志已经收口。适用场景和边界本文讨论的是应用层 ArkTS 日志治理重点在页面、服务、本地仓储、发布前自查这几层。底层输出可以接hilog也可以先接项目已有的 console sink关键是业务代码不要直接散写输出 API。维度本文处理不在本文展开输出入口页面和服务统一走AppLogger云端日志平台选型日志内容阶段、业务 id、错误摘要、耗时完整请求体和用户隐私发布策略debug 详细release 克制后端链路追踪系统验证方式搜索、真机日志、构建、发布前清单平台侧日志采集规则这个边界很重要。日志治理不是让项目失去排障能力而是让每条留下来的日志都有明确用途并且不会把用户数据和临时调试痕迹带进交付包。先把日志拆成四类不要一上来就全局删除console。先把日志按用途分清楚再决定保留、降级、脱敏还是删除。类型例子交付前处理临时调试日志console.info(here)、打印整段对象删除或改成有标签的阶段日志业务阶段日志保存开始、保存成功、刷新失败保留摘要debug 更详细release 降到 warn/error异常日志仓储写入失败、解析失败、权限异常保留错误码和阶段不打印隐私内容mock / 开关日志mock user、debugOnly、测试入口发布前必须能解释不需要就移除这个分类能避免两个极端一种是交付前把日志删光真机出了问题只能猜另一种是所有日志都留下发布包像开发现场。比较稳的方式是让日志在开发态有细节在发布态只留下必要告警和错误。定义标签、等级和事件模型标签是排查入口等级是发布策略入口事件模型负责约束日志内容。先把这三件事写成类型后面页面和服务才不会随手拼字符串。exportenumLogTag{Startupstartup,Navigationnavigation,Learninglearning,Storagestorage,Releaserelease}exportenumLogLevel{Debug10,Info20,Warn30,Error40}exportinterfaceLogEvent{tag:LogTag level:LogLevel message:stringfields?:Recordstring,stringerrorCode?:string}这段代码的重点不是枚举本身而是把“日志是什么”从一行输出变成一条结构化事件。后面不管底层接hilog、console 还是自研 sink业务侧都只交付LogEvent。用 LogSink 隔离底层输出业务代码不要直接依赖某个输出 API。先抽一个很薄的 sink把底层实现藏起来。这样以后从 console 换到hilog或者增加文件日志都不会让页面跟着改。exportinterfaceLogSink{write(event:LogEvent):void}exportclassConsoleLogSinkimplementsLogSink{write(event:LogEvent):void{constfieldsevent.fields?JSON.stringify(event.fields):console.info([${event.tag}]${event.message}${fields})}}exportclassAppLogger{constructor(privatereadonlyminLevel:LogLevel,privatereadonlysink:LogSink){}debug(tag:LogTag,message:string,fields?:Recordstring,string):void{this.write({tag,level:LogLevel.Debug,message,fields})}warn(tag:LogTag,message:string,fields?:Recordstring,string,errorCode?:string):void{this.write({tag,level:LogLevel.Warn,message,fields,errorCode})}error(tag:LogTag,message:string,fields?:Recordstring,string,errorCode?:string):void{this.write({tag,level:LogLevel.Error,message,fields,errorCode})}privatewrite(event:LogEvent):void{if(event.levelthis.minLevel){return}constsafeEvent:LogEvent{...event,fields:event.fields?PrivacyMasker.maskMap(event.fields):undefined}this.sink.write(safeEvent)}}这里的边界很清楚页面和服务只调用debug()、warn()、error()AppLogger负责等级过滤和脱敏LogSink才关心真正怎么输出。发布态要减少日志只调创建 Logger 的配置不逐个文件删。敏感字段要在底层统一脱敏隐私问题不能靠“每个调用者记得打码”。只要日志允许携带字段就要在 Logger 底层统一处理手机号、token、定位、身份证、长文本这些高风险内容。exportclassPrivacyMasker{staticmaskMap(input:Recordstring,string):Recordstring,string{constresult:Recordstring,string{}Object.keys(input).forEach((key:string){result[key]this.maskValue(key.toLowerCase(),input[key])})returnresult}privatestaticmaskValue(key:string,value:string):string{if(key.includes(phone)){returnvalue.replace(/(\d{3})\d{4}(\d{4})/,$1****$2)}if(key.includes(token)||key.includes(password)||key.includes(idcard)){return[masked]}if(key.includes(location)){return[location]}returnvalue.length80?${value.slice(0,80)}...:value}}这段逻辑解决的是“日志入口越多越难保证安全”的问题。即使某个业务同学传了phone或token底层也会先处理。项目里可以继续扩展字段名规则但不要把脱敏散落在每个页面。业务服务只记录阶段和摘要日志有价值不等于要打印完整业务对象。以保存学习记录为例服务层只需要留下阶段、课程 id、耗时和错误摘要不需要打印用户备注、原始 payload 或完整响应。exportinterfaceSaveLearningPayload{courseId:stringminutes:numbernote?:string}exportclassLearningSaveService{constructor(privatereadonlyrepository:LearningRepository,privatereadonlylogger:AppLogger){}asyncsave(payload:SaveLearningPayload):Promisevoid{conststartedAtDate.now()this.logger.debug(LogTag.Learning,start save learning record,{courseId:payload.courseId,minutes:${payload.minutes}})try{awaitthis.repository.save(payload)this.logger.debug(LogTag.Learning,finish save learning record,{courseId:payload.courseId,costMs:${Date.now()-startedAt}})}catch(err){this.logger.error(LogTag.Storage,save learning record failed,{courseId:payload.courseId,costMs:${Date.now()-startedAt}},STORAGE_SAVE_FAILED)throwerr}}}这段代码保留了排障最需要的信息哪条业务链路、哪个课程、在哪一步失败、耗时多少。用户备注没有进日志完整异常也没有直接透出。真正要看堆栈时可以在 debug sink 或本地开发工具里处理不要默认进入发布态输出。发布态策略只在创建 Logger 时切换不要在每个业务方法里写if (release)。构建模式、远程开关或本地配置只应该影响 Logger 的创建。exporttypeBuildModedebug|releaseexportfunctioncreateAppLogger(buildMode:BuildMode):AppLogger{constminLevelbuildModerelease?LogLevel.Warn:LogLevel.DebugreturnnewAppLogger(minLevel,newConsoleLogSink())}这样做有两个好处第一业务代码始终表达“这里发生了什么”第二发布策略集中在一个入口交付前调整不会引发大面积改动。后续如果要接hilog也只需要替换ConsoleLogSink。真机排查要能按标签串起链路好的日志不是数量多而是能把一次真实操作串起来。以“保存学习记录后首页没有刷新”为例至少要能看到这几段。顺序标签期望日志如果缺失1learningstart save learning record页面没有调用服务或入口没埋2storagesave failed / write success仓储层没记录关键结果3learningfinish save learning record保存流程中断4navigationback to home / refresh requested返回或刷新信号没发5releaseonly warn/error in release发布态仍有 debug 噪音排查时不要只问“有没有日志”要问“这条链路能不能被日志解释”。能解释就保留解释不了只是在增加噪音。发布前用搜索命令兜底日志治理最后要靠固定命令收尾。人肉翻文件一定会漏尤其是临时 mock、测试入口和整段对象输出。rgconsole\\.|TODO_LOG|debugOnly|mock|测试数据|临时entry features common rgphone|token|password|idCard|身份证|location|定位entry features common rgAppLogger|LogTag|LogLevel|PrivacyMaskerentry features common.\hvigorw.bat assembleHap--no-daemon第一条找临时输出和 mock 标记第二条找可能泄露的字段第三条确认日志治理入口有没有被绕开最后构建一次确认改动没有破坏工程。命中结果不一定都要删但每一处都要能说明为什么可以留下。风险提示和回退方案日志治理容易出问题的地方通常不是代码写不出来而是边界收得太急或太散。风险触发场景处理方式发布态失明把 warn/error 也删掉release 保留异常和关键告警敏感信息漏出业务直接打印 payload禁止绕过AppLogger底层统一脱敏排查困难标签随手写字符串用LogTag固定模块标签搜索误伤看到字段名就删业务代码先分类日志、模型、真实业务字段分开处理接口切换成本高页面直接调用底层输出 API用LogSink把输出实现隔开如果项目日志已经非常散建议先接入AppLogger和脱敏再分批迁移高风险入口。不要第一天就把所有输出删光否则新问题出现时会失去定位依据。常见问题和处理方式现象常见原因处理方式真机日志很多但找不到失败点没有标签和阶段按业务链路补LogTag和阶段日志发布前到处删console输出入口分散统一走AppLogger再按等级过滤日志里出现手机号或 token脱敏靠调用方手动处理PrivacyMasker放到 Logger 底层mock 标记进了包没有发布前搜索固定rg清单并逐项解释release 仍输出 debug 细节等级策略没集中创建 Logger 时统一设置minLevel这些问题的共同点是日志没有 owner。把 owner 定下来之后删什么、留什么、降级什么都能根据用途判断。验证清单改完日志链路后我会按这组路径复查debug 模式走启动、登录、保存、返回首页主流程确认关键阶段能串起来。人为制造一次仓储失败确认错误日志有标签、阶段和错误码。在日志输出里搜索手机号、token、定位、完整请求体确认没有原始敏感内容。release 模式确认 debug 细节不再输出但 warn/error 仍能看到。运行发布前搜索命令逐项处理console、mock、debugOnly 和测试数据。执行hvigorw assembleHap --no-daemon确认日志治理没有破坏构建。这里的验证重点不是“日志越少越好”而是开发态能排障发布态不泄露交付前能复查。这次问题给我的复盘我后来复盘这个交付包真正的问题不是日志太多而是日志没有分工。页面想记录交互服务想记录业务阶段仓储想记录失败原因发布前又想删掉临时输出。所有目标混在一行console.info里最后只能靠人工判断。如果重新处理一次我会先做三件事先定义LogTag和LogLevel再把敏感字段脱敏放进 Logger最后用搜索命令把旧输出分批收口。这样既不会把排障能力删没也不会让发布包带着开发期噪音出去。小结日志要有用途也要有边界日志太多影响交付时不要先全删。先把日志分成临时调试、业务阶段、异常和发布态四类再用统一 Logger 处理等级、标签和脱敏。页面和服务只表达业务事件底层输出由LogSink负责发布前用搜索和真机路径复查。这样留下来的日志能排障删掉的日志也有理由。
