写一个流式日志输出

写一个流式日志输出

Louis 227 2022-07-27

公司有一个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