初识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;
}

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


感谢阅读!

相关推荐
AlfredZhao12 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户97183563346618 小时前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪19 小时前
linux 拷贝文件或目录到指定的位置
linux
大树881 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠1 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush41 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5201 天前
Linux 11 动态监控指令top
linux
小宇宙Zz1 天前
Maven依赖冲突
java·服务器·maven
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈2 天前
Unix 与 Linux 异同小叙
linux·服务器·unix