2

C++还在用printf/cout进行Debug?学习一下如何自己写日志库吧(上篇)

 2 years ago
source link: https://blog.csdn.net/Fdog_/article/details/120932776
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C++还在用printf/cout进行Debug?学习一下如何自己写日志库吧(上篇)

花狗Fdog 2021-10-24 13:12:43 1228
专栏收录该内容
1 篇文章 0 订阅



哈喽,自从实习以来很久没有更文了,一是没有时间,二是实习了之后突然发现自己能写的东西也没有多少了。赶上1024有征文活动,就写一篇吧,在实习的这段时间,我更加认识到日志的重要性,客户端值没传过来?看日志,服务崩溃了?看日志,没错,日志是出现异常第一个想到的东西,它记录了程序运行过程中所调用的函数,所接受到的值,所执行的行为等等。大家也都看到这篇的标题了,我这个人有一个缺点,就是不太喜欢用别人的东西,如果有能力,我希望自己造,所以今天我们自己来动手撸一个日志库,文章重点讲实现过程,如果需要源码,可以前往github获取FdogLog,一个轻量级C++日志库,用于日志服务。

跪求三连!


二. 基本功能

我们先来捋一捋这个日志库应该实现那些功能。

  1. 日志最最最基本的功能是什么,当然是打印或记录日志。
  2. 信息应该包括哪些信息,时间?运行用户?所在文件?想要显示的信息?(自定义显示信息下篇实现)
  3. 信息虽然显示丰富,但是要尽可能让代码自己获取其他信息,调用者只需要设置最主要的信息。
  4. 信息有重要等级之分,所以我们需要对信息做必要分类,提高效率。
  5. 如何实现全局尽可能简洁的调用。
  6. 如果日志库是运行在多线程环境,如何保证线程安全。(下篇实现)

这些就是一个日志库所具备的最基本的功能,接下来继续思考,还需要什么。

  1. 怎么控制日志的行为。
  2. 如果保存在文件,如何定义文件名。
  3. 随着日志增加,文件会越来越大,如何解决。(下篇实现)

简单规划完一个不那么完美的日志库所具备的能力,现在我们来对这几条做更详细的规划。

  1. 日志最最最基本的功能是什么,当然是打印或记录日志。
  2. 信息应该包括哪些信息,时间?运行用户?所在文件?想要显示的信息?

当我在调用一个名为function的函数时。

function();

你希望它输出怎么样的信息。

我被调用
[2021-10-20 23:27:23] 我被调用
[2021-10-20 23:27:23] INFO 我被调用
[2021-10-20 23:27:23] INFO root 我被调用
[2021-10-20 23:27:23] INFO root 17938 我被调用
[2021-10-20 23:27:23] INFO root 17938 [/media/rcl/FdogIM/service.h function:8] 我被调用

我想大部分人都会选择最后一种输出信息吧(虽然在这之前,我们都大量使用cout输出第一种),所以我们的日志应该包括时间,日志等级,运行用户,进程ID,调用函数所在文件,以及调用时所在行数。当然总会有人不想全部输出,这将在后面给出方案。

  1. 信息虽然显示丰富,但是要尽可能让代码自己获取其他信息,调用者只需要设置最主要的信息。

  2. 信息有重要等级之分,所以我们需要对信息做必要分类,提高效率。

  3. 如何实现全局尽可能简洁的调用.

信息有重要等级之分,要可以对信息做区分,按照常见的等级之分,有:

ERROR: 此信息输出后,主体系统核心模块不能正常工作,需要修复才能正常工作。
WARN:   此信息输出后,系统一般模块存在问题,不影响系统运行。
INFO:     此信息输出后,主要是记录系统运行状态等关联信息。
DEBUG: 最细粒度的输出,除去上面各种情况后,你希望输出的相关信息,都可以在这里输出。
TRACE:  最细粒度的输出,除去上面各种情况后,你希望输出的相关信息,都可以在这里输出。

有了等级之分,如何实现全局尽可能简洁的调用,通俗的说就是去掉一切不必要的调用,只留下最主要的调用。

