HarmonyOS HiLog 日志治理实战:分级、脱敏、采样与线上排查
HarmonyOS HiLog 日志治理实战:分级、脱敏、采样与线上排查
很多线上问题不是没有日志,而是日志太乱:同一个模块多个 tag,错误日志没有 traceId,用户手机号直接打印,灰度包一打开日志量暴涨。真正可用的日志体系要同时满足四件事:开发能定位、线上能控制、隐私不泄露、复盘能串起来。
这篇文章围绕 HiLog 写一套可落地的日志治理方法,把“随手打印”改成“可排查的工程证据”。

1. 资料定位:日志不是输出越多越好
HarmonyOS 提供 HiLog 能力和 hilogtool 等调试工具,适合在开发和排查阶段观察运行状态。项目里要先约定日志的职责边界:Debug 看过程,Info 看关键路径,Warn 看可恢复风险,Error 看失败结果,Fatal 只用于不可继续的严重问题。
| 资料/工具 | 用途 | 本文落地点 |
|---|---|---|
| HiLog | 应用侧输出分级日志 | 统一封装 |
| hilogtool | 设备侧查看日志 | 按 tag/domain 过滤 |
| 隐私规范 | 避免敏感字段明文 | 脱敏函数 |
| 发布开关 | 控制线上日志量 | 采样与级别 |
建议在工程文档里写清楚:当前验证环境、API 版本、是否真机、是否 release 包、日志级别开关在哪里。否则同一段日志在 debug 包能看到,release 包看不到,排查时很容易误判。
2. 总流程:先定字段,再写封装

日志治理不要从“哪里加一行打印”开始,而要从字段开始。最少需要 domain、tag、level、traceId、message、costMs、errorCode。字段稳定后,后续才能做聚合、过滤和复盘。
export type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'
export interface AppLogEvent {
level: LogLevel
domain: number
tag: string
traceId: string
message: string
costMs?: number
errorCode?: string
}
这个接口拥有日志事件的结构边界。它不关心日志最终写到哪里,只规定业务层必须提供哪些字段。它防止日志格式随人变化;下一层统一决定脱敏、采样和输出级别。
3. 结构归属:业务层不要直接拼日志

