初识Linux · 日志编写

目录

前言:

日志的简单说明

编写日志


前言:

在线程池部分我们纵观全文,可以发现全文有很多很多的IO流,看起来还是差点意思的,而我们今天提到的日志,是在今后的代码编写中会经常接触,或者说在这之前,我们也接触过日志,不过我们没有注意而已,比如平常常用的电脑,强制关机什么的都会在日志里面。

接下来,我们将简单介绍一下日志的相关知识,然后进行编写。


日志的简单说明

对于日志来说,我们平常使用的日志有几个等级的,第一个等级是DEBUG,代表调试信息,第二个等级是INFO,代表普通信息,第三个等级是WARNING,代表警告,部分警告我们是不用过多在意的,比如在最新的编译器上使用头文件unistd.h的话,是有警告的,但是我们不必在意,第四个等级是ERROR,代表出错了,这种错误呢我们平时编写高级语言的代码的时候,出现了数据的越界啊,除0什么的都会出现ERROR,最后一个等级就十分危险了,大部分人到到现在应该是没有见到过的,是FATAL,代表的是致命的。

那么对于日志来说,我们需要时间吧?我们需要等级吧?文件名吧?发生了报错需要指定具体的行号吧?最后还有一个打印出来的信息吧?

由以上可得信息所需要的成员变量还是非常多的,加上有些函数我们还没有具体了解,所以其实编写日志也算是一个小小的挑战了。

那么废话不多说,我们直接进入编写日志的环节。


编写日志

编写日志之前,由于日志的有多个不同的等级,所以我们不妨使用枚举变量用来表示日志的不同等级:

cpp 复制代码
enum grade
{
    DEBUG = 1,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

对于日志,我们也分析了需要当前时间,文件名,当前行号,进程pid也可以来一下,打印的信息,日志等级,那么我们可以用个结构体将它们全部组合在一起:

cpp 复制代码
struct log_message
{
    std::string _level;
    pid_t _id;
    std::string _filename;
    int _filenumber;
    std::string _cur_time;
    std::string message_info;
};

好了,现在暂时有两个问题,第一个是,我们如何获取到当前的时间?方法肯定有很多种,这里我们推荐函数localtime。第二个是level我们是使用的枚举类型用来表示的,那么我们肯定需要将整数转换成对应的string类型。

我们不妨先来解决简单的,我们将整数转换为我们需要的日志等级信息,我们可以使用switch函数:

cpp 复制代码
std::string GradeToString(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 "UNKNOWN";
    }
}

这段代码唯一的难点可能就是很久没有用到switch了导致的生疏而已。

那么对于第二个问题,我们先man localtime:

对于这个函数而言,返回值类型是结构体tm指针,所需要的头文件是time.h,那么在C++中,自然就是对应的ctime头文件了,对应的参数是time_t类型的变量,实际上就是time函数的返回值。

那么结构体tm的组成是这样的:

cpp 复制代码
           struct tm {
               int tm_sec;    /* Seconds (0-60) */
               int tm_min;    /* Minutes (0-59) */
               int tm_hour;   /* Hours (0-23) */
               int tm_mday;   /* Day of the month (1-31) */
               int tm_mon;    /* Month (0-11) */
               int tm_year;   /* Year - 1900 */
               int tm_wday;   /* Day of the week (0-6, Sunday = 0) */
               int tm_yday;   /* Day in the year (0-365, 1 Jan = 0) */
               int tm_isdst;  /* Daylight saving time */
           };

year对应的就是年,mon对应的就是月,mday代表的是这个月的第几天,也就是日,hour min sec对应的就是时分秒了,对于其他成员变量我们看旁边的注释也能清楚。

那么get当前时间也挺简单的了就,但是为了方便起见,我们可以返回一个字符串,不然到时候打印的时候我们还需要转格式就有点麻烦了:

cpp 复制代码
std::string GetCurTime()
{
    const time_t t = time(nullptr);
    struct tm* cur_time = localtime(&t);
    char buffer[128];
    snprintf(buffer,sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
        cur_time->tm_year + 1990,
        cur_time->tm_mon + 1,
        cur_time->tm_mday,
        cur_time->tm_hour,
        cur_time->tm_min,
        cur_time->tm_sec
    );
    return buffer;
}

