【Linux】简易日志工具项目

CSDN 2024-08-25 08:07:02 阅读 53

在这里插入图片描述

有些鸟儿是不应该被关在笼子里的,

因为他们的羽毛太丰润了。

当他们飞走,你会由衷地庆贺他获得自由。

--- 肖申克的救赎》---


从零开始构建简易日志系统

1 日志1.1 什么是日志1.2 日志的意义1.3 为什么要构建自己的日志工具

2 构建自己的日志工具2.1 框架搭建2.2 LogMessage函数2.3 线程安全优化2.4 宏定义优化

3 总结

1 日志

日志(Log)是记录软件运行过程中发生的事件、状态变化和错误信息的记录文件。在软件开发和系统运维中,日志起着至关重要的作用

1.1 什么是日志

定义:日志是一种按时间顺序排列的记录,用于记录软件在运行过程中产生的各种信息,包括操作行为、系统状态、错误警告等。就像日记一样,程序每进行一个任务操作都要留下信息,方便他人查看。

日志通常包含以下几种信息:

时间戳:记录事件发生的时间。日志级别:表示日志信息的严重程度,如DEBUG、INFO、WARNING、ERROR、FATAL。来源:指出产生日志的文件行数(可以快速找到对应模块)。消息内容:具体描述事件或错误信息。

目前,在实际开发中我们有非常丰富的日志库可以选择:

spdlog是一个非常快速、支持并发的C++日志库,它提供了易于使用的接口和丰富的特性,包括异步日志记录、多线程支持、格式化输出等。官方网站在这里Glog是由Google开发的C++日志库,它提供了基于C++风格的日志API,支持条件日志记录、日志旋转和严重错误时的信号处理。官方网站在这里

1.2 日志的意义

日志在开发中主要有以下一些作用:

追踪问题:通过日志,开发者可以了解软件在运行过程中的状态,快速定位问题所在。分析原因:日志记录了软件运行过程中的详细信息,有助于分析问题产生的原因。优化性能:通过分析日志,可以发现软件的性能瓶颈,从而进行优化。安全审计:日志记录了软件的操作行为,有助于审计和监控系统的安全性。数据挖掘:在某些场景下,日志数据可以用于数据挖掘,为业务分析和决策提供支持。

同样日志在项目开发中至关重要,从开发调试阶段 - 测试阶段 - 部署阶段 - 运行维护阶段…都具有相当重要的作用!并且一个优雅的日志系统是可以让开发者赏心悦目的进行项目开发,优雅!

总之,日志在项目开发中具有举足轻重的作用。一个完善的日志系统可以提高软件的可靠性、稳定性和可维护性,为软件开发和运维提供有力支持。

1.3 为什么要构建自己的日志工具

从学习的角度出发,开发一个自己的简易日志工具可以带来以下好处:

深入理解日志原理

通过自己实现日志工具,可以更深入地理解日志记录的基本原理,包括日志的格式化、写入、级别控制等。对以后使用第三方日志库有很大帮助掌握核心编程技能

在开发过程中,可以锻炼和提升核心编程技能,如文件操作、字符串处理、时间管理、异常处理等。这是一笔很重要的经验!模块化和抽象思维

日志工具的开发需要良好的模块化和抽象思维能力,这有助于在未来的项目中更好地组织代码。错误处理和调试

在开发过程中,不可避免地会遇到错误和调试问题,这提供了实践错误处理和调试技巧的机会。理解日志在系统中的作用

通过实现日志工具,可以更深刻地理解日志在系统监控、问题排查、性能分析等方面的重要性。增强项目经验

开发日志工具可以作为一个独立的项目经验,有助于在简历上展示实际编程能力和解决问题的能力。

总之,开发一个自己的简易日志工具是一个综合性的学习过程,下面我们来开发一个自己的日志工具!

2 构建自己的日志工具

2.1 框架搭建

设计一个日志系统首先要明确我们希望打印出什么格式的日志信息:

在这里插入图片描述

我们想要呈现出上面这样的日志信息,就需要设置一个信息类<code>logmessage来储存信息,类内需要这些信息:

int _level : 日志等级,通过枚举变量来快速通过数字对应等级pid_t _id : 进程IDstd::string _filename : 文件名int _filenumber : 行号std::string _curr_time : 当前时间std::string _message_info : 日志信息

然后我们在设计一个初步的日志类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函数。

2.2 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();

}

现在我们 运行测试一下:

在这里插入图片描述

可以看到我们的日志工具已经可以规范的打印消息了!非常优雅!

2.3 线程安全优化

单线程的情况,我们的日志工具肯定是没有问题的!如果是多线程呢?我们来看看我们有哪些是全局的变量需要互斥锁保护:只有显示器打印和文件打印是对全局的资源进行操作,所以我们只需要对<code>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;

}

}

这样我们的日志类就可以保证多线程下的安全运行了!

2.4 宏定义优化

上面的代码已经可以满足日志的书写的工作了,但是如果还想要更加的优雅的操作,我们可以使用宏定义来免去书写文件名和行号的操作,并且不在需要手动创建类,可以直接调用宏定义来进行日志的书写!

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;

}

这样是在是优雅:

在这里插入图片描述

这样我们就完成了日志工具项目的构建!!!

3 总结

项目技术栈:

编程语言:C++

操作系统相关:POSIX线程(pthread)、文件操作、时间处理

编程技巧:面向对象编程、设计模式(单例模式、工厂模式)、RAII(资源获取即初始化)

编程技巧与学习点:

日志原理与设计 :文章深入探讨了日志的定义、组成和重要性,以及如何设计一个日志系统。核心编程技能:通过实现日志工具,锻炼了文件流操作、字符串处理、时间管理等技能。错误处理与调试:在开发过程中,实践了错误处理和调试技巧,特别是在多线程环境下的线程安全问题。线程安全 :通过引入互斥锁(mutex)和锁守卫(LockGuard),确保了日志工具在多线程环境下的安全使用。宏定义优化 :使用宏定义简化了日志记录的代码,提高了代码的简洁性和易用性。

我们通过构建一个简易的日志工具,展示了从需求分析、系统设计到具体实现的完整过程。介绍了如何使用C++构建一个具有基本功能的日志系统,包括日志消息的格式化、文件和屏幕输出、日志级别的控制等。实践了日志工具的线程安全优化,确保了其在多线程环境下的稳定性。

通过这个项目,可以学习到如何从零开始构建一个日志系统,掌握相关的编程技能和设计理念,同时也能够加深对日志在软件开发中作用的理解。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。