业务代码直接拼字符串会带来两个问题:格式不统一,敏感字段容易漏。更稳的结构是业务层只提交事件,日志层负责格式化,排查层按 traceId 回放。
import { hilog } from '@kit.PerformanceAnalysisKit'
const DEFAULT_DOMAIN = 0x1901
export class AppLogger {
static info(tag: string, traceId: string, message: string, costMs?: number): void {
this.write({ level: 'INFO', domain: DEFAULT_DOMAIN, tag, traceId, message, costMs })
}
static warn(tag: string, traceId: string, message: string, errorCode?: string): void {
this.write({ level: 'WARN', domain: DEFAULT_DOMAIN, tag, traceId, message, errorCode })
}
private static write(event: AppLogEvent): void {
const safeMessage = LogMasker.mask(event.message)
hilog.info(event.domain, event.tag, 'trace=%{public}s level=%{public}s msg=%{public}s cost=%{public}d code=%{public}s',
event.traceId, event.level, safeMessage, event.costMs ?? -1, event.errorCode ?? '-')
}
}
AppLogger 的边界是统一输出,不处理业务决策。它验证的是日志字段和安全消息,预防随手打印导致的格式分裂。下一层会补采样策略,避免线上日志过多。
4. 脱敏:手机号、Token、身份证不能进明文日志
日志泄露经常不是大事故开始的,而是一句临时排查代码没删。建议把脱敏做成默认行为,业务侧想输出原文也没有入口。
export class LogMasker {
static mask(input: string): string {
return input
.replace(/1\d{10}/g, value => `${value.slice(0, 3)}****${value.slice(7)}`)
.replace(/[A-Za-z0-9_\-]{24,}/g, '***TOKEN***')
.replace(/\b\d{17}[\dXx]\b/g, value => `${value.slice(0, 4)}************${value.slice(-2)}`)
}
}
这段代码处理的是日志安全边界。它不负责判断字段是不是敏感,只对输出文本做最后一道保护。它能减少手机号、长 token、身份证号误入日志的风险;下一步还要通过代码审查禁止直接调用底层日志。
5. traceId:让一次业务操作能串起来
没有 traceId 的日志只能按时间猜。登录、支付、同步、上传这类链路都应该从入口生成 traceId,并贯穿页面、服务、网络、缓存。
export class TraceFactory {
static create(prefix: string): string {
const now = Date.now()
const random = Math.floor(Math.random() * 10000)
return `${prefix}-${now}-${random}`
}
}
export async function runLogin(account: string): Promise<void> {
const traceId = TraceFactory.create('login')
AppLogger.info('LoginPage', traceId, `submit account=${account}`)
await AuthService.login(traceId, account)
AppLogger.info('LoginPage', traceId, 'login finished')
}
这段代码的输入是一次业务动作。TraceFactory 不需要知道业务细节,只负责生成可搜索的链路号。runLogin 把 traceId 传到服务层,防止页面日志和网络日志断开。
6. 分级:Error 只写失败结果,Warn 写可恢复风险
日志级别混乱会导致两个后果:线上错误数虚高,真正问题被噪声淹没。建议先把场景分清楚,再决定级别。
| 场景 | 推荐级别 | 示例 |
|---|---|---|
| 页面进入 | INFO | page_enter |
| 接口耗时偏高 | WARN | api_slow |
| token 过期可刷新 | WARN | token_refresh |
| 支付失败 | ERROR | pay_failed |
| 本地缓存未命中 | DEBUG | cache_miss |
export function logApiResult(traceId: string, api: string, costMs: number, code: number): void {
if (code >= 500) {
AppLogger.warn('HttpClient', traceId, `api=${api} server_error`, String(code))
return
}
if (costMs > 1200) {
AppLogger.warn('HttpClient', traceId, `api=${api} slow`, 'SLOW_API')
return
}
AppLogger.info('HttpClient', traceId, `api=${api} ok`, costMs)
}
这里把失败、慢请求、正常请求分开处理。它预防所有异常都打成 Error,也避免正常请求刷屏。下一层如果接入聚合平台,可以直接按 errorCode 和 tag 分组。
7. 采样:线上日志要可控
release 包里不应该无限输出详细日志。采样策略要和模块、级别、用户范围绑定:Error 全量保留,Info 按比例,Debug 默认关闭。
日志开关建议来自配置层,而不是散落在业务代码里。这样灰度期间可以提高某个模块的 Info 比例,正式放量后再收紧,不需要每次都改页面逻辑。
export const releaseLogSwitch: LogSwitch = {
enableDebug: false,
infoSampleRate: 0.05,
forceTraceIds: []
}
export const grayLogSwitch: LogSwitch = {
enableDebug: false,
infoSampleRate: 0.25,
forceTraceIds: ['login-1783000000000-1024']
}
这段配置拥有发布环境的日志策略边界。它不包含具体业务,只控制输出比例和定向链路。它能防止线上日志量失控,也能在灰度排查时保留足够证据。
export interface LogSwitch {
enableDebug: boolean
infoSampleRate: number
forceTraceIds: string[]
}
export class LogSampler {
constructor(private readonly config: LogSwitch) {}
shouldWrite(event: AppLogEvent): boolean {
if (event.level === 'ERROR' || event.level === 'WARN') return true
if (this.config.forceTraceIds.includes(event.traceId)) return true
if (event.level === 'DEBUG') return this.config.enableDebug
return Math.random() < this.config.infoSampleRate
}
}
LogSampler 的边界是输出决策,不修改日志内容。它信任配置中心下发的比例,但保留 forceTraceIds 给定向排查使用。这样线上既能控量,又能针对某个用户链路放开观察。
8. 排查回放:从一条用户反馈还原完整链路
拿到用户反馈后,排查顺序建议固定:先查 traceId,没有 traceId 就按时间、设备、页面缩小范围;再看 Warn/Error;最后补看 Info 阶段耗时。
export interface LogReplayStep {
time: string
tag: string
level: LogLevel
message: string
}
export function findLastRiskStep(steps: LogReplayStep[]): LogReplayStep | undefined {
return steps
.filter(item => item.level === 'WARN' || item.level === 'ERROR')
.sort((a, b) => a.time.localeCompare(b.time))
.pop()
}
这个函数用于复盘,不用于线上实时逻辑。它的输入是一组已收集日志,输出最后一个风险步骤。它能帮助读者快速判断应该先看网络、缓存、权限还是页面状态。
9. 常见问题排查表
| 现象 | 可能原因 | 查看方式 | 修复建议 |
|---|---|---|---|
| 日志查不到 | tag/domain 不统一 | 按模块搜索 | 收口到 AppLogger |
| 用户信息明文 | 业务直接拼字符串 | 搜索手机号样式 | 默认脱敏 |
| release 日志过多 | 没有采样 | 看日志量趋势 | Info 采样、Debug 关闭 |
| 错误无法串联 | 缺少 traceId | 按时间手工拼 | 入口生成 traceId |
| Error 太多 | 级别滥用 | 看错误分布 | Warn/Error 边界重写 |
这里的重点是把日志问题当成工程问题,而不是“多打几行”。日志字段、级别、脱敏、采样、回放都闭环,线上问题才有定位效率。
10. 发布前验收清单
| 验收项 | 通过标准 |
|---|---|
| 统一入口 | 业务代码不直接调用底层日志 |
| 敏感字段保护 | 手机号、token、证件号自动脱敏 |
| traceId 贯穿 | 页面、服务、网络日志能串联 |
| 级别清晰 | Warn/Error 不混用 |
| 日志量可控 | release 包有采样策略 |
| 可复盘 | 用户反馈能还原关键路径 |
发布前建议抽三个真实链路做回放:登录失败、接口超时、支付取消。每条链路都能看到入口、请求、结果和错误码,日志体系才算可用。
日志回放复现场景:给读者一组可执行核验
日志治理要验证 traceId 能不能串起页面、服务和网络。没有回放能力的日志,只是分散的文本。
| 核验维度 | 读者需要准备的证据 |
|---|---|
| 输入 | 页面入口、用户动作、关键参数 |
| 过程 | 日志、状态变化、异常分支 |
| 输出 | UI 表现、回调结果、持久化结果 |
| 回归 | 同场景重复执行后的结果 |
interface LogReplayCase {
traceId: any
pageStep: any
serviceStep: any
networkStep: any
}
const replay92: LogReplayCase = {
traceId: 'sample',
pageStep: 'sample',
serviceStep: 'sample',
networkStep: 'sample',
}
function assertReplay92(item: LogReplayCase): void {
if (item.traceId.length === 0) throw new Error('日志缺少 traceId')
}
这组核验强调日志链路回放,读者可以用它确认页面、服务和网络日志是否被同一个 traceId 串起。
日志串联回放表:把文章方法变成可复现动作
日志治理要证明能回放一次真实操作。建议从页面点击开始,经过服务层、网络层、解析层,最后回到 UI 状态,全部使用同一个 traceId。
| 回放动作 | 核验方式 |
|---|---|
| 页面日志 | 准备输入、执行操作、记录结果、给出结论 |
| 服务日志 | 准备输入、执行操作、记录结果、给出结论 |
| 网络日志 | 准备输入、执行操作、记录结果、给出结论 |
| 解析日志 | 准备输入、执行操作、记录结果、给出结论 |
日志治理要以回放一次用户操作为验收目标。读者可以从页面点击开始,沿着服务、网络、解析、UI 更新逐层查同一个 traceId。只要某一层断开,排查时就会回到按时间猜测。把日志串起来后,问题定位速度通常比单纯增加日志数量更稳定。
11. 小结:日志治理的目标是可复盘
HiLog 的价值不在于输出多少行,而在于关键时刻能不能回答三个问题:用户做了什么,系统走到哪一步,失败发生在哪一层。把分级、脱敏、采样和 traceId 做成统一封装后,日志就不再是临时调试文本,而是线上稳定性的基础设施。
更多推荐



所有评论(0)