日志代码编写
🌎日志代码编写
文章目录:
日志代码编写
了解日志
日志编写
日志等级
获取时间信息
获取文件名行号及处理可变参数列表
以宏的形式传参
日志加锁
日志消息输出方式
完整代码
🚀了解日志
日志是程序周期性运转或者特定时刻等一些常规或者特殊消息以特殊的形式打印出来,我们称为日志,关于日志,AI是这样回答的:
而我们今天要编写的日志,是使用C++编写的日志,日志格式为纯文本日志类型是应用程序日志。
🚀日志编写
✈️日志等级
日志是有等级的,就类似程序在调试的时候分为警告,错误,和崩溃等 等级一般,日志也有自己的等级,不过这里需要人为的将日志等级进行分类。
enum Level
{
DEBUG = 0, // 普通信息
INFO, // 消息打印
WARNING, // 警告信息
ERROR, // 错误信息
FATAL // 重大错误信息
};
✈️获取时间信息
将来我们需要将日志信息以纯文本的形式正确的打印出来,所以日志信息就作为了打印的格式。
日志等级表示不同的信息情况,那么我们需要把日志等级转换为字符串:
// 将日志等级转化为字符串
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 "Unknown";
}
}
除此之外,日志时间也是尤为重要的,在大型项目中版本经常更新迭代,日志信息的时间就显得尤为重要,在C++中,获取时间可以使用 gettimeofday
获取时间戳:
我们还可以直接使用 time
接口直接获取时间戳:
而我们日志显示信息通常不是以时间戳形式显示的,所以我们需要将获取的时间戳转化为年月日时分秒的形式,我们可以使用 localtime
接口,返回一个结构体 tm:
- 需要注意的是,这里的tm_year是 当前年份减去 1900年的值,tm_mon是当前月份的上一个月。
获取时间及日志等级信息测试:
std::string GetTimeString()
{
time_t curr_time = time(nullptr);
struct tm *format_time = localtime(&curr_time);
if(format_time == nullptr) return "None";
char time_buffer[1024];
// 写回到time_buffer中
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
format_time->tm_year +1900, // 年
format_time->tm_mon + 1, // 月
format_time->tm_mday, // 日
format_time->tm_hour, // 时
format_time->tm_min, // 分
format_time->tm_sec // 秒
);
return time_buffer;
}
void LogMessage(int level, const char* format, ...)
{
std::string levelstr = LevelToString(level);
std::string timestr = GetTimeString();
std::cout << levelstr << ": " << timestr << std::endl;
}
#include <iostream>
#include "Logtest.hpp"
int main()
{
LogMessage(DEBUG, "hello world");
return 0;
}
这样我们就可以获取到当前的时间了。
✈️获取文件名行号及处理可变参数列表
正常的日志绝对少不了文件名和行号的,不然怎么知道是哪里发出的日志信息?而在C语言中,我们曾经学习过下面的语法:
__LINE__ // 获取当前行号
__FILE__ // 获取当前文件名
这样,我们在获取日志信息的函数前加上这两个参数即可。
我上面在写日志信息函数的时候,在形参最后是有着可变参数列表的,为了后面可以传多个参数做准备,而我们虽然有可变参数列表,但是我们如何拿到可变参数才是重中之重。我们常常使用stdarg系列宏来处理可变参数列表。
首先,可变参数列表使用的前提是参数内必须要有一项是确定的,并且这个参数需要再可变参数列表的左侧。首先,我们使用 va_start
宏来使用参数初始化结构体 va_list(实质上是一个类型为 void* 的指针),va_end
来清空va_list:
// num表示可变参数的个数,并且传入函数的都是整数
void test(int num, ...)
{
va_list arg;
va_start(arg, num);// 使用num的地址来初始化arg指针,这样就可以索引到参数列表了
va_end(arg);
}
这个时候我们已经拿到了可变参数列表了,而第一个参数num表示的是可变参数的个数,而我们要处理每一个可变参数,就可以使用 va_arg
宏来处理可变参数:
void test(int num, ...)
{
va_list arg;
va_start(arg, num);
while(num)
{
int data = va_arg(arg, int);// 因为传入的参数都为int类型,则每次处理都会以四个字节为单位索引参数列表中的每一个元素
std::cout << data << " " << std::endl;
num--;
}
va_end(arg);
}
#include <iostream>
#include "Logtest.hpp"
int main()
{
test(4, 11, 22, 33, 44);
return 0;
}
在实际运用当中,没必要这么麻烦,库里已经为我们提供好了接口:
vsprintf表示,ap指针将以用户要求的 format格式 ,向字符串str中进行写入参数信息。而vsnprintf为安全模式下的输入参入信息。
经过上述的了解,我们可以将日志信息改为如下:
void LogMessage(std::string filename, int line, int level, const char* format, ...)
{
std::string levelstr = LevelToString(level);
std::string timestr = GetTimeString();
pid_t selfpid = getpid();
char buffer[4096];
va_list arg;
va_start(arg, format);
vsnprintf(buffer, sizeof(buffer), format, arg);
va_end(arg);
std::cout << levelstr << ": " << timestr << " : " << filename << " : " << line << " : " << buffer << std::endl;
}
int main()
{
LogMessage(__FILE__, __LINE__, DEBUG, "helloworld");
LogMessage(__FILE__, __LINE__, DEBUG, "helloworld: %s", "word");
LogMessage(__FILE__, __LINE__, DEBUG, "helloworld: %s, %d", "word", 10);
LogMessage(__FILE__, __LINE__, DEBUG, "helloworld: %s, %d, %f", "word", 10, 3.14);
return 0;
}
为了美观,可以改变一下输出形式:
void LogMessage(std::string filename, int line, int level, const char* format, ...)
{
std::string levelstr = LevelToString(level);
std::string timestr = GetTimeString();
pid_t selfpid = getpid();
char buffer[4096];
va_list arg;
va_start(arg, format);
vsnprintf(buffer, sizeof(buffer), format, arg);
va_end(arg);
std::cout << "[" << timestr << "]"
<< "[" << levelstr << "]"
<< "[ pid: " << selfpid << "]"
<< "[" << filename << "]"
<< "[" << line << "]"
<< buffer << std::endl;
}
✈️以宏的形式传参
我们直接使用函数调用的形式,需要每次都传参__LINE__, __FILE__ 的字样,这样写起来很不舒服,所以,我们可以采用宏定义的方式规避每次都传入这两个参数。
首先我们应该考虑到,可变参数列表如何进行宏替换,实际上,宏是支持可变参数列表的,但是函数如果要接收所有的参数,则可变参数部分需要使用宏 __VA_ARGS__
来接收所有可变参数。
#define LOG(level, format, ...) LogMessage(__FILE__, __LINE__, level, format, __VA_ARGS__)
int main()
{
LOG(DEBUG, "helloworld, %d", 10);
return 0;
}
尽管如此,如果我们不传入可变参数部分,__VA_ARGS__ 就会出现特殊字符,导致错误发生,所以我们可以在此参数前加上 ##
如果没有可变参数部分就会在内部将其清空。同时为了防止出现嵌套错误,我们可以在宏外侧使用do{}while(0):
#define LOG(level, format, ...) \
do \
{ \
LogMessage(__FILE__, __LINE__, level, format, ##__VA_ARGS__); \
} while (0)
✈️日志加锁
我们的日志可以适用于很多场景,多线程场景也不例外,所以,我们有必要对一些代码进行加锁:
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;
};
LOG.hpp:
#pragma once
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <cstdarg>
#include <unistd.h>
#include "LockGuard.hpp"
#include <string>
#include <ctime>
// 日志等级划分
enum Level
{
DEBUG = 0,
INFO,
WARNING,
ERROR,
FATAL
};
// 将日志等级转化为字符串
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 "Unknown";
}
}
std::string GetTimeString()
{
time_t curr_time = time(nullptr);
struct tm *format_time = localtime(&curr_time);
if (format_time == nullptr)
return "None";
char time_buffer[1024];
// 写回到time_buffer中
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
format_time->tm_year + 1900, // 年
format_time->tm_mon + 1, // 月
format_time->tm_mday, // 日
format_time->tm_hour, // 时
format_time->tm_min, // 分
format_time->tm_sec // 秒
);
return time_buffer;
}
#define LOG(level, format, ...) \
do \
{ \
LogMessage(__FILE__, __LINE__, level, format, ##__VA_ARGS__); \
} while (0)
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 初始化全局互斥锁
void LogMessage(std::string filename, int line, int level, const char *format, ...)
{
std::string levelstr = LevelToString(level);
std::string timestr = GetTimeString();
pid_t selfpid = getpid();
char buffer[4096];
va_list arg;
va_start(arg, format);
vsnprintf(buffer, sizeof(buffer), format, arg);
va_end(arg);
LockGuard lockguard(&lock);// 加锁
std::cout << "[" << timestr << "]"
<< "[" << levelstr << "]"
<< "[ pid: " << selfpid << "]"
<< "[" << filename << "]"
<< "[" << line << "]"
<< buffer << std::endl;
}
✈️日志消息输出方式
日志消息不仅仅可以打印在屏幕上,也可以选择打印在文件当中,在全局范围内设置一个表示,默认是不往文件当中打印的,在日志信息处理那一块,我们对该参数进行判断处理:
const std::string logname = "log.txt";
void SaveFile(const std::string &filename, const std::string &message)
{
std::ofstream out(filename, std::ios::app);
if(!out.is_open())
{
return;
}
out << message;
out.close();
}
void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...)
{
std::string levelstr = LevelToString(level);
std::string timestr = GetTimeString();
pid_t selfpid = getpid();
char buffer[4096];
va_list arg;
va_start(arg, format);
vsnprintf(buffer, sizeof(buffer), format, arg);
va_end(arg);
LockGuard lockguard(&lock);// 加锁
std::string message;
message = "[" + timestr + "]"
+ "[" + levelstr + "]"
+ "[ pid: " + std::to_string(selfpid)
+ "]" + "[" + filename + "]"
+ "[" + std::to_string(line) + "]"
+ buffer + "\n";
LockGuard lockguard(&lock); // 加锁
if (!issave)
{
std::cout << "[" << timestr << "]"
<< "[" << levelstr << "]"
<< "[ pid: " << selfpid << "]"
<< "[" << filename << "]"
<< "[" << line << "]"
<< buffer << std::endl;
}
else
{
SaveFile(logname, message);
}
}
这样就可以选择性的将日志信息保存在文件或者打印到显示器当中了。
🚀完整代码
Log.hpp:
#pragma once
#include <cstdio>
#include <iostream>
#include <time.h>
#include <cstdarg>
#include <fstream>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include "LockGuard.hpp"
#include <string>
#include <pthread.h>
bool gissave = false;// 是否保存到文件
const std::string logname = "log.txt";
// 日志等级划分
enum Level
{
DEBUG = 0,
INFO,
WARNING,
ERROR,
FATAL
};
// 将日志等级转化为字符串
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 "Unknown";
}
}
void SaveFile(const std::string &filename, const std::string &message)
{
std::ofstream out(filename, std::ios::app);
if(!out.is_open())
{
return;
}
out << message;
out.close();
}
std::string GetTimeString()
{
time_t curr_time = time(nullptr);
struct tm *format_time = localtime(&curr_time);
if (format_time == nullptr)
return "None";
char time_buffer[1024];
// 写回到time_buffer中
snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",
format_time->tm_year + 1900, // 年
format_time->tm_mon + 1, // 月
format_time->tm_mday, // 日
format_time->tm_hour, // 时
format_time->tm_min, // 分
format_time->tm_sec // 秒
);
return time_buffer;
}
#define LOG(level, format, ...) \
do \
{ \
LogMessage(__FILE__, __LINE__, gissave, format, ##__VA_ARGS__); \
} while (0)
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;// 初始化全局互斥锁
void LogMessage(std::string filename, int line, bool issave, int level, const char *format, ...)
{
std::string levelstr = LevelToString(level);
std::string timestr = GetTimeString();
pid_t selfpid = getpid();
char buffer[4096];
va_list arg;
va_start(arg, format);
vsnprintf(buffer, sizeof(buffer), format, arg);
va_end(arg);
LockGuard lockguard(&lock);// 加锁
std::string message;
message = "[" + timestr + "]"
+ "[" + levelstr + "]"
+ "[ pid: " + std::to_string(selfpid)
+ "]" + "[" + filename + "]"
+ "[" + std::to_string(line) + "]"
+ buffer + "\n";
LockGuard lockguard(&lock); // 加锁
if (!issave)
{
std::cout << "[" << timestr << "]"
<< "[" << levelstr << "]"
<< "[ pid: " << selfpid << "]"
<< "[" << filename << "]"
<< "[" << line << "]"
<< buffer << std::endl;
}
else
{
SaveFile(logname, message);
}
}
LockGuard.hpp:
#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;
};