C++项目 ------ 基于多设计模式下的同步&异步日志系统(1)
- 工具类(utils)实现
- [struct stat 文件或目录状态信息结构体](#struct stat 文件或目录状态信息结构体)
-
-
- [1. `struct stat` 的定义](#1.
struct stat
的定义) - [2. 代码中 `struct stat` 的作用](#2. 代码中
struct stat
的作用) - [3. `struct` 的用法总结](#3.
struct
的用法总结) - [4. 注意事项](#4. 注意事项)
- [1. `struct stat` 的定义](#1.
-
- 日志消息类的具体实现
- 日志消息格式化类
- 一些小点
-
- localtime_r
- strftime
-
- 函数原型
- [2. **格式化符号对照表**](#2. 格式化符号对照表)
我们今天来用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
表示错误原因。
- 原型:
-
代码逻辑:
cppstruct 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()
函数时,需要包含以下头文件: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;
}