Bootstrap

【Linux系统编程】第四十九弹---日志系统构建指南:从基础结构到时间处理与Log类实现

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

1、日志类的基本结构

1.1、logmessage类

1.2、Log类

2、测试当前时间函数

2.1、获取当前时间的库函数

2.2、转化时间戳格式 

3、Log类实现

3.1、构造析构函数

3.2、初始化日志信息

3.2.1、测试函数 

3.2.2、以指定方式打印日志 

3.2.3、优化日志打印函数 

3.2.4、解决小问题


上一弹我们简要实现了线程池,但是还是有一个问题,就是使用std::cout的次数太对了,对于代码的健壮性不是很好,这一弹我们实现一个日志(软件运行的记录信息,以特定的格式向显示器打印或者向文件打印)版本的线程池。

1、日志类的基本结构

日志类包括存储日志信息的类和操作日志信息的类!!!

1.1、logmessage类

存储日志信息的类包含日志等级(DEBUG,INFO,WARNING,ERROR,FATAL,为了更好的辨认等级是什么,可以使用枚举类型为了更直观,将枚举类型的名字转化成字符串),pid文件名文件行号当前时间(后面测试),日志内容

logmessage类

// 日志等级
enum
{
    DEBUG = 1,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

class logmessage
{
public:
    std::string _level;        // 日志等级
    pid_t _id;                 // pid
    std::string _filename;     // 文件名
    int _filenumber;           // 文件行号
    std::string _curr_time;    // 当前时间
    std::string _message_info; // 日志内容
};

等级转字符串类型函数

 使用switch判断,满足哪个条件直接返回字符串都不满足返回UNKOWN!!!

std::string LevelToString(int level)
{
    switch (level)
    {
    case DEBUG:
        return "DEBUG";
    case INFO:
        return "INFO";
    case WARNING:
        return "WARNING";
    case ERROR:
        return "ERROR";
    case FATAL:
        return "FATAL";
    default:
        return "UNKNOW";
    }
}

1.2、Log类

Log可以实现向显示器打印(默认)和文件打印(自己指定),因此成员变量有打印方式和文件名!!!

class Log
{
public:
    // 默认向显示器打印
    Log(const std::string &logfile = glogfile);
    // 打印方式
    void Enable(int type);
    // 向屏幕打印
    void FlushToScreen(const logmessage &lg);
    // 向文件打印
    void FlushToFile(const logmessage &lg);
    // 刷新日志
    void FlushLog(const logmessage &lg);
    // ... 可变参数(C语言)
    // 初始化日志信息
    void logMessage(std::string filename, int filenumber, int level, const char *format, ...);
    ~Log();

private:
    int _type;            // 打印方式
    std::string _logfile; // 文件名
};

2、测试当前时间函数

2.1、获取当前时间的库函数

使用shell命令查看函数

man gettimeofday # 获取当前的时间戳
man 2 time # 获取当前的时间戳

gettimeofday()

include <sys/time.h>

int gettimeofday(struct timeval *tv, struct timezone *tz);

time() 

#include <time.h>

time_t time(time_t *t);

 

测试代码

// 年月日时分秒
std::string GetCurrTime()
{
    time_t now = time(nullptr); // 时间戳
    std::cout << "now: " << now << std::endl;
    return "";
}

// 调用函数
GetCurrTime();

运行结果

2.2、转化时间戳格式 

时间戳以秒为单位的时间,为了更美观,我们需要将时间戳转化为年月日时分秒的格式!

 使用shell命令查看转化函数

man localtime # 查看时间转化函数

localtime() 

#include <time.h>

struct tm *localtime(const time_t *timep);

获取时间函数(错误演示)

std::string GetCurrTime()
{
    time_t now = time(nullptr); // 时间戳
    struct tm *curr_time = localtime(&now);
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
                curr_time->tm_year,
                curr_time->tm_mon,
                curr_time->tm_mday,
                curr_time->tm_hour,
                curr_time->tm_min,
                curr_time->tm_sec);
    return buffer;
}

