C++项目 —— 基于多设计模式下的同步&异步日志系统(1)
- 工具类(utils)实现
- struct stat 文件或目录状态信息结构体
- 1. `struct stat` 的定义
- 2. 代码中 `struct stat` 的作用
- 3. `struct` 的用法总结
- 4. 注意事项
- 日志消息类的具体实现
- 日志等级类
- 日志消息格式化类
- 格式化项(FormatItem子类)
- 日志消息结构(LogMsg)
- 工作流程示例
- 格式化子项类实现
- 日志消息格式化类实现
- 一些小点
- localtime_r
- strftime
- 函数原型
- 2. **格式化符号对照表**
我们今天来用C++来做一个项目:基于多设计模式下的同步&异步日志系统,我们首先将重点放在日志系统上,我们这次实现的日志系统主要有以下的功能:
1.支持多级别日志消息
2.支持同步日志和异步日志
3.支持可靠写入日志到控制台、文件以及滚动文件中
4.支持多线程程序并发写日志
5.支持扩展不同的日志落地目标地
我们首先看一下这些功能,这次涉及到的技术不算很广:C++,Linux多线程,文件操作。
我们可以根据这些要求,把各个模块之间的关系做出一些大概的梳理:
我们首先关注上面的功能的实现:
工具类(utils)实现
日志系统要经常创建文件,我们可以把这个功能封装起来,为之后的程序编写提供便利:
#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
的常见成员(具体字段可能因操作系统而异):
struct stat {dev_t st_dev; // 文件所在的设备 IDino_t st_ino; // inode 号mode_t st_mode; // 文件类型和权限nlink_t st_nlink; // 硬链接数uid_t st_uid; // 文件所有者的用户 IDgid_t st_gid; // 文件所有者的组 IDdev_t st_rdev; // 特殊文件的设备 IDoff_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
表示错误原因。
- 原型:
-
代码逻辑:
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 stat
和stat()
函数时,需要包含以下头文件:#include <sys/stat.h> // 定义 struct stat 和 stat() #include <cstring> // 如果需要处理字符串(如 pathname.c_str()) #include <cerrno> // 如果需要检查 errno
-
错误处理:如果
stat()
返回 -1,可以通过检查errno
来确定具体的错误原因。例如:#include <cerrno> #include <iostream> if (stat(pathname.c_str(), &st) < 0) {std::cerr << "Error: " << strerror(errno) << std::endl;return false; }
日志消息类的具体实现
对于日志系统来说,主体就是日志消息:
日志等级类
日志消息会有一个等级,这里等级我们封装为一个类,方便我们在其他地方使用:
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";}};
}
在此基础上我们完成日志消息类的构造:
#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)
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"
解析结果
items = {{OtherFormatItem(), "["}, // 原样输出"["{TimeFormatItem(), "%H:%M:%S"}, // 格式化时间{OtherFormatItem(), "] "}, // 原样输出"] "{MsgFormatItem(), ""}, // 提取消息内容{NewLineFormatItem(), ""} // 换行
};
输出结果
[14:30:45] 创建套接字失败
在这之前我们要先实现格式化子项类的实现:
格式化子项类实现
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函数完成打印。
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; // 到了一个新的处理位置}// 将字串格式作为valfmt_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 的线程安全版本,常用于多线程环境:
struct tm *localtime_r(const time_t *timer, struct tm *result);
strftime
strftime
是 C/C++ 标准库中用于将时间信息(struct tm
)格式化为字符串的函数。它是 “string format time” 的缩写,属于 <ctime>
头文件。以下是详细说明:
函数原型
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 |
%% | 百分号字符 | % |
这样我们就可以理解这段代码了:
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;}