一、自定义日志
1.1 什么是日志
日志是程序在执行过程中,主动输出的结构化 / 半结构化文本记录。它不直接影响程序功能,却能客观反映程序的运行状态 ------ 比如 "2025-11-22 14:30:00 [INFO] 用户 xxx 登录成功""2025-11-22 14:35:20 [ERROR] 数据库连接超时",本质是程序的 "运行日记"。

通过在开发过程中引入日志功能可以帮助我们完成:
问题排查与故障定位:生产环境无法单步调试,日志是唯一能还原问题现场的依据。比如用户反馈 "提交订单失败",通过日志可快速定位是参数错误、数据库异常还是第三方接口超时,避免盲目排查。
系统监控与状态感知:通过日志可实时监控程序健康度,比如频繁出现 "WARN" 级别的日志可能预示潜在风险(如内存泄漏、接口响应缓慢),提前预警避免故障扩大。
数据分析与优化决策:日志包含用户行为、性能指标等数据,比如统计接口调用频率、响应时间分布,可为系统扩容、代码优化提供数据支撑(如发现某接口耗时过长,针对性优化算法或加缓存)。
1.2 设计自定义日志
1.2.1 策略模式
策略模式(Strategy Pattern)是行为型设计模式的一种,核心思想是将可变的算法 / 行为封装成独立的策略类,使它们可以相互替换,且算法的变化独立于使用算法的客户端。
策略模式有两个组成部分:
- 策略接口(Strategy):定义所有具体策略必须实现的方法。
- 具体策略(ConcreteStrategy):实现策略接口的具体算法。
比如,我们举个例子:某个电商平台需要我们实现一种支付的功能,支付的方式可以有很多比如微信支付、支付宝支付、银行卡支付等等。在这个功能中,策略接口(必须实现的方法)就是支付接口,而具体的策略就是用户可以选择的支付方式(微信支付、支付宝支付、银行卡支付等等)。此时,当某一个用户调用支付模块时可以:
- 客户端创建一个具体策略对象(如
支付宝支付)。 - 上下文调用策略对象的算法方法(如
支付()),执行具体逻辑。 - 若需切换策略,客户端只需创建另一个具体策略对象并重新设置给上下文即可。
当用户新增支付方式时,只需新增一个具体策略类,无需修改上下文和客户端代码,大大减少了模块之间的耦合度。使得用户可以根据自己得需求动态切换策略(如用户支付时选择不同方式),避免了代码得冗余。

