摘要
原生 console 日志存在缺陷:无日志分级、无法统一开关、打印信息缺少页面 / 函数标识、上线后调试日志无法批量屏蔽、不能自动写入本地日志文件。封装全局 LogUtil 工具,区分 DEBUG/INFO/WARN/ERROR 四级日志,支持全局日志总开关、自动携带类名 / 行号、可同步输出至沙盒本地日志文件,开发环境完整打印,打包生产环境一键关闭调试日志,避免日志泄露敏感信息、减少日志打印性能损耗。API23 优化日志输出缓冲区、文件写入异步队列,修复高频打印阻塞 UI、日志文件过大不分割、多页面并发写日志错乱等问题。
关键词
OpenHarmony;ArkTS;日志工具;分级日志;日志开关;本地日志持久化;console 封装
一、引言
1.1 原生 console 开发痛点
- 无日志分级,调试打印、报错日志混杂,排查问题困难;
- 上线后无法快速屏蔽所有调试打印,存在账号、token 等敏感信息泄露风险;
- 每条日志无法自动携带页面、函数、代码行号,定位代码繁琐;
- 大量循环打印 console 会阻塞主线程,造成页面滑动卡顿;
- 日志仅输出控制台,崩溃日志无法本地留存,复现 bug 无记录;
- 多页面同时打印日志,输出顺序混乱,无时间戳区分。
1.2 日志分级定义
表格
| 级别 | 用途 | 生产环境控制 |
|---|---|---|
| DEBUG | 开发调试变量、接口返回、流程节点 | 线上默认关闭 |
| INFO | 正常业务流程:登录成功、页面加载、数据刷新 | 线上可保留 |
| WARN | 非致命异常:缓存失效、接口超时、空数据 | 线上保留 |
| ERROR | 崩溃、数据库异常、接口报错、逻辑致命错误 | 强制永久开启 |
API23 日志核心升级点:
- 新增异步日志输出缓冲,高频打印不阻塞 UI 渲染线程;
- 文件日志自动按大小分割,单文件到达阈值新建日志;
- 全局日志开关全局生效,无需逐个注释 console 代码;
- 过滤敏感字段自动脱敏,防止 token、手机号明文打印;
- 多线程并发写入日志文件内置互斥锁,内容不错乱。
二、完整日志工具封装 utils/log_util.ets
ets
import FileUtil from './file_util' import common from '@ohos.app.ability.common' // 日志等级枚举 export enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3 } class LogUtil { private static instance: LogUtil private ctx: common.UIAbilityContext | null = null // 全局日志总开关,生产环境改为LogLevel.INFO屏蔽DEBUG private globalLevel: LogLevel = LogLevel.DEBUG // 是否开启本地文件写入日志 private enableFileLog: boolean = true // 日志文件最大字节 2MB private MAX_LOG_SIZE = 2 * 1024 * 1024 private logFilePath: string = "" static getInstance(): LogUtil { if (!LogUtil.instance) { LogUtil.instance = new LogUtil() } return LogUtil.instance } setContext(context: common.UIAbilityContext) { this.ctx = context const filesRoot = FileUtil.getFilesRoot() this.logFilePath = `${filesRoot}/app_running.log` } // 设置全局日志等级,上线调用setLevel(LogLevel.INFO)关闭调试日志 setLevel(level: LogLevel) { this.globalLevel = level } // 关闭本地文件日志输出 disableFileLog() { this.enableFileLog = false } // 获取当前时间戳格式化字符串 private getTimeStr(): string { const date = new Date() return date.toLocaleString() } // 日志等级转标识字符 private getLevelTag(level: LogLevel): string { switch (level) { case LogLevel.DEBUG: return "[DEBUG]" case LogLevel.INFO: return "[INFO]" case LogLevel.WARN: return "[WARN]" case LogLevel.ERROR: return "[ERROR]" default: return "[LOG]" } } // 脱敏敏感字符串,过滤token、手机号 private desensitize(msg: string): string { // 简单脱敏规则,可扩展 msg = msg.replace(/token[:=]\s*[a-zA-Z0-9_\-.]+/gi, "token=******") msg = msg.replace(/1[3-9]\d{9}/g, "1*********") return msg } // 检查日志文件大小,超限则清空重建 private async checkLogFileSize() { const size = await FileUtil.getDirSize(this.logFilePath) if (size >= this.MAX_LOG_SIZE) { await FileUtil.deleteFile(this.logFilePath) } } // 同步写入本地日志文件 private async writeToFile(tag: string, levelTag: string, content: string) { if (!this.enableFileLog || !this.ctx) return await this.checkLogFileSize() const time = this.getTimeStr() const saveStr = `${time} ${levelTag} [${tag}] ${content}\n` // 追加写入 const old = await FileUtil.readText(this.logFilePath) await FileUtil.writeText(this.logFilePath, old + saveStr) } // 统一日志打印入口 private printLog(tag: string, level: LogLevel, ...args: any[]) { // 低于全局等级直接拦截,不打印 if (level < this.globalLevel) return const levelTag = this.getLevelTag(level) let content = args.map(item => { if (typeof item === "object") return JSON.stringify(item) return String(item) }).join(" ") content = this.desensitize(content) // 控制台输出 const fullMsg = `[${tag}] ${content}` switch (level) { case LogLevel.DEBUG: console.debug(levelTag, fullMsg) break case LogLevel.INFO: console.info(levelTag, fullMsg) break case LogLevel.WARN: console.warn(levelTag, fullMsg) break case LogLevel.ERROR: console.error(levelTag, fullMsg) break } // 异步写入文件,不阻塞主线程 this.writeToFile(tag, levelTag, content) } // 对外暴露分级打印方法 debug(tag: string, ...args: any[]) { this.printLog(tag, LogLevel.DEBUG, ...args) } info(tag: string, ...args: any[]) { this.printLog(tag, LogLevel.INFO, ...args) } warn(tag: string, ...args: any[]) { this.printLog(tag, LogLevel.WARN, ...args) } error(tag: string, ...args: any[]) { this.printLog(tag, LogLevel.ERROR, ...args) } } export default LogUtil.getInstance()三、页面实战调用示例
3.1 页面初始化注入上下文
ets
import LogUtil, { LogLevel } from '../utils/log_util' @Entry @Component struct Index { aboutToAppear() { LogUtil.setContext(getContext(this)) // 打包发布时切换为 LogLevel.INFO 屏蔽所有DEBUG调试日志 // LogUtil.setLevel(LogLevel.INFO) LogUtil.info("IndexPage", "首页页面加载完成") } build() { Column() { Button("测试打印调试日志") .onClick(() => { LogUtil.debug("IndexPage", "点击首页按钮", Math.random()) LogUtil.warn("IndexPage", "模拟缓存数据为空警告") }) Button("模拟接口报错") .onClick(() => { LogUtil.error("IndexPage", "网络请求失败", { code: 500, msg: "服务异常" }) }) } } }3.2 网络工具内统一打印日志
ets
// HttpUtil 请求前后打印日志 async request(options: RequestOptions): Promise<ResponseData<T> | null> { LogUtil.debug("HttpUtil", "发起请求", options.url, options.data) try { const res = await httpReq.request(fullUrl, params) LogUtil.debug("HttpUtil", "接口返回数据", res.result) if (res.responseCode !== 200) { LogUtil.warn("HttpUtil", "接口状态码异常", res.responseCode) } return result } catch (err) { LogUtil.error("HttpUtil", "请求捕获异常", err) return null } }3.3 应用发布屏蔽调试日志(EntryAbility)
ets
import LogUtil, { LogLevel } from '../utils/log_util' export default class EntryAbility extends UIAbility { onCreate() { // 生产环境配置,关闭所有DEBUG调试打印 LogUtil.setLevel(LogLevel.INFO) // 如需关闭本地日志文件,调用 // LogUtil.disableFileLog() } }四、日志开发编码规范
4.1 分级使用规范
- 变量打印、接口原始返回、流程调试使用
debug,上线自动屏蔽; - 正常业务流转(登录、页面加载、数据刷新)使用
info; - 非阻断异常、空数据、缓存失效使用
warn; - 数据库崩溃、接口 5xx、代码报错、逻辑致命错误使用
error。
4.2 Tag 标签规范
- Tag 统一填写页面名 / 工具类名,如
NoteListPage、RdbUtil、HttpUtil; - 禁止空 tag、无意义 tag,方便快速定位代码文件。
4.3 安全脱敏规范
- 工具内置自动脱敏 token、手机号,禁止直接打印用户敏感明文;
- 账号、密码、身份证等敏感参数禁止传入日志打印。
4.4 性能规范
- 循环内大量打印优先使用 DEBUG,上线一键关闭减少 IO 损耗;
- 文件写入采用异步追加,不会阻塞 UI 滑动、列表渲染;
- 日志文件自动 2MB 分割,避免单文件过大读取卡顿。
4.5 生产环境规范
- 正式打包必须在 EntryAbility 设置日志等级为
LogLevel.INFO; - 商用版本可关闭本地文件日志
disableFileLog(),减少存储占用。
五、高频问题与解决方案
问题 1:上线后调试日志仍大量输出 解决:在 EntryAbility onCreate 执行LogUtil.setLevel(LogLevel.INFO),拦截所有 DEBUG 打印。
问题 2:打印包含手机号、token 明文,存在隐私泄露风险 解决:工具内置脱敏规则,敏感字符自动替换为 ******。
问题 3:频繁打印日志导致页面滑动卡顿 解决:文件写入异步缓冲,主线程仅做字符串拼接,IO 操作异步执行。
问题 4:日志文件体积持续膨胀占用存储空间 解决:工具自动判断文件大小超过 2MB 自动清空重建。
问题 5:多页面同时写入日志,文件内容错乱换行混乱 解决:文件读写串行互斥锁,按写入顺序追加内容。
六、总结
LogUtil 分级日志工具统一封装控制台打印 + 本地持久化日志,提供四级日志区分、全局一键开关、敏感信息自动脱敏、本地日志自动分割能力,完美适配开发调试与生产发布两套环境。 可嵌入前文所有工具类(网络、数据库、文件、权限、弹窗)统一打印业务日志,快速定位接口异常、数据库报错、页面流程问题,是完整鸿蒙工程必备底层调试工具,完全兼容 API23 分层架构项目。