C++工业级日志项目(四)日志落地

1:上一篇

在上一篇我们完成了日志消息的封装,可以提供给用户自定义设置日志的输出格式,现在我们继续在基础上封装出一个文件,负责将格式化后的日志数据输出到指定目的地(控制台、文件、网络等)。

模块整体设计思路

日志落地模块的核心设计目标是高扩展性低耦合,因此采用了以下经典设计模式与架构思路:

  1. 抽象基类(LogSink):定义日志输出的统一接口,屏蔽不同落地方向的实现差异;

  2. 派生类具体化:针对控制台、单个文件、按大小滚动文件等场景,实现基类的纯虚接口;

  3. 工厂模式(SinkFactory):解耦对象创建与使用,简化不同 Sink 实例的创建流程;

  4. 跨平台兼容:适配 Windows/Linux 的时间处理接口,保证代码可移植性。

    LogSink(抽象基类)
    ├── StdoutSink(控制台输出)
    ├── FileSink(单个文件输出)
    └── RollSizeSink(按大小滚动文件输出)
    SinkFactory(工厂类:创建各类Sink实例)

2:代码模块讲解

1:前置定义与跨平台兼容

cpp 复制代码
#pragma once
/*
日志落地模块的实现
    1抽象落地基类
    2派生子类(根据不同的落地方向派生)
    3使用工厂模式进行创建与表示的分离
*/
#include "util.hpp"  // 工具类(文件操作、时间处理)
#include <memory>    // 智能指针
#include <fstream>   // 文件流
#include <sstream>   // 字符串流
#include <cassert>   // 断言
#include <ctime>     // 时间处理
#include <iomanip>   // 格式化输出(setw/setfill)
#include <chrono>    // 时间戳

// 跨平台时间处理宏:解决localtime线程不安全问题
#ifdef  _WIN32
// Windows下使用localtime_s(安全版本)
#define LOCAL_TIME_SAFE(timestamp,tm_ptr) localtime_s(tm_ptr,&(timestamp))
#else
// Linux下使用localtime_r(安全版本)
#define LOCAL_TIME_SAFE(timestamp,tm_ptr) localtime_r(&(timestamp),tm_ptr)
#endif //  _WIN32

关键说明

  • util.hpp 是自定义工具类,包含文件目录创建、时间戳获取等通用功能(下文会用到);
  • LOCAL_TIME_SAFE 宏解决了localtime的线程安全问题:Windows 使用localtime_s,Linux 使用localtime_r,保证跨平台兼容性。

2:抽象基类LogSink

cpp 复制代码
    /**
     * @brief 日志落地基类,定义日志输出的统一接口
     * 设计思路:采用纯虚函数定义接口,所有派生类必须实现log方法,保证接口一致性
     */
    class LogSink {
    public:
        // 虚析构函数:确保派生类析构时正确调用基类析构
        virtual ~LogSink() = default;

        /**
         * @brief 纯虚函数,用于将日志数据写入具体的目的地
         * @param data 指向待写入日志数据的指针(格式化后的日志字符串)
         * @param len 待写入日志数据的长度(字节数)
         */
        virtual void log(const char* data, size_t len) = 0;
        
        // 类型别名:简化shared_ptr的使用,提高代码可读性
        using ptr = std::shared_ptr<LogSink>;
    };
  • 纯虚函数log强制派生类实现具体的输出逻辑,保证 "开闭原则"(新增落地方向只需新增派生类,无需修改基类);
  • std::shared_ptr 类型别名ptr:简化智能指针声明,避免重复书写std::shared_ptr<LogSink>
  • 虚析构函数:防止基类指针指向派生类对象时,析构不完整导致内存泄漏。

3:控制台输出Sink:StdoutSink

