C++日志 2——实现单线程日志系统

在上一篇《C++ 日志 1------ 日志系统基础设计》中,我们梳理了日志系统的核心需求(日志等级、输出格式、持久化)和基础架构。本篇将基于基础设计,从零实现一个轻量、可用的单线程 C++ 日志系统,兼顾实用性和可扩展性,代码可直接嵌入项目使用。

一、单线程日志系统核心定位

单线程日志系统是日志系统的基础版本,仅支持在单个线程中写入日志,无需考虑线程安全问题,实现简单、性能高效,适合小型项目、工具类程序、单线程服务等场景。

核心功能点:

  1. 支持分级日志(DEBUG/INFO/WARN/ERROR/FATAL);
  2. 自定义日志格式(时间戳、日志等级、文件名、行号、日志内容);
  3. 支持控制台输出 + 文件持久化输出;
  4. 自动创建日志文件,支持日志内容追加;
  5. 接口简洁易用,无第三方依赖。

二、整体架构设计

我们采用单例模式设计日志类(全局唯一日志实例,避免多次创建日志对象),核心模块拆分:

  1. 日志等级枚举:定义日志优先级,过滤低等级日志;
  2. 日志核心类:封装日志初始化、日志写入、格式拼接、文件 / 控制台输出;
  3. 易用宏封装:简化日志调用,自动捕获文件名和行号。

三、完整代码实现

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
  1. 源文件(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();
}
  1. 测试代码(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):

与控制台输出完全一致,实现日志持久化。

六、优缺点分析

优点

  1. 轻量无依赖:纯 C++ 标准库实现,无需引入第三方库;
  2. 易用性高:宏封装后一行代码即可打印日志;
  3. 功能完整:支持分级、格式化、控制台 + 文件输出;
  4. 性能高效:单线程无锁操作,无性能损耗。

缺点

  1. 无线程安全:多线程同时写入会导致日志乱序、文件损坏;
  2. 无日志切割:日志文件会持续增大,不适合长期运行的高并发项目;
  3. 缓冲区固定:单次日志内容超过 1024 字节会被截断。

七、优化方向(为多线程版本铺垫)

当前单线程版本已满足基础需求,后续可针对性优化:

  1. 线程安全:添加互斥锁,支持多线程写入;
  2. 日志切割:按文件大小 / 时间切割日志,避免单文件过大;
  3. 动态缓冲区:取消固定大小限制,支持任意长度日志;
  4. 配置化:通过配置文件设置日志路径、等级、输出方式;
  5. 异步写入:将日志写入放入队列,异步消费,提升性能。

总结

本篇实现的单线程日志系统,是 C++ 日志系统的基础核心,代码简洁、功能实用,完全满足单线程项目的日志需求。通过单例模式、日志分级、格式封装等设计,为后续多线程、异步日志系统打下了坚实基础。

相关推荐
zh_xuan1 小时前
api测试工具添加历史记录功能
c++·libcurl·duilib
布吉岛的石头2 小时前
微服务网关统一鉴权、限流、日志实战
java·spring·微服务
超级无敌葛大侠2 小时前
Redis主从复制
java·redis
休息一下接着来2 小时前
C++ 固定容量环形队列实现
c++·算法
愿^O^~3 小时前
JVM GC 入门 → 进阶
jvm
殷紫川3 小时前
90% Java 开发都踩过坑的 @Resource 与 @Autowired
java
kybs19913 小时前
springboot租车系统--附源码68701
java·hadoop·spring boot·python·django·asp.net·php
过期动态3 小时前
MySQL中的约束
android·java·数据库·spring boot·mysql
wxin_VXbishe3 小时前
springboot新能源车充电站管理系统小程序-计算机毕业设计源码29213
java·c++·spring boot·python·spring·django·php