首页 · ← SuperJ 手册 中文|EN

日志

SuperJ 的日志框架(sj.util.logging)是一个无垃圾、流式 API、内存映射文件的日志器,为单线程服务器与系统程序设计。日志记录直接写进一个内存映射文件 — 每条记录没有 syscall,没有缓冲 I/O,而且记录能在进程崩溃后幸存(即使进程死了,内核也会刷写脏页)。

快速开始

import sj.util.logging.FileLoggerFactory;
import sj.util.logging.LogFormat;
import sj.util.logging.RollPolicy;
import sj.util.logging.Level;
import sj.util.logging.Logger;
import sj.util.logging.LoggerFactory;

public class MyApp {
    public static void main(String[] args) {
        LoggerFactory factory = new FileLoggerFactory(
            "logs/app.log",       // file path
            1024 * 1024,          // 1 MiB capacity per segment
            LogFormat.PLAIN,      // human-readable (tail/grep-friendly)
            RollPolicy.ROLL       // roll to app.log.0, app.log.1, ...
        );
        factory.setLevel(Level.DEBUG);

        Logger log = factory.createLogger("main");

        log.info().log("Server started on port ").log(8080).flush();
        log.debug().log("Config: ").log("debug mode enabled").flush();
        log.warn().log("Cache miss for key ").log("user:42").flush();
        log.error().log("Failed to connect: ").log("connection refused").flush();

        factory.close();  // flush dirty pages and close the mmap
    }
}

编译并运行:

superj run -- main

日志文件 logs/app.log.0 将包含:

Server started on port 8080
Config: debug mode enabled
Cache miss for key user:42
Failed to connect: connection refused

日志级别

级别按从最详细到最不详细排序:

级别序数说明
ALL0记录一切
DEBUG1调试细节
INFO2信息类消息(默认)
WARN3警告
ERROR4错误
FATAL5致命错误
OFF6关闭日志

在创建 logger 之前,先在 factory 上设置级别:

factory.setLevel(Level.WARN);   // only WARN and above will be emitted
Logger log = factory.createLogger("app");

或者直接修改某个 logger 的级别:

log.setLevel(Level.DEBUG);

流式 API

每次 log(...) 调用都返回 logger 本身,所以调用可以链起来。级别选择器(info()、debug() 等)开启一条链 — 如果该级别未启用,这条链里后续的所有 log(...) 调用都是廉价的空操作(一次布尔检查,没有字符串分配):

log.info()
   .log("Request ")
   .log(requestId)
   .log(" from ")
   .log(remoteAddr)
   .flush();

flush() 把累积的缓冲区写到输出并清空它。每条链都必须以 flush() 结尾 — 不然消息不会被写出。

可以记录什么

方法说明
log(string)UTF-8 文本
logAscii(string)ASCII 文本(跳过 UTF-8 编码)
log(int) / log(long) / log(short)十进制表示
log(boolean)"true" / "false"
log(char)单个字符(UTF-8)
log(byte[]) / log(byte[], offset, len)原始字节
logHEX(byte[], offset, len)十六进制字符串
log(Loggable)自定义渲染(见下文)
log(Throwable)异常消息

自定义对象日志

实现 Loggable 即可把一个对象直接渲染进 logger 的字节缓冲区 — 没有中间字符串分配:

import sj.util.logging.Loggable;
import sj.util.ByteArrayBuilder;

public class Point implements Loggable {
    int x, y;

    public void log(ByteArrayBuilder buf) {
        buf.append("Point(");
        buf.append(this.x);
        buf.append(",");
        buf.append(this.y);
        buf.append(")");
    }
}

// usage:
log.info().log(new Point(3, 4)).flush();
// output: Point(3,4)

文件格式

在构造 factory 时从两种落盘格式中选择:

PLAIN — 人类可读

每条消息以原始字节写入,后跟一个换行。文件可以直接用 tail、grep、less 或任何文本编辑器阅读:

tail -f logs/app.log.0
grep ERROR logs/app.log.0

BINARY — 长度前缀记录