1.2.2 两种刷新方式的实现
在我们的自定义日志中需要实现两个功能:
- 将日志消息向指定日志文件中写入
- 将日志消息打印在屏幕上
利用策略模式,我们可以实现一个打印功能的策略接口,此时向文件中写入消息和向屏幕打印消息就是两个具体策略。在代码实现中,我们可以利用多态来完成策略模式:
首先实现一个策略类,其中Strategy就是需要实现的策略接口:
cpp
//首先定义一个策略类
class LogStrategy
{
public:
LogStrategy()
{}
virtual void Strategy(std::string message)=0;
~LogStrategy()
{}
};
其中将Strategy设计成纯虚函数强制规定,之后的派生类必须将其实现。接着我们可以定义出两个派生类ConsoleLogStrategy和FileLogStrategy并分别实现Strategy,对应两种不同的策略方式:
cpp
//向显示器打印日志信息
class ConsoleLogStrategy:public LogStrategy
{
public:
ConsoleLogStrategy()
{}
~ConsoleLogStrategy()
{}
void Strategy(std::string message)
{
LockGuard lock(_mutex);
std::cout<<message<<gsep;
}
private:
Mutex _mutex;
};
//向指定文件打印指定信息
class FileLogStrategy:public LogStrategy
{
public:
FileLogStrategy(std::string path=defaultpath,std::string name=defaultname)
:_path(path)
,_name(name)
,_mutex()
{
//首先检查路径是否存在,不存在就创建
if(!std::filesystem::exists(_path))
{
std::filesystem::create_directories(_path);
}
_filepath=_path+(_path.back()=='/'?"":"/")+_name;
std::cout<<_filepath<<std::endl;
}
~FileLogStrategy()
{}
void Strategy(std::string message)
{
LockGuard lock(_mutex);
std::ofstream out(_filepath.c_str(),std::ios::app);
if(out.is_open())
{
out.write(message.c_str(),message.size());
}
out.close();
}
private:
std::string _path;
std::string _name;
std::string _filepath;
Mutex _mutex;
};
当客户端调用Strategy方法时,程序会在运行时根据指针实际指向的对象类型,动态地决定调用哪个子类的Strategy方法。这就是动态多态(或运行时多态)。
不明白C++多态的铁子们可以进入这个文章呀:
https://blog.csdn.net/yue_2899799318/article/details/151690169
到这里我们的两个刷新方法就写完了为了方便后续的使用,我们可以再封装一个Logger类,其中存在一个基类的回调指针,后面我们可以根据自已的需求设置回调函数指向ConsoleLogStrategy或FileLogStrategy从而灵活地选择两种刷新方式:
cpp
class Logger
{
public:
Logger()
{
Enable_ConsoleLogStrategy();
}
void Enable_FileLogStrategy()
{
_fflush_LogStrategy=std::make_unique<FileLogStrategy>();
}
void Enable_ConsoleLogStrategy()
{
_fflush_LogStrategy=std::make_unique<ConsoleLogStrategy>();
}
~Logger()=default;
private:
std::unique_ptr<LogStrategy> _fflush_LogStrategy;
};
1.2.3 日志格式的实现
日志的核心价值在于可被理解、可被分析,而统一的格式是实现这一价值的关键前提。如果日志消息格式混乱(比如时间戳格式不统一、关键信息缺失、字段顺序杂乱),不仅开发者难以快速定位问题,自动化工具也无法高效解析和分析日志数据。因此,日志格式的规范化是日志系统设计的基础环节。
在上述内容中我们主要实现了日志的刷新方式,其功能是将一个字符串刷新到指定的文件中或者显示器上。但作为一个可被理解、可被分析的日志功能,我们对刷新的一个字符串的格式又做了相应的格式定义,具体说明如下图:

