在上一篇《C++ 日志 1------ 日志系统基础设计》中,我们梳理了日志系统的核心需求(日志等级、输出格式、持久化)和基础架构。本篇将基于基础设计,从零实现一个轻量、可用的单线程 C++ 日志系统,兼顾实用性和可扩展性,代码可直接嵌入项目使用。
一、单线程日志系统核心定位
单线程日志系统是日志系统的基础版本,仅支持在单个线程中写入日志,无需考虑线程安全问题,实现简单、性能高效,适合小型项目、工具类程序、单线程服务等场景。
核心功能点:
- 支持分级日志(DEBUG/INFO/WARN/ERROR/FATAL);
- 自定义日志格式(时间戳、日志等级、文件名、行号、日志内容);
- 支持控制台输出 + 文件持久化输出;
- 自动创建日志文件,支持日志内容追加;
- 接口简洁易用,无第三方依赖。
二、整体架构设计
我们采用单例模式设计日志类(全局唯一日志实例,避免多次创建日志对象),核心模块拆分:
- 日志等级枚举:定义日志优先级,过滤低等级日志;
- 日志核心类:封装日志初始化、日志写入、格式拼接、文件 / 控制台输出;
- 易用宏封装:简化日志调用,自动捕获文件名和行号。
三、完整代码实现
1. 头文件(Logger.h)
cpp
#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_H
- 源文件(Logger.cpp)
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();
}
- 测试代码(main.cpp)
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. 单例模式实现
通过静态局部变量实现单例,保证全局只有一个日志实例,避免多次打开日志文件、重复初始化等问题:
cpp
Logger& Logger::getInstance() {
static Logger instance;
return instance;
}
单线程下无需加锁,性能最优。
2. 日志等级过滤
初始化时设置最低日志等级,低于该等级的日志会直接被过滤,灵活控制日志输出量:
cpp
cpp
// 过滤低等级日志
if (!m_isInit || level < m_level) return;
3. 日志格式拼接
固定输出格式:[时间戳] [日志等级] (文件名:行号) 日志内容,包含调试必备信息,方便快速定位问题。
4. 可变参数支持
使用cstdarg库实现可变参数,支持printf风格的格式化输出,使用成本极低:
cpp
va_list args;
va_start(args, format);
vsnprintf(content, sizeof(content), format, args);
va_end(args);
5. 宏封装简化调用
通过宏自动捕获__FILE__(当前文件名)和__LINE__(当前行号),调用时无需手动传入:
cpp
#define LOG_DEBUG(format, ...) Logger::getInstance().log(DEBUG, __FILE__, __LINE__, format, ##__VA_ARGS__)
五、编译与运行
1. 编译命令
使用 g++ 编译,直接链接三个文件:
cpp
g++ main.cpp Logger.cpp -o logger_demo
2. 运行结果
控制台输出:
cpp
[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++ 日志系统的基础核心,代码简洁、功能实用,完全满足单线程项目的日志需求。通过单例模式、日志分级、格式封装等设计,为后续多线程、异步日志系统打下了坚实基础。