C++日志 2——实现单线程日志系统
在上一篇《C++ 日志 1—— 日志系统基础设计》中,我们梳理了日志系统的核心需求(日志等级、输出格式、持久化)和基础架构。本篇将基于基础设计,从零实现一个轻量、可用的单线程 C++ 日志系统,兼顾实用性和可扩展性,代码可直接嵌入项目使用。
一、单线程日志系统核心定位
单线程日志系统是日志系统的基础版本,仅支持在单个线程中写入日志,无需考虑线程安全问题,实现简单、性能高效,适合小型项目、工具类程序、单线程服务等场景。
核心功能点:
- 支持分级日志(DEBUG/INFO/WARN/ERROR/FATAL);
- 自定义日志格式(时间戳、日志等级、文件名、行号、日志内容);
- 支持控制台输出 + 文件持久化输出;
- 自动创建日志文件,支持日志内容追加;
- 接口简洁易用,无第三方依赖。
二、整体架构设计
我们采用单例模式设计日志类(全局唯一日志实例,避免多次创建日志对象),核心模块拆分:
- 日志等级枚举:定义日志优先级,过滤低等级日志;
- 日志核心类:封装日志初始化、日志写入、格式拼接、文件 / 控制台输出;
- 易用宏封装:简化日志调用,自动捕获文件名和行号。
三、完整代码实现
1. 头文件(Logger.h)
#ifndef LOGGER_H #define LOGGER_H #include <iostream> #include <fstream> #include <string> #include <ctime> #include <cstdarg> // 可变参数支持 // 日志等级枚举 enum LogLevel { DEBUG, // 调试信息 INFO, // 普通信息 WARN, // 警告信息 ERROR, // 错误信息 FATAL // 致命错误 }; // 单线程日志类(单例模式) class Logger { public: // 获取单例实例 static Logger& getInstance(); // 初始化日志:设置日志文件路径、最低输出等级 void init(const std::string& logFile, LogLevel level = INFO); // 核心日志写入函数 void log(LogLevel level, const char* file, int line, const char* format, ...); // 关闭日志(关闭文件句柄) void close(); private: // 私有构造/析构(单例禁止外部创建) Logger(); ~Logger(); // 禁止拷贝和赋值 Logger(const Logger&) = delete; Logger& operator=(const Logger&) = delete; // 工具函数:获取当前时间字符串 std::string getCurrentTime(); // 工具函数:日志等级转字符串 std::string levelToString(LogLevel level); private: std::ofstream m_logFile; // 日志文件流 LogLevel m_level; // 最低输出日志等级 bool m_isInit; // 初始化标记 }; // 易用宏封装:自动传入文件名和行号 #define LOG_DEBUG(format, ...) Logger::getInstance().log(DEBUG, __FILE__, __LINE__, format, ##__VA_ARGS__) #define LOG_INFO(format, ...) Logger::getInstance().log(INFO, __FILE__, __LINE__, format, ##__VA_ARGS__) #define LOG_WARN(format, ...) Logger::getInstance().log(WARN, __FILE__, __LINE__, format, ##__VA_ARGS__) #define LOG_ERROR(format, ...) Logger::getInstance().log(ERROR, __FILE__, __LINE__, format, ##__VA_ARGS__) #define LOG_FATAL(format, ...) Logger::getInstance().log(FATAL, __FILE__, __LINE__, format, ##__VA_ARGS__) #endif // LOGGER_H2. 源文件(Logger.cpp)
#include "Logger.h" // 私有构造函数 Logger::Logger() : m_level(INFO), m_isInit(false) {} // 析构函数:自动关闭文件 Logger::~Logger() { close(); } // 获取单例实例(静态局部变量,线程安全在单线程下无问题) Logger& Logger::getInstance() { static Logger instance; return instance; } // 初始化日志 void Logger::init(const std::string& logFile, LogLevel level) { if (m_isInit) return; // 打开日志文件:追加模式 + 清空缓冲区 m_logFile.open(logFile, std::ios::app | std::ios::out); if (m_logFile.is_open()) { m_level = level; m_isInit = true; LOG_INFO("日志系统初始化成功,日志文件:%s", logFile.c_str()); } else { std::cerr << "日志文件打开失败:" << logFile << std::endl; } } // 关闭日志文件 void Logger::close() { if (m_logFile.is_open()) { m_logFile.close(); m_isInit = false; } } // 获取当前时间字符串(格式:YYYY-MM-DD HH:MM:SS) std::string Logger::getCurrentTime() { time_t now = time(nullptr); char buf[64] = {0}; strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", localtime(&now)); return std::string(buf); } // 日志等级转字符串 std::string Logger::levelToString(LogLevel level) { switch (level) { case DEBUG: return "DEBUG"; case INFO: return "INFO"; case WARN: return "WARN"; case ERROR: return "ERROR"; case FATAL: return "FATAL"; default: return "UNKNOWN"; } } // 核心日志写入函数 void Logger::log(LogLevel level, const char* file, int line, const char* format, ...) { // 1. 过滤低等级日志 if (!m_isInit || level < m_level) return; // 2. 拼接日志前缀:时间 [等级] (文件名:行号) std::string prefix = "[" + getCurrentTime() + "] [" + levelToString(level) + "] (" + std::string(file) + ":" + std::to_string(line) + ") "; // 3. 处理可变参数(格式化日志内容) char content[1024] = {0}; va_list args; va_start(args, format); vsnprintf(content, sizeof(content), format, args); va_end(args); // 4. 拼接完整日志 std::string logMsg = prefix + content + "\n"; // 5. 同时输出到控制台和文件 std::cout << logMsg; m_logFile << logMsg; // 立即刷新缓冲区(防止程序崩溃丢失日志) m_logFile.flush(); }3. 测试代码(main.cpp)
#include "Logger.h" int main() { // 1. 初始化日志:输出到app.log,最低等级为DEBUG Logger::getInstance().init("app.log", DEBUG); // 2. 使用宏打印不同等级日志 LOG_DEBUG("这是一条调试日志,参数:%d,%s", 123, "测试"); LOG_INFO("服务启动成功,端口:%d", 8080); LOG_WARN("磁盘空间不足,剩余:%d%%", 10); LOG_ERROR("数据库连接失败,错误码:%d", 500); LOG_FATAL("程序崩溃,即将退出"); return 0; }四、代码核心解析
1. 单例模式实现
通过静态局部变量实现单例,保证全局只有一个日志实例,避免多次打开日志文件、重复初始化等问题:
Logger& Logger::getInstance() { static Logger instance; return instance; }单线程下无需加锁,性能最优。
2. 日志等级过滤
初始化时设置最低日志等级,低于该等级的日志会直接被过滤,灵活控制日志输出量:
cpp
// 过滤低等级日志 if (!m_isInit || level < m_level) return;3. 日志格式拼接
固定输出格式:[时间戳] [日志等级] (文件名:行号) 日志内容,包含调试必备信息,方便快速定位问题。
4. 可变参数支持
使用cstdarg库实现可变参数,支持printf风格的格式化输出,使用成本极低:
va_list args; va_start(args, format); vsnprintf(content, sizeof(content), format, args); va_end(args);5. 宏封装简化调用
通过宏自动捕获__FILE__(当前文件名)和__LINE__(当前行号),调用时无需手动传入:
#define LOG_DEBUG(format, ...) Logger::getInstance().log(DEBUG, __FILE__, __LINE__, format, ##__VA_ARGS__)五、编译与运行
1. 编译命令
使用 g++ 编译,直接链接三个文件:
g++ main.cpp Logger.cpp -o logger_demo2. 运行结果
控制台输出:
[2025-12-29 15:30:00] [DEBUG] (main.cpp:9) 这是一条调试日志,参数:123,测试 [2025-12-29 15:30:00] [INFO] (main.cpp:10) 服务启动成功,端口:8080 [2025-12-29 15:30:00] [WARN] (main.cpp:11) 磁盘空间不足,剩余:10% [2025-12-29 15:30:00] [ERROR] (main.cpp:12) 数据库连接失败,错误码:500 [2025-12-29 15:30:00] [FATAL] (main.cpp:13) 程序崩溃,即将退出日志文件(app.log):
与控制台输出完全一致,实现日志持久化。
六、优缺点分析
优点
- 轻量无依赖:纯 C++ 标准库实现,无需引入第三方库;
- 易用性高:宏封装后一行代码即可打印日志;
- 功能完整:支持分级、格式化、控制台 + 文件输出;
- 性能高效:单线程无锁操作,无性能损耗。
缺点
- 无线程安全:多线程同时写入会导致日志乱序、文件损坏;
- 无日志切割:日志文件会持续增大,不适合长期运行的高并发项目;
- 缓冲区固定:单次日志内容超过 1024 字节会被截断。
七、优化方向(为多线程版本铺垫)
当前单线程版本已满足基础需求,后续可针对性优化:
- 线程安全:添加互斥锁,支持多线程写入;
- 日志切割:按文件大小 / 时间切割日志,避免单文件过大;
- 动态缓冲区:取消固定大小限制,支持任意长度日志;
- 配置化:通过配置文件设置日志路径、等级、输出方式;
- 异步写入:将日志写入放入队列,异步消费,提升性能。
总结
本篇实现的单线程日志系统,是 C++ 日志系统的基础核心,代码简洁、功能实用,完全满足单线程项目的日志需求。通过单例模式、日志分级、格式封装等设计,为后续多线程、异步日志系统打下了坚实基础。
