目录
前言:
在线程池部分我们纵观全文,可以发现全文有很多很多的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;
}
此时,日志编写就非常完美的结束了。
感谢阅读!