cpp 复制代码
    /**
     * @brief 标准输出日志落地类,将日志打印到控制台
     * 适用场景:开发调试阶段,快速查看日志
     */
    class StdoutSink :public LogSink {
    public:
        /**
         * @brief 将日志数据写入标准输出流
         * @param data 指向待写入日志数据的指针
         * @param len 待写入日志数据的长度
         */
        void log(const char* data, size_t len) override
        {
            // std::cout.write:直接写入字节流,比operator<<更高效(避免字符串拷贝)
            std::cout.write(data, len);
        }
    };
  • 使用std::cout.write而非std::cout << datawrite直接操作字节流,无需解析字符串格式,性能更高;
  • 极简实现:控制台输出无需额外资源管理(如文件句柄),逻辑轻量。

4:单个文件输出Sink:FileSink

cpp 复制代码
    /**
     * @brief 文件日志落地类,将日志写入指定的单个文件
     * 适用场景:小流量日志场景,无需滚动切割
     */
    class FileSink :public LogSink {
    public:
        /**
         * @brief 构造函数,初始化文件路径并打开文件句柄
         * @param pathname 日志文件的完整路径名(如./logs/app.log)
         */
        FileSink(const std::string& pathname) :_pathname(pathname)
        {
            // 1. 自动创建日志文件所在目录(若不存在)
            util::File::createdirectory(util::File::Path(pathname));
            // 2. 以二进制+追加模式打开文件:
            // - 二进制模式:避免Windows下换行符(\n)被转换为\r\n,保证日志字节数准确
            // - 追加模式:每次写入都追加到文件末尾,避免覆盖已有日志
            _ofs.open(pathname,std::ios::binary | std::ios::app);
            // 断言:文件打开失败时直接终止程序(生产环境可替换为异常处理)
            assert(_ofs.is_open());
        }

        /**
         * @brief 将日志消息追加写入指定文件
         * @param data 指向待写入日志数据的指针
         * @param len 待写入日志数据的长度
         */
        void log(const char* data, size_t len) override
        {
            _ofs.write(data, len);
            // 断言:写入失败时终止程序(生产环境可增加重试逻辑)
            assert(_ofs.good());
        }
    private:
        std::string _pathname;  // 日志文件完整路径
        std::ofstream _ofs;     // 文件输出流对象
    };
  • 文件打开模式:std::ios::binary | std::ios::app
    • binary:避免跨平台换行符转换,保证日志内容的一致性;
    • app:追加模式,即使多进程写入(非并发)也不会覆盖已有日志;
  • 目录自动创建:通过util::File::createdirectory自动创建日志目录(如./logs),无需手动提前创建;
  • 断言使用:assert(_ofs.is_open())/assert(_ofs.good()) 快速发现文件操作异常,适合调试阶段(生产环境可替换为日志告警 + 异常捕获)。

5:按大小滚动文件Sink:RollSizeSink