当然了,为了格式的美观,我们可以在除了年的所有格式里面加上02d,以及因为tm_year的值是减去了1990的,所以我们加上,month同理。

此时,时间的问题就解决了。

那么对于日志来说,可以打印在显示器上面,也可以打印到文件里面,所以我们不妨对于日志类设置一个变量,用来表示打印在哪里,一个用来表示文件,一个用来表示显示器文件:

cpp 复制代码
const std::string default_logfile = "./log.txt";
#define SCREEN_TYPE 1
#define FILE_TYPE 2

class log
{
public:
    log(const std::string &logfile = default_logfile)
        : _log_file(logfile), _type(SCREEN_TYPE)
    {
    }
    void ModifyType(int type)
    {
        _type = type;
    }


private:
    int _type;
    std::string _log_file;
};

对于日志的基本框架也搭建好了,现在我们需要考虑的有以下几个逻辑,如何将日志打印到显示器,如何将日志打印到文件里面?

对于以上的逻辑,我们不妨先将我们的信息封装好,在封装信息的这个过程中,我们会用到可变参数,以及用到新的宏和新的类型,我们先来简单介绍一下新的知识,va_list以及va_start,va_end:

其中va_list是通过 __gnuc_va_list重命名过来的,对于va_start和va_end都是一个宏。

对于va_list来说,是获取可变参数列表里面的可变参数,通过是使用一个内部指针指向它。

对于va_start来说,第一个参数是va_list的类型,对于第二个参数往往是可变参数列表的前一个类型。

对于va_end来说是用来清理va_list变量的一个宏。

而要注意的是,va_list这种都是C语言标准库里面的,并不是C++语言直接提供的

现在我们不妨反问一下,如果没有va_list,我们如何访问可变参数列表?

这里使用到的函数栈帧的知识,函数的参数是从右往左进行压栈的,所以如果我们想要访问参数,需要一个指针,并且确切的知道可变参数列表里面参数的类型,便于在栈中易于通过指针运算找到每一个参数。

当然,这种方法是非常不安全也是非常不具有移植性的,非常不推荐,咱们了解一下即可。

有了va_list之后,我们对于可变参数的处理就十分简单了,不过我们现在知道了可变参数可以有va_list获取之后,我们应该如何处理呢?这里就需要使用到函数vnsprintf了,函数原型为:

cpp 复制代码
int vnsprintf(char *str, size_t size, const char *format, va_list ap);
  • char *str:指向存储结果字符串的缓冲区的指针。
  • size_t size :缓冲区的大小,即str可以容纳的最大字符数(包括空字符'\0')。
  • const char *format :格式化字符串,用于指定输出格式。这个字符串可以包含普通字符和格式说明符(如%d%s等),这些格式说明符会被va_list中的相应参数替换。
  • va_list ap :一个可变参数列表,包含了要格式化的数据。这个列表通常使用va_start宏初始化,使用va_arg宏访问,最后使用va_end宏清理。
cpp 复制代码
    void logMessage(const std::string &filename, int filenumber, int level, const char *format, ...)
    {
        log_message lg;

        lg._level = GradeToString(level);
        lg._id = getpid();
        lg._filenumber = filenumber;
        lg._filename = filename;
        lg._cur_time = GetCurTime();

        va_list va;
        va_start(va, format);
        char log_info[1024];
        vsnprintf(log_info, sizeof(log_info), format, va);
        va_end(va);
        lg._message_info = log_info;
        // 开始打印日志
        FlushLog(lg);
    }

信息处理完毕了,我们现在开始实现打印日志的逻辑:

cpp 复制代码
    void FlushLog(log_message &lg)
    {
        pthread_mutex_lock(&gmutex);
        if (_type == SCREEN_TYPE)
            FlushScreen(lg);
        else
            FlushFile(lg);
        pthread_mutex_unlock(&gmutex);
    }

对于打印来说,打印访问的资源都是log_message,所以我们是有必要加锁的,加一把全局的锁就可以了。如果不加锁会发生打印错乱的现象,现在就是最后两个函数了,一个打印到显示器上:

cpp 复制代码
    void FlushScreen(log_message &lg)
    {
        printf("[%s][%d][%s][%d][%s] %s",
               lg._level.c_str(),
               lg._id,
               lg._filename.c_str(),
               lg._filenumber,
               lg._cur_time.c_str(),
               lg._message_info.c_str());
    }
cpp 复制代码
    void FlushFile(log_message &lg)
    {
        std::ofstream out(_log_file, 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._cur_time.c_str(),
                 lg._message_info.c_str());
        out.write(logtxt, strlen(logtxt));
        out.close();
    }

以上是日志的基本编写,别急,我们不妨测试一下:

cpp 复制代码
// 测试日志编写
int main()
{
    log().logMessage("test filename1",1, DEBUG, "Hello world %d\n", 1200);
    log().logMessage("test filename2",2, INFO, "Hello world %d\n", 1201);
    log().logMessage("test filename3",3, FATAL, "Hello world %d\n", 1202);
    log().logMessage("test filename4",4, ERROR, "Hello world %d\n", 1203);
    log().logMessage("test filename5",5, DEBUG, "Hello world %d\n", 1204);
    log().logMessage("test filename6",6, FATAL, "Hello world %d\n", 1205);

    return 0;
}

只能说日志编写太不错了。

但是但是,还是差点意思,我们还要定义匿名对象,还要手动的写文件名,文件行号,实在麻烦,所以我们可以使用宏进行封装,这里使用宏的时候,如果涉及了续航符,编译器是不会省略的,但是之前的printf那些编译器是会省略的。这点需要注意。

cpp 复制代码
log lg;

#define LOG(Level, Format, ...)                                        \
    do                                                                 \
    {                                                                  \
        lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \
    } while (0)
#define EnableScreen()          \
    do                          \
    {                           \
        lg.ModifyType(SCREEN_TYPE); \
    } while (0)
#define EnableFILE()          \
    do                        \
    {                         \
        lg.ModifyType(FILE_TYPE); \
    } while (0)

在class log的最后面,我们不妨定义一个宏,有点函数的意思,对于宏__FILE__ LINE__的意思是当前文件的名字和当前行号,后面的就是等级,格式了,对于##代表的是如果没有可变参数,那么自动忽视__VA_ARGS

cpp 复制代码
int main()
{
    log().logMessage("test filename1",1, DEBUG, "Hello world %d\n", 1200);
    log().logMessage("test filename2",2, INFO, "Hello world %d\n", 1201);
    // log().logMessage("test filename3",3, FATAL, "Hello world %d\n", 1202);
    // log().logMessage("test filename4",4, ERROR, "Hello world %d\n", 1203);
    // log().logMessage("test filename5",5, DEBUG, "Hello world %d\n", 1204);
    // log().logMessage("test filename6",6, FATAL, "Hello world %d\n", 1205);

    LOG(DEBUG,"Hello linux\n", 520,1314);
    LOG(INFO,"Hello linux\n");
    EnableFILE();
    LOG(WARNING,"Hello linux\n");
    LOG(FATAL,"Hello C++", "Today leanring how to write log\n");

    return 0;
}

此时,日志编写就非常完美的结束了。


感谢阅读!

相关推荐
牛奔2 分钟前
解决 Mac(M1/M2)芯片,使用node 14版本
linux·macos·编辑器·vim
wmxz52022 分钟前
Linux环境安装Jenkins
linux·ci/cd·jenkins·持续部署·持续集成
小草儿79922 分钟前
gbase8s之查看锁表的sql
服务器·数据库·mysql
黑蛋同志37 分钟前
CentOS 上下载特定的安装包及其所有依赖包
linux·运维·centos
是程序喵呀1 小时前
部署GitLab服务器
运维·服务器·gitlab
●VON1 小时前
go语言的成神之路-标准库篇-os标准库
linux·运维·服务器·开发语言·后端·学习·golang
TracyGC1 小时前
ubuntu 新建脚本shell并增加图标 双击应用实现python运行
linux·运维·ubuntu
怡雪~1 小时前
k8s的Pod亲和性
linux·容器·kubernetes
清风 0011 小时前
一、使用 mdadm 工具在 Ubuntu 上创建 RAID 1(镜像)
运维·服务器·数据库
白白白白白kkk1 小时前
【Ubuntu】脚本自动化控制终端填充
linux·ubuntu·自动化