所以我们可以在logger中封装一个内部类log_message用来详细地定义一条日志地格式内容:
为什么将log_message设置成logger的内部类??
log_message是专门为logger服务的(它的功能是组装日志内容、调用logger的日志策略输出),和logger的业务强绑定。作为内部类,能明确体现 "log_message是logger的一部分" 的关系,让代码结构更清晰。
log_message不需要被logger之外的代码直接使用(它是日志功能的 "内部实现细节")。作为内部类,可以把它的作用域限制在logger内部,避免外部代码随意创建log_message实例,减少了接口暴露,符合 "高内聚、低耦合" 的设计原则。
简单说:log_message是logger的 "专属工具",放在内部能让逻辑更内聚、访问更方便、封装更彻底
cpp
enum LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string Getlevel(LogLevel& level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKNOW";
}
}
std::string Getcurr_time()
{
time_t tm = time(nullptr);
struct tm curr;
localtime_r(&tm, &curr);
// 这⾥如果不好看,可以考sprintf
// ⽅法1
// std::stringstream ss;
// ss << curr.tm_year + 1900 << "-" << curr.tm_mon << "-" << curr.tm_mday << " "
// << curr.tm_hour << ":" << curr.tm_min << ":" << curr.tm_sec;
// return ss.str();
// ⽅法2
char timebuffer[64];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
curr.tm_year + 1900,
curr.tm_mon,
curr.tm_mday,
curr.tm_hour,
curr.tm_min,
curr.tm_sec);
return timebuffer;
}
class Logger
{
public:
Logger()
{
Enable_ConsoleLogStrategy();
}
void Enable_FileLogStrategy()
{
_fflush_LogStrategy=std::make_unique<FileLogStrategy>();
}
void Enable_ConsoleLogStrategy()
{
_fflush_LogStrategy=std::make_unique<ConsoleLogStrategy>();
}
class log_message
{
public:
log_message(LogLevel level,std::string filename,int linenum,Logger& logger)
:_level(Getlevel(level))
,_curr_time(Getcurr_time())
,_pid(getpid())
,_filename(filename)
,_linenum(linenum)
,_logger(logger)
{
//合并左半部分
std::stringstream ss;
ss<<"["<<_curr_time<<"] "
<<"["<<_level<<"] "
<<"["<<_pid<<"] "
<<"["<<_filename<<"] "
<<"["<<_linenum<<"] "
<<"- ";
_tatolmessage=ss.str();
}
template<class T>
log_message& operator <<(const T& message)
{
std::stringstream ss;
ss<<message;
_tatolmessage+=ss.str();
return *this;
}
~log_message()
{
_logger._fflush_LogStrategy->Strategy(_tatolmessage);
}
private:
std::string _level;
std::string _curr_time;
pid_t _pid;
std::string _filename;
int _linenum;
std::string _tatolmessage;
Logger& _logger;
};
~Logger()=default;
log_message operator ()(LogLevel level,std::string filename,int linenum)
{
//返回一个匿名对象:
return log_message(level,filename,linenum,*this);
}
private:
std::unique_ptr<LogStrategy> _fflush_LogStrategy;
};
在log_messager中我们分别定义了日志左半部分的各个属性字段,其中日志的等级我们使用枚举的方式实现,因为我们需要一个具体等级的字符串所以我们又定义了Getlevel函数用来将对应的枚举类型转化为对应的字符串_level。
在log_messager的构造函数中就将各个属性字段分别设置并合并为一个字符串 _tatolmessage。在后面我们重定义了<<用来将日志的内容追加到 _tatolmessage后面,这里需要注意的是重定义<<时需要使用模板,这么做的原因是用户使用日志功能的时候传进来的日志内容并不一定是字符串类型,也有可能是整形或者其他类型。
cpp
template<class T>
log_message& operator <<(const T& message)
{
std::stringstream ss;
ss<<message;
_tatolmessage+=ss.str();
return *this;
}
当一个log_messager(一个完整的日志信息)析构的时候就会自动调用外部类logger的Strategy函数用来将一条完整的日志信息刷新到指定文件或者屏幕上。
在logger中我们重定义了()用来创建一个log_message的临时对象,临时对象的生命周期只在当前一行。这样我们就可以将一个日志信息实时进行刷新:
cpp
log_message operator ()(LogLevel level,std::string filename,int linenum)
{
//返回一个匿名对象:
return log_message(level,filename,linenum,*this);
}
此时在用户程序中通过这条语句
cpp
logger(level,__FILE__,__LINE__)<<"日志内容";
__FILE__和__LINE__是 C/C++ 编译器内置的预定义宏 ,作用是在编译阶段自动插入当前代码的文件路径 和行号,
就可以完成一条完整日志信息的组装与刷新的流程,具体要点如下图:

为了方便调用与设置相应的刷新方式我们可以定义三个宏函数,并声明一个全局的logger对象用来实时创建并刷新对应的日志信息:
cpp
Logger logger;
#define LOG(level) logger(level,__FILE__,__LINE__)
#define Enable_Console_Log_Strategy() logger.Enable_ConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.Enable_FileLogStrategy()
在用户程序我们可以直接使用如下语句来使用日志功能:
cpp
int main()
{
//将刷新方式设置为刷新到屏幕
Enable_Console_Log_Strategy();
LOG(LogLevel::FATAL)<<"text!!!";
return 0;
}

cpp
int main()
{
//将刷新方式设置为刷新到指定文件
Enable_File_Log_Strategy();
LOG(LogLevel::FATAL)<<"text!!!";
return 0;
}