cpp 复制代码
    /**
     * @brief 滚动文件日志落地类,根据文件大小自动切换日志文件
     * 适用场景:高流量日志场景,避免单个日志文件过大(如超过1GB),便于日志归档/检索
     */
    class RollSizeSink : public LogSink {
    public:
        /**
         * @brief 构造函数,初始化滚动文件策略
         * @param basename 日志文件的基础名称前缀(如./logs/app)
         * @param max_size 单个日志文件的最大允许大小(字节),超过此大小将创建新文件
         */
        RollSizeSink(const std::string& basename, size_t max_size) :
            _basename(basename),
            _max_size(max_size),
            _cur_size(0),
            _name_count(0)
        {
            // 初始化时创建第一个日志文件
            std::string pathname = createNewFile();
            // 创建日志目录
            util::File::createdirectory(util::File::Path(pathname));
            // 打开第一个日志文件
            _ofs.open(pathname, std::ios::binary | std::ios::app);
            assert(_ofs.is_open());
        }

        /**
         * @brief 将日志数据写入当前文件,若超出大小限制则滚动创建新文件
         * @param data 指向待写入日志数据的指针
         * @param len 待写入日志数据的长度
         */
        void log(const char* data, size_t len)override
        {
            // 检查当前文件大小是否超过阈值,若超过则滚动创建新文件
            if (_cur_size >= _max_size)
            {
                std::string pathname = createNewFile();
                _ofs.close();// 关闭旧文件句柄
                _ofs.open(pathname, std::ios::binary | std::ios::app);
                assert(_ofs.is_open());
                _cur_size = 0; // 重置当前文件大小
            }
            // 写入日志数据
            _ofs.write(data, len);
            assert(_ofs.good());
            // 更新当前文件已写入大小
            _cur_size += len;
        }
    private:
        /**
         * @brief 生成新的滚动日志文件名,包含时间戳和序列号
         * @return 新生成的日志文件完整路径字符串
         */
        std::string createNewFile()
        {
            // 获取当前系统时间戳
            time_t t = util::Date::GetTime();
            struct tm lt{};
            // 线程安全的本地时间转换
            LOCAL_TIME_SAFE(t, &lt);
            std::stringstream filename;
            // 拼接文件名:基础名 + 时间戳 + 序列号 + .log
            // 示例:./logs/app2024.05.20-15-30-20-0.log
            filename << _basename;
            // setw/setfill:格式化时间字段(补0,如5月→05,9秒→09)
            filename << std::setw(4) << std::setfill('0') << lt.tm_year + 1900 << "."; // 年(tm_year是从1900开始的偏移量)
            filename << std::setw(2) << std::setfill('0') << lt.tm_mon + 1 << ".";     // 月(tm_mon从0开始)
            filename << std::setw(2) << std::setfill('0') << lt.tm_mday << "-";        // 日
            filename << std::setw(2) << std::setfill('0') << lt.tm_hour << "-";        // 时
            filename << std::setw(2) << std::setfill('0') << lt.tm_min << "-";         // 分
            filename << std::setw(2) << std::setfill('0') << lt.tm_sec << "-";         // 秒
            filename << _name_count++; // 序列号(避免同一秒创建多个文件)
            filename << ".log";        // 后缀
            return filename.str();
        }
    private:
        size_t _name_count;    // 日志文件序列号(同一时间戳下的文件计数)
        std::string _basename; // 日志文件基础前缀(如./logs/app)
        std::ofstream _ofs;    // 当前打开的文件输出流
        size_t _max_size;      // 单个文件最大大小(字节)
        size_t _cur_size;      // 当前文件已写入大小(字节)
    };
  • 滚动触发逻辑 :每次写入日志前检查_cur_size是否超过_max_size,若超过则关闭旧文件、创建新文件;
  • 文件名生成规则
    • 时间戳格式:年.月.日-时-分-秒,保证日志文件按时间有序;
    • 序列号:避免同一秒内文件大小超限,生成多个文件时通过序列号区分;
  • 格式化工具std::setw(设置字段宽度)+std::setfill('0')(补 0)保证时间字段格式统一(如2024.05.01而非2024.5.1);
  • 状态管理_cur_size实时记录当前文件写入大小,_name_count自动递增保证文件名唯一。

6:工厂模式:SinkFactory

cpp 复制代码
    /**
     * @brief 日志落地对象工厂类,用于创建不同类型的LogSink实例
     * 设计思路:解耦对象创建与使用,用户无需直接new,只需通过工厂创建,降低耦合
     */
    class SinkFactory {
    public:
        /**
         * @brief 模板静态方法,创建指定类型的LogSink共享指针
         * @tparam SinkType 要创建的LogSink派生类类型(如StdoutSink、FileSink)
         * @tparam Args 构造函数参数类型包(支持任意参数个数/类型)
         * @param args 传递给SinkType构造函数的参数包
         * @return 指向新创建的LogSink对象的std::shared_ptr
         */
        template<typename SinkType, typename ...Args>
        static LogSink::ptr create(Args &&...args)
        {
            // 完美转发:保证参数的左值/右值属性不变,提高效率
            return std::make_shared<SinkType>(std::forward<Args>(args)...);
        }
    };

