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

C++项目 ------ 基于多设计模式下的同步&异步日志系统(1)

我们今天来用C++来做一个项目:基于多设计模式下的同步&异步日志系统 ,我们首先将重点放在日志系统上,我们这次实现的日志系统主要有以下的功能:

1.支持多级别日志消息

2.支持同步日志和异步日志

3.支持可靠写入日志到控制台、文件以及滚动文件中

4.支持多线程程序并发写日志

5.支持扩展不同的日志落地目标地

我们首先看一下这些功能,这次涉及到的技术不算很广:C++,Linux多线程,文件操作

我们可以根据这些要求,把各个模块之间的关系做出一些大概的梳理:

我们首先关注上面的功能的实现:

工具类(utils)实现

日志系统要经常创建文件,我们可以把这个功能封装起来,为之后的程序编写提供便利:

cpp 复制代码
#ifndef __M_UTIL_H__
#define __M_UTIL_H__

// 工具的实现
namespace logs
{
    namespace utils
    {
        class Date
        {
        public:
            static size_t get_time()
            {
                // 获取时间
                return (size_t)time(nullptr);
            }
        };

        // 关于文件的一些操作
        class File
        {
        public:
            // 判断文件是否存在
            static bool exist(const std::string &pathname)
            {
                struct stat st;
                if (stat(pathname.c_str(), &st) < 0)
                {
                    return false;
                }
                return true;
            }

            // 获取路径名字
            static std::string path(const std::string &pathname)
            {
                size_t pos = pathname.find_last_of("/\\"); // 找到最后一个反斜杠

                if (pos == std::string::npos)
                {
                    return ".";
                }

                return pathname.substr(0, pos + 1);
            }

            // 创建文件(递归创建文件)
            static void createDiretory(const std::string &pathname)
            {
                size_t pos, idx = 0;

                while (idx < pathname.size())
                {
                    // 找到第一个反斜杠
                    pos = pathname.find_first_of("/\\", idx);

                    if (pos == std::string::npos)
                    {
                        mkdir(pathname.c_str(), 0777);
                        return;
                    }

                    // 截取父级目录
                    std::string parent_dir = pathname.substr(0, pos + 1);

                    //判断父级目录是否存在
                    if(exist(parent_dir) == true)
                    {
                        idx = pos + 1;
                        continue;
                    }

                    //如果父级目录不存在,就创建父级目录
                    mkdir(parent_dir.c_str(),0777);
                    idx = pos + 1;
                }
            }
        };
    }
}
#endif

这里介绍一下这里面的一些新用法:

struct stat 文件或目录状态信息结构体

Linux 2号手册可以查到对应的信息:

在这段代码中,struct stat 是 C/C++ 中用于获取文件或目录状态信息的一个结构体。它的用法可以分解如下:

1. struct stat 的定义

struct stat 是 POSIX 标准中的一个结构体,定义在头文件 <sys/stat.h> 中。它用来存储文件或目录的元信息(如大小、权限、修改时间等)。以下是 struct stat 的常见成员(具体字段可能因操作系统而异):

c 复制代码
struct stat {
    dev_t     st_dev;      // 文件所在的设备 ID
    ino_t     st_ino;      // inode 号
    mode_t    st_mode;     // 文件类型和权限
    nlink_t   st_nlink;    // 硬链接数
    uid_t     st_uid;      // 文件所有者的用户 ID
    gid_t     st_gid;      // 文件所有者的组 ID
    dev_t     st_rdev;     // 特殊文件的设备 ID
    off_t     st_size;     // 文件大小(字节数)
    time_t    st_atime;    // 最后访问时间
    time_t    st_mtime;    // 最后修改时间
    time_t    st_ctime;    // 最后状态改变时间
    blksize_t st_blksize;  // 文件系统 I/O 块大小
    blkcnt_t  st_blocks;   // 分配的块数
};

2. 代码中 struct stat 的作用