// 调用函数
std::cout << GetCurrTime() << std::endl;

运行结果

 获取时间函数(正确演示)

std::string GetCurrTime()
{
    time_t now = time(nullptr); // 时间戳
    struct tm *curr_time = localtime(&now);
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
                curr_time->tm_year + 1900,
                curr_time->tm_mon + 1,
                curr_time->tm_mday,
                curr_time->tm_hour,
                curr_time->tm_min,
                curr_time->tm_sec);
    return buffer;
}

// 调用函数
std::cout << GetCurrTime() << std::endl;

运行结果 

3、Log类实现

3.1、构造析构函数

构造函数初始化文件名和打印方式(默认向显示器打印),析构函数暂不做处理。

#define SCREEN_TYPE 1
#define FILE_TYPE 2

const std::string glogfile = "./log.txt";

// 默认向显示器打印
Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE)
{}

~Log()
{}

3.2、初始化日志信息

初始化日志信息即初始化存储日志信息类成员的信息,格式如下:

log.logMessage(""/*文件名*/,12/*文件行号*/,INFO/*日志等级*/,
"this is a %d message,%f,%s,hello world"/*日志内容*/,x,,);

日志内容需要支持可变参数,此处使用C语言版本的可变参数,较为简单!!

  • 此处直接使用vsnprintf()转化可变参数,无需自己手动实现!!
#include <stdarg.h>

int vsnprintf(char *str, size_t size, const char *format, va_list ap);

3.2.1、测试函数 

// ... 可变参数(C语言)
// 初始化日志信息
void logMessage(std::string filename, int filenumber, int level, const char *format, ...)
{
    logmessage lg;

    lg._level = LevelToString(level);
    lg._id = getpid();
    lg._filename = filename;
    lg._filenumber = filenumber;
    lg._curr_time = GetCurrTime();

    va_list ap;           // va_list-> char*指针
    va_start(ap, format); // 初始化一个va_list类型的变量
    char log_info[1024];
    vsnprintf(log_info, sizeof(log_info), format, ap);
    va_end(ap); // 释放由va_start宏初始化的va_list资源
    lg._message_info = log_info;

    std::cout << lg._message_info << std::endl; // 测试
}

 运行结果

3.2.2、以指定方式打印日志 

1、因为此处的日志是临界资源,因此需要加锁,博主使用的是前面实现的以RAII方式封装的LockGuard类!!!

2、根据Log类的_type成员调用不同的打印日志函数!!

LockGuard类 

#pragma once 
#include <pthread.h>

class LockGuard
{
public:
    LockGuard(pthread_mutex_t* mutex):_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mutex);
    }
private:
    pthread_mutex_t* _mutex;
};

修改打印方式

#define SCREEN_TYPE 1  // 显示器打印
#define FILE_TYPE 2    // 文件打印

// 打印方式
void Enable(int type)
{
    _type = type;
}

向屏幕打印 

// 向屏幕打印
void FlushToScreen(const logmessage &lg)
{
    printf("[%s][%d][%s][%d][%s] %s",
            lg._level.c_str(),
            lg._id,
            lg._filename.c_str(),
            lg._filenumber,
            lg._curr_time.c_str(),
            lg._message_info.c_str());
}

向文件打印

// 向文件打印
void FlushToFile(const logmessage &lg)
{
    std::ofstream out(_logfile, std::ios::app); // 追加打开文件
    if (!out.is_open())
        return; // 打开失败直接返回

    char logtxt[2048];
    snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",
                lg._level.c_str(),
                lg._id,
                lg._filename.c_str(),
                lg._filenumber,
                lg._curr_time.c_str(),
                lg._message_info.c_str());
    out.write(logtxt, strlen(logtxt)); // 写文件
    out.close();                       // 关闭文件
}

刷新日志函数

根据Log类的_type成员调用不同的打印日志函数!!

