✨个人主页: 熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】
目录
上一弹我们简要实现了线程池,但是还是有一个问题,就是使用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;
}
运行结果