每条消息写成一个 4 字节小端长度前缀,后跟原始字节。这种分帧是自描述的,读取方无需扫描分隔符即可重放记录,而且消息里可以包含换行:

LoggerFactory factory = new FileLoggerFactory(
    "logs/events.bin",
    4 * 1024 * 1024,     // 4 MiB
    LogFormat.BINARY,
    RollPolicy.RING
);

滚动策略

当内存映射区域写满时,滚动策略决定接下来发生什么:

ROLL — 编号分段(完整历史)

当前文件被同步并关闭,然后打开下一个编号文件:app.log.0、app.log.1、app.log.2 等。全部历史都保留。重启时,factory 会找到已存在的最大分段号并从那里继续。

new FileLoggerFactory("logs/app.log", 1024 * 1024, LogFormat.PLAIN, RollPolicy.ROLL);
// files: app.log.0, app.log.1, app.log.2, ...

RING — 环形缓冲(仅保留最近)

单个固定大小的文件。写满后,写头回卷到开头并覆盖最老的记录。这是"黑匣子"模式 — 你始终拥有最近 N 字节的日志历史,磁盘用量有界:

new FileLoggerFactory("logs/app.log", 1024 * 1024, LogFormat.PLAIN, RollPolicy.RING);
// single file: app.log (overwrites oldest data when full)

到哪里找日志文件

日志文件创建在你传给 FileLoggerFactory 的路径上。路径相对于进程的工作目录:

配置创建的文件
PLAIN + ROLLpath.0, path.1, path.2, ...
PLAIN + RINGpath(单文件,循环覆盖)
BINARY + ROLLpath.0, path.1, path.2, ...
BINARY + RINGpath(单文件,循环覆盖)

每个文件有一个 64 字节的头(LOGS 魔数 + 版本 + 写头)。实际的日志数据从第 64 字节开始。PLAIN 格式的数据部分是人类可读文本;BINARY 则是长度前缀记录。

重启时,factory 从保存的写头处恢复 — 新记录追加在最后一条已写记录之后,没有空隙。

自定义 Logger

要写到文件以外的目的地(stdout、网络 socket、内存缓冲区),继承 AbstractLogger 并实现 writeOut:

import sj.util.logging.AbstractLogger;
import sj.util.ByteArrayBuilder;

public class ConsoleLogger extends AbstractLogger {
    protected void writeOut(ByteArrayBuilder buffer) {
        System.out.println(buffer.getBuffer(), 0, buffer.length());
    }
}

AbstractLogger 基类负责级别过滤、流式链和缓冲。你的子类只需提供最后一步输出。

println(byte[], off, len) 把缓冲区的字节原样写到流上。优先用它,而不是 println(buffer.toString()):toString() 每行都把字节拷贝进一个全新的 String,因此那样写的 logger 即便 builder 被复用,也会每条记录分配一次。以上面那行 — route=/v1/orders status=200,27 字节 — 实测:100 条记录经 toString() 花费 6,400 字节,经区间形式花费 0。它是唯一一个什么都不构建的输出重载:其他所有重载都先把参数渲染成 String。字节按原样写出,所以缓冲区必须已经持有你想要的编码。

守卫昂贵的消息

级别选择器返回设置了 active 标志的 this。当级别未启用时,log(...) 调用是空操作 — 但参数仍会被求值。对昂贵的参数(字符串拼接、对象构造),先用级别检查守卫:

if (log.logDebug()) {
    log.debug().log("State: ").log(dumpAllState()).flush();
}

SDK 组件中的惯用法

SDK 自己的组件(例如网络适配器)遵循这个模式:

if (this.logger != null && this.logger.logInfo()) {
    this.logger.log("Listening at port ").log(port).flush();
}

先判空 logger(它可能未配置),再检查级别,然后链起消息并 flush。

与内存事件日志的关系

日志框架(sj.util.logging)与内存事件日志(--mem-track / superj memlog)是两回事。内存事件日志为排查内存问题追踪 arena 分配的增长;日志框架面向应用层的日志消息。两者不共享任何代码,用途不同。

SuperJ — manual · generated from logging.md at pack time · Powered by superJ — this site is served by superj_web 中文|EN