【LINUX操作系统】日志系统——自己实现一个简易的日志系统
经过一段时间的操作系统的学习,现在是时候让读者朋友们利用学过的技术知识自己完成一个简单的日志系统。认识、了解日志系统既是对已有多线程知识的运用,也是进一步提升项目技术能力的必须步骤。
1. 什么是日志
⽇志认识计算机中的⽇志是记录系统和软件运⾏中发⽣事件的⽂件,主要作⽤是监控运⾏状态、记录异常信息,帮助快速定位问题并⽀持程序员进⾏问题修复。它是系统维护、故障排查和安全管理的重要⼯具。
一般情况下,一个日志都有以下内容:
时间戳()必有⽇志等级(必有)日志内容(必有)以下⼏个指标是可选的:•⽂件名⾏号•进程,线程相关id信息等⽇志有现成的解决⽅案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采⽤⾃定义⽇志的⽅式。
ll或者cat一下都能获得日志信息
以下是开机之后操作系统的日志信息
日志实际上也有一些现成的解决方案,例如spdlog、glog、Boost.Log、Log4cXx等等。本次日志系统就是为了实现一个简易版的日志系统,显示出来的日志效果如下:
[可读性很好的时间][日志等级][进程pid][打印对应日志的文件名][行号] - 消息内容,支持可变参数 [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc][16] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc][17] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc][18] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc][20] - hello world [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc][21] - hello world [2024-08-04 12:27:03] [WARNING] [202938] [main.cc][23] - hello world
设计模式-策略模式,此处先卖一个关子:
IT⾏业这么⽕, 涌⼊的⼈很多. 俗话说林⼦⼤了什么⻦都有. ⼤佬和菜鸡们两极分化的越来越严重. 为了让 菜鸡们不太拖⼤佬的后腿, 于是⼤佬们针对⼀些经典的常⻅的场景, 给定了⼀些对应的解决⽅案, 这个就 是 设计模式
使用上,我们希望形如:
//LOG(DEFAULT)<<2<<3.14<<"hello lsnm"
括号中传入的是日志等级,后面是自定义的日志信息。
日志等级:
日志信息:自己传入的自定义信息,要支持传入可变参数,可能需要借助流“<<”来完成任务
等级 说明 典型场景 DEBUG 调试信息,记录详细的程序流程和变量状态。 开发阶段排查问题时启用,生产环境通常禁用。 INFO 普通信息,记录程序正常运行的关键事件。 用户操作(如登录、下单)、服务启动/停止。 WARN 警告信息,记录潜在问题但不影响当前运行。 配置错误、资源不足(如磁盘空间不足)、第三方服务响应变慢。 ERROR 错误信息,记录业务逻辑错误或可恢复的异常。 数据库连接失败、参数校验不通过、文件读取失败。 FATAL 致命错误,记录导致系统崩溃或无法恢复的严重问题。 内存耗尽、主线程终止、关键依赖服务不可用。
2 . 构建日志系统
首先,日志系统大概分为两个部分:构建一个完整的日志字符串,再刷新落盘。
落盘可能是刷新到文件系统里,也可能是希望直接刷新到屏幕上。
开始构建字符串需要的数据
在自己的命名空间中确定一下需要使用并且在外部定义的数据
namespace LogModule {enum class LogLevel{DEBUG=1,INFO,WARN,ERROR,FATAL};std::string DefaultLogName="log.txt";std::string DefaultPathName="./log/"; }
希望默认在log文件夹里放置一个名字叫log.txt的文件
设计刷新策略
我们希望有一个接口,用于让日志动态进行刷新
定义一个顶级的策略 (基类)
引入之前实现的Mutex(或者直接用库中的也可以),完成RAII风格的封装,避免多线程访问屏幕带来的并发问题。
控制台刷新:
文件中刷新:
要向文件里写,就一定要打开文件,现在需要先确定这个目录一定存在:
引入头文件<filesystem>
简单学习一下:
exists和create_directories的参数既可以是官方建议的std::filesystem::path,也可以直接就是我们设计的_log_path(string),此处我们先namespace fs=std::filesystem一下
这个接口设计的目的:因为最后不清楚日志会在哪里使用,可能存在当前目录无法创建、无权限访问等问题,所以抛异常处理;另外,为了避免多个线程同时创建文件夹,所以引入锁
public:FileStrategy(std::string log_name=DefaultLogName,std::string log_path=DefaultLogPath):_log_name(log_name),_log_path(log_path){MutexGuard lock(_mutex);if(fs::exists(_log_path)){return ;}try{fs::create_directories(_log_path);}catch(const fs::filesystem_error &e){std::cerr << e.what() << '\n';}}
filesystem中的很多接口都是对文件操作的封装
如何刷新到文件里呢?
为了使用C++的输入输出流,再引入一个头文件<fstream>,在filestrategy中使用一下C++中的 文件函数接口:ofstream (就是文件输出的流,也有ifstream,不同于之前直接fstream 一个file再继续调用open,这是偏底层的C语言接口) ,一个输出文件流,ios::app表示追加
is_open接口用于确定文件流是否打开
刷新的时候要保证刷新的安全,所以加锁。
最后不要忘了写上继承
// 文件系统刷新模式class FileStrategy : public LogStrategy{public:FileStrategy(std::string log_name=DefaultLogName,std::string log_path=DefaultLogPath):_log_name(log_name),_log_path(log_path){//确认logpath是存在的MutexGuard lock(_mutex);if(fs::exists(_log_path)){return ;}try{fs::create_directories(_log_path);}catch(const fs::filesystem_error &e){std::cerr << e.what() << '\n';}}~FileStrategy(){}void SyncLog(const std::string& message){std::string _file = _log_path+_log_name;//log/log.txtstd::ofstream out(_file,std::ios::app);if(!out.is_open())//out的接口明显就C++的味道{return;}out<<message<<std::endl;out.close();}private:Mutex _mutex;std::string _log_name;std::string _log_path;};
完成了两种策略模式,现在可以着手构建Log来使用我们的策略模式了。
构建日志类
纯虚类()不能定义对象,但是能定义指针
class Logger{public:Logger(){_strategy = std::make_shared<ConsoleStrategy>();//make_shared直接构造一个实体对象}void EnableConsole(){_strategy = std::make_shared<ConsoleStrategy>();}void EnableFile(){_strategy = std::make_shared<FileStrategy>();}~Logger(){}private:std::shared_ptr<LogStrategy> _strategy;};
默认使用控制台参数,如果想要改可以使用EnableFileStrategy去使能文件日志系统。
注意,构造FileLogStrategy是可以有参数的,只是这里我们没有传
类中类:LogMessage
整个LogMessage是独立性很强的一个类,我们单独把他设置在Logger里面。
也就是说,我们希望一个LogMessage就是一条完整的日志信息。
时间戳(time_stamp)小彩蛋
完成一些基本的初始化之后,要获得完整的时间戳:
先来了解一下<cstream>中的两个概念:time_t 类型 ;struct tm结构体
另外,本次我们不再使用localtime获取本地时间,而是localtime_r(线程安全的!)
线程安全的函数似乎都有这种风格,如果正常执行,第二个输出型参数和返回值其实是冗余的;如果非正常执行,返回值是nullptr,输出型参数不得而知。
单独封装出一个可以获得可视性很强的时间戳的函数:
time_stamp,即时间戳
现在准备构建完整的日志info。
但是enum下都是int,我们想将其转换成string,使用一个switch case语句(转换所有的日志等级,因为日志等级的本质其实是整形)+stringstream来把刚刚的所有日志信息给拼到一起。
什么是stringstream?
在 C++ 中,字符串流(stringstream)是一种特殊的流类,它允许将字符串作为输入和输出流进行处理。字符串流提供了一种方便的方式,可以将字符串与其他基本类型进行转换、拼接、解析等操作。
string s="aaa";stringstream line;line<< s;line << 1;//intline<<0.12; //floatline<<'b'; //charcout<<line.str()<<endl;//结果为:aaa10.12b
stringstream还可以用作输出,将流里的字符串内容按照接受内容的类型去放进去。
这样一来,除了我们自主连续“<<”输入的调试信息,其他都的都构造好了。
连续的<<输入,需要重载LogMessage这个类的操作符
template<typename T>LogMessage& operator<<(const T& info){std::stringstream _buffer;_buffer<<info;_all_log_info+=_buffer.str();return *this;}
return*this可以保证能够连续<<
3. 如何使用日志系统?
看起来我们似乎已经完成了整个日志信息的构建
事实是,用的不是LogMessage类,而用的是Logger,LogMessage都是建立在整个Logger类里面的。直接初始化Logger,只会生成一个策略模式,而不会生成一个LogMessage,因为LogMessage只是被我们声明在里面,不去主动调用是不会生成的。
但是很明显,我们希望生成一个LogMessage,并且还是自动生成一个。
一处精妙的设计:
在Logger类里面实现一个圆括号重载,并且在命名空间中自带一个生成好的logger,我们想获得生成一个LogMessage只要调用对应的()访问就可以了。
并且,返回的是一个LogMessage的拷贝,在执行完对应的输出策略之后,这个变量就会销毁,还可以再利用logger调用一次方括号访问,达到刷新的目的
为了便于使用:
注意,#define末尾不能加;, 否则LOG(,,,)会直接调用这个函数并完成这个函数,无法使用重载的流输入
现在的问题是,尽管我们设计好了全部的消息和策略模式,可是我们不希望显示调用这个策略模式(代码不优雅!!)依赖于刚刚我们设计的LogMessage是拷贝返回
这个LogMessage被拷贝回调用这个构造函数的logger的时候,由于没有人接受他,他会自动销毁,也就是调用析构函数,那么。。。。。
伪代码思路如下:
Logger {LoggerMessage{public:LoggerMessage(....... , Logger& logger):....,logger(logger)//构造时多一个成员变量~LoggerMessage(){_logger.SyncLog();//利用创建自己的logger找到对应的策略....}private:......Logger& _logger;//增加一个_logger成员的引用,用来找到创建这个LoggerMessage的logger}LogMessage operator()(.....){return LogMessage(......,*this); // Logger在调用这个圆括号的时候把自己传进去!}}
两个类中的游龙一般的思路。
~LogMessage(){if(_logger._strategy){_logger._strategy->SyncLog(_all_log_info);}}
现在再回头来看使用方法,其实是
本质调用是第一行,实则是宏定义的2~6行,2~6行实则是重载的最后一行。
测试:
再美化一下策略模式的转换
最终测试:
4. 策略模式
策略模式的核心思想
策略模式的核心思想是将算法的使用与算法的实现分离开来。一个类的行为或其算法可以在运行时进行更改,这种类型的设计模式属于行为型模式。通过策略模式,可以避免使用多重条件判断语句,同时提高算法的保密性和安全性。
策略模式的组成
策略模式主要包含以下三个角色:
- 抽象策略(Strategy)类:这是一个抽象类或接口,它定义了一个或多个抽象方法,这些方法代表了具体的策略算法。
- 具体策略(Concrete Strategy)类:这些类实现了抽象策略类中定义的抽象方法,提供了具体的算法实现。也就是本文中的ConsoleStrategy和FileStrategy,一般继承自抽象策略
- 上下文(Context)类:这个类维护一个对抽象策略类的引用,并定义一个接口来允许客户端请求一个策略对象。上下文类还可能负责具体的策略执行。