3、HiLog 高级特性:私有参数格式化(%{private}s),日志流控与丢帧处理

好,咱们继续往下聊。前面两章我们把 HiLog 的基本用法和日志分级讲透了。这一章,我要带你看看 HiLog 真正「高级」的地方——私有参数格式化、日志流控和丢帧处理。

说实话,这三个特性平时你可能用不上。但一旦遇到线上问题排查、性能压测、或者安全审计,它们就是救命稻草。我自己就在项目里踩过坑,今天一并讲清楚。

3.1 私有参数格式化:%{private}s 到底在保护什么?

先问一个问题:你写的日志里,有没有出现过用户手机号、设备 IMEI、甚至明文密码?

嗯,我知道很多新手会直接这么写:

HiLog.info(LABEL, "用户登录成功,手机号:%s", phoneNumber);

这在开发阶段没问题。但一旦应用发布到生产环境,日志文件被第三方拿到……后果你懂的。

HiLog 提供了一个非常优雅的解决方案——私有参数格式化。说白了,就是给敏感数据加个「马赛克」。

3.1.1 三种格式化类型

类型 写法 效果
公开 %{public}s 明文输出,和普通 %s 一样
私有 %{private}s 输出为 <private> 占位符
敏感 %{sensitive}s 输出为 <sensitive> 占位符

举个例子你就明白了:

HiLog.info(LABEL, "公开信息:%{public}s", "Hello");
HiLog.info(LABEL, "私有信息:%{private}s", "13800138000");
HiLog.info(LABEL, "敏感信息:%{sensitive}s", "password123");

输出结果:

公开信息:Hello
私有信息:<private>
敏感信息:<sensitive>

你看,手机号和密码直接被屏蔽了。这在合规审计时特别有用。

我的习惯: 所有用户相关数据,一律用 %{private}s。哪怕只是用户名,也建议私有化。你永远不知道日志文件会流向哪里。

3.1.2 什么时候用 %{public}s?

你可能会问:那 %{public}s 还有存在的必要吗?

有。比如打印设备型号、系统版本、或者你自定义的调试开关状态——这些不涉及用户隐私,用 %{public}s 反而方便排查。

HiLog.info(LABEL, "设备型号:%{public}s,系统版本:%{public}s", deviceModel, osVersion);

这样,线上日志里你能看到设备信息,但看不到用户手机号。两全其美。

注意: 私有参数格式化只在 HiLog 中生效。如果你混用 printf 或普通日志库,这个保护机制就失效了。我曾经见过有人把 HiLog 日志又用 Log.d 打了一遍……嗯,隐私保护直接破功。

3.2 日志流控:为什么日志会突然消失?

你有没有遇到过这种情况:程序跑着跑着,日志突然不打了?

别慌,大概率是触发了 HiLog 的流控机制

HiLog 底层有一个环形缓冲区,大小有限。如果短时间内日志量太大,缓冲区满了,后面的日志就会被丢弃。这是为了保护系统性能,防止日志把 CPU 和存储打满。

3.2.1 流控触发条件

我记得官方文档里提过几个关键阈值:

  • 单进程每秒日志条数超过 1000 条,触发流控
  • 单条日志长度超过 1024 字节,会被截断
  • 全局日志缓冲区写满,直接丢帧

说白了,HiLog 不是给你无限制刷屏用的。你想想看,如果某个循环里每秒打一万条日志,系统还怎么干活?

3.2.2 如何避免流控?

我个人的经验是:高频日志一定要降频

比如你在一个定时器里每秒打一次日志,可以改成每 10 秒打一次:

static int logCounter = 0;
if (++logCounter % 10 == 0) {
    HiLog.info(LABEL, "当前状态:运行中,计数:%{public}d", counter);
}

或者用 HiLog 自带的 HiLog.isLoggable() 先判断一下:

if (HiLog.isLoggable(LABEL, HiLog.INFO)) {
    HiLog.info(LABEL, "这条日志只在 INFO 级别开启时才会打印");
}

这样既能保留关键信息,又不会刷爆缓冲区。

避坑指南: 我曾经在一个网络重连逻辑里,每 100ms 打一次日志。结果线上用户反馈手机发烫、卡顿。查了半天,发现是日志流控导致 CPU 飙升。从那以后,我所有高频循环里的日志都加了降频处理。

3.3 丢帧处理:日志丢了怎么办?

流控触发后,日志就丢了。这其实是个设计取舍——宁可丢日志,也不能丢性能

但问题来了:如果丢的日志恰好是崩溃前的关键信息,你怎么排查?

3.3.1 丢帧的典型场景

  • 崩溃前瞬间:应用 crash 前疯狂打日志,缓冲区满了,最后几条关键日志被丢弃
  • 启动阶段:应用启动时大量初始化日志,容易触发流控
  • 高并发场景:多线程同时写日志,竞争激烈导致丢帧

3.3.2 如何检测丢帧?

HiLog 提供了一个 API 可以查询丢帧情况:

int droppedCount = HiLog.getDroppedCount();
if (droppedCount > 0) {
    HiLog.warn(LABEL, "检测到日志丢帧,已丢弃 %{public}d 条", droppedCount);
}

我建议你在应用启动后、或者关键业务流程结束后,主动检查一次。如果丢帧严重,说明你的日志策略需要优化。

3.3.3 丢帧后的补救措施

说实话,丢帧了就是丢了,没法恢复。但我们可以做几件事来降低影响:

  1. 关键日志用 HiLog.FATAL:FATAL 级别的日志优先级最高,几乎不会被流控丢弃
  2. 日志分级:DEBUG 日志可以丢,但 ERROR 和 FATAL 必须保留
  3. 本地文件备份:对于特别重要的日志,可以同时写入本地文件(但要注意性能)
// 关键信息双重保障
HiLog.fatal(LABEL, "严重错误:内存不足,%{private}s", errorDetail);
// 同时写入本地文件
saveToLocalFile("crash.log", errorDetail);
我的建议: 不要过度依赖日志。日志是辅助手段,不是万能药。真正关键的逻辑,建议用断言、异常捕获、或者状态机来保证。

3.4 实战:一个完整的日志配置示例

最后,我给你一个我在项目中实际用过的日志配置模板。你可以直接拿来用:

// 日志标签
static constexpr HiLogLabel LABEL = {LOG_CORE, 0, "MyApp"};

// 初始化时检查丢帧
void initLogging() {
    int dropped = HiLog.getDroppedCount();
    if (dropped > 100) {
        HiLog.warn(LABEL, "启动时丢帧严重:%{public}d 条", dropped);
        // 考虑调整日志级别或降频
    }
}

// 安全打印用户信息
void logUserLogin(const std::string& phone, const std::string& deviceId) {
    HiLog.info(LABEL, "用户登录 - 手机号:%{private}s,设备:%{public}s",
               phone.c_str(), deviceId.c_str());
}

// 高频日志降频
void logHeartbeat(int counter) {
    static int beatCount = 0;
    if (++beatCount % 100 == 0) {
        HiLog.info(LABEL, "心跳正常,计数:%{public}d", counter);
    }
}

这个模板里,我做了三件事:

  • 启动时检查丢帧,提前预警
  • 用户信息用 %{private}s 保护
  • 心跳日志每 100 次才打一次,避免流控

嗯,基本上够用了。你如果遇到更复杂的场景,比如多进程日志隔离、或者日志加密,那又是另一个话题了。咱们后面章节再聊。

好,这一章就到这里。记住:日志不是越多越好,而是越精越好。用好私有参数和流控,你的日志系统才算真正「高级」了。