在这段代码中,struct stat st; 定义了一个名为 st 的变量,用来存储通过 stat() 函数获取的文件或目录的状态信息。

  • stat() 函数

    • 原型:int stat(const char *pathname, struct stat *buf);
    • 功能:根据路径 pathname 获取文件或目录的状态信息,并将其填充到 buf 指向的 struct stat 结构体中。
    • 返回值:
      • 成功时返回 0。
      • 失败时返回 -1,并设置 errno 表示错误原因。
  • 代码逻辑

    cpp 复制代码
    struct stat st;
    if (stat(pathname.c_str(), &st) < 0)
    {
        return false;  // 如果 stat 调用失败,返回 false
    }
    return true;       // 如果 stat 调用成功,返回 true
    • stat(pathname.c_str(), &st)
      • pathname 转换为 C 风格字符串(const char*),并调用 stat() 获取其状态信息。
      • 如果 stat() 返回值小于 0,说明发生了错误(例如文件不存在或没有权限),函数返回 false
      • 如果 stat() 成功,则返回 true

3. struct 的用法总结

  • struct 是 C/C++ 中用于定义结构体的关键字。
  • 在这段代码中,struct stat 表示一个结构体类型,st 是该类型的变量。
  • stat() 函数通过填充 struct stat 类型的变量来提供文件或目录的详细信息。

4. 注意事项

  • 头文件 :使用 struct statstat() 函数时,需要包含以下头文件:

    cpp 复制代码
    #include <sys/stat.h>  // 定义 struct stat 和 stat()
    #include <cstring>     // 如果需要处理字符串(如 pathname.c_str())
    #include <cerrno>      // 如果需要检查 errno
  • 错误处理 :如果 stat() 返回 -1,可以通过检查 errno 来确定具体的错误原因。例如:

    cpp 复制代码
    #include <cerrno>
    #include <iostream>
    if (stat(pathname.c_str(), &st) < 0)
    {
        std::cerr << "Error: " << strerror(errno) << std::endl;
        return false;
    }

日志消息类的具体实现

对于日志系统来说,主体就是日志消息:

日志等级类

日志消息会有一个等级,这里等级我们封装为一个类,方便我们在其他地方使用:

cpp 复制代码
namespace logs
{
    class Loglevel
    {
    public:
        enum class value
        {
            UNKOWN = 0,
            DEBUG,
            INFO,
            WARN,
            ERROR,
            FATAL,
            OFF
        };

        //将类型转换为字符串
        static const char* toString(Loglevel::value level)
        {
            switch(level)
            {
                case Loglevel::value::DEBUG: return "DEBUG";
                case Loglevel::value::ERROR: return "DEBUG";
                case Loglevel::value::FATAL: return "DEBUG";
                case Loglevel::value::INFO: return "DEBUG";
                case Loglevel::value::OFF:  return "OFF";
                case Loglevel::value::WARN: return "WARN";
            }

            return "UNKOWN";
        }
    };
}

在此基础上我们完成日志消息类的构造:

cpp 复制代码
#ifdef __M_MEASSAGE_H__
#define __M_MEASSAGE_H__
#include "utils.hpp"
#include "level.hpp"
#include <ctime>
#include <memory>
#include <thread>

namespace logs
{
    struct logMsg
    {
        size_t _ctime; //时间戳
        Loglevel::value _level; //日志等级
        std::string _file_name; //源文件名称
        size_t _line; //行号
        std::thread::id _tid; //线程名称
        std::string _playload; //日志器主体消息
        std::string _logger_name; //日志器名称

        logMsg(Loglevel::value level,
        const std::string file_name,
        size_t line,
        const std::string playload,
        const std::string logger_name)
            :_ctime(logs::utils::Date::get_time())
            ,_level(level)
            ,_file_name(file_name)
            ,_line(line)
            ,_tid(std::this_thread::get_id())
            ,playload(_playload)
            ,_logger_name(logger_name)
        {

        }
    
    };
}
#endif

日志消息格式化类

格式化类主要就是对消息进行格式化处理,处理成我们想要的格式类型:

因为我们这里一条消息包含的格式化子项很多,而且类型也不同,所以我们会考虑用一个基类的数组来存储这些不同的子项

