项目中基本都需要日志系统,通过日志系统来查询问题,目前也有很多成熟的日志库,比如log4cpp,glog等等,这里想自己动手实现一个c++的日志库。
日志级别
日志系统中最基础的是日志级别,不同级别的日志用来表示不同的处理方式,这里设计4个不同的日志级别,通过枚举来表示不同的日志级别。
typedef enum LogLevel
{
INFO = 0,
DEBUG,
ERROR,
FATAL
}LogLevel;//日志等级
任务结构
我们需要确定日志输出的任务结构内容,根据实际需求来确定输出内容,正常情况下日志输出的一条内容包含时间,任务等级,文件名,行数,函数,内容
时间:表示输出内容的时间节点
任务等级:用于表示任务的输出级别
文件名:输出内容在哪个文件中,方便定位
行数:输出内容在文件的哪一行,进一步定位
函数:输出内容是由哪个函数进行输出的,定位到具体函数
内容:输出具体内容
比如:
2024-3-1 15:34:44:442|INFO|C:\Users\xwh87\Desktop\AsyncLog\AsyncLog\AsyncLogger.hpp:175|[AsyncLogger::write]0
具体的任务结构如下:
typedef struct LogRecord
{
LogLevel level; //日志等级
std::string current_time;//当前时间
std::string file_name;//文件名
int line;//行数,文件的第几行
std::string func_name;//函数名
std::string content;//输出内容
}LogRecord;//日志输出内容
日志设计
1.专门启动一个线程用于维护日志内容,所有的任务塞入专门的队列,线程从队列中去读取,如果队列不为空的时候,则将内容从队列中取出来,然后写入文件。如果队列为空的时候,则等待。
2.维护一个文件,对文件进行写操作,这里不需要对文件进行读操作,c++中对文件进行写操作可以使用std::ofstream
3.前面定义的日志等级用枚举进行表示,希望输入到文件中的时候以字符串的形式进行输出,方便查看,比如LogLevel::INFO,输出到文件中是INFO,而不是数字1,字符串INFO更加直观,这里通过std::map映射,使其以字符串的形式输出到文件
4.C++获取当前时间,可以使用时间库std::chrono,c++中貌似没有像QT那样QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss");直接获取当前时间并转换成字符串形式输出,需要自己做处理
class AsyncLog
{
public:
AsyncLog():m_stop(false)
{
m_map[LogLevel::INFO] = "INFO";
m_map[LogLevel::DEBUG] = "DEBUG";
m_map[LogLevel::ERROR] = "ERROR";
m_map[LogLevel::FATAL] = "FATAL";
m_thread = std::thread(&AsyncLog::LogThread, this);
}
~AsyncLog()
{
m_stop.store(true);
m_cv.notify_one();
if (m_thread.joinable())
m_thread.join();
}
//open打开文件,提供两种方式
bool open(std::string &filepath)
{
m_stream.open(filepath);
return m_stream.is_open();
}
bool open(const char* filepath)
{
m_stream.open(filepath,std::ios_base::out);
return m_stream.is_open();
}
//将日志记录内容塞入队列中,当队列中有数据的时候,唤醒一个线程
void push(LogRecord &record)
{
std::unique_lock<std::mutex> lk(m_mutex);
m_queue.push(record);
lk.unlock();
m_cv.notify_one();
}
private:
//线程执行的函数
//会实时去获取队列中是否有数据,如果有数据的话,就取出来,然后将里面的内容转换成字符串输出
void LogThread()
{
while (!m_stop)
{
std::unique_lock<std::mutex> lk(m_mutex);
m_cv.wait(lk, [&]()
{
return !m_queue.empty() || m_stop.load();
});
while (!m_queue.empty())
{
//从队列中取出数据
LogRecord record = m_queue.front();
m_queue.pop();
//std::ostringstream是字符集操作模板类,用于执行串行输出
std::ostringstream os;
os << record.current_time
<< "|"
<< LevelToStr(record.level)
<< "|"
<< record.file_name
<<":"
<< record.line
<< "|"<<"["
<<record.func_name
<<"]"
<<record.content << std::endl;
//先将os的内容转成str的形式在转成c语言格式char*写入文件
m_stream.write(os.str().c_str(),strlen(os.str().c_str()));
}
}
}
//将任务等级转换成字符串形式进行输出
std::string LevelToStr(LogLevel level) const
{
return m_map.at(level);
}
private:
std::thread m_thread; //日志线程
std::atomic_bool m_stop; //线程是否启动
std::mutex m_mutex;//互斥锁,跟条件变量配合使用
std::condition_variable m_cv;//条件变量,用于唤醒会通知队列
std::queue<LogRecord> m_queue;//任务队列
std::ofstream m_stream;//用于文件的写操作,文件读操作std::ifstream
std::map<LogLevel, std::string> m_map;//用于映射日志等级
};
日志调用
上述的日志设计完成之后,最后在设计一个专门调用的日志类,在往日志中写数据的时候,每次都是往一个文件中写,因为将该类设计为一个单例模式,确保只能有一个实例对象调用,外部在调用的时候需要写明日志等级,文件名,行数,函数等内容信息。
class AsyncLogger
{
public:
AsyncLogger(const AsyncLogger&) = delete;
AsyncLogger& operator = (const AsyncLogger&) = delete;
static AsyncLogger& GetInstance()
{
static AsyncLogger instance;
return instance;
}
bool open(std::string &filepath)
{
return m_async_log.open(filepath);
}
bool open(const char* filepath)
{
return m_async_log.open(filepath);
}
//往日志中写内容
void write(LogLevel level,std::string file_name,int line,std::string func_name ,std::string content)
{
//获取系统时间
auto now = std::chrono::system_clock::now();
auto now_time_t = std::chrono::system_clock::to_time_t(now);
auto local_time = std::localtime(&now_time_t);
//获取毫秒,如果不需要获取到毫秒级别,可以不用
auto duration_since_epoch = std::chrono::system_clock::now().time_since_epoch();
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(duration_since_epoch).count() % 1000;
std::ostringstream time;
time << 1900 + local_time->tm_year << '-'
<< 1 + local_time->tm_mon << '-'
<< local_time->tm_mday << ' '
<< local_time->tm_hour << ':'
<< local_time->tm_min << ':'
<< local_time->tm_sec << ':'
<< millis;
//日志记录中的内容
LogRecord record;
record.level = level; //日志等级
record.content = content;//日志输出的内容
record.current_time = time.str();//当前时间
record.func_name = func_name;//函数名
record.line = line;//行号
record.file_name = file_name;//文件名
m_async_log.push(record);//将日志记录的内容塞入队列
}
private:
AsyncLogger() = default;
private:
AsyncLog m_async_log;
};
上述代码我在VS2019Z中编译的时候,报错,提示localtime这个函数不是一个线程安全的函数,这里我从网上查找的解决办法是在文件中添加#pragma warning(disable:4996)就编译通过了
我们在调用日志写内容的时候,可以这样:
//在当前文件夹打开1.log这个文件,如果没有则创建这个文件
AsyncLogger::GetInstance().open("1.log");
//往日志中写入内容为log test,级别是INFO
//__FILE__获取文件名
//__LINE__获取行号
//__FUNCTION__获取函数名
AsyncLogger::GetInstance().write(LogLevel::INFO,__FILE__,__LINE__,__FUNCTION__,"log test");
这样调用的时候非常麻烦,每次都需要写很多参数,因此调用写日志的write函数用宏稍微封装一下,这样就不用每次去写函数名,行号等信息,只需要写输出内容即可。
即LOGINFO("log test");这样调用就简单很多
#ifndef LOGINFO
#define LOGINFO(Format) \
AsyncLogger::GetInstance().write(LogLevel::INFO,__FILE__,__LINE__,__FUNCTION__,Format);
#endif
#ifndef LOGDEBUG
#define LOGDEBUG(Format) \
AsyncLogger::GetInstance().write(LogLevel::DEBUG,__FILE__,__LINE__,__FUNCTION__,Format);
#endif
#ifndef LOGERROR
#define LOGERROR(Format) \
AsyncLogger::GetInstance().write(LogLevel::ERROR,__FILE__,__LINE__,__FUNCTION__,Format);
#endif
#ifndef LOGFATAL
#define LOGFATAL(Format) \
AsyncLogger::GetInstance().write(LogLevel::FATAL,__FILE__,__LINE__,__FUNCTION__,Format);
#endif
完整代码
#pragma once
#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>
#include <fstream>
#include <atomic>
#include <sstream>
#include <map>
#include <chrono>
#include <ctime>
#pragma warning(disable:4996)
#ifndef LOGINFO
#define LOGINFO(Format) \
AsyncLogger::GetInstance().write(LogLevel::INFO,__FILE__,__LINE__,__FUNCTION__,Format);
#endif
#ifndef LOGDEBUG
#define LOGDEBUG(Format) \
AsyncLogger::GetInstance().write(LogLevel::DEBUG,__FILE__,__LINE__,__FUNCTION__,Format);
#endif
#ifndef LOGERROR
#define LOGERROR(Format) \
AsyncLogger::GetInstance().write(LogLevel::ERROR,__FILE__,__LINE__,__FUNCTION__,Format);
#endif
#ifndef LOGFATAL
#define LOGFATAL(Format) \
AsyncLogger::GetInstance().write(LogLevel::FATAL,__FILE__,__LINE__,__FUNCTION__,Format);
#endif
typedef enum LogLevel
{
INFO = 0,
DEBUG,
ERROR,
FATAL
}LogLevel; //日志等级
typedef struct LogRecord
{
LogLevel level; //日志等级
std::string current_time;//当前时间
std::string file_name;//文件名
int line;//行数,文件的第几行
std::string func_name;//函数名
std::string content;//输出内容
}LogRecord;//日志输出内容
class AsyncLog
{
public:
AsyncLog():m_stop(false)
{
m_map[LogLevel::INFO] = "INFO";
m_map[LogLevel::DEBUG] = "DEBUG";
m_map[LogLevel::ERROR] = "ERROR";
m_map[LogLevel::FATAL] = "FATAL";
m_thread = std::thread(&AsyncLog::LogThread, this);
}
~AsyncLog()
{
m_stop.store(true);
m_cv.notify_one();
if (m_thread.joinable())
m_thread.join();
}
bool open(std::string &filepath)
{
m_stream.open(filepath);
return m_stream.is_open();
}
bool open(const char* filepath)
{
m_stream.open(filepath,std::ios_base::out);
return m_stream.is_open();
}
void close()
{
m_stream.close();
}
void push(LogRecord &record)
{
std::unique_lock<std::mutex> lk(m_mutex);
m_queue.push(record);
lk.unlock();
m_cv.notify_one();
}
private:
void LogThread()
{
while (!m_stop)
{
std::unique_lock<std::mutex> lk(m_mutex);
m_cv.wait(lk, [&]()
{
return !m_queue.empty() || m_stop.load();
});
while (!m_queue.empty())
{
LogRecord record = m_queue.front();
m_queue.pop();
std::ostringstream os;
os << record.current_time
<< "|"
<< LevelToStr(record.level)
<< "|"
<< record.file_name
<<":"
<< record.line
<< "|"<<"["
<<record.func_name
<<"]"
<<record.content << std::endl;
m_stream.write(os.str().c_str(),strlen(os.str().c_str()));
}
}
}
std::string LevelToStr(LogLevel level) const
{
return m_map.at(level);
}
private:
std::thread m_thread;
std::atomic_bool m_stop;
std::mutex m_mutex;
std::condition_variable m_cv;
std::queue<LogRecord> m_queue;
std::ofstream m_stream;
std::map<LogLevel, std::string> m_map;
};
class AsyncLogger
{
public:
AsyncLogger(const AsyncLogger&) = delete;
AsyncLogger& operator = (const AsyncLogger&) = delete;
static AsyncLogger& GetInstance()
{
static AsyncLogger instance;
return instance;
}
bool open(std::string &filepath)
{
return m_async_log.open(filepath);
}
bool open(const char* filepath)
{
return m_async_log.open(filepath);
}
void write(LogLevel level,std::string file_name,int line,std::string func_name ,std::string content)
{
auto now = std::chrono::system_clock::now();
auto now_time_t = std::chrono::system_clock::to_time_t(now);
auto local_time = std::localtime(&now_time_t);
auto duration_since_epoch = std::chrono::system_clock::now().time_since_epoch();
auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(duration_since_epoch).count() % 1000;
std::ostringstream time;
time << 1900 + local_time->tm_year << '-'
<< 1 + local_time->tm_mon << '-'
<< local_time->tm_mday << ' '
<< local_time->tm_hour << ':'
<< local_time->tm_min << ':'
<< local_time->tm_sec << ':'
<< millis;
LogRecord record;
record.level = level;
record.content = content;
record.current_time = time.str();
record.func_name = func_name;
record.line = line;
record.file_name = file_name;
m_async_log.push(record);
}
private:
AsyncLogger()
{
}
~AsyncLogger()
{
}
private:
AsyncLog m_async_log;
};
调用:
int main()
{
AsyncLogger::GetInstance().open("1.log");
for (int i = 0; i < 100; i++)
{
std::ostringstream os;
os << i;
LOGINFO(os.str());
LOGDEBUG(os.str());
LOGERROR(os.str());
LOGFATAL(os.str());
}
return 0;
}