日志
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
日志级别
级别按从最详细到最不详细排序:
| 级别 | 序数 | 说明 |
|---|---|---|
ALL | 0 | 记录一切 |
DEBUG | 1 | 调试细节 |
INFO | 2 | 信息类消息(默认) |
WARN | 3 | 警告 |
ERROR | 4 | 错误 |
FATAL | 5 | 致命错误 |
OFF | 6 | 关闭日志 |
在创建 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 + ROLL | path.0, path.1, path.2, ... |
PLAIN + RING | path(单文件,循环覆盖) |
BINARY + ROLL | path.0, path.1, path.2, ... |
BINARY + RING | path(单文件,循环覆盖) |
每个文件有一个 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 分配的增长;日志框架面向应用层的日志消息。两者不共享任何代码,用途不同。