C++造轮子之:日志库

1,040 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

C++程序员喜欢造轮子。这里造一个线程安全的日志库。自己造轮子,安全可控,100%国产化。

如何使用

支持stream, printf, format三种日志输出方式。看如下样例。

LOG(INFO) << "hello" << " " << "world" << " " << (INT_MIN%10);
LOG(DEBUG)("%s %d ", "abc", 333) << 345;
LOG(WARNING).Fmt("I am {}, age {}, go go go", "adson", 30);
LOG(ERROR).Fmt("Hello, Fmt");
LOG(FATAL)("this is fatal");

输出效果

I 2022-9-2 14:57:51.755068 trans_acc.cpp:41 TestLog|hello world -8
D 2022-9-2 14:57:51.755085 trans_acc.cpp:42 TestLog|abc 333 345
W 2022-9-2 14:57:51.755090 trans_acc.cpp:43 TestLog|I am adson, age 30, go go go
E 2022-9-2 14:57:51.755095 trans_acc.cpp:44 TestLog|Hello, Fmt
F 2022-9-2 14:57:51.755103 trans_acc.cpp:45 TestLog|this is fatal

依赖头文件

最小依赖的几个头文件。iostream用于向标准输出打日志。sstream用于格式化日志。time用于在日志中打印日期。

#pragma once
#include <iostream>
#include <sstream>
#include <sys/time.h>

日志级别

6种日志级别,分级在日志中很重要。避免日志太多。这里可以在编译的时候指定日志级别。 大于DEFAULT级别的日志不会走格式,也不会输出。不消耗性能。

打印日志头

日志头包括日志级别,日期时间,文件名,函数名,行号。日志级别用FEWIDT代表6种日志级别。文件名用__FILE__取得,函数名用__FUNCTION__取得,行号用__LINE__取得。因为要用这些宏,这也就是为什么C++打日志必须用宏的原因。日期和时间能精确到毫秒。使用gettimeofday函数获取,并使用线程安全的localtime_r函数来转为本地时区的日期和时间。

Log &Print(LogLevel lvl, const char *file_name, const char *func_name, int line_no){
    cur_level = lvl;
    if(cur_level > level) return *this; #判断级别
    const char *p = "FEWIDT"; 
    stream << p[lvl] << ' ';
    log_time();
    stream << file_name << ':' << line_no << ' ' << func_name << "|";
    return *this;
}

printf格式输出日志

C/C++中大家熟悉的printf,用起来很方便。用格式化字符串"%d %s %.2f"来格式化日志很方便。这里通过重载operator()来实现printf的支持。LOG(INFO)("%s %d", "hello", 123);

template<typename ...Args>
Log &operator()(const char *fmt, Args... args){
    if(cur_level > level) return *this;
    snprintf(log_data, BUF_SIZE, fmt, args...); //先打印到buf中,再输出到stream.性能略差。
    stream << log_data;
    return *this;
}

C++ stream 方式输出日志

printf的方便输出日志有个缺陷,不同的类型要用不同的占位符,如果不小把%s对应到整数上,会报错。让使用者不胜其烦。而C++的iostream里可以用<<来输出任意类型。这里也支持了。重载了operator<<就能达到这种效果。LOG(INFO) << "abc" << 123;

template<typename T>
Log &operator<<(const T &a) {
    if(cur_level > level) return *this;
    stream << a;
    return *this;
}

format 形式输出字符串

c++20开始在标准库支持std::format了。这里自己实现了std::format的部分功能。它也解决了Printf中%d,%s容易弄错的问题。只要LOG(INFO).Fmt("hello {}, {}", "world", 123);即可实现printf的功能。使用C++11的不定数量模板参数实现。加上模板函数与重载与递归。

template<typename ...Args, typename First>
Log &Fmt(std::string fmt, First first, Args... args){
    if(cur_level > level) return *this;
    for(size_t i = 0; i < fmt.size() - 1; i++){
        if(fmt[i] == '{' && fmt[i+1] == '}'){
            stream << fmt.substr(0, i) << first;
            return Fmt(fmt.substr(i+2, fmt.size()), args...);
        }
    }
    return *this;
}
Log &Fmt(std::string fmt){
    if(cur_level > level) return *this;
    stream << fmt;
    return *this;
}

