You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Logback删除日志文件最旧行以维持指定文件大小限制?

我懂你的痛点——你想要的是单文件内循环覆盖的环形日志:当文件达到大小上限时,自动删除最旧的行来腾出空间,而不是像现在这样直接归档旧文件、新建空文件继续写。

先给你拆解下当前代码为什么不符合预期:你用的FixedWindowRollingPolicy配合SizeBasedTriggeringPolicy是Logback的标准滚动归档策略,当主文件ts.log达到256KB时,会把它重命名为ts.1.log(因为你设置了setMaxIndex(1)),然后新建一个空的ts.log继续写入。但因为你只保留1个归档文件,新的归档会覆盖旧的,所以看起来就像是主文件被清空重写了,这和你要的“在同一个文件里保留最新内容”完全不是一回事。

遗憾的是,Logback原生并没有提供直接支持这种单文件循环覆盖的Appender,不过我们可以自己实现一个,或者用自定义逻辑来满足需求。下面给你一个可行的解决方案:

方案:自定义环形日志Appender

我们可以继承Logback的OutputStreamAppender,实现一个自定义的OutputStream,让它在文件达到大小上限时,自动删除最旧的行,再追加新内容。

第一步:实现环形文件输出流

这个类负责核心的循环覆盖逻辑:

import java.io.*;

public class CircularFileOutputStream extends OutputStream {
    private final File targetFile;
    private final long maxFileSize;
    private RandomAccessFile raf;
    private long currentFileSize;

    public CircularFileOutputStream(File file, long maxSize) throws IOException {
        this.targetFile = file;
        this.maxFileSize = maxSize;
        
        // 初始化文件和随机访问流
        if (targetFile.exists()) {
            currentFileSize = targetFile.length();
            raf = new RandomAccessFile(targetFile, "rw");
        } else {
            targetFile.getParentFile().mkdirs();
            raf = new RandomAccessFile(targetFile, "rw");
            currentFileSize = 0;
        }
        // 定位到文件末尾,准备追加写入
        raf.seek(currentFileSize);
    }

    @Override
    public void write(int b) throws IOException {
        ensureEnoughSpace(1);
        raf.write(b);
        currentFileSize++;
    }

    @Override
    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        ensureEnoughSpace(len);
        raf.write(b, off, len);
        currentFileSize += len;
    }

    /**
     * 检查剩余空间,如果不足则删除最旧的行腾出空间
     */
    private void ensureEnoughSpace(int bytesToWrite) throws IOException {
        if (currentFileSize + bytesToWrite <= maxFileSize) {
            return;
        }

        // 计算需要腾出的空间,逐行跳过旧内容
        raf.seek(0);
        BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(raf.getFD())));
        StringBuilder retainedContent = new StringBuilder();
        String line;
        long retainedSize = 0;
        final String lineSeparator = System.lineSeparator();
        final int separatorSize = lineSeparator.getBytes().length;

        while ((line = reader.readLine()) != null) {
            long lineTotalSize = line.getBytes().length + separatorSize;
            // 如果保留这一行后,加上新内容仍超过上限,就跳过这一行
            if (retainedSize + lineTotalSize + bytesToWrite > maxFileSize) {
                continue;
            }
            retainedContent.append(line).append(lineSeparator);
            retainedSize += lineTotalSize;
        }

        // 清空文件,写入保留的最新内容
        raf.setLength(0);
        raf.write(retainedContent.toString().getBytes());
        currentFileSize = retainedSize;
        // 回到文件末尾,准备写入新内容
        raf.seek(currentFileSize);
    }

    @Override
    public void close() throws IOException {
        if (raf != null) {
            raf.close();
        }
    }

    @Override
    public void flush() throws IOException {
        if (raf != null) {
            raf.getFD().sync();
        }
    }
}

第二步:实现自定义Logback Appender

把上面的输出流包装成Logback可以识别的Appender:

import ch.qos.logback.core.OutputStreamAppender;
import java.io.File;
import java.io.IOException;

public class CircularFileAppender<E> extends OutputStreamAppender<E> {
    private String fileName;
    private long maxSize;

    @Override
    public void start() {
        try {
            // 初始化自定义输出流
            setOutputStream(new CircularFileOutputStream(new File(fileName), maxSize));
            super.start();
        } catch (IOException e) {
            addError("Failed to initialize circular file appender", e);
        }
    }

    // 提供配置属性的setter
    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public void setMaxSize(long maxSize) {
        this.maxSize = maxSize;
    }
}

第三步:替换原有代码使用自定义Appender

把你原来的RollingFileAppender相关代码替换成下面的逻辑:

LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
lc.reset();

final String LOG_DIR = context.getExternalCacheDir() + File.separator + ".ts";
File logFile = new File(LOG_DIR, "ts.log");

// 初始化自定义环形日志Appender
CircularFileAppender<ILoggingEvent> circularAppender = new CircularFileAppender<>();
circularAppender.setContext(lc);
circularAppender.setFileName(logFile.getAbsolutePath());
circularAppender.setMaxSize(1024 * 256); // 设置为256KB
circularAppender.setAppend(true);

// 配置日志编码器(和你原来的一致)
PatternLayoutEncoder encoder = new PatternLayoutEncoder();
encoder.setPattern("%msg%n");
encoder.setContext(lc);
encoder.start();

circularAppender.setEncoder(encoder);
circularAppender.start();

// 绑定到根日志器
ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
root.addAppender(circularAppender);

注意事项

  1. 性能考虑:这个实现每次需要腾出空间时都会读取并重写文件,在高并发写入场景下可能有性能损耗。如果你的日志写入量很大,可以考虑优化逻辑(比如缓存每行的大小,避免重复计算),或者改用更高效的环形缓冲区实现。
  2. 长日志行:如果单条日志行的大小超过了设置的maxSize,这个逻辑可能会出现异常,你可以根据需要添加额外的处理逻辑。
  3. 线程安全:当前实现没有做线程同步,如果是多线程写入日志,建议在write方法上加锁,避免并发写入导致的文件损坏。

内容的提问来源于stack exchange,提问作者Павел Олейник

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 09:00:52