#include<iostream>
#include"fdoglogger.h"  //添加日志库头文件

using namespace fdog;   //日志库的命名空间

int main(){
    FdogError("错误");
    FdogWarn("警告");
    FdogInfo("信息");
    FdogDebug("调试");
    FdogTrace("追踪");
    return 0;
}

你不必初始化什么信息,调用什么多余的初始化函数,只需要用这五个类似函数的东西来输出即可,同样,如果是另一个源文件,依旧是这样的调用方式(这里可以使用单一模式来实现,其意图是保证一个类仅有一个实列,并提供一个访问它的全局访问点,该实例被所有程序模块共享。就比如日志的输出。)。

  1. 如果日志库是运行在多线程环境,如何保证线程安全。

到目前,一个基本的日志库的调用基本成形,如果在单线程,它可以很好的工作,但是到了多线程环境下,就不能保证了,第一点就是单例模式的创建,当两个线程同时去初始化时,无法保证单一实例被成功创建,第二,日志既然是输出到文件,不同线程写入文件时,如何保证写入数据不会错乱。既然写的是C++的日志输出,必然用到了cout ,cout 不是原子性的操作,所以在多线程下是不安全的,这些都是我们需要考虑到的。

  1. 怎么控制日志的行为。

这里使用配置文件进行日志的行为规定,包括打印什么日志,输入到文件,还是终端,输出的等级,以及日志开关,等等,配置文件将在程序启动时被读取。(提醒各位千万不要写死代码,后患无穷!!!)

  1. 如果保存在文件,如何定义文件名。

  2. 随着日志增加,文件会越来越大,如何解决。

日志的文件名由配置文件指定,但是创建时会在后面加上创建日期后缀,并且可以在配置文件中配置每隔多少天创建一个新的日志文件,如果配置中心有设置日志文件大小,则会优先大小判断,超过便创建一个新文件。


三. 代码实现

1. fdoglogger.h
#ifndef FDOGLOGGER_H
#define FDOGLOGGER_H
#include<iostream>
#include<fstream>
#include<map>
#include<mutex>
#ifndef linux
#include<unistd.h>
#include<sys/syscall.h>
#include<sys/stat.h>
#include<sys/types.h>
#include <pwd.h>
#endif
#ifndef WIN32
//TODO
#endif
using namespace std;

namespace fdog {

#define RED   "\e[1;31m"
#define BLUE  "\e[1;34m"
#define GREEN "\e[1;32m"
#define WHITE "\e[1;37m"
#define DEFA  "\e[0m"

enum class coutType: int {Error, Warn, Info, Debug, Trace};
enum class fileType: int {Error, Warn, Info, Debug, Trace};
enum class terminalType: int {Error, Warn, Info, Debug, Trace};

struct Logger {
    string logSwitch;           //日志开关
    string logFileSwitch;       //是否写入文件
    string logTerminalSwitch;   //是否打印到终端
    string logName;             //日志文件名字
    string logFilePath;         //日志文件保存路径
    string logMixSize;          //日志文件最大大小
    string logBehavior;         //日志文件达到最大大小行为
    string logOverlay;          //日志文件覆盖时间
    string logOutputLevelFile;  //日志输出等级(file)
    string logOutputLevelTerminal;//日志输出等级
};

class FdogLogger {
public:
    void initLogConfig();

    void releaseConfig();

    static FdogLogger* getInstance();

    string getCoutType(coutType coutType);

    bool getFileType(fileType fileCoutBool);

    bool getTerminalType(terminalType terminalCoutTyle);

    string getLogCoutTime();

    string getLogNameTime();

    string getFilePash();

    string getLogCoutProcessId();

    string getLogCoutThreadId();

    string getLogCoutUserName();

    bool createFile(string filePash);

    bool logFileWrite(string messages);

    bool bindFileCoutMap(string value1, fileType value2);