pattern成员

  • 存储日志格式字符串(如 "[%d{%H:%M:%S}] %m%n"
  • 支持以下格式标记:
标记 说明 对应数据
%d 日期时间 LogMsg::_ctime
%T 制表符缩进 固定\t
%t 线程ID LogMsg::_tid
%p 日志级别 LogMsg::_level
%c 日志器名称 LogMsg::_name
%f 源码文件名 LogMsg::_file
%l 源码行号 LogMsg::_line
%m 日志消息内容 LogMsg::_payload
%n 换行符 固定\n

格式化项(FormatItem子类)
类名 功能描述 输出示例
MsgFormatItem 提取日志消息内容 "创建套接字失败"
LevelFormatItem 提取日志级别 "ERROR"
NameFormatItem 提取日志器名称 "root"
ThreadFormatItem 提取线程ID "0x1234"
TimeFormatItem 格式化时间戳 "14:30:45"
FileFormatItem 提取源码文件名 "main.cpp"
LineFormatItem 提取源码行号 "42"
TabFormatItem 插入制表符 \t
NewLineFormatItem 插入换行符 \n
OtherFormatItem 原样输出非格式字符串 "[" 或 "]"

日志消息结构(LogMsg)
cpp 复制代码
struct LogMsg {
    size_t _line;           // 行号(如:22)
    time_t _ctime;          // 时间戳(如:12345678)
    std::thread::id _tid;   // 线程ID(如:0x12345678)
    std::string _name;      // 日志器名称(如:"logger")
    std::string _file;      // 文件名(如:"main.cpp")
    std::string _payload;   // 日志内容(如:"创建套接字失败")
    LogLevel::value _level; // 日志级别(如:ERROR)
};

工作流程示例

输入格式
"[%d{%H:%M:%S}] %m%n"

解析结果

cpp 复制代码
items = {
    {OtherFormatItem(), "["},      // 原样输出"["
    {TimeFormatItem(), "%H:%M:%S"}, // 格式化时间
    {OtherFormatItem(), "] "},     // 原样输出"] "
    {MsgFormatItem(), ""},         // 提取消息内容
    {NewLineFormatItem(), ""}      // 换行
};

输出结果

plaintext 复制代码
[14:30:45] 创建套接字失败

在这之前我们要先实现格式化子项类的实现:

格式化子项类实现

cpp 复制代码
   class FometterItem
    {
    public:
        using ptr = std::shared_ptr<FometterItem>;                           // 重命名
        virtual void format(std::ostream &ost, const logMsg &Msg) = 0; // 接口
    };

    // 派生格式化子类基项--消息,等级,时间,行号,线程ID,日志器名称,制表符,换行,其他
    class MsgFormetItem : public FometterItem
    {
    public:
        void format(std::ostream &out, const logMsg &Msg) override
        {
            out << Msg._playload;
        }
    };

    class LevelFormetItem : public FometterItem
    {
    public:
        void format(std::ostream &out, const logMsg &Msg) override
        {
            out << Loglevel::toString(Msg._level);
        }
    };

    class TimeFormetItem : public FometterItem
    {
    public:
        TimeFormetItem(const std::string &fmt = "%H:%M:%S")
            : _time_fmt(fmt)
        {
        }
        void format(std::ostream &out, const logMsg &Msg) override
        {
            struct tm t;
            localtime_r(&Msg._ctime, &t);
            char tmp[32] = {0};
            strftime(tmp, 31, _time_fmt.c_str(), &t);
            out << tmp;
        }

    private:
        std::string _time_fmt; //%H:%M:%S
    };

    class FileFormetItem : public FometterItem
    {
    public:
        void format(std::ostream &out, const logMsg &Msg) override
        {
            out << Msg._file_name;
        }
    };

    class LineFormetItem : public FometterItem
    {
    public:
        void format(std::ostream &out, const logMsg &Msg) override
        {
            out << Msg._line;
        }
    };

    class ThreadFormetItem : public FometterItem
    {
    public:
        void format(std::ostream &out, const logMsg &Msg) override
        {
            out << Msg._tid;
        }
    };

    class LoggerFormetItem : public FometterItem
    {
    public:
        void format(std::ostream &out, const logMsg &Msg) override
        {
            out << Msg._logger_name;
        }
    };

    class TabFormetItem : public FometterItem
    {
    public:
        void format(std::ostream &out, const logMsg &Msg) override
        {
            out << "\t";
        }
    };

    class NLineFormetItem : public FometterItem
    {
    public:
        void format(std::ostream &out, const logMsg &Msg) override
        {
            out << "\n";
        }
    };

    class OtherFormetItem : public FometterItem
    {
    public:
        OtherFormetItem(const std::string &str)
            : _str(str)
        {
        }
        void format(std::ostream &out, const logMsg &Msg) override
        {
            out << _str;
        }

    private:
        std::string _str;
    };

格式项子类主要的功能就是从对应的msg消息主题中找到自己对应的那部分,然后把这个部分放到一块空间中

日志消息格式化类实现

日志消息格式化类实现主要是:我们会根据自己的需求去创建一个我们想要的日志格式,日志消息格式化类会把我们输入的格式化字符串 进行分类别处理,然后会有一个基类数组存储这些不同的子类 ,最后,我们遍历这个父类数组,让数组成员调用他们各自的format函数完成打印。

cpp 复制代码
   class Formetter
    {
    public:
        using ptr = std::shared_ptr<Formetter>;
        Formetter(const std::string &pattenstr = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n")
            : _pattenstr(pattenstr)
        {
            // 处理消息字符串
            assert(parsePatten());
        }

        std::string format(const logMsg &msg)
        {
            std::stringstream ss;
            format(ss, msg);
            return ss.str();
        }

        void format(std::ostream &ost, const logMsg &msg)
        {
            for (auto &it : _item)
            {
                it->format(ost, msg);
            }
        }

    private:
        // 消息字符串处理函数
        bool parsePatten()
        {
            std::vector<std::pair<std::string, std::string>> fmt_order;
            size_t pos = 0;

            std::string key, val;

            while (pos < _pattenstr.size())
            {
                // 1.处理原始字符串
                if (_pattenstr[pos] != '%')
                {
                    val.push_back(_pattenstr[pos++]);
                    continue;
                }

                // 处理双百分号情况
                if (pos < _pattenstr.size() && _pattenstr[pos + 1] == '%')
                {
                    val.push_back('%');
                    pos += 2;
                    continue;
                }

                // 处理完原始字符串,将原始字符串
                if (val.empty() == false)
                {
                    fmt_order.push_back(std::make_pair("", val));
                    val.clear(); // 清空值,为后面的格式化字符串做铺垫
                }

                // 2.格式化字串格式了,指向%
                pos += 1;
                if (pos == _pattenstr.size())
                {
                    std::cout << "%之后,没有对应格式化的格式化字符串\n";
                    return false;
                }
                key = _pattenstr[pos];

                pos += 1;                                              // 检查是否有字串格式
                if (pos < _pattenstr.size() && _pattenstr[pos] == '{') // 此时就是一个字串格式
                {
                    pos += 1; // 指向字串格式的内容
                    while (pos < _pattenstr.size() && _pattenstr[pos] != '}')
                    {
                        val.push_back(_pattenstr[pos++]);
                    } // 格式化字串处理完毕

                    // 走到了末尾,跳出了循环
                    if (pos == _pattenstr.size())
                    {
                        std::cout << "子规则匹配出错!{}\n";
                        return false; // 没有找到},代表格式是错误的
                    }

                    pos += 1; // 到了一个新的处理位置
                }

                // 将字串格式作为val
                fmt_order.push_back(std::make_pair(key, val));
                key.clear();
                val.clear();
            }

            for (auto &it : fmt_order)
            {
                _item.push_back(createItem(it.first, it.second));
            }
            return true;
        }

        // 根据不同的格式化字符创建不同的格式化子项对象
        FometterItem::ptr createItem(const std::string &key, const std::string &val)
        {
            /*
                %d 表示日期 包含子格式{%H:%M:%S}
                %t 线程id
                %c 表示日志器名称
                %f 表示源码文件名
                %l 表示源码行号
                %p 表示日志级别
                %T 表示缩进
                %m 表示主体消息
                %n 表示换行
            */

            if (key == "d")
                return std::make_unique<TimeFormetItem>(val);
            if (key == "t")
                return std::make_unique<ThreadFormetItem>();
            if (key == "c")
                return std::make_unique<LoggerFormetItem>();
            if (key == "f")
                return std::make_unique<FileFormetItem>();
            if (key == "l")
                return std::make_unique<LineFormetItem>();
            if (key == "p")
                return std::make_unique<LevelFormetItem>();
            if (key == "T")
                return std::make_unique<TabFormetItem>();
            if (key == "m")
                return std::make_unique<MsgFormetItem>();
            if (key == "n")
                return std::make_unique<NLineFormetItem>();
            if (key == "")
                return std::make_unique<OtherFormetItem>(val);

            std::cout << "没有对应的格式化字符: %" << key << std::endl;
            abort();
            return FometterItem::ptr();
        }
        std::string _pattenstr; // 格式化字符串
        std::vector<FometterItem::ptr> _item;
    };

一些小点

localtime_r

localtime_r 是 C/C++ 标准库中用于将时间戳(time_t)转换为本地时间(struct tm)的线程安全函数。它是 localtime 的线程安全版本,常用于多线程环境:

cpp 复制代码
struct tm *localtime_r(const time_t *timer, struct tm *result);

strftime

strftime 是 C/C++ 标准库中用于将时间信息(struct tm)格式化为字符串的函数。它是 "string format time" 的缩写,属于 <ctime> 头文件。以下是详细说明:


函数原型

c 复制代码
size_t strftime(char* str, size_t count, const char* format, const struct tm* timeptr);
  • 参数
    • str:输出缓冲区(存放结果字符串)
    • count:缓冲区最大容量(防止溢出)
    • format:格式化字符串(类似 printf 的格式)
    • timeptr:指向 tm 结构体的指针
  • 返回值:成功时返回写入的字符数(不含终止符),失败返回 0

2. 格式化符号对照表

符号 说明 示例
%Y 四位年份 2023
%y 两位年份 23
%m 月份(01-12) 07
%d 日(01-31) 15
%H 24小时制小时(00-23) 14
%M 分钟(00-59) 05
%S 秒(00-61) 30
%A 完整星期名 Monday
%a 缩写星期名 Mon
%B 完整月份名 July
%b 缩写月份名 Jul
%c 本地日期时间表示 Mon Jul 15 14:05:30 2023
%% 百分号字符 %

这样我们就可以理解这段代码了:

cpp 复制代码
void format(std::ostream &out, const logMsg &Msg) override
 {
     struct tm t;
     localtime_r(&Msg._ctime, &t);
     char tmp[32] = {0};
     strftime(tmp, 31, _time_fmt.c_str(), &t);
     out << tmp;
 }
相关推荐
巨可爱熊2 小时前
高并发内存池(定长内存池基础)
linux·运维·服务器·c++·算法
码农新猿类4 小时前
服务器本地搭建
linux·网络·c++
GOTXX5 小时前
【Qt】Qt Creator开发基础:项目创建、界面解析与核心概念入门
开发语言·数据库·c++·qt·图形渲染·图形化界面·qt新手入门
徐行1105 小时前
C++核心机制-this 指针传递与内存布局分析
开发语言·c++
序属秋秋秋5 小时前
算法基础_数据结构【单链表 + 双链表 + 栈 + 队列 + 单调栈 + 单调队列】
c语言·数据结构·c++·算法
mldl_6 小时前
(个人题解)第十六届蓝桥杯大赛软件赛省赛C/C++ 研究生组
c语言·c++·蓝桥杯
一个小白17 小时前
C++ 用红黑树封装map/set
java·数据库·c++
Lenyiin7 小时前
《 C++ 点滴漫谈: 三十三 》当函数成为参数:解密 C++ 回调函数的全部姿势
c++·回调函数·lenyiin
埜玊7 小时前
C++之 多继承
c++
1024熙9 小时前
【C++】——lambda表达式
开发语言·数据结构·c++·算法·lambda表达式