LogManager

对Log对象进行管理。Log对象比较重,有stream, buf_data等重对象。不能频繁创建和销毁。每次打日志创建一个轻量的LogManager对象,用户可以持有此对象在循环中进行打日志。在LogManager销毁时会把已经缓存的日志Flush到stdout。

class LogManager{
public:
    LogManager(){

    }
    Log* GetLog() {
        static thread_local Log log;
        return &log;
    }
    ~LogManager(){
        GetLog()->Flush();
    }

};

日志输出宏

主要是为了打印行号等宏才能实现的功能。Log类在实现时如Flush, 等使用了虚函数,可以把gflags等现有的日志库封装到子类中使用。

#define LOG(lvl)\
    LogManager().GetLog()->Print(lvl, __FILE__, __FUNCTION__, __LINE__)

全量代码,复制粘贴用去吧

#pragma once
#include <iostream>
#include <sstream>
#include <sys/time.h>
namespace AdsonLib
{

enum LogLevel{
    FATAL = 0,
    ERROR = 1,
    WARNING = 2,
    INFO = 3,
    DEBUG = 4,
    TRACE = 5,
};
#ifndef DEFAULT_LOG_LEVEL
#define DEFAULT_LOG_LEVEL TRACE
#endif
class Log {
public:
    Log(LogLevel lvl = (LogLevel)DEFAULT_LOG_LEVEL):level(lvl){}
    int ResetLevel(LogLevel lvl){
        std::swap(lvl, level);
        level = lvl;
        return lvl;
    }
    int GetLevel() const {
        return level;
    }
    Log &Print(LogLevel lvl, const char *file_name, const char *func_name, int line_no){
        cur_level = lvl;
        if(cur_level > level) return *this;
        const char *p = "FEWIDT";
        stream << p[lvl] << ' ';
        log_time();
        stream << file_name << ':' << line_no << ' ' << func_name << "|";
        return *this;
    }
    template<typename ...Args>
    Log &operator()(const char *fmt, Args... args){
        if(cur_level > level) return *this;
        snprintf(log_data, BUF_SIZE, fmt, args...);
        stream << log_data;
        return *this;
    }
    template<typename ...Args, typename First>
    Log &Fmt(std::string fmt, First first, Args... args){
        if(cur_level > level) return *this;
        for(size_t i = 0; i < fmt.size() - 1; i++){
            if(fmt[i] == '{' && fmt[i+1] == '}'){
                stream << fmt.substr(0, i) << first;
                return Fmt(fmt.substr(i+2, fmt.size()), args...);
            }
        }
        return *this;
    }
    Log &Fmt(std::string fmt){
        if(cur_level > level) return *this;
        stream << fmt;
        return *this;
    }

    virtual void Flush() {
        if(cur_level <= level){
            if(cur_level >= WARNING){
                std::cerr << stream.str() << std::endl;
            }else{
                std::cout << stream.str() << std::endl;
            }
        }
        stream.str("");
        stream.flush();
    }
    virtual ~Log(){
        delete log_data;
    }
    template<typename T>
    Log &operator<<(const T &a) {
        if(cur_level > level) return *this;
        stream << a;
        return *this;
    }

void log_time()
{
    struct timeval    tv;
    struct tm         tm;
    gettimeofday(&tv, NULL);
    localtime_r(&tv.tv_sec, &tm);
    stream << tm.tm_year + 1900 << '-' << tm.tm_mon + 1 << '-' << tm.tm_mday << ' ' 
           << tm.tm_hour << ':' << tm.tm_min << ":" << tm.tm_sec << '.' << tv.tv_usec << ' ';
}
private:
    const int BUF_SIZE = 4096;
    std::ostringstream stream;
    LogLevel level;
    LogLevel cur_level;
    char *log_data = new char[BUF_SIZE];
};

class LogManager{
public:
    LogManager(){

    }
    Log* GetLog() {
        static thread_local Log log;
        return &log;
    }
    ~LogManager(){
        GetLog()->Flush();
    }

};

#define LOG(lvl)\
    LogManager().GetLog()->Print(lvl, __FILE__, __FUNCTION__, __LINE__)

} //namespace AdsonLib