简化对象创建 :用户无需关注std::make_shared的细节,只需调用SinkFactory::create<类型>(参数)

cpp 复制代码
// 创建控制台Sink
auto stdout_sink = SinkFactory::create<StdoutSink>();
// 创建单个文件Sink
auto file_sink = SinkFactory::create<FileSink>("./logs/app.log");
// 创建按大小滚动Sink(最大1GB)
auto roll_sink = SinkFactory::create<RollSizeSink>("./logs/app", 1024*1024*1024);
  • 类型安全:模板参数强制指定 Sink 类型,编译期检查类型正确性;
  • 完美转发std::forward<Args>(args) 保证构造函数参数的传递效率,避免不必要的拷贝。

3:使用样例

cpp 复制代码
#include "sink.hpp"
#include <string>

int main() {
    // 1. 创建控制台Sink
    my_log::LogSink::ptr stdout_sink = my_log::SinkFactory::create<my_log::StdoutSink>();
    std::string log1 = "【DEBUG】这是控制台日志\n";
    stdout_sink->log(log1.c_str(), log1.size());

    // 2. 创建单个文件Sink
    my_log::LogSink::ptr file_sink = my_log::SinkFactory::create<my_log::FileSink>("./logs/app.log");
    std::string log2 = "【INFO】这是文件日志\n";
    file_sink->log(log2.c_str(), log2.size());

    // 3. 创建按大小滚动Sink(最大100字节)
    my_log::LogSink::ptr roll_sink = my_log::SinkFactory::create<my_log::RollSizeSink>("./logs/roll", 100);
    for (int i = 0; i < 5; ++i) {
        std::string log3 = "【WARN】这是滚动文件日志,第" + std::to_string(i) + "条\n";
        roll_sink->log(log3.c_str(), log3.size());
    }

    return 0;
}

4:总结

  • 高扩展性 :新增落地方向(如网络 Sink、数据库 Sink)只需继承LogSink并实现log方法;
  • 高性能:字节流写入、二进制模式、无冗余拷贝,适合高并发日志场景;
  • 跨平台:时间处理、文件操作兼容 Windows/Linux;
  • 易用性:工厂模式简化对象创建,智能指针自动管理资源。

日志落地模块是日志库的核心组件,本文实现的Sink模块通过抽象基类 + 派生类 + 工厂模式,兼顾了扩展性、性能与易用性。从控制台调试到高流量文件滚动,覆盖了大部分常见的日志落地场景,同时预留了充足的扩展空间,可根据业务需求快速适配新的落地方向。

在实际项目中,可结合日志格式化模块、日志级别过滤模块,构建一套完整的高性能 C++ 日志库,满足从开发调试到生产部署的全场景需求。

相关推荐
Dymc1 小时前
【Ubuntu系统指令启动】一招解决:Ubuntu 20.04 桌面双击 .desktop 文件不再“用文本编辑器打开”
linux·运维·ubuntu·一键运行
哼?~1 小时前
C++11 并发支持库中 atomic
c++
小熊Coding1 小时前
Python二手图书市场行为分析系统
开发语言·爬虫·python·django·计算机毕业设计·数据可视化分析·二手图书分析系统
玖釉-1 小时前
单词搜索:二维网格中的 DFS 回溯与剪枝优化
c++·windows·算法·深度优先·剪枝
吴可可1231 小时前
C++与C#版Teigha样条离散化差异解析
c++·算法·c#
搬砖的小码农_Sky1 小时前
macOS Sequoia上如何安装gcc/g++环境?
c语言·c++·macos
诸葛务农1 小时前
如何用windows自带的录音机录制(内录)电脑播放的音乐
windows·电脑
MC皮蛋侠客1 小时前
C++17 多线程系列(二):共享数据与同步——mutex 与 condition_variable
开发语言·c++·多线程
愈努力俞幸运1 小时前
python 三引号
android·开发语言·python