如何在 C++20 中使用 std::format 实现可定制化日志系统?

在现代 C++20 标准中,std::format 提供了一种类型安全且语法优雅的字符串格式化机制,完美取代了传统的 printf 风格。利用它可以轻松构建一个可插拔、可配置的日志系统,支持多种输出目标(控制台、文件、网络)和日志级别。下面给出一个完整示例,展示如何定义日志级别枚举、构建线程安全的日志器、实现可定制化的格式化模板,并演示多线程环境下的使用。

1. 日志级别枚举与字符串映射

#include <string_view>
#include <unordered_map>

enum class LogLevel {
    Trace,
    Debug,
    Info,
    Warn,
    Error,
    Fatal
};

constexpr std::unordered_map<LogLevel, std::string_view> LogLevelNames = {
    {LogLevel::Trace, "TRACE"},
    {LogLevel::Debug, "DEBUG"},
    {LogLevel::Info,  "INFO" },
    {LogLevel::Warn,  "WARN" },
    {LogLevel::Error, "ERROR"},
    {LogLevel::Fatal, "FATAL"}
};

inline std::string_view to_string(LogLevel level) {
    return LogLevelNames.at(level);
}

2. 输出目标基类与具体实现

#include <ostream>
#include <memory>
#include <mutex>
#include <fstream>
#include <iostream>

class LogSink {
public:
    virtual ~LogSink() = default;
    virtual void write(const std::string& msg) = 0;
};

class ConsoleSink : public LogSink {
public:
    void write(const std::string& msg) override {
        std::lock_guard<std::mutex> lock(mutex_);
        std::cout << msg << '\n';
    }
private:
    std::mutex mutex_;
};

class FileSink : public LogSink {
public:
    explicit FileSink(const std::string& filename) : file_(filename, std::ios::app) {}
    void write(const std::string& msg) override {
        std::lock_guard<std::mutex> lock(mutex_);
        file_ << msg << '\n';
    }
private:
    std::ofstream file_;
    std::mutex mutex_;
};

3. 日志器核心实现

#include <format>
#include <chrono>
#include <iomanip>
#include <vector>

class Logger {
public:
    Logger() : level_(LogLevel::Info), formatTemplate_("[{timestamp}] [{level}] {message}") {}

    void setLevel(LogLevel lvl) { level_ = lvl; }
    void setFormat(std::string_view tmpl) { formatTemplate_ = tmpl; }

    void addSink(std::shared_ptr <LogSink> sink) {
        std::lock_guard<std::mutex> lock(sinkMutex_);
        sinks_.push_back(std::move(sink));
    }

    template<typename... Args>
    void log(LogLevel lvl, std::string_view fmt, Args&&... args) {
        if (lvl < level_) return;
        std::string formattedMsg = std::vformat(fmt, std::make_format_args(args...));
        std::string finalMsg = formatMessage(lvl, formattedMsg);
        writeToSinks(finalMsg);
    }

    // Convenience wrappers
    template<typename... Args>
    void trace(std::string_view fmt, Args&&... args) { log(LogLevel::Trace, fmt, std::forward <Args>(args)...); }
    template<typename... Args>
    void debug(std::string_view fmt, Args&&... args) { log(LogLevel::Debug, fmt, std::forward <Args>(args)...); }
    template<typename... Args>
    void info(std::string_view fmt, Args&&... args)  { log(LogLevel::Info,  fmt, std::forward <Args>(args)...); }
    template<typename... Args>
    void warn(std::string_view fmt, Args&&... args)  { log(LogLevel::Warn,  fmt, std::forward <Args>(args)...); }
    template<typename... Args>
    void error(std::string_view fmt, Args&&... args) { log(LogLevel::Error, fmt, std::forward <Args>(args)...); }
    template<typename... Args>
    void fatal(std::string_view fmt, Args&&... args) { log(LogLevel::Fatal, fmt, std::forward <Args>(args)...); }

private:
    std::string formatMessage(LogLevel lvl, const std::string& msg) {
        auto now = std::chrono::system_clock::now();
        std::time_t tt = std::chrono::system_clock::to_time_t(now);
        std::tm tm;
#if defined(_MSC_VER)
        localtime_s(&tm, &tt);
#else
        localtime_r(&tt, &tm);
#endif
        std::ostringstream oss;
        oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
        std::string timestamp = oss.str();

        std::string result = formatTemplate_;
        replaceAll(result, "{timestamp}", timestamp);
        replaceAll(result, "{level}", std::string(to_string(lvl)));
        replaceAll(result, "{message}", msg);
        return result;
    }

    void writeToSinks(const std::string& msg) {
        std::lock_guard<std::mutex> lock(sinkMutex_);
        for (auto& sink : sinks_) {
            sink->write(msg);
        }
    }

    // Simple string replace helper
    static void replaceAll(std::string& str, const std::string& from, const std::string& to) {
        if (from.empty()) return;
        size_t pos = 0;
        while ((pos = str.find(from, pos)) != std::string::npos) {
            str.replace(pos, from.length(), to);
            pos += to.length();
        }
    }

    LogLevel level_;
    std::string formatTemplate_;
    std::vector<std::shared_ptr<LogSink>> sinks_;
    std::mutex sinkMutex_;
};

4. 使用示例

#include <thread>
#include <vector>
#include <chrono>

int main() {
    Logger logger;
    logger.setLevel(LogLevel::Debug);
    logger.setFormat("[{timestamp}] [{level}] {message}");

    logger.addSink(std::make_shared <ConsoleSink>());
    logger.addSink(std::make_shared <FileSink>("app.log"));

    logger.info("程序启动,线程数 {}", std::thread::hardware_concurrency());

    // 启动多线程演示
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i) {
        threads.emplace_back([&, i]() {
            for (int j = 0; j < 10; ++j) {
                logger.debug("线程 {} 计数 {}", i, j);
                std::this_thread::sleep_for(std::chrono::milliseconds(50));
            }
        });
    }

    for (auto& t : threads) t.join();

    logger.info("程序结束");
}

运行效果(控制台)

[2026-01-09 12:34:56] [INFO] 程序启动,线程数 8
[2026-01-09 12:34:56] [DEBUG] 线程 0 计数 0
[2026-01-09 12:34:56] [DEBUG] 线程 1 计数 0
...
[2026-01-09 12:35:05] [INFO] 程序结束

日志文件 app.log(与控制台内容相同)

[2026-01-09 12:34:56] [INFO] 程序启动,线程数 8
[2026-01-09 12:34:56] [DEBUG] 线程 0 计数 0
[2026-01-09 12:34:56] [DEBUG] 线程 1 计数 0
...
[2026-01-09 12:35:05] [INFO] 程序结束

5. 可扩展性与改进

  1. 异步日志:将日志写入队列,后台线程批量写入文件或网络,进一步降低 I/O 阻塞。
  2. 滚动文件:在 FileSink 中实现文件大小或日期滚动,避免单文件过大。
  3. 网络输出:实现 NetworkSink,通过 TCP/UDP 将日志发送至日志收集中心。
  4. 配置文件:从 JSON/YAML 文件读取日志级别、格式、sink 列表,支持热重载。
  5. 多进程共享:利用共享内存 + 进程间消息队列实现跨进程日志收集。

6. 小结

C++20 的 std::format 为日志系统提供了强大且类型安全的格式化能力,使得构建可读、可维护的日志变得异常简单。通过上述设计,你可以轻松将日志系统集成到任何项目中,并在需要时按需扩展输出目标或格式化策略。祝编码愉快!

发表评论