基本思路:
日志类提供3个基本的接口(成员函数):
- Start() 启动日志组件
- Stop() 停用日志组件
- Commit() 提交日志消息
类中维护一个日志消息队列和一个常驻线程;
使用者通过Start()函数开启一个常驻线程来从日志消息队列中取出日志并保存到磁盘文件中;
通过Commit()函数向日志消息队列中插入新的日志消息;
线程中总是定时轮询检查日志消息队列是否有新的日志,如果有新的日志则取出并以追加方式写到文件。
Talk is cheap. Show me the code
以下代码按照上述思路实现了一个简单的日志类(SimpleLogger)
相关注释已包含在代码中,该代码可以直接使用到应用程序中,但需要注意以下内容:
要求:编译需要支持C++11(及以上)标准
用法:
- 在程序(或业务)初始化阶段调用SimpleLogger::Start()函数,启动日志组件;
- 在希望打印日志的地方调用SimpleLogger::Commit()函数,向日志组件提交一条日志消息;
- 在程序(或业务)终止时调用SimpleLogger::Stop()函数,关闭日志组件。
注意事项:
该组件所有接口均为静态成员函数,无需实例化。
头文件(SimpleLogger.h)
//SimpleLogger.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include <queue>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
//简单日志类 SimpleLogger
//描述:实现了应用程序日志信息保存到磁盘文件中,所有接口均为静态,无需实例化
//要求:编译需要支持C++11(及以上)标准
//用法:
// *在程序(或业务)初始化阶段调用SimpleLogger::Start()函数,启动日志组件;
// *在希望打印日志的地方调用SimpleLogger::Commit()函数,向日志组件提交一条日志消息;
// *在程序(或业务)终止时调用SimpleLogger::Stop()函数,关闭日志组件。
class SimpleLogger
{
//内部结构定义
public:
//日志消息结构
typedef struct LogMsg
{
LogMsg(const time_t t = 0, const std::string txt = "") : time(t), text(txt) {}
time_t time; //消息提交的时间戳
std::string text; //消息文本内容
}*LogMsgPtr;
//构造&析构
protected:
//不允许类外实例化
SimpleLogger() {}
~SimpleLogger() { }
//公共接口
public:
//标识当前组件是否已停止
static bool IsStoped() { return m_bStoped; }
//启动日志组件
static bool Start(std::string strDirPath = "", size_t PollingInterval = 500);
//停止日志组件
static void Stop();
//提交一条日志消息
static void Commit(const char* pstrFormat, ...);
//通用接口
public:
//将time_t时间戳格式化为字符串文本,默认格式为"%Y-%m-%d %H:%M:%S"
static std::string FormatTimeToStr(time_t t, const char * szFormat = "%Y-%m-%d %H:%M:%S");
//获取当前模块所在文件夹路径(结尾带'\\')
static std::string GetCurModuleDir();
//获取当前模块的文件名(exe或dll名称,不带后缀名)
static std::string GetCurModuleName();
//内部接口
protected:
//线程函数 - 将消息队列中的日志取出并保存到磁盘文件中,常驻
static int fnPrintLog();
//私有数据
private:
static bool m_bStoped; //标识日志组件实例是否已停止
static size_t m_PollingInterval; //日志消息队列轮询间隔(单位ms)
static std::string m_strDirPath; //日志文件夹路径,将被用作日志文件根目录
static std::queue<LogMsg> m_MsgQue; //日志消息队列
static std::mutex m_mtxMsgQue; //日志消息队列访问互斥量
static std::condition_variable m_cvHaveMsg; //条件变量-日志消息队列种是否有新消息
static std::thread m_thPrintLog; //线程对象-将日志消息从队列取出,保存到文件
};
源文件(SimpleLogger.cpp)
//SimpleLogger.cpp
#include <atlstr.h>
#include <fstream>
#include <chrono>
#include <DbgHelp.h>
#pragma comment(lib,"DbgHelp.lib")
#include "SimpleLogger.h"
//静态成员初始化
bool SimpleLogger::m_bStoped = true;
size_t SimpleLogger::m_PollingInterval = 500;
std::string SimpleLogger::m_strDirPath = "";
std::queue<SimpleLogger::LogMsg> SimpleLogger::m_MsgQue;
std::mutex SimpleLogger::m_mtxMsgQue;
std::condition_variable SimpleLogger::m_cvHaveMsg;
std::thread SimpleLogger::m_thPrintLog;
//启动日志组件
//启动时需要指定日志文件夹根目录以及消息队列轮询间隔(ms)
//轮询间隔默认500ms,建议设置范围[50,1000]
bool SimpleLogger::Start(std::string strDirPath/* = ""*/, size_t PollingInterval/* = 500*/)
{
if (IsStoped())
{
m_bStoped = false;
m_PollingInterval = PollingInterval;
m_strDirPath = strDirPath;
//若未指定根目录则默认根目录将设为当前模块所在目录
if (m_strDirPath.empty())
m_strDirPath = SimpleLogger::GetCurModuleDir();
//追加结尾的'\\'
if (m_strDirPath.back() != '\\')
m_strDirPath += "\\";
//创建并启动一个保存日志到文件的线程
m_thPrintLog = std::move(std::thread(fnPrintLog));
}
return true;
}
//停止日志组件
void SimpleLogger::Stop()
{
if (!IsStoped())
{
{
//将m_bStoped置true表示希望停用组件,唤醒所有日志线程
std::unique_lock<std::mutex> lock(m_mtxMsgQue);
m_bStoped = true;
}
//唤醒所有日志线程,让它们自己正常结束
m_cvHaveMsg.notify_all();
m_thPrintLog.join();
}
}
//提交一条日志消息
void SimpleLogger::Commit(const char * pstrFormat, ...)
{
va_list Args;
va_start(Args, pstrFormat);
int nLen = _vsntprintf(nullptr, 0, pstrFormat, Args); //先得到日志消息文本的长度
char *szBuffer = new char[nLen + 1];
ZeroMemory(szBuffer, (nLen + 1) * sizeof(char));
nLen = _vsntprintf(szBuffer, nLen + 1, pstrFormat, Args); //格式化日志消息
{
//向队列中插入一条新的日志
std::unique_lock<std::mutex> lock(m_mtxMsgQue);
m_MsgQue.push(LogMsg(::time(nullptr), szBuffer));
//m_cvHaveMsg.notify_one();
}
delete[] szBuffer;
va_end(Args);
}
//将time_t时间戳格式化为字符串文本,默认格式为"%Y-%m-%d %H:%M:%S"
std::string SimpleLogger::FormatTimeToStr(time_t t, const char * szFormat/* = "%Y-%m-%d %H:%M:%S"*/)
{
if (!szFormat)
return "";
if (t < 0)
t = 0;
char szTime[40] = { 0 };
struct tm local_time;
localtime_s(&local_time, &t);
strftime(szTime, sizeof(szTime), szFormat, &local_time);
return std::string(szTime);
}
//获取当前模块所在文件夹路径(结尾带'\\')
std::string SimpleLogger::GetCurModuleDir()
{
char szModuleName[MAX_PATH] = { 0 };
::GetModuleFileNameA(nullptr, szModuleName, sizeof(szModuleName));
std::string strModuleName(szModuleName);
size_t pos = strModuleName.find_last_of('\\');
return strModuleName.substr(0, pos + 1);
}
//获取当前模块的文件名(exe或dll文件名称,不带后缀名)
std::string SimpleLogger::GetCurModuleName()
{
char szModuleName[MAX_PATH] = { 0 };
::GetModuleFileNameA(nullptr, szModuleName, sizeof(szModuleName));
std::string strModuleName(szModuleName);
size_t pos = strModuleName.find_last_of('\\');
strModuleName.erase(0, pos + 1);
return strModuleName.substr(0, strModuleName.size() - 4);
}
//线程函数 - 将消息队列中的日志取出并保存到磁盘文件中,常驻
int SimpleLogger::fnPrintLog()
{
OutputDebugString(_T("MyLogger:日志线程已启动."));
//构造日志文件夹路径(文件夹根目录 + 当前模块名称-log\\)
std::string strLogDirPath = m_strDirPath + SimpleLogger::GetCurModuleName() + "-log\\";
MakeSureDirectoryPathExists(strLogDirPath.data());
std::string strCurLogFilePath; //当前日志保存路径
std::ofstream ofsLog; //日志文件流对象
ofsLog.imbue(std::locale("chs"));
//不断地将日志取出保存到文件,直到外部使用者关闭了组件
while (true)
{
//保存将要从消息队列中取出的日志
LogMsg msg;
{
//每隔m_PollingInterval(ms)检查当前消息队列中是否有新日志,若有新日志,则取出保存到文件,否则挂起m_PollingInterval(ms)
std::unique_lock<std::mutex> lock(m_mtxMsgQue);
if (!m_cvHaveMsg.wait_for(lock, std::chrono::milliseconds(m_PollingInterval),
[] { return IsStoped() || !m_MsgQue.empty(); }))
continue;
//若m_bStoped为true,则代表外部停止了组件,应退出线程
if (IsStoped() && m_MsgQue.empty())
break;
//否则队列中还有待保存的日志,取出一条日志
msg = std::move(m_MsgQue.front());
m_MsgQue.pop();
}
//写入当前日志文件
std::string strLogFilePath = strLogDirPath + FormatTimeToStr(msg.time, "%Y-%m-%d.log");
if (strCurLogFilePath != strLogFilePath)
{
//当前日志消息提交的时间属于新的一天,应关闭先前的日志文件,并将新的日志消息保存到新一天的日志文件中
strCurLogFilePath = strLogFilePath;
if (ofsLog.is_open())
ofsLog.close();
MakeSureDirectoryPathExists(strLogFilePath.data());//创建今天的日志文件
ofsLog.open(strLogFilePath, std::ios_base::out | std::ios_base::app);
}
//将日志消息提交的时间和文本内容保存到文件
ofsLog << FormatTimeToStr(msg.time) << "\t" << msg.text << std::endl;
}
//关闭当前打开的日志文件
if (ofsLog.is_open())
ofsLog.close();
OutputDebugString(_T("MyLogger:日志线程已停止."));
return 0;
}