🌈欢迎来到Linux专栏 ~~ 日志类
- 🌍博客主页 :张小姐的猫~江湖背景
- 🔥所属专栏 :Linux ~ 不破不立
- 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏

日志与策略模式
- [🌈欢迎来到Linux专栏 ~~ 日志类](#🌈欢迎来到Linux专栏 ~~ 日志类)
- 一、日志与策略模式
- 二、策略模式
- 三、日志类以及日志生成
- 四、完整代码
- 📢写在最后

现在开始,我们结合我们之前所做的所有封装,进行一个线程池的设计。在写之前,我们要做如下准备
🔹 准备线程的封装
🔹 准备锁和条件变量的封装
🔹 引入日志,对线程进行封装
前两个我们都做过了,接下来聊聊日志 ~
一、日志与策略模式
什么是设计模式?
IT行业这么火,涌入的人很多。俗话说林子大了啥鸟都有,大佬和菜鸡们两极分化的越来越严重.。为了让菜鸡们不太拖大佬的后腿,于是大佬们针对⼀些经典的常见的场景,给定了⼀些对应的解决方案,这个就是设计模式
接下来认识一下日志 :
计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要⼯具。
日志格式以下几个指标是必须得有的
- 时间戳
- 日志等级
- 日志内容
以下几个指标是可选的
- 文件名行号
- 进程,线程相关
id信息等
日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。
这里我们采用设计模式-策略模式来进行日志的设计,目标格式如下:
cpp
[可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容,⽀持可变参数
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [17] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [18] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [20] - hello world
[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [21] - hello world
[2024-08-04 12:27:03] [WARNING] [202938] [main.cc] [23] - hello world
接下来我们直接动手实现,需要用到以下两个函数
1️⃣gettimeofday 是 POSIX 系统调用 ,用于获取当前时间,精度为微秒(µs)
cpp
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
tv:输出型参数,其类型是一个结构体
cpp
struct timeval {
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微秒
};
tz:时区信息 ,默认北京时区,已废弃,传 nullptr 即可
2️⃣时间戳信息转换 :localtime_r() 线程安全版本
c
#include <time.h>
struct tm* localtime_r(const time_t* timer, struct tm* result);
函数功能:把秒为单位的时间戳,转化成struct tm结构体
timer:传入的时间戳
返回值:成功返回result结构体变量的地址,失败返回NULL
cpp
struct tm {
int tm_sec; /* Seconds (0-60) */
int tm_min; /* Minutes (0-59) */
int tm_hour; /* Hours (0-23) */
int tm_mday; /* Day of the month (1-31) */
int tm_mon; /* Month (0-11) */
int tm_year; /* Year - 1900 */
int tm_wday; /* Day of the week (0-6, Sunday = 0) */
int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */
int tm_isdst; /* Daylight saving time */
};
二、策略模式
策略模式的 骨架 :基类是用纯虚函数来定义,作为策略的接口!
真正实现时,是子类去继承基类的函数,进行重新实现!
首先来看看基类的实现
cpp
//策略接口 ~ 基类
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
default:让编译器生成默认实现,同时保持虚析构属性
= 0:表示纯虚函数,不提供实现,子类必须重写所有纯虚函数
📌向显示器打印策略
此处不需要实现构造函数,因其私有成员只有一把锁,在LockGuard lockguard(_mutex)处就可以完成锁的无参构造以及初始化
cpp
// 控制台日志刷新策略,日志将来想显示器打印
class ConsoleStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cerr << message << std::endl;
}
~ConsoleStrategy()
{}
private:
Mutex _mutex;
};
override :编译器检查是否真正覆盖了基类虚函数
📌文件策略
直接放上源码------
cpp
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename) :
_logpath(path),
_logfilename(filename)
{
LockGuard lockguard(_mutex); //保证在进行文件操作时是原子性的
if(std::filesystem::exists(_logpath))
return;
try
{
std::filesystem::create_directories(_logpath);//不存在就创建
}
catch(const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
if(!_logpath.empty() && _logpath.back() != '/') //back返回字符串的最后一个字符
{
_logpath += "/";
}
}
void SyncLog(const std::string &message) override
{
{
LockGuard lockguard(_mutex); //同理加锁保护
std::string targetlog = _logpath + _logfilename; //"./loglog.txt" 需要加分隔符
std::ofstream out(targetlog, std::ios::app); //以追加形式进行写入
if(!out.is_open())
{
std::cerr << "open" << targetlog << "failed" << std::endl;
return;
}
//向文件写入
out << message << "\n";
out.close(); //文件打开了,不用记得关!
}
}
~FileLogStrategy()
{}
private:
std::string _logpath;
std::string _logfilename;
Mutex _mutex;
};
向指定的文件进行写入,那么其私有成员必须有文件名以及文件路径
此处故意把二者分开,因为有可能该路径下的目录是不存在的,就需要手动的创建了
今天我们不打算使用系统调用(open等)去实现文件新建等工作,而是使用C++17的新特性:
cpp
#include <filesystem>
那么我们怎么判断一个文件的路径是否真的存在呢?
stat()根据 文件路径 获取文件属性信息。
c
#include <sys/stat.h>
int stat(const char *pathname, struct stat *buf);
pathname → 文件路径(输入)
buf → 文件属性(输出)
输出的结构体里包括了
cpp
struct stat
{
mode_t st_mode; // 文件类型+权限
off_t st_size; // 文件大小
time_t st_atime; // 访问时间
time_t st_mtime; // 修改时间
ino_t st_ino; // inode号
};
今天我们要用文件系统 封装的exists函数,其功能:判断路径是否存在 ,
不存在就调用create_directories函数去帮我们创建
由于创建目录成功与否关键在于当前目录是否有执行权限x ,创建失败就需要抛出异常
cpp
FileLogStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename) :
_logpath(path),
_logfilename(filename)
{
LockGuard lockguard(_mutex); //保证在进行文件操作时是原子性的
if(std::filesystem::exists(_logpath))
return;
try
{
std::filesystem::create_directories(_logpath);//不存在就创建
}
catch(const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
接着是C++中的文件操作分为三大类:
ifstream:读文件ofstream: 写文件fstream: 读+写
三、日志类以及日志生成
上述的所有工作都是为了服务于日志类
日志类的目标:
- 帮助日志的生成
- 根据不同的策略,来进行刷新
1️⃣日志刷新
根据不同的策略,来进行刷新
cpp
//日志对象,全局使用 ------ 把日志对象变成一个大的临界资源
Logger logger;
//不暴露全局对象,直接使用宏来调用全局对象
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();
class Logger
{
public:
Logger()
{
}
void UseConsoleStrategy()
{
// 1. 堆上创建一个 ConsoleStrategy 对象
// 2. 自动包进 unique_ptr 管理生命周期
// 3. 隐式转化 为 unique_ptr<LogStrategy>
// 4. 移动赋值给 _strategy
_strategy = std::make_unique<ConsoleStrategy>();
}
void UseFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
void Debug(const std::string &message)
{
if(_strategy != nullptr)
{
_strategy->SyncLog(message);
}
}
~Logger()
{
}
private:
// 虽然虚基类不能直接定义对象,但可以定义指针对象
std::unique_ptr<LogStrategy> _strategy; // 刷新策略
};
实现注意细节:
- 💥想把日志对象在全局上进行使用,也就要把日志对象变成一个大的临界资源。-
- 为了方便我们后续的使用,还提供了两个宏,从而不暴露全局对象,直接使用宏来调用全局对象
接着我们直接测试一下,顺便捋顺执行顺序,Main.cc代码如下
c
#include "Logger.hpp"
using namespace LOG_MODULE;
int main()
{
ENABLE_CONSOLE_LOG_STRATEGY();
logger.Debug("你好哇\n");
logger.Debug("你好哇\n");
logger.Debug("你好哇\n");
logger.Debug("你好哇\n");
logger.Debug("你好哇\n");
ENABLE_FILE_LOG_STRATEGY();
logger.Debug("早上坏\n");
logger.Debug("早上坏\n");
logger.Debug("早上坏\n");
return 0;
}
运行结果如下:

整体的流程如下:
- 调用
Logger()无参构造 ENABLE_CONSOLE_LOG_STRATEGY() 宏展开 为logger.UseConsoleStrategy();,并且创建一个智能指针指向刚创建的ConsoleStrategy对象,最后赋值给_strategy- Logger::Debug ------ 进入去调用
Debug _strategy实际指向的是ConsoleStrategy,通过虚函数表调用:打印对应对象的打印方式- 重复执行
- 最后智能指针自动析构
定义基类策略接口,子类对象定义好,最终在日志类里定义一个基类指针,后续指针指向哪个策略就能拿到对应策略的方法
这意味着后续如果想新增策略,就只需新增一组新的继承关系,并实现基类的方法,就可以解耦的去执行 ~ 优雅
🔥策略模式 :是基于纯虚接口,用统一的接口类,让子类去继承并且实现方法,从而实现代码横向扩展的设计模式
2️⃣日志生成
如何生成一天天的日志信息呢? ------ 设计一个内部类:表示一条完整的日志信息
cpp
左半部分的组成
[2024-08-04 12:27:03.123456] [DEBUG] [202938] [main.cc] [16] - hello world
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~ ~~~~~~~ ~~~~~~~~ ~~
时间戳 日志等级 PID 文件名 行号
\_______________ 左半部分(元数据) _______________/
完整格式:[时间] [等级] [PID] [文件] [行号] -
其中左半部分的格式是相对固定的! 变化也只是值发生变化。而日志信息的右半部分的形式是会随格式变而变,于是先实现左半部分
使用 std::stringstream(字符串文件流) 拼接左半部分:
std::stringstream 是一种内存文件流 ------它将各种类型的数据(int、pid_t、std::string)通过 operator<< 统一格式化为字符串,起到类似"文件流"的缓冲和拼接作用。它与实际的文件流 std::ofstream 形成对照:

日志部分,为了在进行使用的时候,想仿照C++输出模式进行使用,就对()进行重载
重载 :这是对 Logger 类的函数调用运算符 operator() 的重载,使 Logger 对象可以像函数一样被调用。它充当工厂方法------接收 (level, filename, line) 参数,调用 LogMessage 构造函数创建临时对象并拷贝返回
cpp
LogMessage operator()(LogLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line); // 传入时,构建临时拷贝构建Message,返回新对象
}
- 此处不属于内部类, 对
()进行重载,故意采用拷贝LogMessage
接着进行拼接右半部分:
cpp
template<typename T>
LogMessage &operator << (const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
//this指针是成员函数的隐含指针,指向当前LogMessage对象
return *this; //返回当前LogMessage对象,方便下次继续使用 <<
}
函数模版 :template<typename T> --- 支持任意类型 (int、double、const char*、std::string 等)进行转换
字符串文件流 :将任意类型 T 通过 operator<< 统一转为字符串 ,然后追加到 _loginfo
返回this指针 :返回 LogMessage& 引用

举个例子:
c
logger(DEBUG, "f.cc", 42) << "x=" << 42 << " y=" << 3.14;
// ① LogMessage ② ③ ④

cpp
this = &当前对象 // 地址,类型 LogMessage*
*this = 当前对象 // 对象本身,类型 LogMessage&
*this 解引用得到对象引用 ,才能让下一个 << 继续作用在同一个 LogMessage 对象上,实现标准库风格的连续拼接。

接下来我想让 LogMessage以RAII风格进行刷新:
- 构造是构建出左半部分,<< 重载一直获取右半部分,最后在析构时进行刷新,让后释放
-于是乎在内部类里引用外部类,方便后续在析构时进行策略性刷新
cpp
~LogMessage()
{
// 析构时判断
if (_logger._strategy)
{
//此时_loginfo就是完整日志信息了
_logger._strategy->SyncLog(_loginfo);
}
}
最后我们来理顺一下从整体的输出流程:

当LogMessage调用完后,会被自动析构(同时根据外部类的策略进行刷新)
最后跑一下测试:


四、完整代码
Main.cc:
cpp
#include "Logger.hpp"
using namespace LOG_MODULE;
int main()
{
ENABLE_CONSOLE_LOG_STRATEGY();
LOG(LogLevel::WARNING) << "hello" << 10 << 1.2 << "下午坏";
ENABLE_FILE_LOG_STRATEGY();
LOG(LogLevel::WARNING) << "hello" << 10 << 1.2 << "下午坏";
LOG(LogLevel::WARNING) << "hello" << 10 << 1.2 << "下午坏";
LOG(LogLevel::WARNING) << "hello" << 10 << 1.2 << "下午坏";
return 0;
}
Logger.hpp:
cpp
#ifndef LOGGER_HPP
#define LOGGER_HPP
#include <iostream>
#include <string>
#include <ctime>
#include <time.h>
#include <stdio.h>
#include <memory>
#include <unistd.h>
#include <sys/time.h>
#include "Mutex.hpp"
#include <filesystem> //C++17
#include <fstream>
#include <sstream>
namespace LOG_MODULE
{
// 日志等级
enum class LogLevel
{
INFO, // 正常
WARNING, // 告警
ERROR,
FATAL,
DEBUG
};
std::string LogLevel2Message(LogLevel level)
{
switch (level)
{
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
case LogLevel::DEBUG:
return "DEBUG";
default:
return "UNKOWN";
}
}
// 1.时间戳 2.日期 + 时间
std::string GetCurrentTime()
{
struct timeval current_time;
int n = gettimeofday(¤t_time, nullptr);
(void)n;
// current_time.tv_sec 、current_time.tv_usec
struct tm struct_time;
localtime_r(&(current_time.tv_sec), &struct_time);
char timestr[128];
snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d-%02d-%02d.%ld",
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,
current_time.tv_usec);
return timestr;
}
// 输出角度 -- 刷新策略
// 1. 显示器打印
// 2. 文件写入
// 日志的生成
// 1. 构建日志字符串
// 2. 根据不同的策略进行刷新
// 策略模式 策略接口 ~ 基类
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 控制台日志刷新策略,日志将来想显示器打印
class ConsoleStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cerr << message << std::endl;
}
~ConsoleStrategy()
{
}
private:
Mutex _mutex;
};
const std::string defaultpath = "./log";
const std::string defaultfilename = "log.txt";
// 文件策略:
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename)
: _logpath(path),
_logfilename(filename)
{
LockGuard lockguard(_mutex); // 保证在进行文件操作时是原子性的
if (std::filesystem::exists(_logpath))
return;
try
{
std::filesystem::create_directories(_logpath); // 不存在就创建
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << '\n';
}
}
void SyncLog(const std::string &message) override
{
{
LockGuard lockguard(_mutex); // 同理加锁保护
if (!_logpath.empty() && _logpath.back() != '/') // back返回字符串的最后一个字符
{
_logpath += "/";
}
std::string targetlog = _logpath + _logfilename; //"./loglog.txt" 需要加分隔符
std::ofstream out(targetlog, std::ios::app); // 以追加形式进行写入
if (!out.is_open())
{
std::cerr << "open" << targetlog << "failed" << std::endl;
return;
}
// 向文件写入
out << message << "\n";
out.close(); // 文件打开了,不用记得关!
}
}
~FileLogStrategy()
{
}
private:
std::string _logpath;
std::string _logfilename;
Mutex _mutex;
};
class Logger
{
public:
Logger()
{
// 用户不选择,默认选择控制台策略
UseConsoleStrategy();
}
void UseConsoleStrategy()
{
// 1. 堆上创建一个 ConsoleStrategy 对象
// 2. 自动包进 unique_ptr 管理生命周期
// 3. 隐式转化 为 unique_ptr<ConsoleStrategy>
// 4. 移动赋值给 _strategy
_strategy = std::make_unique<ConsoleStrategy>();
}
void UseFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
// 测试策略选择
// void Debug(const std::string &message)
// {
// if(_strategy != nullptr)
// {
// _strategy->SyncLog(message);
// }
// }
// 内部类,表示一条完整的日志信息
class LogMessage
{
public:
LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
: _level(level),
_curr_time(GetCurrentTime()),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
//[2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - hello world 只剩下日志内容了
// 先构建左半部分 ------ 字符串拼接
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LogLevel2Message(_level) << "] " // LogLevel是枚举类型,在输出是会被流式输出当做是整数去打印
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< "- ";
_loginfo = ss.str();
}
// 右半部分:重载<<
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
// this指针是成员函数的隐含指针,指向当前LogMessage对象
return *this; // 返回当前LogMessage对象,方便下次继续使用 <<
}
~LogMessage()
{
// 析构时判断
if (_logger._strategy)
{
// 此时_loginfo就是完整日志信息了
_logger._strategy->SyncLog(_loginfo);
}
}
private:
LogLevel _level;
std::string _curr_time;
pid_t _pid;
std::string _filename;
int _line;
std::string _loginfo; // 一条完整的日志信息
// 引用外部的Logger类对象
Logger &_logger; // 引用外部类,方便后续进行策略性刷新
};
// 此处不属于内部类了 对()进行重载
// 故意采用拷贝LogMessage
LogMessage operator()(LogLevel level, std::string filename, int liner)
{
// 还需传入当前的外部类对象:*this即可
return LogMessage(level, filename, liner, *this); // 传入时,构建临时拷贝构建Message,返回新对象
}
~Logger()
{
}
private:
// 虽然虚基类不能直接定义对象,但可以定义指针对象
std::unique_ptr<LogStrategy> _strategy; // 刷新策略
};
// 日志对象,全局使用 ------ 把日志对象变成一个大的临界资源
Logger logger;
// 不暴露全局对象,直接使用宏来调用全局对象
#define LOG(level) logger(level, __FILE__, __LINE__)
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();
};
#endif
拓展:文件策略 | 区分日志等级来进行保存
📢写在最后
接下来是线程池与线程安全 ------ 冲冲冲🚀

