跳到正文
本頁目錄

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 風格格式化。

C++
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

C++
enum class LogLevel : int {
    Off = -1,
    Fatal,
    Error,
    Warn,
    Info,
    Debug,
    Trace,
};

LoggerRegistry

C++
namespace ll::io {
class LoggerRegistry {
public:
    static LoggerRegistry& getInstance();
    std::shared_ptr<Logger> getLogger(std::string_view name);
};
}

使用方法

基本日誌記錄

C++
#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);
}

設置日誌級別

C++
#include "ll/api/io/Logger.h"

void configureLogger(ll::io::Logger& logger) {
    // 仅记录警告及以上级别
    logger.setLevel(ll::io::LogLevel::Warn);

    // 在错误时自动刷新
    logger.setFlushLevel(ll::io::LogLevel::Error);
}

添加自定義輸出目標

C++
#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 並將其 返回。

C++
#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 在每條消息寫入前 判斷,壓縮與清理相關的設置則在歸檔產生之後異步執行。

字段默認值含義
rotateOnOpentrue打開已有內容的文件時先將其歸檔,讓每次運行都從一個新文件開始
maxFileSize32 MiB寫入下一條消息會讓文件超過該字節數時,先輪轉再寫入。單條消息不會被拆分,超過上限的消息會單獨佔一個文件。設為 0 關閉
intervalDaily消息時間戳進入新的小時(Hourly)或新的一天(Daily)時輪轉,None 關閉。判斷依據是消息產生的時間,而不是寫入文件的時間
compresstrue在後臺線程中將歸檔壓縮為 gzip,正在寫入的文件不會被壓縮
keepUncompressed7最新的這麼多個歸檔保持明文,便於直接搜索最近的日誌
cleanuptrue是否刪除歸檔。為 false 時下面三項限制都不生效
maxFiles1024最多保留的歸檔數,壓縮與未壓縮的合併計數。設為 0 關閉
maxAgeDays120刪除最後修改時間在這麼多天之前的歸檔。設為 0 關閉
totalSizeCap512 MiB從最舊的開始刪除歸檔,直到磁盤上的總體積不超過該值。設為 0 關閉
字段默認值含義
rotateOnOpentrue打開文件時,如果裡面還有上次運行留下的內容,就先把它歸檔
maxFileSize32 MiB寫入下一條消息會超過此大小時先輪轉;0 表示關閉。單條消息不會被拆開,超大的消息會單獨佔一個文件
intervalDaily按消息自身的時間跨過整點或日期時輪轉;None 表示關閉
compresstrue在後臺線程中用 gzip 壓縮歸檔
keepUncompressed7最新的多少個歸檔保持不壓縮
cleanuptrue是否允許清理刪除文件
maxFiles1024最多保留的歸檔數;0 表示關閉
maxAgeDays120刪除最後修改時間早於這麼多天的歸檔;0 表示關閉
totalSizeCap512 MiB從最舊的歸檔開始刪除,直到磁盤上的總大小不超過此值;0 表示關閉
C++
#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 作為輪轉策略。

條件日誌記錄

C++
#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 格式字符串,具有編譯期檢查:

C++
logger.info("玩家 {} 加入", playerName);           // 正确
logger.info("位置: ({}, {}, {})", x, y, z);        // 正确
// logger.info("值: {}", x, y);                    // 编译错误:参数过多

相關模塊