C++20 异步日志系统实现说明
源码实现
头文件
#pragma once
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <memory>
#include <mutex>
#include <ostream>
#include <queue>
#include <source_location>
#include <sstream>
#include <streambuf>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
/**
* @brief 日志等级。
*
* 等级从低到高排列,便于使用整数比较实现最低日志等级过滤。
*/
enum class LogLevel {
Debug = 0,
Info,
Warn,
Error,
};
/**
* @brief 日志系统配置。
*/
struct LogConfig {
/**
* @brief 日志文件路径。
*
* 默认写入当前工作目录下的 Log.txt。父目录不存在时 FileSink 会自动创建。
*/
std::filesystem::path file_path{"Log.txt"};
/**
* @brief 最低日志等级。
*
* 低于该等级的日志会在 LogSystem::log() 中被过滤,不进入异步队列。
*/
LogLevel min_level{LogLevel::Debug};
/**
* @brief 异步队列最大容量。
*
* 队列满时新日志会被丢弃并增加 droppedCount()。设置为 0 表示不限制容量。
*/
std::size_t max_queue_size{8192};
/**
* @brief 单个日志文件最大字节数。
*
* 超过该大小后 FileSink 会执行滚动。设置为 0 表示不按大小滚动。
*/
std::uintmax_t max_file_size{10 * 1024 * 1024};
/**
* @brief 保留的滚动日志文件数量。
*
* 例如值为 3 时,会保留 app.log.1、app.log.2、app.log.3。
*/
std::size_t max_rotated_files{3};
};
/**
* @brief 结构化日志记录。
*
* 前台线程创建 LogRecord,后台 sink 根据这些字段决定格式化、flush、滚动等输出策略。
*/
struct LogRecord {
/**
* @brief 日志等级。
*/
LogLevel level{LogLevel::Info};
/**
* @brief 日志创建时间点。
*/
std::chrono::system_clock::time_point timestamp{};
/**
* @brief 产生日志的线程 id。
*/
std::thread::id thread_id{};
/**
* @brief 日志调用点源码位置。
*/
std::source_location location{};
/**
* @brief 日志正文。
*/
std::string message;
};
/**
* @brief 异步日志队列。
*
* 该队列是多生产者、单消费者场景下的并发边界。前台线程 push,后台线程 pop。
*/
class LogQueue {
public:
/**
* @brief 构造指定容量的日志队列。
*
* @param max_queue_size 最大队列容量,0 表示不限制容量。
*/
explicit LogQueue(std::size_t max_queue_size = 8192);
/**
* @brief 向队列写入一条结构化日志。
*
* @param record 待入队的日志记录。
* @return true 日志成功入队。
* @return false 队列已经关闭或队列已满,日志被丢弃。
*/
bool push(LogRecord record);
/**
* @brief 从队列弹出一条日志。
*
* @param record 输出参数,成功时接收一条日志记录。
* @return true 成功弹出日志。
* @return false 队列已经关闭且没有剩余日志。
*/
bool pop(LogRecord& record);
/**
* @brief 关闭队列并唤醒等待线程。
*
* 关闭后不接受新日志,但已有日志仍可被后台线程消费完。
*/
void shutdown() noexcept;
/**
* @brief 返回当前排队日志条数。
*
* @return 队列中尚未被后台线程消费的日志数量。
*/
[[nodiscard]] std::size_t size() const;
/**
* @brief 返回被队列丢弃的日志条数。
*
* @return 因关闭或队列满而丢弃的日志数量。
*/
[[nodiscard]] std::uint64_t droppedCount() const noexcept;
private:
/**
* @brief 判断当前队列是否达到容量上限。
*
* @return true 队列有容量上限且已经满。
* @return false 队列仍可接收新日志。
*/
[[nodiscard]] bool isFullUnlocked() const;
/**
* @brief 已入队但尚未写入 sink 的日志记录。
*/
std::queue<LogRecord> mQueue;
/**
* @brief 队列最大容量,0 表示不限制容量。
*/
std::size_t mMaxQueueSize{0};
/**
* @brief 保护 mQueue、mMaxQueueSize 和 mShutdown 的互斥锁。
*/
mutable std::mutex mMutex;
/**
* @brief 后台线程等待新日志或关闭信号的条件变量。
*/
std::condition_variable mCondVar;
/**
* @brief 队列是否已经关闭。
*/
bool mShutdown{false};
/**
* @brief 被丢弃的日志数量。
*/
std::atomic<std::uint64_t> mDroppedCount{0};
};
/**
* @brief 日志输出端抽象。
*
* LogSystem 只负责调度 LogRecord,具体输出到文件、控制台或网络由 LogSink 子类决定。
*/
class LogSink {
public:
/**
* @brief 默认虚析构。
*/
virtual ~LogSink() = default;
/**
* @brief 写入一条日志记录。
*
* @param record 结构化日志记录。
*/
virtual void write(const LogRecord& record) = 0;
/**
* @brief 刷新 sink 内部缓冲。
*/
virtual void flush() = 0;
};
/**
* @brief 文件日志输出端。
*
* FileSink 负责把 LogRecord 格式化成文本行,并按文件大小执行滚动。
*/
class FileSink final : public LogSink {
public:
/**
* @brief 构造文件 sink。
*
* @param file_path 日志文件路径。
* @param max_file_size 单个日志文件最大字节数,0 表示不滚动。
* @param max_rotated_files 保留的滚动文件数量。
*/
FileSink(
std::filesystem::path file_path,
std::uintmax_t max_file_size,
std::size_t max_rotated_files);
/**
* @brief 析构前刷新文件缓冲。
*/
~FileSink() override;
FileSink(const FileSink&) = delete;
FileSink& operator=(const FileSink&) = delete;
/**
* @brief 写入一条日志到文件。
*
* @param record 结构化日志记录。
*/
void write(const LogRecord& record) override;
/**
* @brief 刷新文件缓冲。
*/
void flush() override;
/**
* @brief 返回当前日志文件路径。
*
* @return const std::filesystem::path& 当前文件路径。
*/
[[nodiscard]] const std::filesystem::path& filePath() const noexcept;
private:
/**
* @brief 打开当前日志文件。
*/
void openFile();
/**
* @brief 根据当前文件大小判断是否需要滚动。
*
* @param next_write_size 即将写入的日志行字节数。
*/
void rotateIfNeeded(std::size_t next_write_size);
/**
* @brief 执行日志文件滚动。
*/
void rotateFiles();
/**
* @brief 格式化一条日志记录。
*
* @param record 结构化日志记录。
* @return std::string 可直接写入文件的日志行,不包含换行符。
*/
[[nodiscard]] std::string formatRecord(const LogRecord& record) const;
/**
* @brief 日志等级转字符串。
*
* @param level 日志等级。
* @return std::string_view 等级名称。
*/
[[nodiscard]] static std::string_view levelToString(LogLevel level) noexcept;
/**
* @brief 判断日志等级是否需要立即 flush。
*
* @param level 日志等级。
* @return true 需要立即 flush。
* @return false 可以依赖文件缓冲。
*/
[[nodiscard]] static bool shouldFlush(LogLevel level) noexcept;
/**
* @brief 当前主日志文件路径。
*/
std::filesystem::path mFilePath;
/**
* @brief 单个日志文件最大字节数,0 表示不滚动。
*/
std::uintmax_t mMaxFileSize{0};
/**
* @brief 保留的滚动文件数量。
*/
std::size_t mMaxRotatedFiles{0};
/**
* @brief 当前日志文件输出流。
*/
std::ofstream mFile;
/**
* @brief 当前主日志文件已知大小。
*
* 该值包含已经写入 ofstream 的字节数,避免文件缓冲导致 filesystem::file_size() 滞后。
*/
std::uintmax_t mCurrentFileSize{0};
};
/**
* @brief 进程级异步日志系统。
*
* 前台线程负责等级过滤和创建 LogRecord,后台线程负责把记录分发给 sink。
*/
class LogSystem {
public:
/**
* @brief 获取全局日志系统实例。
*
* @param log_file_path 可选日志文件路径,仅在单例首次创建时生效。
* @return LogSystem& 全局日志系统实例。
*/
static LogSystem& getInstance(std::filesystem::path log_file_path = {});
/**
* @brief 设置默认日志配置。
*
* @param config 日志配置。
* @return true 设置成功,后续首次 getInstance() 会使用该配置。
* @return false 日志系统已经创建,配置没有被修改。
*/
static bool setDefaultConfig(LogConfig config);
/**
* @brief 设置默认日志文件路径。
*
* @param log_file_path 日志文件路径。
* @return true 设置成功,后续首次 getInstance() 会使用该路径。
* @return false 日志系统已经创建,路径没有被修改。
*/
static bool setDefaultLogFilePath(std::filesystem::path log_file_path);
/**
* @brief 析构日志系统,关闭队列并等待后台线程退出。
*/
~LogSystem();
LogSystem(const LogSystem&) = delete;
LogSystem& operator=(const LogSystem&) = delete;
/**
* @brief 写入一条日志。
*
* @param level 日志等级。
* @param message 日志正文。
* @param location 调用位置,默认由 std::source_location 自动填充。
* @return true 日志成功入队。
* @return false 日志被等级过滤、队列已满或系统已关闭。
*/
bool log(
LogLevel level,
std::string_view message,
const std::source_location& location = std::source_location::current()) noexcept;
/**
* @brief 设置最低日志等级。
*
* @param level 新的最低日志等级。
*/
void setMinLevel(LogLevel level) noexcept;
/**
* @brief 判断指定等级是否会被当前配置记录。
*
* @param level 日志等级。
* @return true 该等级会被记录。
* @return false 该等级会被过滤。
*/
[[nodiscard]] bool shouldLog(LogLevel level) const noexcept;
/**
* @brief 主动关闭日志系统。
*/
void shutdown() noexcept;
/**
* @brief 返回当前等待后台线程消费的日志条数。
*
* @return 当前日志队列长度。
*/
[[nodiscard]] std::size_t pendingCount() const;
/**
* @brief 返回被丢弃的日志数量。
*
* @return 因队列满或关闭而丢弃的日志数量。
*/
[[nodiscard]] std::uint64_t droppedCount() const noexcept;
/**
* @brief 返回当前日志文件路径。
*
* @return const std::filesystem::path& 当前日志文件路径。
*/
[[nodiscard]] const std::filesystem::path& logFilePath() const noexcept;
private:
/**
* @brief 使用指定配置创建日志系统。
*
* @param config 日志配置。
*/
explicit LogSystem(LogConfig config);
/**
* @brief 后台线程主循环。
*/
void writerLoop();
/**
* @brief 创建文件 sink。
*
* @param config 日志配置。
*/
void createDefaultSink(const LogConfig& config);
/**
* @brief 日志队列。
*/
LogQueue mLogQueue;
/**
* @brief 当前日志系统配置。
*/
LogConfig mConfig;
/**
* @brief 当前注册的输出端列表。
*/
std::vector<std::unique_ptr<LogSink>> mSinks;
/**
* @brief 后台写日志线程。
*/
std::jthread mWorkerThread;
/**
* @brief 保护 mShutdown 和 mSinks 的互斥锁。
*/
mutable std::mutex mStateMutex;
/**
* @brief 日志系统是否已经关闭。
*/
bool mShutdown{false};
/**
* @brief 当前最低日志等级,用整数保存便于原子读写。
*/
std::atomic<int> mMinLevel{static_cast<int>(LogLevel::Debug)};
};
/**
* @brief ostream 缓冲区,把流式日志内容转交给 LogSystem。
*/
class LogBuffer : public std::stringbuf {
public:
/**
* @brief 构造指定等级和调用位置的日志缓冲区。
*
* @param level 日志等级。
* @param location 宏展开处的源代码位置。
*/
explicit LogBuffer(LogLevel level, std::source_location location);
/**
* @brief 析构时自动提交尚未 flush 的日志内容。
*/
~LogBuffer() override;
LogBuffer(const LogBuffer&) = delete;
LogBuffer& operator=(const LogBuffer&) = delete;
/**
* @brief ostream flush 时提交当前缓冲内容。
*
* @return int 成功返回 0。
*/
int sync() override;
private:
/**
* @brief 将当前 stringbuf 内容提交到异步日志系统。
*/
void putOutput() noexcept;
/**
* @brief 当前流对应的日志等级。
*/
LogLevel mLevel;
/**
* @brief 当前流创建时的源代码位置。
*/
std::source_location mLocation;
};
/**
* @brief 支持 LOG_INFO << "message" 风格的临时输出流。
*/
class LogStream : public std::ostream {
public:
/**
* @brief 构造指定等级和调用位置的日志流。
*
* @param level 日志等级。
* @param location 宏展开处的源代码位置。
*/
explicit LogStream(
LogLevel level,
const std::source_location& location = std::source_location::current());
private:
/**
* @brief 日志流内部缓冲区。
*/
LogBuffer mBuffer;
};
/**
* @brief Debug 等级日志流。
*/
#define LOG_DEBUG LogStream(LogLevel::Debug, std::source_location::current())
/**
* @brief Info 等级日志流。
*/
#define LOG_INFO LogStream(LogLevel::Info, std::source_location::current())
/**
* @brief Warn 等级日志流。
*/
#define LOG_WARN LogStream(LogLevel::Warn, std::source_location::current())
/**
* @brief Error 等级日志流。
*/
#define LOG_ERR LogStream(LogLevel::Error, std::source_location::current())
源文件
#include "MyLogger.hpp"
#include <chrono>
#include <ctime>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <utility>
namespace {
/**
* @brief 将 time_t 转换成本地时间。
*
* @param time 从 system_clock 转换得到的秒级时间。
* @return std::tm 本地时间结构体。
*/
std::tm makeLocalTime(std::time_t time) {
std::tm local_time{};
#if defined(_WIN32)
localtime_s(&local_time, &time);
#else
localtime_r(&time, &local_time);
#endif
return local_time;
}
/**
* @brief 保护默认日志配置的互斥锁。
*
* @return std::mutex& 配置互斥锁。
*/
std::mutex& logConfigMutex() {
static std::mutex mutex;
return mutex;
}
/**
* @brief 保存单例创建前配置的日志参数。
*
* @return LogConfig& 可修改的默认配置。
*/
LogConfig& configuredLogConfig() {
static LogConfig config;
return config;
}
/**
* @brief 标记 LogSystem 单例是否已经开始创建。
*
* @return bool& 单例创建标记。
*/
bool& logSystemCreated() {
static bool created = false;
return created;
}
/**
* @brief 解析首次构造 LogSystem 时使用的配置。
*
* @param requested_path getInstance() 调用方临时指定的日志路径。
* @return LogConfig 最终用于构造日志系统的配置。
*/
LogConfig resolveLogConfig(std::filesystem::path requested_path) {
std::lock_guard lock(logConfigMutex());
// getInstance(path) 是一次性初始化入口,只有首次构造前传入才生效。
if (!requested_path.empty()) {
configuredLogConfig().file_path = std::move(requested_path);
}
logSystemCreated() = true;
return configuredLogConfig();
}
/**
* @brief 规范化日志配置。
*
* @param config 调用方传入的日志配置。
* @return LogConfig 修正后的日志配置。
*/
LogConfig normalizeConfig(LogConfig config) {
if (config.file_path.empty()) {
config.file_path = "Log.txt";
}
return config;
}
/**
* @brief 准备日志文件路径,并在需要时创建父目录。
*
* @param file_path 日志文件路径。
* @return std::filesystem::path 可直接传给 std::ofstream 的路径。
*/
std::filesystem::path prepareFilePath(std::filesystem::path file_path) {
if (file_path.empty()) {
file_path = "Log.txt";
}
const std::filesystem::path parent_path = file_path.parent_path();
if (!parent_path.empty()) {
std::filesystem::create_directories(parent_path);
}
return file_path;
}
/**
* @brief 生成滚动日志文件路径。
*
* @param base_path 主日志文件路径。
* @param index 滚动文件编号。
* @return std::filesystem::path 滚动文件路径。
*/
std::filesystem::path rotatedPath(const std::filesystem::path& base_path, std::size_t index) {
return std::filesystem::path(base_path.string() + "." + std::to_string(index));
}
} // namespace
LogQueue::LogQueue(std::size_t max_queue_size)
: mMaxQueueSize(max_queue_size) {
}
bool LogQueue::push(LogRecord record) {
{
std::lock_guard lock(mMutex);
if (mShutdown || isFullUnlocked()) {
mDroppedCount.fetch_add(1, std::memory_order_relaxed);
return false;
}
mQueue.push(std::move(record));
}
// 新日志入队后只需要唤醒一个后台写线程。
mCondVar.notify_one();
return true;
}
bool LogQueue::pop(LogRecord& record) {
std::unique_lock lock(mMutex);
// 等到有日志可写,或者队列进入关闭状态。
mCondVar.wait(lock, [this] {
return !mQueue.empty() || mShutdown;
});
// shutdown 后仍然先消费完已有日志,只有队列为空才真正退出。
if (mQueue.empty()) {
return false;
}
record = std::move(mQueue.front());
mQueue.pop();
return true;
}
void LogQueue::shutdown() noexcept {
{
std::lock_guard lock(mMutex);
mShutdown = true;
}
// 唤醒后台线程,让它把剩余日志 drain 完后退出。
mCondVar.notify_all();
}
std::size_t LogQueue::size() const {
std::lock_guard lock(mMutex);
return mQueue.size();
}
std::uint64_t LogQueue::droppedCount() const noexcept {
return mDroppedCount.load(std::memory_order_relaxed);
}
bool LogQueue::isFullUnlocked() const {
return mMaxQueueSize != 0 && mQueue.size() >= mMaxQueueSize;
}
FileSink::FileSink(
std::filesystem::path file_path,
std::uintmax_t max_file_size,
std::size_t max_rotated_files)
: mFilePath(prepareFilePath(std::move(file_path))),
mMaxFileSize(max_file_size),
mMaxRotatedFiles(max_rotated_files) {
openFile();
}
FileSink::~FileSink() {
flush();
}
void FileSink::write(const LogRecord& record) {
const std::string line = formatRecord(record);
rotateIfNeeded(line.size() + 1);
mFile << line << '\n';
mCurrentFileSize += line.size() + 1;
if (shouldFlush(record.level)) {
mFile.flush();
}
}
void FileSink::flush() {
if (mFile.is_open()) {
mFile.flush();
}
}
const std::filesystem::path& FileSink::filePath() const noexcept {
return mFilePath;
}
void FileSink::openFile() {
mFile.open(mFilePath, std::ios::out | std::ios::app);
if (!mFile.is_open()) {
throw std::runtime_error("Failed to open log file: " + mFilePath.string());
}
mCurrentFileSize = 0;
if (std::filesystem::exists(mFilePath)) {
mCurrentFileSize = std::filesystem::file_size(mFilePath);
}
}
void FileSink::rotateIfNeeded(std::size_t next_write_size) {
if (mMaxFileSize == 0 || mMaxRotatedFiles == 0) {
return;
}
if (mCurrentFileSize + next_write_size <= mMaxFileSize) {
return;
}
rotateFiles();
}
void FileSink::rotateFiles() {
if (mFile.is_open()) {
mFile.flush();
mFile.close();
}
// 删除最老的滚动文件,为后续 rename 腾出位置。
const std::filesystem::path oldest_path = rotatedPath(mFilePath, mMaxRotatedFiles);
if (std::filesystem::exists(oldest_path)) {
std::filesystem::remove(oldest_path);
}
// 从后往前移动,避免 app.log.1 被 app.log 覆盖前丢失。
for (std::size_t index = mMaxRotatedFiles; index > 1; --index) {
const std::filesystem::path from = rotatedPath(mFilePath, index - 1);
const std::filesystem::path to = rotatedPath(mFilePath, index);
if (std::filesystem::exists(from)) {
std::filesystem::rename(from, to);
}
}
if (std::filesystem::exists(mFilePath)) {
std::filesystem::rename(mFilePath, rotatedPath(mFilePath, 1));
}
openFile();
}
std::string FileSink::formatRecord(const LogRecord& record) const {
const std::time_t now_time = std::chrono::system_clock::to_time_t(record.timestamp);
const std::tm local_time = makeLocalTime(now_time);
std::ostringstream output;
output << '[' << std::put_time(&local_time, "%Y-%m-%d %H:%M:%S") << ']'
<< '[' << levelToString(record.level) << ']'
<< "[tid=" << record.thread_id << ']'
<< '[' << record.location.file_name() << ':' << record.location.line() << ']'
<< ' ' << record.message;
return output.str();
}
std::string_view FileSink::levelToString(LogLevel level) noexcept {
switch (level) {
case LogLevel::Debug:
return "DEBUG";
case LogLevel::Info:
return "INFO";
case LogLevel::Warn:
return "WARN";
case LogLevel::Error:
return "ERROR";
}
return "UNKNOWN";
}
bool FileSink::shouldFlush(LogLevel level) noexcept {
return level == LogLevel::Error;
}
LogSystem& LogSystem::getInstance(std::filesystem::path log_file_path) {
static LogSystem instance(resolveLogConfig(std::move(log_file_path)));
return instance;
}
bool LogSystem::setDefaultConfig(LogConfig config) {
std::lock_guard lock(logConfigMutex());
if (logSystemCreated()) {
return false;
}
configuredLogConfig() = normalizeConfig(std::move(config));
return true;
}
bool LogSystem::setDefaultLogFilePath(std::filesystem::path log_file_path) {
std::lock_guard lock(logConfigMutex());
if (logSystemCreated()) {
return false;
}
configuredLogConfig().file_path = std::move(log_file_path);
return true;
}
LogSystem::LogSystem(LogConfig config)
: mLogQueue(config.max_queue_size),
mConfig(normalizeConfig(std::move(config))),
mMinLevel(static_cast<int>(mConfig.min_level)) {
createDefaultSink(mConfig);
// sink 创建完成后再启动后台线程,避免线程看到半初始化状态。
mWorkerThread = std::jthread([this](std::stop_token) {
writerLoop();
});
}
LogSystem::~LogSystem() {
shutdown();
}
bool LogSystem::log(
LogLevel level,
std::string_view message,
const std::source_location& location) noexcept {
if (!shouldLog(level)) {
return false;
}
{
std::lock_guard lock(mStateMutex);
if (mShutdown) {
return false;
}
}
try {
LogRecord record;
record.level = level;
record.timestamp = std::chrono::system_clock::now();
record.thread_id = std::this_thread::get_id();
record.location = location;
record.message = std::string(message);
return mLogQueue.push(std::move(record));
} catch (const std::exception& error) {
// 日志系统自身失败时不向业务代码抛异常,降级写入 stderr。
std::cerr << "[LOGGER][ERROR] " << error.what() << '\n';
return false;
} catch (...) {
std::cerr << "[LOGGER][ERROR] unknown logger failure\n";
return false;
}
}
void LogSystem::setMinLevel(LogLevel level) noexcept {
mMinLevel.store(static_cast<int>(level), std::memory_order_relaxed);
}
bool LogSystem::shouldLog(LogLevel level) const noexcept {
return static_cast<int>(level) >= mMinLevel.load(std::memory_order_relaxed);
}
void LogSystem::shutdown() noexcept {
{
std::lock_guard lock(mStateMutex);
if (mShutdown) {
return;
}
mShutdown = true;
}
// 先关闭队列,后台线程会继续消费队列中已有日志直到 pop() 返回 false。
mLogQueue.shutdown();
if (mWorkerThread.joinable()) {
mWorkerThread.join();
}
std::lock_guard lock(mStateMutex);
for (auto& sink : mSinks) {
sink->flush();
}
}
std::size_t LogSystem::pendingCount() const {
return mLogQueue.size();
}
std::uint64_t LogSystem::droppedCount() const noexcept {
return mLogQueue.droppedCount();
}
const std::filesystem::path& LogSystem::logFilePath() const noexcept {
return mConfig.file_path;
}
void LogSystem::writerLoop() {
LogRecord record;
while (mLogQueue.pop(record)) {
std::lock_guard lock(mStateMutex);
for (auto& sink : mSinks) {
sink->write(record);
}
}
}
void LogSystem::createDefaultSink(const LogConfig& config) {
mSinks.push_back(std::make_unique<FileSink>(
config.file_path,
config.max_file_size,
config.max_rotated_files));
}
LogBuffer::LogBuffer(LogLevel level, std::source_location location)
: mLevel(level),
mLocation(std::move(location)) {
}
LogBuffer::~LogBuffer() {
if (pbase() != pptr()) {
putOutput();
}
}
int LogBuffer::sync() {
putOutput();
return 0;
}
void LogBuffer::putOutput() noexcept {
const std::string message = str();
if (message.empty()) {
return;
}
// 清空缓冲区要在 log() 前后都安全;log() 本身保证不向外抛异常。
str("");
try {
LogSystem::getInstance().log(mLevel, message, mLocation);
} catch (...) {
// 日志流析构期间不能把异常继续抛给业务代码。
}
}
LogStream::LogStream(LogLevel level, const std::source_location& location)
: std::ostream(nullptr),
mBuffer(level, location) {
rdbuf(&mBuffer);
}
核心目的
这个日志系统已经从简单的异步文件写入,演化成更接近生产结构的 logger:
-
结构化日志记录:前台线程创建
LogRecord,记录等级、时间、线程 id、源码位置和正文。 -
有界异步队列:
LogQueue限制最大容量,队列满时丢弃新日志并统计 dropped count。 -
等级过滤:
LogSystem支持minLevel,低于最低等级的日志不会进入队列。 -
sink 架构:
LogSystem只负责调度,具体输出由LogSink子类完成。 -
文件 sink:当前实现
FileSink,负责格式化文本、写文件、ERROR 级别立即 flush、按文件大小滚动。
当前使用方式
默认写入 Log.txt:
LOG_INFO << "hello logger";
LOG_WARN << "something happened";
LOG_ERR << "fatal error";
程序启动阶段可以配置日志:
LogConfig config;
config.file_path = "logs/app.log";
config.min_level = LogLevel::Info;
config.max_queue_size = 8192;
config.max_file_size = 10 * 1024 * 1024;
config.max_rotated_files = 3;
LogSystem::setDefaultConfig(config);
路径也可以单独设置:
LogSystem::setDefaultLogFilePath("logs/app.log");
这些配置只在 第一次创建 LogSystem 前 生效。第一次 LOG_INFO 或 LogSystem::getInstance() 后,单例已经创建,再修改默认配置会返回 false。
架构概览
这张类图里有三条主线:
-
前台写入链路:
LOG_INFO创建LogStream,内部LogBuffer收集流式内容。 -
异步调度链路:
LogSystem把日志变成LogRecord,写入LogQueue。 -
输出链路:后台线程从队列取出
LogRecord,分发给LogSink,当前只有FileSink。
从宏到文件的数据流
这条链路的重点是:业务线程只走到队列入队。文件格式化、滚动和磁盘写入都在后台线程里完成。
结构化 LogRecord
LogRecord 是系统升级后的核心。它把原来的一整条字符串拆成可决策的字段:
-
level:用于等级过滤、ERROR flush、未来按等级输出到不同 sink。 -
timestamp:日志创建时间,避免后台线程延迟导致时间不准。 -
thread_id:记录生产日志的业务线程,便于排查并发问题。 -
location:由std::source_location捕获文件名和行号。 -
message:流式日志正文。
结构化记录的好处是:后续做 JSON、过滤、多个 sink、采样、按等级 flush,都不用从字符串里反解析。
等级过滤
LogSystem 用 mMinLevel 保存最低日志等级:
当前过滤发生在 LogSystem::log() 内。也就是说,LOG_DEBUG << ... 的流式拼接已经发生,但不会进入队列。后续如果要进一步减少 DEBUG 开销,可以把宏改成“等级关闭时不构造 LogStream”的短路宏。
有界队列和丢弃计数
LogQueue 支持 max_queue_size:
-
max_queue_size == 0:不限制队列长度。 -
max_queue_size > 0:队列达到容量后,新日志会被丢弃。 -
droppedCount():返回因为队列满或 shutdown 后写入失败而丢弃的日志数量。
有界队列是生产系统里很重要的保护:当磁盘很慢或者日志爆发时,logger 不应该无限吃内存。
多生产者单消费者模型
并发边界集中在 LogQueue:
-
生产者:多个业务线程同时
push(),由mMutex串行保护队列。 -
消费者:一个后台线程
pop(),没有日志时阻塞在mCondVar。 -
关闭语义:shutdown 后不接受新日志,但已入队日志会继续 drain。
Sink 输出架构
LogSink 是抽象输出端:
当前只实现了 FileSink,但 LogSystem 已经不再绑定具体输出方式。后续可以自然增加:
-
ConsoleSink:输出到 stdout/stderr。
-
JsonFileSink:写 JSON Lines。
-
NetworkSink:发送到日志服务。
-
MultiFileSink:不同等级写不同文件。
FileSink 文件滚动
FileSink 根据 max_file_size 和 max_rotated_files 执行滚动:
如果配置为:
config.file_path = "logs/app.log";
config.max_file_size = 10 * 1024 * 1024;
config.max_rotated_files = 3;
滚动后会保留:
-
logs/app.log -
logs/app.log.1 -
logs/app.log.2 -
logs/app.log.3
shutdown 流程
关闭流程采用 drain shutdown:
-
先设置
LogSystem::mShutdown:新日志不再接受。 -
关闭
LogQueue:唤醒后台线程。 -
后台线程继续消费旧日志:队列为空后退出。
-
join 后 flush sink:确保文件缓冲尽量落盘。
当前限制和下一步方向
-
等级过滤还没有宏级短路:低等级日志不会入队,但流式表达式仍会执行。后续可以设计
LOG_DEBUG在关闭时不构造LogStream。 -
队列满策略目前只有 DropNewest:生产上可以增加
Block、DropOldest、SyncFallback。 -
sink 注册接口暂未公开:内部已经是 sink 架构,后续可以公开
addSink(std::unique_ptr<LogSink>)。 -
文件滚动没有压缩和清理策略之外的保留规则:生产环境可以加按日期滚动、压缩历史日志。
-
格式化还固定为文本:后续可以抽象
LogFormatter,支持文本、JSON、key-value 等格式。

2605

被折叠的 条评论
为什么被折叠?



