为什么实现日志?
- 通过日志可以避免直接在屏幕打印错误信息,当信息过多时无法精准定位
- 我们可以将日志的信息进行类型划分,正常的消息信息写入INFO,错误的运行问题写入ERROR
- 由此帮助我们在程序运行错误、项目代码优化调整时提供参照依据
- 帮助我们保存运行记录
如何实现?
在Rpc框架的调用过程中,我们需要引入一个消息队列(中间件)进行异步传输日志内容,因为写入日志的过程为磁盘 I/O ,避免拖慢Rpc执行的效率,我们直接将执行中的信息写入消息队列,再由消息队列将信息写入日志文件
需要注意:
多工作线程可能同时写入消息队列,需要保障线程安全!
消息队列为空时,消息队列写出至日志文件的线程需要等待唤醒
#pragma once
#include"lockqueue.h"
#include<string>
enum Loglevel
{
INFO,//普通消息
ERROR,//错误信息
};
//定义宏,直接调用具体的实现日志记录,不需要用户自行调用函数接口
#define LOG_INFO(logmsgformat, ...) \
do \
{ \
Logger &logger = Logger::GetInstance(); \
logger.SetLogLevel(INFO); \
char c[1024] = {0}; \
snprintf(c, 1024, logmsgformat, ##__VA_ARGS__); \
logger.Log(c); \
} while(0)
#define LOG_ERROR(logmsgformat, ... ) \
do \
{ \
Logger &logger=Logger::GetInstance();\
logger.SetLogLevel(ERROR);\
char c[1024]={0};\
snprintf(c,1024,logmsgformat,##__VA_ARGS__);\
logger.Log(c);\
} while(0)
// 定义宏 LOG_INFO("xxx %d %s", 20, "xxxx")
class Logger{
public:
//获取单例对象
static Logger& GetInstance();
//设置日志级别
void SetLogLevel(Loglevel level);
//写日志
void Log(std::string msg);
private:
int m_loglevel;//日志级别
LockQueue<std::string>m_locQue;//日志队列缓冲区
Logger();//单例模式只初始化一次
Logger(const Logger&)=delete;
Logger(const Logger&&)=delete;
};
代码解析:
在定义了logger类的完整功能(初始化、获取实例、写入日志、写入日志级别)
定义了宏LOG_INFO \ LOG_ERROR,帮助用户直接调用宏,便可以根据参数将信息写入日志:
LOG_ERROR(" request parse error! content is: %s",args_str.c_str());
直接通过宏定义方法,将(获取实例、写入消息类型、构建字符串、写入日志)的方法封装成集合,通过“logmsgformat, ...”获取输入的字符串、可变参,并加入当前时刻信息构建日志内容。
#include "logger.h"
#include"time.h"
#include<iostream>
Logger::Logger()
{
//启动线程:写入日志文件
std::thread writeLogTask([&](){
for(;;)
{
time_t now=time(nullptr);
tm* nowtm=localtime(&now);
char file_name[128];
sprintf(file_name,"%d-%d-%d-log.txt",nowtm->tm_year+1900,nowtm->tm_mon+1,nowtm->tm_mday);
FILE*pf=fopen(file_name,"a+");
if(pf==nullptr)
{
//文件打开失败
std::cout<<"logger file:"<<file_name<<"open error!"<<std::endl;
exit(EXIT_FAILURE);
}
std::string msg=m_locQue.Pop();
char time_buf[128]={0};
//为每一条日志记录加入具体时间:
snprintf(time_buf,128,"%d:%d:%d->[%s] :",nowtm->tm_hour,nowtm->tm_min,nowtm->tm_sec,
(m_loglevel==INFO?"INFO":"ERROR"));
msg.insert(0,time_buf);
msg.append("\n");//尾部添加换行
fputs(msg.c_str(),pf);
fclose(pf);
}
});
// 设置分离线程,守护线程
writeLogTask.detach();
}
// 设置日志级别
void Logger::SetLogLevel(Loglevel level)
{
m_loglevel=level;
}
// 写日志
void Logger::Log(std::string msg)
{
m_locQue.Push(msg);//将信息写入缓冲区lockqueue
}
// 获取单例对象
Logger &Logger::GetInstance()
{
static Logger logger;
return logger;
}
消息队列:
消息队列由于使用了模板类型,无法分为.h \.cc 文件,在该类中主要实现写入消息队列、写出消息队列并写入日志的两个功能
#pragma once
#include<queue>
#include<thread>
#include<mutex>//互斥锁
#include<condition_variable>//条件变量
//异步写入日志
template<typename T>
class LockQueue
{
public:
void Push(const T& data)
{
std::lock_guard<std::mutex>lock(m_mutex);//lock_guard上锁,生命周期在该大括号内,出了括号自动析构该对象,解锁
m_queue.push(data);
//写入数据后,唤醒写出线程,将数据写入日志文件
m_condvariable.notify_one();
}
T Pop()
{
std::unique_lock<std::mutex>lock(m_mutex);
while(m_queue.empty())
{
//消息队列空,线程进入wait 状态
m_condvariable.wait(lock);
}
T data=m_queue.front();
m_queue.pop();
return data;
}
private:
std::queue<T> m_queue;
std::mutex m_mutex;
std::condition_variable m_condvariable;
};
代码解析:
成员数据:队列、锁、进程同步原语
Push:
- 在多线程写入消息队列的时候,我们为确保线程安全、同一时刻只允许一个线程写入消息队列,我们在Push的开始前上锁,以mutex为参数创建lock_guard对象,该对象构造函数自动上锁,该锁在}后出生命周期后,lock_guard对象生命周期结束,自动调用析构函数解锁;
- 继而我们将信息内容写入消息队列,信息类型由具体传入参数决定
- 在当前消息队列内不为空的时刻,我们可以唤醒另一等待线程(若无阻塞线程,该语句无效),将消息队列问题写入日志文件
Pop:
- 保障同一时刻仅有一个线程写入日志文件,上锁(同上)
- 判断队列是否为空,空则进入阻塞状态等待队列内由写入时被唤醒,在阻塞期间,其他线程由于Pop开始时的锁未解开,无法进入Pop方法,则同一时刻只有洗个线程被阻塞并等待唤醒
- 队列非空,则获取对头信息并出队,将信息写入日志文件