    bool bindTerminalCoutMap(string value1, terminalType value2);

private:
    char szbuf[128];
    Logger logger;
    static FdogLogger * singleObject;
    static mutex * mutex_new;
    map<coutType, string> coutTypeMap;
    map<fileType, bool> fileCoutMap;
    map<terminalType, bool> terminalCoutMap;

private:
    FdogLogger();
    ~FdogLogger();
};

#define Error1 __FDOGNAME__(Error)
#define Warn1 __FDOGNAME__(Warn)
#define Info1 __FDOGNAME__(Info)
#define Debug1 __FDOGNAME__(Debug)
#define Trace1 __FDOGNAME__(Trace)


#define SQUARE_BRACKETS_LEFT " ["
#define SQUARE_BRACKETS_RIGHT "] "
#define SPACE " "
#define LINE_FEED "\n"
#define COLON ":"
#define SLASH "/"

#define __FDOGTIME__  FdogLogger::getInstance()->getLogCoutTime()          //时间宏
#define __FDOGPID__   FdogLogger::getInstance()->getLogCoutProcessId()     //进程宏
#define __FDOGTID__   FdogLogger::getInstance()->getLogCoutThreadId()      //线程宏
#define __FDOGFILE__  __FILE__        //文件名宏
#define __FDOGPASH__  FdogLogger::getInstance()->getFilePash() + __FDOGFILE__ //文件路径
#define __FDOGFUNC__   __func__        //函数名宏
#define __FDOGLINE__  __LINE__        //行数宏
#define __USERNAME__  FdogLogger::getInstance()->getLogCoutUserName()     //获取调用用户名字
#define __FDOGNAME__(name) #name        //名字宏


#define COMBINATION_INFO_FILE(coutTypeInfo, message) \
    do{\
        string messagesAll = __FDOGTIME__ + coutTypeInfo + __USERNAME__ + __FDOGTID__ + SQUARE_BRACKETS_LEFT + \
        __FDOGPASH__  + SPACE +__FDOGFUNC__ + COLON + to_string(__FDOGLINE__) + SQUARE_BRACKETS_RIGHT + message + LINE_FEED;\
        FdogLogger::getInstance()->logFileWrite(messagesAll); \
    }while(0);

#define COMBINATION_INFO_TERMINAL(coutTypeInfo, message) \
    do{\
        string messagesAll = __FDOGTIME__ + WHITE + coutTypeInfo + DEFA + __USERNAME__ + __FDOGTID__ + SQUARE_BRACKETS_LEFT + \
        __FDOGPASH__  + SPACE +__FDOGFUNC__ + COLON + to_string(__FDOGLINE__) + SQUARE_BRACKETS_RIGHT + message + LINE_FEED;\
        cout << messagesAll;\
    }while(0);

#define LoggerCout(coutTyle, coutTypeInfo, fileCoutBool, terminalCoutBool, message) \
    do {\
        string coutType = FdogLogger::getInstance()->getCoutType(coutTyle);\
        if (FdogLogger::getInstance()->getFileType(fileCoutBool)) {\
            COMBINATION_INFO_FILE(coutTypeInfo, message)\
        }\
        if (FdogLogger::getInstance()->getTerminalType(terminalCoutBool)) {\
            COMBINATION_INFO_TERMINAL(coutTypeInfo, message)\
        }\
    }while(0);

#define FdogError(message) \
    do{\
        LoggerCout(fdog::coutType::Error, Error1, fdog::fileType::Error, fdog::terminalType::Error, message)\
    }while(0);

#define FdogWarn(message)  \
    do{\
        LoggerCout(fdog::coutType::Warn, Warn1, fdog::fileType::Warn, fdog::terminalType::Warn, message)\
    }while(0);

#define FdogInfo(message)  \
    do{\
        LoggerCout(fdog::coutType::Info, Info1, fdog::fileType::Info, fdog::terminalType::Info, message)\
    }while(0);

#define FdogDebug(message) \
    do{\
        LoggerCout(fdog::coutType::Debug, Debug1, fdog::fileType::Debug, fdog::terminalType::Debug, message)\
    }while(0);

#define FdogTrace(message) \
    do{\
        LoggerCout(fdog::coutType::Trace, Trace1, fdog::fileType::Trace, fdog::terminalType::Trace, message)\
    }while(0);

}