// 刷新日志
void FlushLog(const logmessage &lg)
{
    // 加过滤逻辑 --- TODO
    // ...

    LockGuard lockguard(&glock); // RAII锁
    switch (_type)
    {
    case SCREEN_TYPE:
        FlushToScreen(lg);
        break;
    case FILE_TYPE:
        FlushToFile(lg);
        break;
    }
}

初始化日志信息函数 

// 初始化日志信息
void logMessage(std::string filename, int filenumber, int level, const char *format, ...)
{
    logmessage lg;

    lg._level = LevelToString(level);
    lg._id = getpid();
    lg._filename = filename;
    lg._filenumber = filenumber;
    lg._curr_time = GetCurrTime();

    va_list ap;           // va_list-> char*指针
    va_start(ap, format); // 初始化一个va_list类型的变量
    char log_info[1024];
    vsnprintf(log_info, sizeof(log_info), format, ap);
    va_end(ap); // 释放由va_start宏初始化的va_list资源
    lg._message_info = log_info;

    FlushLog(lg);
}

运行结果 

3.2.3、优化日志打印函数 

前面的打印日志初始化函数还有两个缺陷

1、文件名和文件行号是固定的

2、调用函数太冗长了

解决办法:

1、使用C语言的宏关键字

2、使用宏函数替换

宏关键字 

__FILE__ // 文件名
__LINE__ // 文件行号

 测试一(宏关键字):

int main()
{
    Log lg;
    // C语言宏替代文件名行号
    lg.logMessage(__FILE__,__LINE__,DEBUG,"hello %d,hello %c hello %f\n",1000,'A',3.14);
    sleep(1);

    return 0;
}

运行结果 

 测试二(宏函数):

可以将类调用函数封装到宏LOG函数中,而且我们不需要传文件名和文件行号这两个参数;将修改文件打印方式封装成宏函数!!!

LOG(有小问题)

Log lg;
// 打印日志封装成宏,使用函数方式调用
#define LOG(Level, Format, ...)                                          \
    do                                                                   \
    {                                                                    \
        lg.logMessage(__FILE__, __LINE__, Level, Format, __VA_ARGS__); \
    } while (0)

EnableScreen

// 设置打印方式,使用函数方式调用
#define EnableScreen()          \
    do                          \
    {                           \
        lg.Enable(SCREEN_TYPE); \
    } while (0)

EnableFile

// 设置打印方式,使用函数方式调用
#define EnableFile()          \
    do                        \
    {                         \
        lg.Enable(FILE_TYPE); \
    } while (0)

主函数

int main()
{
    Log lg;
    // 直接使用使用宏像函数一样打印
    LOG(DEBUG,"hello %d,hello %c hello %f\n",1000,'A',3.14);
    sleep(1);
    // 向哪里打印也使用宏
    EnableScreen();
    LOG(WARNING,"hello %d,hello %c hello %f\n",1000,'A',3.14);
    sleep(1);
    LOG(DEBUG,"hello %d,hello %c hello %f\n",1000,'A',3.14);
    sleep(1);
    EnableFile();
    LOG(WARNING,"hello %d,hello %c hello %f\n",1000,'A',3.14);
    sleep(1);
    LOG(DEBUG,"hello %d,hello %c hello %f\n",1000,'A',3.14);
    sleep(1);

    return 0;
}

运行结果

3.2.4、解决小问题

有可能可变参数没有内容,此时就会报错!!! 

 运行结果

 解决办法:

可变参数前面加两个# 号,如果可变参数有内容就使用可变参数内容,没有内容就当可变参数不存在!!!

LOG宏

// 打印日志封装成宏,使用函数方式调用
#define LOG(Level, Format, ...)                                          \
    do                                                                   \
    {                                                                    \
        lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
    } while (0)

主函数 

int main()
{
    Log lg;

    // 没有可变参数
    EnableScreen();
    LOG(INFO,"helloworld\n"); // 宏替换时加##
    return 0;
}

运行结果 

;