日志优化:日志分级、日志异步写入、日志压缩、日志上传策略

日志这东西,说大不大,说小不小。我见过不少团队,线上出了bug,翻半天日志发现关键信息被淹没了,或者日志文件把存储撑爆了。说白了,日志优化不是让你少打日志,而是让你打得聪明、存得高效、传得及时。

今天咱们就聊聊日志优化的四个核心维度:分级、异步写入、压缩、上传策略。嗯,每个点我都踩过坑,咱们一个一个说。

一、日志分级:别让所有日志都挤在一起

你想想看,如果Debug日志和Error日志混在一起,排查问题的时候就像在垃圾堆里找针。我个人的习惯是,严格区分日志级别,并且让不同级别的日志走不同的通道。

级别 含义 建议输出 存储策略
VERBOSE 最细粒度信息 仅开发阶段 不落盘
DEBUG 调试信息 开发/测试包 按需落盘,自动清理
INFO 关键流程节点 全量包 保留7天
WARN 潜在问题 全量包 保留30天
ERROR 明确异常 全量包 永久保留(压缩后)
我的经验:我曾经在一个项目中,线上包开了VERBOSE级别的日志,结果一天产生了2GB的日志文件,直接把用户存储干爆了。后来我们强制在Release包中只保留WARN及以上级别,问题瞬间解决。

二、日志异步写入:别让日志拖慢主线程

很多新手喜欢直接在业务代码里写日志,比如:

// 错误示范:同步写入,阻塞主线程
Log.d("TAG", "用户点击了按钮");
FileUtils.writeToFile("/sdcard/log.txt", logContent);

这有什么问题?文件I/O操作是耗时的,尤其是在低端机上。你想想看,用户点了个按钮,结果界面卡了200ms,就因为你写了个日志文件。

正确的做法是异步写入。我一般用生产者-消费者模式:

// 异步日志写入器
public class AsyncLogWriter {
    private BlockingQueue<String> logQueue = new LinkedBlockingQueue<>(1024);
    private ExecutorService executor = Executors.newSingleThreadExecutor();

    public void init() {
        executor.submit(() -> {
            while (true) {
                try {
                    String log = logQueue.take(); // 阻塞获取
                    writeToFile(log); // 在后台线程写入
                } catch (InterruptedException e) {
                    break;
                }
            }
        });
    }

    public void log(String msg) {
        if (!logQueue.offer(msg)) {
            // 队列满了,丢弃最旧的日志
            logQueue.poll();
            logQueue.offer(msg);
        }
    }
}
注意:异步写入虽然不阻塞主线程,但要注意队列大小。我曾经遇到过队列无限增长,导致OOM的情况。建议设置一个上限,比如1024条,满了就丢弃最旧的。

三、日志压缩:存储空间是宝贵的

日志文件如果不压缩,几天就能吃掉几个GB。我建议在日志落盘后,对历史日志进行压缩。常用的压缩算法有GZIP和LZ4。

GZIP压缩率高,但速度慢;LZ4压缩率稍低,但速度快。我个人倾向于用LZ4做实时压缩,GZIP做归档压缩。

// LZ4压缩示例
public byte[] compressWithLZ4(byte[] data) {
    LZ4Factory factory = LZ4Factory.fastestInstance();
    int maxCompressedLength = factory.fastCompressor().maxCompressedLength(data.length);
    byte[] compressed = new byte[maxCompressedLength];
    int compressedLength = factory.fastCompressor().compress(data, 0, data.length, compressed, 0, maxCompressedLength);
    return Arrays.copyOf(compressed, compressedLength);
}

压缩时机也很关键。我建议在日志文件轮转时进行压缩,而不是每写一条就压缩一次。比如每小时轮转一次日志文件,轮转后立即对旧文件进行压缩。

四、日志上传策略:什么时候传?传多少?

日志上传是个双刃剑。传得太频繁,浪费用户流量和电量;传得太少,问题排查不及时。我总结了一套策略:

  • 按级别上传:ERROR日志立即上传,WARN日志批量上传,INFO日志仅在WiFi下上传
  • 按条件触发:当应用发生崩溃时,上传崩溃前5分钟的日志
  • 按时间间隔:每天凌晨上传一次前一天的日志(仅WiFi)
  • 按存储阈值:当日志文件总大小超过50MB时,触发上传并清理
核心原则:用户无感知。上传操作必须在后台进行,不能影响用户正常使用。我建议使用WorkManager来调度上传任务,系统会自动选择合适的时机。

知识体系总览

下面这张图总结了日志优化的整体架构,你可以对照着看看自己的项目在哪个环节还有优化空间:

日志优化知识体系 日志优化 日志分级 VERBOSE / DEBUG INFO / WARN / ERROR 不同级别不同存储策略 异步写入 生产者-消费者模式 BlockingQueue + 单线程 不阻塞主线程 日志压缩 GZIP / LZ4 轮转时压缩 节省存储空间 上传策略 按级别 / 按条件 按时间 / 按存储阈值 WorkManager调度 核心目标 不阻塞主线程 · 不浪费存储 · 不消耗用户流量 · 问题可追溯

避坑指南

最后,分享几个我踩过的坑:

  • 日志文件轮转:我曾经没有做轮转,一个日志文件写到几个GB,用户想删都删不掉。建议按大小(比如10MB)或按时间(比如每小时)轮转。
  • 敏感信息过滤:日志里不要打印用户密码、Token等敏感信息。我见过有人把用户身份证号直接打出来的,这要是被第三方拿到,麻烦就大了。
  • 上传失败重试:上传失败要有退避策略。我曾经设置每5秒重试一次,结果用户一晚上重试了上千次,电量直接崩了。
一个小技巧:在日志中加入唯一标识符(比如SessionId),这样上传到服务器后,可以方便地把同一用户的多条日志关联起来,排查问题效率翻倍。

好了,日志优化就聊到这儿。记住一句话:日志是给未来的自己看的,别让未来的自己骂现在的你。


专注资料整理