#endif

2. fdoglogger.cpp
#include"fdoglogger.h"

using namespace fdog;


FdogLogger * FdogLogger::singleObject = nullptr;
mutex * FdogLogger::mutex_new = new(mutex);

FdogLogger::FdogLogger(){
    initLogConfig();
}
FdogLogger::~FdogLogger(){

}

FdogLogger* FdogLogger::getInstance(){
    mutex_new->lock();
    if (singleObject == nullptr) {
        singleObject = new FdogLogger();
    }
    mutex_new->unlock();
    return singleObject;
}

void FdogLogger::initLogConfig(){

    map<string, string *> flogConfInfo;
    flogConfInfo["logSwitch"] = &this->logger.logSwitch;
    flogConfInfo["logFileSwitch"] = &this->logger.logFileSwitch;
    flogConfInfo["logTerminalSwitch"] = &this->logger.logTerminalSwitch;
    flogConfInfo["logName"] = &this->logger.logName;
    flogConfInfo["logFilePath"] = &this->logger.logFilePath;
    flogConfInfo["logMixSize"] = &this->logger.logMixSize;
    flogConfInfo["logBehavior"] = &this->logger.logBehavior;
    flogConfInfo["logOverlay"] = &this->logger.logOverlay;
    flogConfInfo["logOutputLevelFile"] = &this->logger.logOutputLevelFile;
    flogConfInfo["logOutputLevelTerminal"] = &this->logger.logOutputLevelTerminal;

    string str;
    ifstream file;
    char str_c[100]={0};
    file.open("fdoglogconf.conf");
    if(!file.is_open()){
        cout<<"文件打开失败\n";
    }
    while(getline(file, str)){
        if(!str.length()) {
            continue;
        }
        string str_copy = str;
        //cout<<"获取数据:"<<str_copy<<endl;
        int j = 0;
        for(int i = 0; i < str.length(); i++){
            if(str[i]==' ')continue;
            str_copy[j] = str[i];
            j++;
        }
        str_copy.erase(j);
        if(str_copy[0]!='#'){
            sscanf(str_copy.data(),"%[^=]",str_c);
            auto iter = flogConfInfo.find(str_c);
            if(iter!=flogConfInfo.end()){
                sscanf(str_copy.data(),"%*[^=]=%s",str_c);
                *iter->second = str_c;
            } else {
            }
        }
    }
    logger.logName = logger.logName + getLogNameTime() + ".log";

    bindFileCoutMap("5", fileType::Error);
    bindFileCoutMap("4", fileType::Warn);
    bindFileCoutMap("3", fileType::Info);
    bindFileCoutMap("2", fileType::Debug);
    bindFileCoutMap("1", fileType::Trace);

    bindTerminalCoutMap("5", terminalType::Error);
    bindTerminalCoutMap("4", terminalType::Warn);
    bindTerminalCoutMap("3", terminalType::Info);
    bindTerminalCoutMap("2", terminalType::Debug);
    bindTerminalCoutMap("1", terminalType::Trace);

    if(logger.logFileSwitch == "on"){
        if(!createFile(logger.logFilePath)){
            std::cout<<"Log work path creation failed\n";
        }
    }

    cout << "|========FdogLogger v2.0==========================|" <<endl << endl;
    cout << "  日志开关:" << logger.logSwitch << endl;
    cout << "  文件输出:" << logger.logFileSwitch << endl;
    cout << "  终端输出:" << logger.logTerminalSwitch << endl;
    cout << "  日志输出等级(文件):" << logger.logOutputLevelFile << endl;    
    cout << "  日志输出等级(终端):" << logger.logOutputLevelTerminal << endl;
    cout << "  日志文件名:" << logger.logName << endl;
    cout << "  日志保存路径:" << logger.logFilePath << endl;
    cout << "  单文件最大大小:"<< logger.logMixSize << "M" << endl;
    cout << "  日志保存时间 :" << logger.logOverlay << "天" << endl << endl;
    cout << "|=================================================|" <<endl;
    
    return;
}

