摘要

原生 console 日志存在缺陷:无日志分级、无法统一开关、打印信息缺少页面 / 函数标识、上线后调试日志无法批量屏蔽、不能自动写入本地日志文件。封装全局 LogUtil 工具,区分 DEBUG/INFO/WARN/ERROR 四级日志,支持全局日志总开关、自动携带类名 / 行号、可同步输出至沙盒本地日志文件,开发环境完整打印,打包生产环境一键关闭调试日志,避免日志泄露敏感信息、减少日志打印性能损耗。API23 优化日志输出缓冲区、文件写入异步队列,修复高频打印阻塞 UI、日志文件过大不分割、多页面并发写日志错乱等问题。

关键词

OpenHarmony;ArkTS;日志工具;分级日志;日志开关;本地日志持久化;console 封装

一、引言

1.1 原生 console 开发痛点

  1. 无日志分级,调试打印、报错日志混杂,排查问题困难;
  2. 上线后无法快速屏蔽所有调试打印,存在账号、token 等敏感信息泄露风险;
  3. 每条日志无法自动携带页面、函数、代码行号,定位代码繁琐;
  4. 大量循环打印 console 会阻塞主线程,造成页面滑动卡顿;
  5. 日志仅输出控制台,崩溃日志无法本地留存,复现 bug 无记录;
  6. 多页面同时打印日志,输出顺序混乱,无时间戳区分。

1.2 日志分级定义

表格

级别 用途 生产环境控制
DEBUG 开发调试变量、接口返回、流程节点 线上默认关闭
INFO 正常业务流程:登录成功、页面加载、数据刷新 线上可保留
WARN 非致命异常:缓存失效、接口超时、空数据 线上保留
ERROR 崩溃、数据库异常、接口报错、逻辑致命错误 强制永久开启

API23 日志核心升级点:

  1. 新增异步日志输出缓冲,高频打印不阻塞 UI 渲染线程;
  2. 文件日志自动按大小分割,单文件到达阈值新建日志;
  3. 全局日志开关全局生效,无需逐个注释 console 代码;
  4. 过滤敏感字段自动脱敏,防止 token、手机号明文打印;
  5. 多线程并发写入日志文件内置互斥锁,内容不错乱。

二、完整日志工具封装 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 分级使用规范

  1. 变量打印、接口原始返回、流程调试使用 debug,上线自动屏蔽;
  2. 正常业务流转(登录、页面加载、数据刷新)使用 info
  3. 非阻断异常、空数据、缓存失效使用 warn
  4. 数据库崩溃、接口 5xx、代码报错、逻辑致命错误使用 error

4.2 Tag 标签规范

  1. Tag 统一填写页面名 / 工具类名,如NoteListPageRdbUtilHttpUtil
  2. 禁止空 tag、无意义 tag,方便快速定位代码文件。

4.3 安全脱敏规范

  1. 工具内置自动脱敏 token、手机号,禁止直接打印用户敏感明文;
  2. 账号、密码、身份证等敏感参数禁止传入日志打印。

4.4 性能规范

  1. 循环内大量打印优先使用 DEBUG,上线一键关闭减少 IO 损耗;
  2. 文件写入采用异步追加,不会阻塞 UI 滑动、列表渲染;
  3. 日志文件自动 2MB 分割,避免单文件过大读取卡顿。

4.5 生产环境规范

  1. 正式打包必须在 EntryAbility 设置日志等级为LogLevel.INFO
  2. 商用版本可关闭本地文件日志disableFileLog(),减少存储占用。

五、高频问题与解决方案

问题 1:上线后调试日志仍大量输出 解决:在 EntryAbility onCreate 执行LogUtil.setLevel(LogLevel.INFO),拦截所有 DEBUG 打印。

问题 2:打印包含手机号、token 明文,存在隐私泄露风险 解决:工具内置脱敏规则,敏感字符自动替换为 ******。

问题 3:频繁打印日志导致页面滑动卡顿 解决:文件写入异步缓冲,主线程仅做字符串拼接,IO 操作异步执行。

问题 4:日志文件体积持续膨胀占用存储空间 解决:工具自动判断文件大小超过 2MB 自动清空重建。

问题 5:多页面同时写入日志,文件内容错乱换行混乱 解决:文件读写串行互斥锁,按写入顺序追加内容。

六、总结

LogUtil 分级日志工具统一封装控制台打印 + 本地持久化日志,提供四级日志区分、全局一键开关、敏感信息自动脱敏、本地日志自动分割能力,完美适配开发调试与生产发布两套环境。 可嵌入前文所有工具类(网络、数据库、文件、权限、弹窗)统一打印业务日志,快速定位接口异常、数据库报错、页面流程问题,是完整鸿蒙工程必备底层调试工具,完全兼容 API23 分层架构项目。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