Log 异步日志模块
高性能异步日志,支持文件滚动(按大小/时间)、GZIP 压缩归档、MDC 上下文、命名 Logger(每个 Logger 独立文件)、ANSI 彩色控制台、可插拔格式化器、ERROR 告警回调、运行时级别热切换。写入前经队列缓冲,不阻塞业务线程。
初始化(可选)
零配置启动(默认行为)
// 无需任何初始化——直接使用即可。
// 首次写日志时自动启动,日志输出到 JAR 所在目录下的 vklogs/,INFO 级别。
Vostok.Log.info("server started");
显式初始化
// 使用内置默认配置
Vostok.Log.init();
// 自定义配置
Vostok.Log.init(new VKLogConfig()
.level(VKLogLevel.DEBUG)
.outputDir("logs")
.filePrefix("app")
.maxFileSizeMb(64) // 64 MB 按大小滚动
.maxBackups(20)
.maxBackupDays(30)
.rollInterval(VKLogRollInterval.DAILY)
.compressRolledFiles(true)
.consoleEnabled(true)
.consoleColor(true)
.queueCapacity(32768)
.queueFullPolicy(VKLogQueueFullPolicy.DROP)
.flushIntervalMs(1000)
);
vklogs/(IDE 运行时为 class 目录下的 vklogs/),
INFO 级别,开启控制台输出,自动 GZIP 压缩,所有命名 Logger 共用单文件(autoCreateLoggerSink=false)。
如需独立配置,在首次写日志前调用 init(VKLogConfig) 即可。
JVM 退出时会自动 flush 并关闭所有日志队列。
写日志
静态 API(自动以调用方类名作为 logger 名)
Vostok.Log.trace("entering method");
Vostok.Log.debug("config loaded: {}", configPath);
Vostok.Log.info("server started on port {}", port);
Vostok.Log.warn("slow query: {} ms", elapsed);
Vostok.Log.error("failed to connect");
Vostok.Log.error("connect failed: {}", host, exception); // 最后一个 Throwable 参数自动识别
Vostok.Log.error("payment failed", exception); // 单独传 Throwable
占位符为 SLF4J 风格 {},多余参数追加在末尾([value]),不足时占位符保留原样。
级别检查(高频路径防护)
// 避免在级别未启用时执行昂贵的参数计算
if (Vostok.Log.isDebugEnabled()) {
Vostok.Log.debug("state: {}", computeExpensiveState());
}
命名 Logger
命名 Logger 将日志路由到独立文件(当 autoCreateLoggerSink=true 时)。同名 Logger 返回相同实例(缓存复用)。
// 获取命名 Logger(两种写法等价)
VKLogger log = Vostok.Log.logger("payment");
VKLogger log = Vostok.Log.getLogger("payment");
log.info("processing order {}", orderId);
log.warn("retry #{}", retryCount);
log.error("payment failed", ex);
// 高频路径防护
if (log.isDebugEnabled()) {
log.debug("detail: {}", detail());
}
VKLogger 提供与静态 API 完全相同的方法集:
| 方法 | 说明 |
|---|---|
name() | 返回此 Logger 的名称 |
isTraceEnabled() / isDebugEnabled() / … | 级别检查 |
trace/debug/info/warn/error(msg) | 纯消息日志 |
trace/debug/info/warn/error(template, args...) | 模板日志({} 占位符) |
error(msg, Throwable) | 附带异常栈的 ERROR 日志 |
MDC(映射诊断上下文)
MDC 已集成到 Vostok.Log 门面,也可直接操作底层 VKLogMDC 类,两种方式完全等价。写入的键值对会自动注入到当前线程后续所有日志中。
// 通过门面访问 MDC(推荐,与其他 Log API 风格统一)
Vostok.Log.mdcPut("requestId", requestId);
Vostok.Log.mdcPut("userId", userId);
Vostok.Log.info("handling request"); // 日志自动包含 requestId / userId
// 移除单个键
Vostok.Log.mdcRemove("userId");
// 读取当前线程 MDC 值
String rid = Vostok.Log.mdcGet("requestId");
// 获取全部 MDC 快照(只读视图)
Map<String, String> snapshot = Vostok.Log.mdcGetAll();
// 批量设置(常用于跨线程传播)
Vostok.Log.mdcPutAll(snapshot);
// 清除当前线程全部 MDC(请求结束时务必调用,防止线程复用时上下文泄漏)
Vostok.Log.mdcClear();
Vostok.Log.mdcPut() 等门面方法与直接调用 VKLogMDC.put() 完全等价,底层共享同一个 ThreadLocal。
门面方式更简洁,不需要额外 import;VKLogMDC 方式适合已有大量直接调用的代码场景。
Web 请求中的典型用法
// 在拦截器 / 过滤器中
Vostok.Log.mdcPut("requestId", ctx.requestId());
Vostok.Log.mdcPut("userId", String.valueOf(ctx.userId()));
try {
handle(ctx);
} finally {
Vostok.Log.mdcClear(); // 必须清除,防止泄漏到下一个请求
}
跨线程传播 MDC
// 在提交任务前捕获快照
Map<String, String> snapshot = Vostok.Log.mdcGetAll();
executor.submit(() -> {
Vostok.Log.mdcPutAll(snapshot); // 在子线程中恢复 MDC
try {
doWork();
} finally {
Vostok.Log.mdcClear();
}
});
命名 Logger 路由
每个命名 Logger 的日志路由策略由 autoCreateLoggerSink 控制:
| autoCreateLoggerSink | throwOnUnknownLogger | 行为 |
|---|---|---|
true(默认) | — | 未注册的 logger 自动创建独立文件 sink(文件名为 <filePrefix>-<loggerName>.log) |
false | false(默认) | 未注册的 logger 路由到默认 sink(单文件模式) |
false | true | 未注册的 logger 抛 IllegalArgumentException(严格注册模式) |
预注册 Logger 并指定独立配置
Vostok.Log.init(new VKLogConfig()
.outputDir("logs")
.level(VKLogLevel.INFO)
.autoCreateLoggerSink(false) // 关闭自动创建,使用严格注册模式
.throwOnUnknownLogger(true) // 调用未注册 logger 时抛异常
// 仅预注册名称,使用全局配置
.registerLogger("payment")
.registerLoggers("order", "inventory")
// 预注册并指定独立的 sink 配置
.registerLogger("audit", new VKLogSinkConfig()
.outputDir("logs/audit")
.filePrefix("audit")
.level(VKLogLevel.DEBUG)
.rollInterval(VKLogRollInterval.HOURLY)
.compressRolledFiles(true)
)
);
动态配置(运行时热切换)
以下方法无需重启,实时生效,会推送到所有现有 sink:
// 修改全局日志级别
Vostok.Log.setLevel(VKLogLevel.DEBUG);
// 修改指定命名 Logger 的级别(不影响其他 Logger)
Vostok.Log.setLevel("payment", VKLogLevel.TRACE);
// 查询当前全局级别
VKLogLevel lv = Vostok.Log.level();
// 其他运行时修改(不常用,但支持)
Vostok.Log.setConsoleEnabled(true);
Vostok.Log.setConsoleColor(true);
Vostok.Log.setMaxFileSizeMb(128);
Vostok.Log.setMaxBackups(30);
Vostok.Log.setMaxBackupDays(7);
Vostok.Log.setRollInterval(VKLogRollInterval.HOURLY);
Vostok.Log.setCompressRolledFiles(true);
Vostok.Log.setQueueFullPolicy(VKLogQueueFullPolicy.BLOCK);
Vostok.Log.setFlushIntervalMs(500);
Vostok.Log.setFormatter(myFormatter);
Vostok.Log.setErrorListener(myListener);
自定义格式化器
VKLogFormatter 是函数式接口,可完全自定义输出格式。默认格式为:yyyy-MM-dd HH:mm:ss.SSS [LEVEL] [loggerName] {mdc} message。
// JSON 格式示例
VKLogFormatter jsonFmt = (level, loggerName, msg, t, ts, mdc) -> {
StringBuilder sb = new StringBuilder();
sb.append("{\"ts\":").append(ts)
.append(",\"level\":\"").append(level).append("\"")
.append(",\"logger\":\"").append(loggerName).append("\"")
.append(",\"msg\":\"").append(msg).append("\"");
if (!mdc.isEmpty()) {
sb.append(",\"mdc\":").append(mdc);
}
sb.append("}\n");
return sb.toString();
};
Vostok.Log.init(new VKLogConfig()
.outputDir("logs")
.formatter(jsonFmt) // 全局格式化器
);
// 或运行时动态修改
Vostok.Log.setFormatter(jsonFmt);
Vostok.Log.setFormatter(null); // 传 null 恢复默认格式
VKLogFormatter.format() 参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
level | VKLogLevel | 日志级别 |
loggerName | String | Logger 名称(命名 Logger 名 或 调用方类名) |
msg | String | 已完成 {} 替换的消息 |
t | Throwable | 关联异常,可为 null |
ts | long | 事件时间戳(epoch millis) |
mdc | Map<String,String> | MDC 上下文快照(不可修改),无上下文时为空 Map |
ERROR 告警回调
每次写入 ERROR 日志后,在 worker 线程上调用 VKLogErrorListener。实现必须非阻塞、低延迟,不可执行 IO 或网络请求。
Vostok.Log.init(new VKLogConfig()
.outputDir("logs")
.errorListener((loggerName, message, error, timestamp) -> {
// 投递到独立告警队列,避免阻塞 worker 线程
alertQueue.offer(new AlertEvent(loggerName, message, timestamp));
})
);
刷盘与关闭
// 刷新所有 logger 队列(等待异步写入完成)
Vostok.Log.flush();
// 关闭日志系统,等待队列清空(JVM 退出时自动调用)
Vostok.Log.close();
Vostok.Log.shutdown(); // close() 的别名
// 重新初始化(替换配置,重建所有 sink)
Vostok.Log.reinit(new VKLogConfig().level(VKLogLevel.WARN));
// 重置为默认配置
Vostok.Log.resetDefaults();
监控指标
// 因队列满而丢弃的日志条数(DROP 策略时)
long dropped = Vostok.Log.droppedLogs();
// 队列满时同步写入的次数(SYNC_FALLBACK 策略时)
long fallback = Vostok.Log.fallbackWrites();
// 文件写入错误次数(磁盘满等 IO 异常)
long writeErrors = Vostok.Log.fileWriteErrors();
// 当前排队等待异步 GZIP 压缩的文件数(等于 0 表示压缩全部完成)
int pending = Vostok.Log.pendingCompressions();
接入外部日志框架(VKLogBackend)
通过 VKLogBackend SPI,可将 Vostok 的日志输出委托给任意外部框架(SLF4J、Log4j2、Logback 等)。AsyncEngine 在 flush 时判断:配置了 Backend 则调用 backend.emit(),内置文件写入与控制台输出被替换;未配置时行为不变。
VKLogBackend 接口
public interface VKLogBackend {
/**
* 输出一条日志事件。在 AsyncEngine worker 线程调用,必须非阻塞。
*
* @param level 日志级别
* @param loggerName Logger 名称(命名 Logger 名 或 调用方类名)
* @param formattedMsg 已完成 {} 替换的消息
* @param t 关联异常,可为 null
* @param tsMillis 事件时间戳(epoch millis)
* @param mdc 调用线程的 MDC 快照(不可修改)
*/
void emit(VKLogLevel level, String loggerName,
String formattedMsg, Throwable t, long tsMillis,
Map<String, String> mdc);
default void start() {} // 初始化时调用(可在此建立连接、分配资源)
default void stop() {} // 关闭时调用(可在此释放资源)
}
emit() 在 AsyncEngine 的 worker 线程调用,必须非阻塞、无 IO(与 VKLogErrorListener 相同)。可调用外部框架的异步 API 或投递到外部队列。配置 Backend 后,内置文件写入和控制台输出被跳过;如需同时保留,使用 VKCompositeBackend 组合多个 Backend。
配置方式
// 全局 Backend(替换内置文件+控制台)
Vostok.Log.init(new VKLogConfig()
.backend(new Slf4jBackend())
);
// Per-logger Backend(仅覆盖指定命名 Logger 的输出)
Vostok.Log.init(new VKLogConfig()
.registerLogger("audit", new VKLogSinkConfig()
.backend(new Slf4jBackend())
)
);
// 多目标扇出:内置文件写入 + 外部框架同时输出
VKLogBackend composite = VKCompositeBackend.of(
VKBuiltinBackend.INSTANCE, // 保留内置文件写入
new Slf4jBackend() // 同时推给 SLF4J
);
Vostok.Log.init(new VKLogConfig().backend(composite));
SLF4J Backend 适配器示例
以下适配器将 Vostok 日志事件委托给 SLF4J,classpath 中需包含 slf4j-api 及任意实现(Logback、Log4j2-SLF4J 绑定等)。适用于 Vostok 运行在已有 SLF4J 体系的应用中,统一由 logback.xml / log4j2.xml 管控输出行为。
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import yueyang.vostok.log.VKLogBackend;
import yueyang.vostok.log.VKLogLevel;
import java.util.Map;
public class Slf4jBackend implements VKLogBackend {
@Override
public void emit(VKLogLevel level, String loggerName,
String formattedMsg, Throwable t,
long tsMillis, Map<String, String> mdc) {
// 1. 将 Vostok MDC 快照同步到 SLF4J MDC(emit 在 worker 线程运行,SLF4J MDC 为空)
if (!mdc.isEmpty()) {
MDC.setContextMap(mdc);
}
try {
// 2. 按 loggerName 获取 SLF4J Logger(SLF4J 内部有缓存,无需自行缓存)
org.slf4j.Logger log = LoggerFactory.getLogger(loggerName);
// 3. t 为 null 时调用无 Throwable 重载,避免 SLF4J 将 null 打印为 "null"
switch (level) {
case TRACE -> { if (t != null) log.trace(formattedMsg, t); else log.trace(formattedMsg); }
case DEBUG -> { if (t != null) log.debug(formattedMsg, t); else log.debug(formattedMsg); }
case INFO -> { if (t != null) log.info(formattedMsg, t); else log.info(formattedMsg); }
case WARN -> { if (t != null) log.warn(formattedMsg, t); else log.warn(formattedMsg); }
case ERROR -> { if (t != null) log.error(formattedMsg, t); else log.error(formattedMsg); }
}
} finally {
// 4. 清除 worker 线程上的 SLF4J MDC,防止泄漏到下一条日志
if (!mdc.isEmpty()) {
MDC.clear();
}
}
}
}
formattedMsg,再将 t 传给 SLF4J 会造成重复输出。建议配合简洁格式化器,只传递消息正文,异常栈交由 SLF4J / Logback 统一渲染:
Vostok.Log.init(new VKLogConfig()
.backend(new Slf4jBackend())
// 仅输出消息正文,不追加异常栈(由 SLF4J / Logback 渲染)
.formatter((level, name, msg, t, ts, mdc) -> msg + "\n")
.consoleEnabled(false) // 控制台由 Logback 接管
);
接入完整示例
// 应用启动时配置:
Vostok.Log.init(new VKLogConfig()
.level(VKLogLevel.DEBUG)
.backend(new Slf4jBackend())
.formatter((level, name, msg, t, ts, mdc) -> msg + "\n")
.consoleEnabled(false)
);
// 业务代码无需任何改动,以下调用最终路由到 Logback,受 logback.xml 统一管控:
Vostok.Log.info("server started on port {}", port);
VKLogger log = Vostok.Log.logger("payment");
log.error("payment failed", ex); // → Logback logger "payment",含完整异常栈
配置参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| 基础 | |||
| level | VKLogLevel | INFO | 全局日志级别(TRACE/DEBUG/INFO/WARN/ERROR) |
| outputDir | String | "logs" | 日志文件输出目录(懒加载默认为 JAR 所在目录/vklogs) |
| filePrefix | String | "vostok" | 日志文件名前缀,命名 Logger 文件名为 <prefix>-<name>.log |
| defaultLoggerName | String | "app" | 静态 API 使用的默认 Logger 名 |
| 滚动与归档 | |||
| maxFileSizeBytes | long | 64 MB | 单文件最大大小,超出后滚动(也可用 maxFileSizeMb() 设置) |
| rollInterval | VKLogRollInterval | DAILY | 按时间滚动:NONE / HOURLY / DAILY / WEEKLY,与大小滚动同时生效 |
| maxBackups | int | 20 | 保留历史文件数量 |
| maxBackupDays | int | 30 | 历史文件保留天数 |
| maxTotalSizeBytes | long | 1 GB | 历史文件总大小上限(也可用 maxTotalSizeMb() 设置) |
| compressRolledFiles | boolean | false | 归档文件异步 GZIP 压缩(懒加载默认 true) |
| 控制台 | |||
| consoleEnabled | boolean | true | 同时输出到控制台(System.out) |
| consoleColor | boolean | false | 控制台 ANSI 彩色(TRACE=灰/DEBUG=青/INFO=绿/WARN=黄/ERROR=红) |
| 异步队列 | |||
| queueCapacity | int | 32768 | 异步队列容量(每个 sink 独立) |
| queueFullPolicy | VKLogQueueFullPolicy | DROP | 队列满时策略:DROP / BLOCK / SYNC_FALLBACK |
| flushIntervalMs | long | 1000 | 自动刷盘间隔(ms) |
| flushBatchSize | int | 256 | 每次 flush 最多消费的日志条数 |
| 可靠性 | |||
| fsyncPolicy | VKLogFsyncPolicy | NEVER | fsync 策略:NEVER / EVERY_FLUSH / EVERY_WRITE |
| shutdownTimeoutMs | long | 5000 | 关闭时等待队列清空的超时时间(ms) |
| fileRetryIntervalMs | long | 3000 | 文件写入失败后的重试间隔(ms) |
| 命名 Logger 路由 | |||
| autoCreateLoggerSink | boolean | true | 是否为未注册的命名 Logger 自动创建独立文件 sink |
| throwOnUnknownLogger | boolean | false | autoCreateLoggerSink=false 时,未注册的 logger 是否抛异常 |
| 扩展 | |||
| formatter | VKLogFormatter | null(内置格式) | 自定义日志格式化器(函数式接口),null 表示使用默认格式 |
| errorListener | VKLogErrorListener | null(禁用) | ERROR 级别事件回调(函数式接口),在 worker 线程调用,必须非阻塞 |
| backend | VKLogBackend | null(内置) | 自定义输出 Backend(SPI 接口),null 时走内置文件 + 控制台路径;配置后内置输出被替换,可通过 VKCompositeBackend 同时保留 |
枚举说明
VKLogQueueFullPolicy
| 枚举值 | 行为 |
|---|---|
DROP | 队列满时丢弃当前日志(默认,不阻塞业务线程) |
BLOCK | 队列满时阻塞业务线程直到有空位 |
SYNC_FALLBACK | 队列满时在业务线程上同步写入文件 |
VKLogFsyncPolicy
| 枚举值 | 行为 |
|---|---|
NEVER | 不调用 fsync,依赖 OS 刷盘(默认,性能最高) |
EVERY_FLUSH | 每次批量 flush 后调用一次 fsync |
EVERY_WRITE | 每条日志写入后立即 fsync(最安全,性能最低) |
VKLogRollInterval
| 枚举值 | 行为 |
|---|---|
NONE | 不按时间滚动,仅按文件大小滚动 |
HOURLY | 按小时滚动,整点触发 |
DAILY | 按天滚动(默认),每日零点触发 |
WEEKLY | 按周滚动,每周一零点触发(ISO 周) |
API 速查
初始化与生命周期
| 方法 | 返回值 | 说明 |
|---|---|---|
init() | void | 以内置默认配置显式初始化 |
init(VKLogConfig) | void | 显式初始化(已初始化时幂等,不重复初始化) |
reinit(VKLogConfig) | void | 重新初始化(替换配置,重建所有 sink) |
resetDefaults() | void | 重置为默认配置并重新初始化 |
initialized() | boolean | 是否已初始化 |
flush() | void | 刷新所有 logger 队列 |
close() | void | 关闭日志系统,等待队列清空(JVM 退出时自动调用) |
shutdown() | void | close() 的别名 |
静态日志 API
| 方法 | 说明 |
|---|---|
trace(msg) / trace(template, args...) | TRACE 级别日志 |
debug(msg) / debug(template, args...) | DEBUG 级别日志 |
info(msg) / info(template, args...) | INFO 级别日志 |
warn(msg) / warn(template, args...) | WARN 级别日志 |
error(msg) / error(template, args...) | ERROR 级别日志 |
error(msg, Throwable) | ERROR 级别日志(含异常栈) |
isTraceEnabled() … isErrorEnabled() | 全局级别检查 |
level() | 获取当前全局日志级别 |
命名 Logger
| 方法 | 返回值 | 说明 |
|---|---|---|
logger(name) | VKLogger | 获取命名 Logger 实例(缓存复用) |
getLogger(name) | VKLogger | logger(name) 的别名 |
动态配置
| 方法 | 说明 |
|---|---|
setLevel(VKLogLevel) | 修改全局日志级别(热切换) |
setLevel(loggerName, VKLogLevel) | 修改指定命名 Logger 的级别 |
setOutputDir(dir) | 修改日志输出目录 |
setFilePrefix(prefix) | 修改文件名前缀 |
setMaxFileSizeMb(mb) | 修改单文件最大大小(MB) |
setMaxFileSizeBytes(bytes) | 修改单文件最大大小(字节) |
setMaxBackups(n) | 修改保留备份数 |
setMaxBackupDays(days) | 修改备份保留天数 |
setMaxTotalSizeMb(mb) | 修改历史文件总大小上限(MB) |
setConsoleEnabled(bool) | 开启/关闭控制台输出 |
setConsoleColor(bool) | 开启/关闭控制台 ANSI 彩色 |
setRollInterval(interval) | 修改时间滚动策略 |
setCompressRolledFiles(bool) | 开启/关闭归档压缩 |
setQueueFullPolicy(policy) | 修改队列满策略 |
setQueueCapacity(n) | 修改队列容量 |
setFlushIntervalMs(ms) | 修改自动刷盘间隔 |
setFlushBatchSize(n) | 修改每次 flush 批量大小 |
setShutdownTimeoutMs(ms) | 修改关闭等待超时 |
setFsyncPolicy(policy) | 修改 fsync 策略 |
setFileRetryIntervalMs(ms) | 修改文件写入重试间隔 |
setFormatter(VKLogFormatter) | 修改格式化器(null 恢复默认格式) |
setErrorListener(VKLogErrorListener) | 修改 ERROR 告警回调(null 禁用) |
setThrowOnUnknownLogger(bool) | 修改未注册 Logger 是否抛异常 |
MDC
门面方法(Vostok.Log.mdc*())与 VKLogMDC 静态方法完全等价,任选其一。
| 门面方法 | 等价的底层方法 | 返回值 | 说明 |
|---|---|---|---|
Vostok.Log.mdcPut(key, value) | VKLogMDC.put(key, value) | void | 设置当前线程 MDC 键值 |
Vostok.Log.mdcPutAll(map) | VKLogMDC.putAll(map) | void | 批量设置(跨线程传播时使用) |
Vostok.Log.mdcGet(key) | VKLogMDC.get(key) | String | 读取指定键的值,不存在时返回 null |
Vostok.Log.mdcGetAll() | VKLogMDC.getAll() | Map<String,String> | 获取全部 MDC 只读快照 |
Vostok.Log.mdcRemove(key) | VKLogMDC.remove(key) | void | 移除指定键 |
Vostok.Log.mdcClear() | VKLogMDC.clear() | void | 清除当前线程全部 MDC(请求结束时务必调用) |
监控指标
| 方法 | 返回值 | 说明 |
|---|---|---|
droppedLogs() | long | 因队列满丢弃的日志条数(DROP 策略) |
fallbackWrites() | long | 队列满时同步写入的次数(SYNC_FALLBACK 策略) |
fileWriteErrors() | long | 文件写入错误次数 |
pendingCompressions() | int | 等待异步 GZIP 压缩的文件数,为 0 表示全部完成 |