日志(Log)是记录软件运行过程中发生的事件、状态变化和错误信息的记录文件。在软件开发和系统运维中,日志起着至关重要的作用。
定义:日志是一种按时间顺序排列的记录,用于记录软件在运行过程中产生的各种信息,包括操作行为、系统状态、错误警告等。就像日记一样,程序每进行一个任务操作都要留下信息,方便他人查看。
日志通常包含以下几种信息:
目前,在实际开发中我们有非常丰富的日志库可以选择:
日志在开发中主要有以下一些作用:
同样日志在项目开发中至关重要,从开发调试阶段 - 测试阶段 - 部署阶段 - 运行维护阶段…都具有相当重要的作用!并且一个优雅的日志系统是可以让开发者赏心悦目的进行项目开发,优雅!
总之,日志在项目开发中具有举足轻重的作用。一个完善的日志系统可以提高软件的可靠性、稳定性和可维护性,为软件开发和运维提供有力支持。
从学习的角度出发,开发一个自己的简易日志工具可以带来以下好处:
总之,开发一个自己的简易日志工具是一个综合性的学习过程,下面我们来开发一个自己的日志工具!
设计一个日志系统首先要明确我们希望打印出什么格式的日志信息:

我们想要呈现出上面这样的日志信息,就需要设置一个信息类logmessage来储存信息,类内需要这些信息:
然后我们在设计一个初步的日志类Log,我们希望的是通过:
Log lg;
lg.LogMessage(__FILE__ , __LINE__ , DEBUG , "%d %s %f" , 1 , "你好" , 3.14);这样的上层调用来实现日志信息的打印,所以Log内部不需要设置信息类logmessage。只需要在LogMessage函数中设置一个临时变量,保证每次调用都会通过这个临时来储存信息。为了可以区分是向显示器打印还是向文件打印,我们添加一个成员变量_type来方便后期确认打印方式!
#pragma once
#include <string>
#include <sys/types.h>
#include <unistd.h>
#include <aio.h>
#include <stdarg.h>
#include <fstream>
#include <cstring>
//打印方式
#define SCREEN_TYPE 1
#define FILE_TYPE 2
const std::string file = "log.txt";
// 等级划分
enum
{
DEBUG = 1,
INFO,
WARNING,
ERROR,
FATAL,
};
// 信息类
class logmessage
{
public:
std::string _level; // 日志信息等级
int _id; // 进程ID
std::string _curr_time; // 当前时间
std::string _filename; // 文件名
int _filenumber; // 行号
std::string _message_info; // 日志信息
};
// 日志类
class Log
{
private:
std::string LevelToString(int level)
{
switch (level)
{
case 1:
return "DEBUG";
case 2:
return "INFO";
case 3:
return "WARNING";
case 4:
return "ERROR";
case 5:
return "FATAL";
default:
return "UNKNOW";
}
}
public:
// 空的构造函数
Log() : _type(SCREEN_TYPE)
{
}
// 处理数据
void LogMessage(std::string filename, int filenumber, int level, const char *format, ...)
{
}
~Log()
{
}
private:
int _type;
};接下来我们来处理最重要的LogMessage函数。
在LogMessage函数中我们需要依次处理传入的信息,并储存在logmessage类中。函数一定要支持可变参数,才能更好的支持外部调用的功能性!
logMessage(std::string filename , int level , int filenumber , const char* format , ...) 接下来我们进行信息类的处理,依次处理 日志等级、进程ID、文件名、行号、当前时间、日志信息:
_level :通过公共方法LevelToString()将 等级 转换为 字符串:简单的通过switch语句实现
std::string LevelToString(int level)
{
switch (level)
{
case 1:
return "DEBUG";
case 2:
return "INFO";
case 3:
return "WARNING";
case 4:
return "ERROR";
case 5:
return "FATAL";
default:
return "UNKNOW";
}
}获取 pid + 文件名 + 行号:这个很简单!
// 处理文件名 行号
lm._filename = filename;
lm._filenumber = filenumber;
// 获取进程ID
lm._id = getpid();_curr_time: 获取时间 time() ,再通过localtime()得到当前时间的结构体,然后通过方法 TimeToString() 转换为字符串就可以了,需要注意的是,获取的时间结构体内的时间和原本时间有出入,需要进行一些处理:
std::string TimeToString()
{
time_t now = time(nullptr);
struct tm *t = localtime(&now);
int year = t->tm_year;
int mon = t->tm_mon;
int day = t->tm_mday;
int hour = t->tm_hour;
int min = t->tm_min;
int sec = t->tm_sec;
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
year + 1900,
mon + 1,
day,
hour,
min,
sec);
return buffer;
}_message_info:日志信息是一段带有可变参数的字符串,使用vsnprintf可以简单解决。首先进行va_list 的初始化,然后 vsnprintf() 可以直接将可变参数中进行提取 ,(va_start标定开始位置 , va_end结束)
// 日志信息
char buffer[1024];
va_list ap;
va_start(ap, format);
vsnprintf(buffer, sizeof(buffer), format, ap);
va_end(ap);
lm._message_info = buffer;这样最重要的数据转换我们就完成了,接下来就是打印的问题了,我们设计一个FlushLog刷新日志信息的函数,在里面进行打印的处理,根据打印格式打印对应信息:
// 刷新数据
void FlushLog(const logmessage &lg)
{
switch (_type)
{
case 1:
FlushToScreen(lg);
break;
case 2:
FlushToFile(lg);
break;
}
}打印方式有两种:
printf打印特定格式就好fstream快速进行写入处理(非常好用!) void FlushToScreen(const logmessage &lg)
{
printf("[%s][%d][%s][%d][%s] %s \n",
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::fstream out(_logfile.c_str(), std::ios_base::out | std::ios_base::app );
char buffer[1024];
snprintf(buffer, sizeof(buffer), "[%s][%d][%s][%d][%s] %s \n",
lg._level.c_str(),
lg._id,
lg._filename.c_str(),
lg._filenumber,
lg._curr_time.c_str(),
lg._message_info.c_str());
out.write(buffer, strlen(buffer));
out.close();
}现在我们 运行测试一下:

可以看到我们的日志工具已经可以规范的打印消息了!非常优雅!
单线程的情况,我们的日志工具肯定是没有问题的!如果是多线程呢?我们来看看我们有哪些是全局的变量需要互斥锁保护:只有显示器打印和文件打印是对全局的资源进行操作,所以我们只需要对FlushLog中进行线程保护即可!
//全局锁
pthread_mutex_t _mtx = PTHREAD_MUTEX_INITIALIZER;为了更加优雅的进行操作,我们使用之前编写的RAII规则的锁守卫LockGuard进行保护:
// 刷新数据
void FlushLog(const logmessage &lg)
{
LockGuard lock(&_mtx);
switch (_type)
{
case 1:
FlushToScreen(lg);
break;
case 2:
FlushToFile(lg);
break;
}
}这样我们的日志类就可以保证多线程下的安全运行了!
上面的代码已经可以满足日志的书写的工作了,但是如果还想要更加的优雅的操作,我们可以使用宏定义来免去书写文件名和行号的操作,并且不在需要手动创建类,可以直接调用宏定义来进行日志的书写!
Log lg;
#define Log(Level, Format, ...) \
do \
{ \
lg.LogMessage(__FILE__, __LINE__, Level , Format, ##__VA_ARGS__); \
} while (0)
#define EnableScreen() \
do \
{ \
lg.Enable(SCREEN_TYPE); \
} while (0)
#define EnableFile() \
do \
{ \
lg.Enable(FILE_TYPE); \
} while (0)宏定义会在调用位置直接进行打开,所以__FILE__, __LINE__,就直接可以传入文件和行数了,不在需要我们书写:
int main()
{
int cnt = 5;
while (cnt--)
{
Log(DEBUG, "%d %s %f", cnt, "你好", 3.1415);
sleep(1);
}
EnableFile();
cnt = 5;
while (cnt--)
{
Log(DEBUG, "%d %s %f", cnt, "你好", 3.1415);
sleep(1);
}
return 0;
}这样是在是优雅:

这样我们就完成了日志工具项目的构建!!!
项目技术栈:
编程语言:C++ 操作系统相关:POSIX线程(pthread)、文件操作、时间处理 编程技巧:面向对象编程、设计模式(单例模式、工厂模式)、RAII(资源获取即初始化)
编程技巧与学习点:
我们通过构建一个简易的日志工具,展示了从需求分析、系统设计到具体实现的完整过程。介绍了如何使用C++构建一个具有基本功能的日志系统,包括日志消息的格式化、文件和屏幕输出、日志级别的控制等。实践了日志工具的线程安全优化,确保了其在多线程环境下的稳定性。
通过这个项目,可以学习到如何从零开始构建一个日志系统,掌握相关的编程技能和设计理念,同时也能够加深对日志在软件开发中作用的理解。