string FdogLogger::getCoutType(coutType coutType){
    return singleObject->coutTypeMap[coutType];
}

bool FdogLogger::getFileType(fileType fileCoutBool){
    return singleObject->fileCoutMap[fileCoutBool];
}

bool FdogLogger::getTerminalType(terminalType terminalCoutTyle){
    return singleObject->terminalCoutMap[terminalCoutTyle];
}

string FdogLogger::getLogCoutTime(){
    time_t timep;
    time (&timep);
    char tmp[64];
    strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S",localtime(&timep));
    string tmp_str = tmp;

    return SQUARE_BRACKETS_LEFT + tmp_str + SQUARE_BRACKETS_RIGHT;
}

string FdogLogger::getLogNameTime(){
    time_t timep;
    time (&timep);
    char tmp[64];
    strftime(tmp, sizeof(tmp), "%Y-%m-%d-%H:%M:%S",localtime(&timep));
    return tmp;
}

string FdogLogger::getFilePash(){
    getcwd(szbuf, sizeof(szbuf)-1);
    string szbuf_str = szbuf;
    return szbuf_str + SLASH;
}

string FdogLogger::getLogCoutProcessId(){
#ifndef linux
    return to_string(getpid());
#endif
#ifndef WIN32
//  unsigned long GetPid(){
//     return GetCurrentProcessId();
// }
#endif
}

string FdogLogger::getLogCoutThreadId(){
#ifndef linux
    return to_string(syscall(__NR_gettid));
#endif
#ifndef WIN32
//  unsigned long GetTid(){
//     return GetCurrentThreadId();
// }
#endif
}

string FdogLogger::getLogCoutUserName(){
    struct passwd *my_info;
    my_info = getpwuid(getuid());
    string name = my_info->pw_name;
    return SPACE + name + SPACE;
}

bool FdogLogger::createFile(string filePash){
    int len = filePash.length();
    if(!len){
        filePash = "log";
        if (0 != access(filePash.c_str(), 0)){
            if(-1 == mkdir(filePash.c_str(),0)){
                std::cout<<"没路径";
                return 0;
            }
        }
    }
    std::string filePash_cy(len,'\0');
    for(int i =0;i<len;i++){
        filePash_cy[i]=filePash[i];
        if(filePash_cy[i]=='/' || filePash_cy[i]=='\\'){
            if (-1 == access(filePash_cy.c_str(), 0)){
                if(0!=mkdir(filePash_cy.c_str(),0)){
                    std::cout<<"有路径";
                    return 0;
                }
            }
        }
    }
    return 1;
}

bool FdogLogger::logFileWrite(string messages){
    ofstream file;
    file.open(logger.logFilePath + logger.logName, ::ios::app | ios::out);
    if(!file){
        cout<<"写失败"<<endl;
        return 0;
    }
    file << messages;
    file.close();
    return 1;
}

bool FdogLogger::bindFileCoutMap(string value1, fileType value2){
    if(logger.logOutputLevelFile.find(value1)!=std::string::npos) {
        fileCoutMap[value2] = true;
    } else {
        fileCoutMap[value2] = false;
    }
}

bool FdogLogger::bindTerminalCoutMap(string value1, terminalType value2){
    if(logger.logOutputLevelTerminal.find(value1)!=std::string::npos) {
        terminalCoutMap[value2] = true;
    } else {
        terminalCoutMap[value2] = false;
    }
}

四. 测试用例

在这里插入图片描述

1. fdoglogger_test.cpp
#include<iostream>
#include"fdoglogger.h"  //添加日志库头文件

using namespace fdog;   //日志库的命名空间

int main(){
    FdogError("错误");
    FdogWarn("警告");
    FdogInfo("信息");
    FdogDebug("调试");
    FdogTrace("追踪");
    return 0;
}

在这里插入图片描述

在这里插入图片描述

暂时考虑到的就是这些,如有缺陷,欢迎评论区补充。(比如文件写入打开就关闭,很浪费资源,如何优化,下篇见)。

源码已上传github,还原star! FdogLog,一个轻量级C++日志库,用于日志服务。



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK