公司有一个QT程序,由于是小工具随便写写,没有做日志系统。后期用户越来越多了,莫名其妙的崩溃经常出现,缺乏日志系统导致问题无法排查。
于是同事简单写了一个:
std::string log_str = "this is log msg";
CLog::getInstance().writeLog(log_str , LOG_INFO);
这种写法比较不爽,打印一行log需要两行代码,需要先把字符串声明出来,然后再把字符传入log函数。
如果打两行日志,还需要声明log_str1
logstr2
之类的奇怪东西,否则有天前面的日志不用了,删掉了log_str
的声明,会导致编译不过。
去查看了writeLog的函数实现:
void CLog::writeLog(std::string& strlog, logLevel level)
{
int tid = (int)QThread::currentThreadId();
QDateTime current_date_time = QDateTime::currentDateTime();
QString current_date = current_date_time.toString("yyyy-MM-dd");
QString current_time = current_date_time.toString("hh:mm:ss.zzz");
std::string levelStr("Info");
switch(level)
{
//区分日志级别的操作
}
g_logMutex.lock();
m_logFile << "[" + current_date.toStdString() \
+ " " + current_time.toStdString() \
+ "] tid[" \
<< tid << "] [" << levelStr << "]: " << strlog << std::endl;
g_logMutex.unlock();
}
有使用互斥锁来保证多线程打印日志的时候不会混打,但是传入的字符串不带const
的话,就不支持下面这种写法。
CLog::getInstance().writeLog("some log msg" , LOG_INFO);
我理想中的日志系统,当然是流式打印,直接一行搞定要打的东西和需要输出的变量。
LOG_INFO() << "some log msg x = " << x << " y = " << y;
单例模式避免反复构造Log对象
一个合格log类必须避免每次都重新对log进行init,需要实现单例模式保存一个全局的log类对象
static Logger* sInstance = 0;
Logger& Logger::instance()
{
if (!sInstance)
sInstance = new Logger;
return *sInstance;
}
如此,每次调用只需要Logger::instance().xxx
就能调用对应的功能。初始化也只需要做一次。
写一个LOG_INFO()头部
LOG_INFO()
类似一个cout
,接收一个输入流,并将日志输出出来,那流输出进cout
之后发生了什么呢,下面分析一种常用的打印。
std::cout << "hello world";
cout
首先是一个类ostream的一个对象,而这个对象有一个成员重载运算符函数:operator <<
//iostream
class ostream
{
public:
ostream operator << (int n){输出n}
ostream operator << (double n){输出n}
...
}
ostream cout;
这种写法保证了 cout<<"hello"
的调用方式,但是要使用cout << "hello" << " world"
这种一直往后的流式调用。
在ostream::operator <<
里还有一句return *this
。当执行语句cout<<a<<b<<c
;时,先执行cout.operator<<(a)
,然后这个函数会返回cout
,其实就是返回自己本身,然后再去执行cout.operator<<(b)
,然后又会去执行下一个函数,以此类推。
所以一个log对象,应该有一个可以不断新增内容的buffer。直到析构时再去输出
class Helper {
public:
explicit Helper(Level logLevel) :
level(logLevel) {}
~Helper() {
writeToLog();
};
std::ostream &stream() {
return ss;
}
private:
void writeToLog() {
std::cout << ss.str();
}
Level level;
std::stringstream ss;
};
做一个宏定义快速调用log
为了实现开头这种调用,需要做一些宏定义,快速生成不同日志级别的日志对象
#define LOG_INFO Logger(InfoLevel).stream()
至于stream,如前文所说,返回一个可以累加的buffer,案例中使用stringstream
class Logger {
public:
explicit Logger(Level logLevel) :
level(logLevel) {}
~Logger() {
writeToLog();
};
std::ostream &stream() {
return ss;
}
private:
void writeToLog() {
std::cout << ss.str();
}
Level level;
std::stringstream ss;
};
当多个输入流传入的时候,log对象不断将传进来的东西存在stringstream
中
析构时将日志存入文件,实例中是输出到屏幕
完整示例
#include "ostream"
#include "iostream"
#include "sstream"
enum Level {
InfoLevel
};
#define LOG_INFO Logger(InfoLevel).stream()
class Logger {
public:
explicit Logger(Level logLevel) :
level(logLevel) {}
~Logger() {
writeToLog();
};
std::ostream &stream() {
return ss;
}
private:
void writeToLog() {
std::cout << ss.str();
}
Level level;
std::stringstream ss;
};
int main() {
LOG_INFO << "hello " << "world";
return 0;
}
非常优雅的流式日志输出就ok了
参考:
C++流:练习写一个“日志流” - 知乎 (zhihu.com)
代码参考:
GitHub - victronenergy/QsLog: Forked from https://bitbucket.org/razvanpetru/qt-components/wiki/QsLog