本章目标
1.日志
1.日志
在前面,我们在多线程的情况下,对于一些结果打印,往往会出现错乱的问题.这些因为在我们Linux当中始终遵循一个规则,就是一切皆文件.我们需要向显示器文件进行写入我们的日志信息.但是在多线程的情况下.我们每个线程都回会向显示器文件进行写入,如果没有进行对共享资源的保护进行这样就会出现数据不一致的问题.为了解决这个问题我们要设计一个拥有锁进行保护的日志模块.
对于市面上有很多成熟的日志模块.spdlog、glog、Boost.Log、Log4cxx等等,但是我们要去深入的了解这个东西最好的方法还是自己写一个.
我们基于策略模式这种设计模式,这种设计模式跟我们之前说过的建造者模式很像,他们都属于我们的构建类的设计模式.
对于一个日志最重要的三个东西
1.时间戳.
2.日志等级
3.日志内容
cpp
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world
我们最后做出来的效果至少要像上面那样
第一个就是时间,第二个是日志等级,第三个是进程的pid,文件名,以及日志所处的行数,最好才是真正的正文部分.
1.1框架
对于一个日志来说,要基于策略模式进行设计就要有他们的各自的刷新策略.
我们需要提供一个基类为所有刷新策略提供公共的方法
cpp
class LogStrategy
{
public:
virtual void SyncStrategy(const std::string &s) = 0;
virtual ~LogStrategy() = default;
private:
};
第一种,向显示器打印
cpp
class WindowSyncStregy : public LogStrategy
{
public:
WindowSyncStregy() {}
~WindowSyncStregy() =default;
void SyncStrategy(const std::string &s) override
{
{
Mutex_Moudle::Mutex_Grard guard(lock);
std::cout << s << std::endl;
}
}
private:
Mutex_Moudle::Mutex lock;
};
只需要重写对应的方法即可.但是需要加一把锁,来保证在多线程的时候,不会出现的数据不一致导致打印结果错乱的问题.
第二种向文件刷新
cpp
std::string defaultname = "log.txt";
std::string defaultpath = "./log";
class FileSyncStregy : public LogStrategy
{
public:
FileSyncStregy(const std::string name = defaultname, const std::string path = defaultpath)
: _name(name), _path(path)
{
{
Mutex_Moudle::Mutex_Grard guard(lock);
if (std::filesystem::exists(_path))
return;
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
}
virtual void SyncStrategy(const std::string &s) override
{
{
Mutex_Moudle::Mutex_Grard guard(lock);
// 判断路径最后一个字符是否带/
if (!_path.empty() && _path.back() != '/')
{
_path += '/';
}
// 最终文件名称
std::string filename = _path + _name;
std::ofstream ofs(filename, std::ios::app);
if (!ofs.is_open())
{
std::cerr << "file not open" << std::endl;
return;
}
ofs << s << "\n";
ofs.close();
}
}
~FileSyncStregy() =default;
private:
std::string _name;
std::string _path;
Mutex_Moudle::Mutex lock;
};
对于像文件刷新,我们需要保证我们刷新的内容,需要保存在哪里,我们在这里选择,在./log下的目录下进行刷新.这就引入了另外一个问题,我们需要保证这个路径下存在.第一个如果没有就创建它,如果后续使用,直接返回.在这里我们没有选用Linux给我们提供系统调用.而是选择了c++17给我们提供的filesystem来去实现的这个地方.对于可能出错的地方原则抛异常返回
对于向文件写入同样选择c++的io操作.对于我们打开的文件对于多线程来说也同样是共享资源,仍然需要需要一把锁来进行保护
这里我们传进来的文件可能是不带/,需要额外判断.
1.2解决时间戳
对于时间戳问题,我们并不希望直接拿到那么一个数字,我们希望把它做出一个结构化有年月日的时间,所以在这个小模块当中,我们至少要完成两件事,第一件是为了拿到时间戳,第二个是将时间戳转化为我们能够看懂的年月日的时间.
时间戳的获取我们采用下面这个系统调用.

它要求我们传一个结构体,第二个是时区,我们不管,直接给空,用我们的东八区.

这个结构体当中就是我们的时间戳,第一个时秒为单位的,第二个时微妙

我们将我们的时间戳转化为结构化数据的接口如上,第一个参数就是时间戳,第二个时一个输出型,保存着我们的结果

但是这个结构化数据优点坑,它的年是-1900的,我们需要再重新给它加上1900,他的月份实际上是下标值需要+1.
最后我们将整个时间用snpriintf拼接起来,时间戳如下
cpp
std::string gettime()
{
// 1.拿到时间戳
struct timeval current_time;
int n = gettimeofday(¤t_time, nullptr);
// 2.拿到格式化信息
(void)n;
struct tm struct_time;
localtime_r(¤t_time.tv_sec, &struct_time);
char time[1024];
memset(time, 0, sizeof time);
snprintf(time, sizeof time, "%04d-%02d-%02d: %02d-%0d-%02d",
struct_time.tm_year + 1900,
struct_time.tm_mon + 1,
struct_time.tm_mday,
struct_time.tm_hour,
struct_time.tm_min,
struct_time.tm_sec);
return time;
}
1.3logger类
我们logger类要提供上面刷新策略的所有开启的方法,它的成员变量只有只有一个指向刷新策略基类的指针
cpp
class logger
{
public:
logger()
{
_strategy = nullptr;
}
~logger() {}
void usewindowstrategy()
{
_strategy = std::make_unique<WindowSyncStregy>();
}
void usefilestrategy()
{
_strategy = std::make_unique<FileSyncStregy>();
}
private:
std::unique_ptr<LogStrategy> _strategy;
};
我们在其中的内部定义一个消息组成的内部类
然后根据我们上面所说去构建整个日志的组成
cpp
class loggermassger
{
public:
loggermassger(LOGLEVEL level, size_t line, const std::string &filename, logger &Logger)
: _level(level), _line(line), _filename(filename), _logger(Logger), _pid(getpid()), current_time(gettime())
{
std::stringstream ss;
ss << "[" << current_time << "] "
<< "[" << LogLevel2String(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _line << "] "
<< "[" << _filename << "] ";
loginfo = ss.str();
}
// 模板类型支持各自输出
template <typename T>
loggermassger &operator<<(const T &data)
{
std::stringstream ss;
ss << data;
loginfo += ss.str();
return *this;
}
~loggermassger()
{
// 最后刷新在这里
_logger._strategy->SyncStrategy(loginfo);
}
private:
std::string current_time;
LOGLEVEL _level;
pid_t _pid;
size_t _line;
std::string _filename;
logger &_logger; // 使用刷新策略
std::string loginfo;
};
cpp
loggermassger operator()(LOGLEVEL level, const std::string &filename, size_t line)
{
return loggermassger(level, line, filename, *this);
}
// 1.LOG用宏定义封装
// 2.重载()创建loggermassger,这里必须拷贝,为了刷新
// 3. 在loggermassger里重载<<输出
// 4.<<返回自身类型的引用,支持多次输出
// 5.loggermassger析构使用外部传进来的刷新策略
在这要注意以下我们这个()的重载一定是传值操作,方便我们的消息析构之后能够直接通过我们上面的设定的策略直接刷新.
cpp
logger log;
#define WINDOWS_LOG_INITAL() \
do \
{ \
log.usewindowstrategy(); \
} while (0);
#define FILE_LOG_INITAL() \
do \
{ \
log.usefilestrategy(); \
} while (0);
为了方便直接使用.我们在这里面直接定义一格logger 对象,同时通过宏来确定对应的刷新策略
cpp
#define LOG(level) log(level, __FILE__, __LINE__)
我们后续使用的时候直接使用这个宏去操作.
我们在这两个宏,FILE ,__LINE__分别能拿到对应的相应的文件名,以及行号.
日志模块具体实现