IO & Logger(輸入輸出與日誌)
ll/api/io/ · 通用
概述
IO 模塊提供了線程安全的格式化日誌系統,支持多個輸出目標、日誌級別和可自定義的格式化器。還包括文件 I/O 工具和流重定向。
頭文件
| 頭文件 | 說明 |
|---|---|
ll/api/io/Logger.h | 主日誌類,支持 fmt 格式化 |
ll/api/io/LogLevel.h | 日誌嚴重性級別 |
ll/api/io/LoggerRegistry.h | 日誌實例管理 |
ll/api/io/Sink.h | 日誌輸出目標接口 |
ll/api/io/FileSink.h | 基於文件的日誌輸出,可選輪轉 |
ll/api/io/ConsoleSink.h | 控制台日誌輸出 |
ll/api/io/RotatePolicy.h | 日誌文件何時輪轉、歸檔保留多久 |
ll/api/io/FileUtils.h | 文件 I/O 工具 |
ll/api/io/StdoutRedirector.h | 將 stdout 重定向到日誌 |
核心類
Logger
線程安全的日誌器,支持 fmt 風格格式化。
namespace ll::io {
class Logger {
public:
template <typename... Args>
void log(LogLevel level, fmt::format_string<Args...> fmt, Args&&... args) const;
template <typename... Args>
void fatal(fmt::format_string<Args...> fmt, Args&&... args) const;
template <typename... Args>
void error(fmt::format_string<Args...> fmt, Args&&... args) const;
template <typename... Args>
void warn(fmt::format_string<Args...> fmt, Args&&... args) const;
template <typename... Args>
void info(fmt::format_string<Args...> fmt, Args&&... args) const;
template <typename... Args>
void debug(fmt::format_string<Args...> fmt, Args&&... args) const;
template <typename... Args>
void trace(fmt::format_string<Args...> fmt, Args&&... args) const;
std::string const& getTitle() const noexcept;
LogLevel getLevel() const noexcept;
bool shouldLog(LogLevel level) const noexcept;
void setLevel(LogLevel level);
void setFlushLevel(LogLevel level);
void setFormatter(Polymorphic<Formatter> formatter);
void flush() const;
std::shared_ptr<SinkBase> setSink(std::string_view name, std::shared_ptr<SinkBase> sink) const;
std::shared_ptr<SinkBase> removeSink(std::string_view name) const;
std::shared_ptr<SinkBase> getSink(std::string_view name) const;
void clearSinks() const;
coro::Generator<std::pair<std::string_view, SinkBase&>> sinks() const;
};
}Logger 按名稱持有各個 sink。每個 logger 初始都帶有一個名為 "ll_default_console" 的控制台
sink,以及一個名為 "ll_default_file"、寫入 logs/latest.log 的 sink。
LogLevel
enum class LogLevel : int {
Off = -1,
Fatal,
Error,
Warn,
Info,
Debug,
Trace,
};LoggerRegistry
namespace ll::io {
class LoggerRegistry {
public:
static LoggerRegistry& getInstance();
std::shared_ptr<Logger> getLogger(std::string_view name);
};
}使用方法
基本日誌記錄
#include "ll/api/io/Logger.h"
#include "ll/api/mod/Mod.h"
void example(ll::mod::Mod& mod) {
auto& logger = mod.getLogger();
logger.info("服务器已启动");
logger.warn("内存不足: {} MB", 128);
logger.error("加载配置失败");
logger.debug("玩家数量: {}", 5);
}設置日誌級別
#include "ll/api/io/Logger.h"
void configureLogger(ll::io::Logger& logger) {
// 仅记录警告及以上级别
logger.setLevel(ll::io::LogLevel::Warn);
// 在错误时自动刷新
logger.setFlushLevel(ll::io::LogLevel::Error);
}添加自定義輸出目標
#include "ll/api/io/Logger.h"
#include "ll/api/io/FileSink.h"
#include "ll/api/io/PatternFormatter.h"
void addFileSink(ll::io::Logger& logger) {
auto fileSink = std::make_shared<ll::io::FileSink>(
"logs/mymod.log",
makePolymorphic<ll::io::PatternFormatter>("[{3:.3%F %T.} {2}][{1}] {0}", false)
);
logger.setSink("mymod", fileSink);
}若同名的 sink 已經存在,setSink 會替換它並返回被替換掉的 sink;removeSink 則移除一個 sink 並將其
返回。
#include "ll/api/io/ConsoleSink.h"
#include "ll/api/io/FileSink.h"
#include "ll/api/io/Logger.h"
#include "ll/api/io/PatternFormatter.h"
void customizeSinks(ll::io::Logger& logger) {
// 只修改控制台输出的样式
logger.setSink(
"ll_default_console",
std::make_shared<ll::io::ConsoleSink>(makePolymorphic<ll::io::PatternFormatter>("[{2}] {0}"))
);
// 改为写入模组自己的文件,而不是 logs/latest.log
logger.setSink(
"ll_default_file",
std::make_shared<ll::io::FileSink>(
"logs/mymod.log",
makePolymorphic<ll::io::PatternFormatter>("[{3:.3%F %T.} {2}][{1}] {0}", false)
)
);
}日誌消息在後臺線程中寫入。Logger::setFormatter 和 Logger::setFlushLevel 會等待正在進行的寫入,因此在
logger 使用期間,應優先使用它們或通過 setSink 替換 sink,而不是直接修改 getSink 返回的 sink。
FileSink 會使用規範化後的路徑定位共享的文件狀態。指向同一文件的 sink 會共享文件句柄、輪轉狀態和策略,
但每個 sink 保留自己的 formatter 和刷新級別。因此,不同 logger 可以向同一個文件寫入不同格式的日誌,而不
會互相覆蓋 formatter。
輪轉日誌文件
不傳入 RotatePolicy 時,FileSink 使用 RotatePolicy::disabled(),只會一直追加,不做切分。
RotatePolicy{} 的各項默認值本身就是一套可直接使用的配置,所以通常只要把它傳進去就夠了。策略中的字段在
三個不同時機被讀取:rotateOnOpen 只在打開文件時讀取一次,maxFileSize 和 interval 在每條消息寫入前
判斷,壓縮與清理相關的設置則在歸檔產生之後異步執行。
| 字段 | 默認值 | 含義 |
|---|---|---|
rotateOnOpen | true | 打開已有內容的文件時先將其歸檔,讓每次運行都從一個新文件開始 |
maxFileSize | 32 MiB | 寫入下一條消息會讓文件超過該字節數時,先輪轉再寫入。單條消息不會被拆分,超過上限的消息會單獨佔一個文件。設為 0 關閉 |
interval | Daily | 消息時間戳進入新的小時(Hourly)或新的一天(Daily)時輪轉,None 關閉。判斷依據是消息產生的時間,而不是寫入文件的時間 |
compress | true | 在後臺線程中將歸檔壓縮為 gzip,正在寫入的文件不會被壓縮 |
keepUncompressed | 7 | 最新的這麼多個歸檔保持明文,便於直接搜索最近的日誌 |
cleanup | true | 是否刪除歸檔。為 false 時下面三項限制都不生效 |
maxFiles | 1024 | 最多保留的歸檔數,壓縮與未壓縮的合併計數。設為 0 關閉 |
maxAgeDays | 120 | 刪除最後修改時間在這麼多天之前的歸檔。設為 0 關閉 |
totalSizeCap | 512 MiB | 從最舊的開始刪除歸檔,直到磁盤上的總體積不超過該值。設為 0 關閉 |
| 字段 | 默認值 | 含義 |
|---|---|---|
rotateOnOpen | true | 打開文件時,如果裡面還有上次運行留下的內容,就先把它歸檔 |
maxFileSize | 32 MiB | 寫入下一條消息會超過此大小時先輪轉;0 表示關閉。單條消息不會被拆開,超大的消息會單獨佔一個文件 |
interval | Daily | 按消息自身的時間跨過整點或日期時輪轉;None 表示關閉 |
compress | true | 在後臺線程中用 gzip 壓縮歸檔 |
keepUncompressed | 7 | 最新的多少個歸檔保持不壓縮 |
cleanup | true | 是否允許清理刪除文件 |
maxFiles | 1024 | 最多保留的歸檔數;0 表示關閉 |
maxAgeDays | 120 | 刪除最後修改時間早於這麼多天的歸檔;0 表示關閉 |
totalSizeCap | 512 MiB | 從最舊的歸檔開始刪除,直到磁盤上的總大小不超過此值;0 表示關閉 |
#include "ll/api/io/FileSink.h"
#include "ll/api/io/PatternFormatter.h"
#include "ll/api/io/RotatePolicy.h"
void addRotatingFileSink(ll::io::Logger& logger) {
ll::io::RotatePolicy policy{};
policy.maxFileSize = 8ull * 1024 * 1024; // 超过 8 MiB 时轮转
policy.interval = ll::io::RotateInterval::Daily; // 跨天时也轮转
policy.maxFiles = 64; // 最多保留 64 个归档
logger.setSink(
"mymod",
std::make_shared<ll::io::FileSink>(
"logs/mymod.log",
makePolymorphic<ll::io::PatternFormatter>("[{3:.3%F %T.} {2}][{1}] {0}", false),
policy
)
);
}大小與時間兩個觸發條件相互獨立,哪個先滿足就由它觸發輪轉。setPolicy 用於替換已有 sink 的策略,
rotate 會立即輪轉(文件為空時什麼都不做)。
maxFiles、maxAgeDays 和 totalSizeCap 各自獨立生效,最終取並集刪除,因此放寬其中一個不會讓另外
兩個失效。清理只會處理符合歸檔命名規則的文件,同目錄下的其他文件不受影響。由於 totalSizeCap 統計的是
壓縮後的體積,關閉 compress 會明顯縮短該上限內能保留的歷史長度。
歸檔名由 namer 生成,再加上當前文件的擴展名。默認的 RotatePolicy::defaultNamer 生成
2026-09-19.log,按小時輪轉時為 2026-09-19_14.log,同一週期內的後續輪轉依次加上 .1、.2。自定義
namer 會收到一個 ArchiveNameInfo:time 是觸發輪轉的時間,index 是本次輪轉在當前週期內的序號(從
0 開始),interval 是策略的輪轉週期。返回值必須以四位年份加 - 開頭,否則壓縮和清理都無法識別這些
歸檔。
RotatePolicy 繼承自 RotateConfig,後者包含除 namer 以外的全部字段,因此可以反射到配置文件中。
LeviLamina 自己的 logs/latest.log 使用其配置中的 logRotate 作為輪轉策略。
條件日誌記錄
#include "ll/api/io/Logger.h"
void expensiveLogging(ll::io::Logger& logger) {
if (logger.shouldLog(ll::io::LogLevel::Debug)) {
// 仅在启用调试日志时计算
auto debugInfo = computeExpensiveDebugInfo();
logger.debug("调试信息: {}", debugInfo);
}
}格式字符串安全性
日誌器使用 fmt 格式字符串,具有編譯期檢查:
logger.info("玩家 {} 加入", playerName); // 正确
logger.info("位置: ({}, {}, {})", x, y, z); // 正确
// logger.info("值: {}", x, y); // 编译错误:参数过多相關模塊
- Expected(錯誤處理) —
Error::log()輸出到 Logger - Mod(模組系統) —
Mod::getLogger()返回模組的日誌器