C++项目 —— 基于多设计模式下的同步&异步日志系统(2)(工厂模式)

发布于:2025-04-17 ⋅ 阅读:(39) ⋅ 点赞:(0)

我们在之前把日志消息的主体已经组织好了,而且我们创建的适当的类控制日志格式,并且按照我们的格式组织日志消息。如果还没有看过的小伙伴可以点击这里:

https://blog.csdn.net/qq_67693066/article/details/147162921?sharetype=blogdetail&sharerId=147162921&sharerefer=PC&sharesource=qq_67693066&spm=1011.2480.3001.8118

我们这次的任务是要完成日志器中的一个小功能实现:日志输出的方向,我们日志输出的方向有:控制台固定文件滚动文件。这个模块的实现还会涉及到工厂模式的使用

基类实现

首先设计思想还是比较清晰的,设计一个基类,派生出三个不同的方向,我们先实现两个方向比较简单的:

namespace logs
{
    //基类实现
    class BaseSink
    {
    public:
        using ptr = std::shared_ptr<BaseSink>; 
        BaseSink()
        {

        }

        virtual ~BaseSink() {}
        virtual void log(const char *data, size_t len) = 0; //要继承实现的接口
    };

    class StdoutSink : public BaseSink
    {
    public:
        //将日志消息写到标准输出
        void log(const char *data, size_t len)
        {
            std::cout.write(data, len);
        }
    };

    class FixFileSink : public BaseSink 
    {
    public:
        FixFileSink(const std::string& pathname)
            :_pathname(pathname)
        {
            //1.创建文件所在路径
            logs::utils::File::createDiretory(logs::utils::File::path(_pathname));

            //2.创建文件并打开
            _ofs.open(_pathname,std::ios::binary | std::ios::app);
            assert(_ofs.is_open());
        }
        //将日志消息写到固定文件中
        void log(const char *data, size_t len)
        {
            _ofs.write(data,len);
            assert(_ofs.good());
        }

    private:
        std::string _pathname; //创建文件时的文件路径
        std::ofstream _ofs; //流式文件操作
    };
}

我们也可以顺便测试一下:

#include"utils.hpp"
#include"level.hpp"
#include"message.hpp"
#include"fometter.hpp"
#include "sink.hpp"

int main()
{
    // std::cout << logs::utils::File::path("./abc/def");

    // logs::utils::File::createDiretory("./abc/def");

    //std::cout <<logs::Loglevel::toString(logs::Loglevel::value::DEBUG);

    logs::logMsg msg(logs::Loglevel::value::DEBUG,"main.cc",53,"root","格式化功能测试....");

    logs::Formetter fmt("abc[%d{%H:%M:%S}][%c]%T%m%n");

    std::string str = fmt.format(msg);

    //标准输入测试
    size_t len = str.size();
    logs:: StdoutSink st;
    st.log(str.c_str(),len);

    //文件测试
    logs::FixFileSink fx("../test/test.txt");
    fx.log(str.c_str(),len);
}

滚动文件

滚动文件意思是,如果这个文件日志已经被写满了,会自动换到一个新的文件写日志,写满了的话又继续换。

文件名问题

既然会不停的创建文件,我们就不得不考虑一个问题,文件名。文件名是不能重复的,那么有没有一种方式,能保证我们的文件名是绝对不会重复的呢?有的,时间戳,时间只要在流逝,时间戳就会变。我们可以让时间戳作为我们文件名的一部分,这样就可以保证文件绝对不会重复了。

    class RollFileSink : public BaseSink
    {
    public:
    private:
        std::string createNewFile()
        {
            //1.获取当前时间戳
            time_t time = logs::utils::Date::get_time();
            struct tm lt;
            localtime_r(&time,&lt);

            std::stringstream filename;

            filename << _basename;
            filename << lt.tm_year + 1900;
            filename << lt.tm_mon + 1;
            filename << lt.tm_mday;
            filename << lt.tm_hour;
            filename << lt.tm_min;
            filename << lt.tm_sec;
            filename << "-";
            filename << _name_count++;
            filename << ".log";

            return filename.str();
        }
        // 基础文件名 + 扩展文件名(以时间生成)组成一个实际的当前输出文件名
        size_t _name_count;
        std::string _basename; //基础文件名
        std::ofstream _ofs; //流式文件
        size_t _max_size; //最大文件大小
        size_t _cur_size: //当前文件大小
    };
   class RollFileSink : public BaseSink
    {
    public:
        RollFileSink(const std::string& basename,size_t max_size)
            :_basename(basename)
            ,_name_count(0)
            ,_max_size(max_size)
            ,_cur_size(0)
        {
            std::string pathname = createNewFile();

            //1、创建文件的所在路径
            logs::utils::File::createDiretory(logs::utils::File::path(pathname));

            // 2.创建并打开日志文件
            _ofs.open(pathname, std::ios::binary | std::ios::app);
            assert(_ofs.is_open());
        }

         // 将日志消息写到固定文件中
         void log(const char *data, size_t len)
         {
            if(_cur_size > _max_size)
            {
                _ofs.close();
                std::string pathname = createNewFile();
                //1、创建文件的所在路径
                logs::utils::File::createDiretory(logs::utils::File::path(pathname));
                // 2.创建并打开日志文件
                _ofs.open(pathname, std::ios::binary | std::ios::app);
                assert(_ofs.is_open());
            }

            _ofs.write(data, len);
            assert(_ofs.good());
            _cur_size += len;
         }
    private:
        std::string createNewFile()
        {
            //1.获取当前时间戳
            time_t time = logs::utils::Date::get_time();
            struct tm lt;
            localtime_r(&time,&lt);

            std::stringstream filename;

            filename << _basename;
            filename << lt.tm_year + 1900;
            filename << lt.tm_mon + 1;
            filename << lt.tm_mday;
            filename << lt.tm_hour;
            filename << lt.tm_min;
            filename << lt.tm_sec;
            filename << "-";
            filename << _name_count++;
            filename << ".log";

            return filename.str();
        }
        // 基础文件名 + 扩展文件名(以时间生成)组成一个实际的当前输出文件名
        size_t _name_count;
        std::string _basename; //基础文件名
        std::ofstream _ofs; //流式文件
        size_t _max_size; //最大文件大小
        size_t _cur_size; //当前文件大小
    };

