对于一个服务器而言,不论是在调试中还是在运行中,都需要通过打日志的方式来记录程序的运行情况。本文设计的日志系统实现了同步与异步两种功能,原理见下图:
同步日志:日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。
异步日志:将所写的日志内容先存入阻塞队列中,写线程从阻塞队列中取出内容,写入日志。
日志的运行流程:
1、使用单例模式(局部静态变量方法)获取实例Log::getInstance()。
2、通过实例调用Log::getInstance()->init()函数完成初始化,若设置阻塞队列大小大于0则选择异步日志,等于0则选择同步日志,更新isAysnc变量。
3、通过实例调用write_log()函数写日志,首先根据当前时刻创建日志(前缀为时间,后缀为".log",并更新日期today和当前行数lineCount。
4、在write_log()函数内部,通过isAsync变量判断写日志的方法:如果是异步,工作线程将要写的内容放进阻塞队列中,由写线程在阻塞队列中取出数据,然后写入日志;如果是同步,直接写入日志文件中。
日志的分级与分文件:
分级情况:
- Debug,调试代码时的输出,在系统实际运行时,一般不使用。
- Warn,这种警告与调试时终端的warning类似,同样是调试代码时使用。
- Info,报告系统当前的状态,当前执行的流程或接收的信息等。
- Erro,输出系统的错误信息
分文件情况:
- 按天分,日志写入前会判断当前today是否为创建日志的时间,若为创建日志时间,则写入日志,否则按当前时间创建新的log文件,更新创建时间和行数。
- 按行分,日志写入前会判断行数是否超过最大行限制,若超过,则在当前日志的末尾加lineCount / MAX_LOG_LINES为后缀创建新的log文件。
Log.h
#ifndef LOG_H
#define LOG_H
#include "blockqueue.h"
#include <mutex>
#include <thread>
#include "buffer.h"
#include <string>
#include <stdarg.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <assert.h>
class Log
{
public:
static Log* getInstance()
{
static Log instance;
return &instance;
}
//初始化日志实例(阻塞队列最大容量、日志保存路径、日志文件后缀)
void init(int maxQueueCapacity = 1024,
const char* path_="./log",
const char* suffix_=".log");
//异步写日志公有方法,调用私有方法asyncWrite
static void flushLogThread()
{
Log::getInstance()->asyncWrite();
}
//将输出内容按照标准格式整理
void writeLog(int level, const char* format, ...);
private:
Log();
~Log();
//异步写日志方法
void asyncWrite();
private:
const int LOG_NAME_LEN=256; //日志文件最长文件名
const int MAX_LOG_LINES=50000;//日志文件内的最长日志条数
const char* path; //路径名
const char* suffix; //后缀名
int lineCount; //日志行数记录
int today; //按当天日期区分文件
FILE* fp; //打开log的文件指针
Buffer buff; //输出的内容
std::unique_ptr<BlockQueue<std::string>> deque; //阻塞队列
std::unique_ptr<std::thread> writeThread; //写线程
bool isAsync; //是否开启异步日志
std::mutex mtx; //同步日志必需的互斥量
};
//四个宏定义,主要用于不同类型的日志输出
#define LOG_DEBUG(format, ...) Log::getInstance()->writeLog(0, format, ##__VA_ARGS__)
#define LOG_INFO(format, ...) Log::getInstance()->writeLog(1, format, ##__VA_ARGS__)
#define LOG_WARN(format, ...) Log::getInstance()->writeLog(2, format, ##__VA_ARGS__)
#define LOG_ERROR(format, ...) Log::getInstance()->writeLog(3, format, ##__VA_ARGS__)
#endif // !LOG_H
Log.cpp
#include "log.h"
Log::Log():lineCount(0),
today(0),
fp(nullptr),
deque(nullptr),
writeThread(nullptr),
isAsync(false){}
Log::~Log()
{
if(writeThread&&writeThread->joinable())
{
while(!deque->empty())//清空阻塞队列中的全部任务
{
deque->flush();
}
deque->close();
writeThread->join();//等待当前线程完成手中的任务
}
if(fp)//冲洗文件缓冲区,关闭文件描述符
{
std::lock_guard<std::mutex> lock(mtx);
fflush(fp);
fclose(fp);
}
}
void Log::init(int maxQueueCapacity,const char* path_,const char* suffix_)
{
if(maxQueueCapacity>0)//异步方式
{
isAsync=true;
if(!deque)
{
std::unique_ptr<BlockQueue<std::string>> newDeque(new BlockQueue<std::string>(maxQueueCapacity));
deque=std::move(newDeque);
std::unique_ptr<std::thread> newThread(new std::thread(flushLogThread));
writeThread=std::move(newThread);
}
}
else//同步方式
{
isAsync=false;
}
lineCount=0;
//生成日志文件名
time_t timer=time(nullptr);
struct tm* sysTime=localtime(&timer);
struct tm t=*sysTime;
path=path_;
suffix=suffix_;
char filename[LOG_NAME_LEN]={0};
snprintf(filename,LOG_NAME_LEN-1,"%s/%04d_%02d_%02d%s",
path,t.tm_year+1900,t.tm_mon+1,t.tm_mday,suffix);
today=t.tm_mday;
{
std::lock_guard<std::mutex> lock(mtx);
buff.retrieveAll();
if(fp)
{
fflush(fp);
fclose(fp);
}
fp=fopen(filename,"a");
if(fp==nullptr)
{
mkdir(path,0777);//先生成目录文件(最大权限)
fp=fopen(filename,"a");
}
assert(fp!=nullptr);
}
}
void Log::writeLog(int level, const char* format, ...)
{
struct timeval now={0,0};
gettimeofday(&now,nullptr);
time_t tSec=now.tv_sec;
struct tm* sysTime=localtime(&tSec);
struct tm t=*sysTime;
va_list vaList;
if(today!=t.tm_mday||(lineCount&&(lineCount%MAX_LOG_LINES==0)))
{
//生成最新的日志文件名
char newFile[LOG_NAME_LEN];
char tail[36]={0};
snprintf(tail,35,"%04d_%02d_%02d",t.tm_year+1900,t.tm_mon+1,t.tm_mday);
if(today!=t.tm_mday)//时间不匹配,则替换为最新的日志文件名
{
snprintf(newFile,LOG_NAME_LEN-1,"%s/%s%s",path,tail,suffix);
today=t.tm_mday;
lineCount=0;
}
else//长度超过日志最长行数,则生成xxx-1、xxx-2文件
{
snprintf(newFile,LOG_NAME_LEN-1,"%s/%s-%d%s",path,tail,(lineCount/MAX_LOG_LINES),suffix);
}
if(fp)
{
std::lock_guard<std::mutex> lock(mtx);
fflush(fp);
fclose(fp);
}
fp=fopen(newFile,"a");
assert(fp!=nullptr);
}
//在buffer内生成一条对应的日志信息
{
std::lock_guard<std::mutex> lock(mtx);
lineCount++;
//添加年月日时分秒微秒———"2022-12-29 19:08:23.406500"
int n=snprintf(buff.beginWrite(),128,"%04d-%02d-%02d %02d:%02d:%02d.%06ld ",
t.tm_year+1900,t.tm_mon+1,t.tm_mday,
t.tm_hour,t.tm_min,t.tm_sec,now.tv_usec);
buff.hasWritten(n);
//添加日志等级———"2022-12-29 19:08:23.406539 [debug]: "
switch(level)
{
case 0:
buff.append("[debug]: ", 9);
break;
case 1:
buff.append("[info] : ", 9);
break;
case 2:
buff.append("[warn] : ", 9);
break;
case 3:
buff.append("[error]: ", 9);
break;
default:
buff.append("[info] : ", 9);
break;
}
//添加使用日志时的格式化输入———"2022-12-29 19:08:23.535531 [debug]: Test 222222222 8 ============= "
va_start(vaList, format);
int m = vsnprintf(buff.beginWrite(), buff.writableBytes(), format, vaList);
va_end(vaList);
buff.hasWritten(m);
//添加换行符与字符串结尾
buff.append("\n\0", 2);
}
if(isAsync&&deque&&!deque->full())//异步方式(加入阻塞队列中,等待写线程读取日志信息)
{
deque->push_back(buff.retrieveAllAsString());
}
else//同步方式(直接向文件中写入日志信息)
{
std::lock_guard<std::mutex> lock(mtx);
fputs(buff.peek(),fp);
}
{//清理buffer缓冲区
std::lock_guard<std::mutex> lock(mtx);
buff.retrieveAll();
}
}
void Log::asyncWrite()
{
std::string str="";
while (deque->pop(str))
{
std::lock_guard<std::mutex> lock(mtx);
fputs(str.c_str(),fp);
}
}
测试程序:test.cpp
分别采用同步和异步方式,各写60000(15000*4)条日志信息。
#include "log.h"
void TestLog()
{
int cnt = 0;
Log::getInstance()->init(0,"./testlog1");//同步日志
for(int j = 0; j < 15000; j++ )
{
LOG_DEBUG("%s 111111111 %d ============= ", "Test", cnt++);
LOG_INFO ("%s 111111111 %d ============= ", "Test", cnt++);
LOG_WARN ("%s 111111111 %d ============= ", "Test", cnt++);
LOG_ERROR("%s 111111111 %d ============= ", "Test", cnt++);
}
cnt = 0;
Log::getInstance()->init(1024,"./testlog2");//异步日志
for(int j = 0; j < 15000; j++ )
{
LOG_DEBUG("%s 222222222 %d ============= ", "Test", cnt++);
LOG_INFO ("%s 222222222 %d ============= ", "Test", cnt++);
LOG_WARN ("%s 222222222 %d ============= ", "Test", cnt++);
LOG_ERROR("%s 222222222 %d ============= ", "Test", cnt++);
}
}
int main()
{
TestLog();
}
实验结果:
同步日志:
由于共写60000条日志,一份日志文件设置最大行数为50000,所以分为两个文件
异步日志:
参考资料:
最新版Web服务器项目详解 - 09 日志系统(上)
最新版Web服务器项目详解 - 10 日志系统(下)