我们可以测试一下:

    logs::RollFileSink roll("../test/mytest",1024);
    size_t cur = 0;
    while(cur < 1024 * 2)
    {
        roll.log(str.c_str(),len);
        cur+=len;
    }

在这里插入图片描述

工厂模式

代码定义了一个名为 SinkFactory 的工厂类,用于创建日志输出器(Sink)的智能指针实例。它使用了现代 C++ 的模板和可变参数特性,是一个非常灵活通用的工厂实现:

    class SinkFactory
    {
    public:
        template<typename SinkType,typename... Args>
        static BaseSink::ptr create(Args && ...args)
        {
            return std::make_shared<SinkType>(std::forward<Args>(args)...);
        }
    };
    auto st1 = logs::SinkFactory::create<logs::StdoutSink>();
    st1->log(str.c_str(),len);

这样我们不用用户直接接触接口,而是通过工厂,这样更具灵活性。

一些扩展点

struct tm 是 C/C++ 标准库中用于表示日历时间的结构体,定义在 <ctime> 头文件中。以下是其成员变量及详细说明:


struct tm 成员列表

成员 类型 说明 取值范围 注意事项
tm_sec int 0-61 (通常 0-59) 允许闰秒
tm_min int 分钟 0-59
tm_hour int 小时(24小时制) 0-23
tm_mday int 月中的第几天(Day of month) 1-31
tm_mon int 月份(从0开始) 0-11 (0=1月) 使用时需 +1
tm_year int 年份(从1900开始) 0+=1900年 使用时需 +1900
tm_wday int 星期几(从0开始,0=周日) 0-6 (0=周日)
tm_yday int 年中的第几天(从0开始) 0-365
tm_isdst int 夏令时标志:
正数=启用
0=禁用
负数=信息不可用
-1, 0, 1

关键注意事项

  1. 特殊计数规则

    • 月份tm_mon 从 0 开始(0=1月,11=12月),显示时需要 +1
    • 年份tm_year 是 1900 年起的偏移量,真实年份 = tm_year + 1900
    • 星期tm_wday 中 0 表示周日
  2. 夏令时处理

    if (timeinfo.tm_isdst > 0) {
        std::cout << "夏令时生效";
    }
    
  3. 有效范围扩展

    • tm_sec 允许 60-61 以兼容闰秒
    • 其他字段超出范围时,mktime() 会自动标准化

使用示例

1. 获取当前时间并打印
#include <ctime>
#include <iostream>

int main() {
    time_t now = time(nullptr);
    struct tm timeinfo;
    localtime_r(&now, &timeinfo);  // 线程安全版本

    std::cout << "当前时间: " 
              << 1900 + timeinfo.tm_year << "-" 
              << 1 + timeinfo.tm_mon << "-"
              << timeinfo.tm_mday << " "
              << timeinfo.tm_hour << ":"
              << timeinfo.tm_min << ":"
              << timeinfo.tm_sec;
}
2. 与 strftime 配合使用
char buf[64];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &timeinfo);
std::cout << "格式化时间: " << buf;
3. 构造自定义时间
struct tm custom_time = {0};
custom_time.tm_year = 2023 - 1900;  // 2023年
custom_time.tm_mon = 6 - 1;         // 6月
custom_time.tm_mday = 15;           // 15日
time_t t = mktime(&custom_time);    // 转换为time_t

常见问题

  1. 为什么年份从1900开始?
    历史原因,早期系统用2位数存储年份,1900为基准年。

  2. 如何获取时区信息?
    通过 tm_gmtofftm_zone(非标准扩展,需检查平台支持):

    #ifdef __linux__
    std::cout << "UTC偏移: " << timeinfo.tm_gmtoff / 3600 << "小时";
    #endif
    
  3. 线程安全注意

    • 优先使用 localtime_r()(POSIX)或 localtime_s()(Windows)
    • 避免使用非线程安全的 localtime()

可视化记忆表

struct tm {
    int tm_sec;    // 秒 [0,61]
    int tm_min;    // 分 [0,59]
    int tm_hour;   // 时 [0,23]
    int tm_mday;   // 日 [1,31]
    int tm_mon;    // 月 [0,11] ← 注意+1
    int tm_year;   // 年-1900  ← 注意+1900
    int tm_wday;   // 周几 [0,6] (0=周日)
    int tm_yday;   // 年内天数 [0,365]
    int tm_isdst;  // 夏令时标志
};

掌握这些成员的含义和特性,可以高效处理各种时间操作需求。


网站公告

今日签到

点亮在社区的每一天
去签到