【Linux 实战】从0到1手搓日志系统:附完整代码

【Linux 实战】从0到1手搓日志系统:附完整代码
前言:

        上文我们讲了线程的同步以及理解并实现生产者消费者模式【Linux系统】深入理解线程同步,实现生产消费模型-ZEEKLOG博客

        本文我们来讲一下如何手搓日志库,为下一篇文件:线程池的实现做铺垫!

日志与策略模式

什么是设计模式

        IT行业这么火,涌入的人很多,俗话说林子大了啥鸟都有。大佬和菜鸡们两极分化的越来越严重。为了让菜鸡们不太拖大佬的后腿,于是大佬们针对一些经典的常见的场景,给定了一些对应的解决方案,这个就是设计模式。

认识日志

        计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。

        日志已有现成的解决方案,如:spdlog、glog、Boost.Log等等。

日志的格式有以下的标准:

        1.时间戳        2.日志的等级        3.日志的内容

还可以加入:

        1.文件名        2.行号        3.线程or进程的id

        这里我们采用设计模式 - 策略模式来进行日志的设计!

我们想要实现的日志格式如下:

[可读性很好的时间] [⽇志等级] [进程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 

前提

实现日志需要使用到锁,这里我们可以使用上一篇文章中封装好了的锁接口。【Linux系统】深入理解线程同步,实现生产消费模型-ZEEKLOG博客
Mutex.hpp // 封装锁接口 #pragma once #include <pthread.h> class Mutex { public: Mutex() { pthread_mutex_init(&mutex, nullptr); } ~Mutex() { pthread_mutex_destroy(&mutex); } void Lock() { pthread_mutex_lock(&mutex); } void Unlock() { pthread_mutex_unlock(&mutex); } private: pthread_mutex_t mutex; }; class LockGuard { public: LockGuard(Mutex &mutex) : _Mutex(mutex) { _Mutex.Lock(); } ~LockGuard() { _Mutex.Unlock(); } private: // 为了保证锁的底层逻辑,锁是不能够拷贝的,并且也是没有拷贝构造函数的 // 避免拷贝,应该引用 Mutex &_Mutex; };

日志刷新策略实现

日志的刷新策略可以分为两种:向屏幕刷新、向指定文件刷新

第一步,先实现日志中的刷新策略:

        我们可以通过C++中的继承与多态来实现日志选择不同的刷新策略。

        基类作为抽象类没有任何方法实现,不同的子类来继承后实现不同的刷新方式,其中不论是显示器刷新还是指定文件刷新,其都是临界资源,不能同时访问写入数据,否则将会导致信息错乱。所有刷新的过程全部都要加锁!

        向显示器刷新没什么好说的

        向文件刷新:其中对文件的路径要进行判断,如果文件的路径不存在,则需要我们创建对应的路径。其中需要使用的接口如下:

#include <filesystem> //C++17标准 命名空间:std::filesystem 位于filesystem命名空间中 判断路径p是否存在,返回bool:exists(const path& p) 创建对应的路径: create_directories(const path& p)

打开对应的路径:

#include <fstream> 命名空间:std 位于std命名空间中 创建对象时打开文件: ofstream ofs(path,openmode) 打开模式设置为追加模式:ios::app

具体实现:

const string end = "\r\n"; // 实现刷新策略:a.向显示器刷新 b.向指定文件刷新 // 利用多态机制实现 // 包含至少一个纯虚函数的类称为抽象类,不能实例化,只能被继承 class LogStrategy // 基类 { public: //"=0"声明为纯虚函数。纯虚函数强制派生类必须重写该函数 virtual void SyncLog(const string& message) = 0; }; // 向显示器刷新:子类 class ConsoleLogStrategy : public LogStrategy { public: void SyncLog(const string& message) override { // 加锁,访问显示器,显示器也是临界资源 LockGuard lockguard(_mutex); cout << message << end; } private: Mutex _mutex; }; // 向指定文件刷新:子类 const string defaultpath = "./log"; const string defaultfile = "my.log"; class FileLogStrategy : public LogStrategy { public: FileLogStrategy(const string& path = defaultpath, const string& file = defaultfile) : _path(path), _file(file) { LockGuard lockguard(_mutex); // 判断路径是否存在,如果不存在就创建对应的路径 if (!(filesystem::exists(_path))) filesystem::create_directories(_path); } void SyncLog(const string& message) override { // 合成最后路径 string Path = _path + (_path.back() == '/' ? "" : "/") + _file; // 打开文件 ofstream out(Path, ios::app); //以流方式向文件写入数据 out << message << end; } private: string _path; string _file; Mutex _mutex; };

日志文件生成与选择刷新策略实现

完成日志文件的刷新策略,下面实现日志文件的形成与刷新策略的选择。

第二步,实现日志文件对刷新策略的选择:

        利用多态机制灵活选择刷新策略

class Logger { public: Logger() { // 默认选择显示器刷新 Strategy = make_unique<ConsoleLogStrategy>(); } //选择在显示器刷新 void EnableConsoleLogStrategy() { Strategy = make_unique<ConsoleLogStrategy>(); } //选择向指定文件刷新 void EnableFileLogStrategy() { Strategy = make_unique<FileLogStrategy>(); } private: //智能指针 unique_ptr<LogStrategy> Strategy; };

第三步,实现日志文件的生成:

        我们选择Logger类中实现LogMessage类的方式,来实现日志信息。

补充:

        外部类只能通过内部类的实例化对象,来访问内部类中的方法与成员,且受修饰符限制

        内部类可以直接访问外部类的方法以及成员,没有限制
class Logger { public: Logger() { // 默认选择显示器刷新 Strategy = make_unique<ConsoleLogStrategy>(); } //选择在显示器刷新 void EnableConsoleLogStrategy() { Strategy = make_unique<ConsoleLogStrategy>(); } //选择向指定文件刷新 void EnableFileLogStrategy() { Strategy = make_unique<FileLogStrategy>(); } //日志信息类 class LogMessage { //..... } private: //智能指针 unique_ptr<LogStrategy> Strategy; };

LogMessage:

我们想要实现的日志信息格式如下:

[可读性很好的时间] [⽇志等级] [进程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 

        首先,我们知道在日志信息中存在日志等级,而日志等级不能由外部随意传入,需要一定的规范。所以我们可以先在类外通过枚举的方式给出日志等级,便于后续日志信息类的实现。

// 日志等级 // enum class:强类型枚举。1.必须通过域名访问枚举值 2.枚举值不能隐式类型转化为整型 enum class LogLevel { DEBUG, // 调试级 INFO, // 信息级 WARNING, // 警告级 ERROR, // 错误级 FATAL // 致命级 }; // 将等级转化为字符串 string LevelToStr(LogLevel level) { switch (level) { case LogLevel::DEBUG: return "DEBUG"; case LogLevel::INFO: return "DEBUG"; case LogLevel::WARNING: return "WARNING"; case LogLevel::ERROR: return "ERROR"; case LogLevel::FATAL: return "FATAL"; default: return "UNKOWN"; } }

        其次,时间的获取也是一个难点,我们在类外定义一个函数用于获取时间。并需要用到一下接口:

        time函数:用于返回类型为time_t的时间戳。

        localtime_r函数:讲时间戳转化为对应的本地时间。

        struct tm结构体:转化后的本地时间存储放在这个结构体中。
// 获取时间 string GetTime() { // time函数:获取当前系统的时间戳 // localtime_r函数:将时间戳转化为本地时间(可重入函数,localtime则是不可重入函数) // struct tm结构体,会将转化之后的本地时间存储在结构体中 time_t curr = time(nullptr); struct tm curr_time; localtime_r(&curr, &curr_time); char buffer[128]; snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d-%02d %02d:%02d:%02d", curr_time.tm_year + 1900, // 年份是从1900开始计算的,需要加上1900才能得到正确的年份 curr_time.tm_mon + 1, // 月份了0~11,需要加上1才能得到正确的月份 curr_time.tm_mday, // 日 curr_time.tm_hour, // 时 curr_time.tm_min, // 分 curr_time.tm_sec); // 秒 return buffer; }

        pid号可由系统调用getpid()直接获得,而行号、名称、信息等可由外部作为参数传入。

实现:

        我们预想的日志调用方式是:日志信息对象(日志信息) << 信息<<信息

        在以下类中实现了日志信息的创建,以及对<<运算符的重载。并在对象销毁时进行刷新

// 日志信息 class LogMessage { public: LogMessage(const LogLevel& level, const string& name, const int& line, Logger& logger) : _level(level), _name(name), _logger(logger), _line_member(line) { _pid = getpid(); _time = GetTime(); // 合并:日志信息的左半部分 stringstream ss; // 创建输出流对象,stringstream可以将输入的所有数据全部转为为字符串 ss << "[" << _time << "] " << "[" << LevelToStr(_level) << "] " << "[" << _pid << "] " << "[" << _name << "] " << "[" << _line_member << "] " << " - "; // 返回ss中的字符串 _loginfo = ss.str(); } // 日志文件的右半部分:可变参数,重载运算符<< // e.g. <<"huang"<<123<<"dasd"<<24 template <class T> LogMessage& operator<<(const T& message) // 引用返回可以让后续内容不断追加 { stringstream ss; ss << message; _loginfo += ss.str(); // 返回对象! return *this; } // 销毁时,将信息刷新 ~LogMessage() { // 日志文件 _logger.Strategy->SyncLog(_loginfo); } private: string _time; LogLevel _level; pid_t _pid; string _name; int _line_member; string _loginfo; // 合并之后的一条完整信息 // 日志对象 Logger& _logger; };

再在Logger类中实现对运算符()的重载!实现对LogMessage对象的快速创建。

// 重载运算符(),便于创建LogMessage对象 // 这里返回临时对象:当临时对象销毁时,调用对应的析构函数,自动对象中创建好的日志信息进行刷新! // 其次局部对象也不能传引用返回! LogMessage operator()(const LogLevel& level, const string& name, const int& line) { return LogMessage(level, name, line, *this); }

基本实现,日志调用方式:日志信息对象(日志信息) << 信息<<信息

但是我们仍然需要手动的传入文件名、行号。为了用户更加方便快捷的使用:我们可以通过宏来传入这两个参数!顺带可以将刷新策略的选择一并封装为宏。

// 创建日志,并刷新 //__FILE__ 和 __LINE__ 是编译器预定义的宏,作用是获取当前代码所在的文件名、行号 #define LOG(level) logger(level, __FILE__, __LINE__) // 细节:不加; // 切换刷新策略 #define Enable_Console_LogStrategy() logger.EnableConsoleLogStrategy(); #define Enable_File_LogStrategy() logger.EnableFileLogStrategy();

接口调用演示:

这样就完整实现了一个日志系统!调用方式演示如下:

#include "Log.hpp" using namespace LogModule; int main() { // 测试日志文件的刷新 // 文件刷新 Enable_File_LogStrategy(); LOG(LogLevel::DEBUG) << "调试" << 1; LOG(LogLevel::ERROR) << "错误" << 2; LOG(LogLevel::WARNING) << "警告" << 3; }

完整代码

Log.hpp // 实现日志模块 #pragma once #include <iostream> #include <sstream> // 包含stringstream类 #include <filesystem> //C++17文件操作接口库 #include <fstream> #include <sys/types.h> #include <unistd.h> #include "Mutex.hpp" using namespace std; // 补充:外部类只能通过内部类的实例化对象,来访问内部类中的方法与成员,且受修饰符限制 // 内部类可以直接访问外部类的方法以及成员,没有限制 namespace LogModule { const string end = "\r\n"; // 实现刷新策略:a.向显示器刷新 b.向指定文件刷新 // 利用多态机制实现 // 包含至少一个纯虚函数的类称为抽象类,不能实例化,只能被继承 class LogStrategy // 基类 { public: //"=0"声明为纯虚函数。纯虚函数强制派生类必须重写该函数 virtual void SyncLog(const string &message) = 0; }; // 向显示器刷新:子类 class ConsoleLogStrategy : public LogStrategy { public: void SyncLog(const string &message) override { // 加锁,访问显示器,显示器也是临界资源 LockGuard lockguard(_mutex); cout << message << end; } private: Mutex _mutex; }; // 向指定文件刷新:子类 const string defaultpath = "./log"; const string defaultfile = "my.log"; class FileLogStrategy : public LogStrategy { public: FileLogStrategy(const string &path = defaultpath, const string &file = defaultfile) : _path(path), _file(file) { LockGuard lockguard(_mutex); // 判断路径是否存在,如果不存在就创建对应的路径 if (!(filesystem::exists(_path))) filesystem::create_directories(_path); } void SyncLog(const string &message) override { // 合成最后路径 string Path = _path + (_path.back() == '/' ? "" : "/") + _file; // 打开文件 ofstream out(Path, ios::app); out << message << end; } private: string _path; string _file; Mutex _mutex; }; // // 日志等级 // enum class:强类型枚举。1.必须通过域名访问枚举值 2.枚举值不能隐式类型转化为整型 enum class LogLevel { DEBUG, // 调试级 INFO, // 信息级 WARNING, // 警告级 ERROR, // 错误级 FATAL // 致命级 }; // // 将等级转化为字符串 string LevelToStr(LogLevel level) { switch (level) { case LogLevel::DEBUG: return "DEBUG"; case LogLevel::INFO: return "DEBUG"; case LogLevel::WARNING: return "WARNING"; case LogLevel::ERROR: return "ERROR"; case LogLevel::FATAL: return "FATAL"; default: return "UNKOWN"; } } // 获取时间 string GetTime() { // time函数:获取当前系统的时间戳 // localtime_r函数:将时间戳转化为本地时间(可重入函数,localtime则是不可重入函数) // struct tm结构体,会将转化之后的本地时间存储在结构体中 time_t curr = time(nullptr); struct tm curr_time; localtime_r(&curr, &curr_time); char buffer[128]; snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d-%02d %02d:%02d:%02d", curr_time.tm_year + 1900, // 年份是从1900开始计算的,需要加上1900才能得到正确的年份 curr_time.tm_mon + 1, // 月份了0~11,需要加上1才能得到正确的月份 curr_time.tm_mday, // 日 curr_time.tm_hour, // 时 curr_time.tm_min, // 分 curr_time.tm_sec); // 秒 return buffer; } // // 实现日志信息,并选择刷新策略 class Logger { public: Logger() { // 默认选择显示器刷新 Strategy = make_unique<ConsoleLogStrategy>(); } void EnableConsoleLogStrategy() { Strategy = make_unique<ConsoleLogStrategy>(); } void EnableFileLogStrategy() { Strategy = make_unique<FileLogStrategy>(); } // 日志信息 class LogMessage { public: LogMessage(const LogLevel &level, const string &name, const int &line, Logger &logger) : _level(level), _name(name), _logger(logger), _line_member(line) { _pid = getpid(); _time = GetTime(); // 合并:日志信息的左半部分 stringstream ss; // 创建输出流对象,stringstream可以将输入的所有数据全部转为为字符串 ss << "[" << _time << "] " << "[" << LevelToStr(_level) << "] " << "[" << _pid << "] " << "[" << _name << "] " << "[" << _line_member << "] " << " - "; // 返回ss中的字符串 _loginfo = ss.str(); } // 日志文件的右半部分:可变参数,重载运算符<< // e.g. <<"huang"<<123<<"dasd"<<24 template <class T> LogMessage &operator<<(const T &message) // 引用返回可以让后续内容不断追加 { stringstream ss; ss << message; _loginfo += ss.str(); // 返回对象! return *this; } // 销毁时,将信息刷新 ~LogMessage() { // 日志文件 _logger.Strategy->SyncLog(_loginfo); } private: string _time; LogLevel _level; pid_t _pid; string _name; int _line_member; string _loginfo; // 合并之后的一条完整信息 // 日志对象 Logger &_logger; }; // 重载运算符(),便于创建LogMessage对象 // 这里返回临时对象:当临时对象销毁时,调用对应的析构函数,自动对象中创建好的日志信息进行刷新! // 其次局部对象也不能传引用返回! LogMessage operator()(const LogLevel &level, const string &name, const int &line) { return LogMessage(level, name, line, *this); } private: unique_ptr<LogStrategy> Strategy; }; // 为了用户使用更方便,我们使用宏封装一下 Logger logger; // 切换刷新策略 #define Enable_Console_LogStrategy() logger.EnableConsoleLogStrategy(); #define Enable_File_LogStrategy() logger.EnableFileLogStrategy(); // 创建日志,并刷新 //__FILE__ 和 __LINE__ 是编译器预定义的宏,作用是获取当前代码所在的文件名、行号 #define LOG(level) logger(level, __FILE__, __LINE__) // 细节:不加; };
Mutex.hpp // 封装锁接口 #pragma once #include <pthread.h> class Mutex { public: Mutex() { pthread_mutex_init(&mutex, nullptr); } ~Mutex() { pthread_mutex_destroy(&mutex); } void Lock() { pthread_mutex_lock(&mutex); } void Unlock() { pthread_mutex_unlock(&mutex); } private: pthread_mutex_t mutex; }; class LockGuard { public: LockGuard(Mutex &mutex) : _Mutex(mutex) { _Mutex.Lock(); } ~LockGuard() { _Mutex.Unlock(); } private: // 为了保证锁的底层逻辑,锁是不能够拷贝的,并且也是没有拷贝构造函数的 // 避免拷贝,应该引用 Mutex &_Mutex; };

Read more

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

摘要:本文聚焦OpenClaw从测试环境走向生产环境的核心痛点,围绕“性能优化、安全加固、监控运维”三大维度展开实操讲解。先明确生产环境硬件/系统选型标准,再通过硬件层资源管控、模型调度策略、缓存优化等手段提升响应速度(实测响应效率提升50%+);接着从网络、权限、数据三层构建安全防护体系,集成火山引擎安全方案拦截高危操作;最后落地TenacitOS可视化监控与Prometheus告警体系,配套完整故障排查清单和虚拟实战案例。全文所有配置、代码均经实测验证,兼顾新手入门实操性和进阶读者的生产级部署需求,帮助开发者真正实现OpenClaw从“能用”到“放心用”的跨越。 优质专栏欢迎订阅! 【DeepSeek深度应用】【Python高阶开发:AI自动化与数据工程实战】【YOLOv11工业级实战】 【机器视觉:C# + HALCON】【大模型微调实战:平民级微调技术全解】 【人工智能之深度学习】【AI 赋能:Python 人工智能应用实战】【数字孪生与仿真技术实战指南】 【AI工程化落地与YOLOv8/v9实战】【C#工业上位机高级应用:高并发通信+性能优化】 【Java生产级避坑指南:

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言  一、实验基础说明 1.1、互斥体简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 互斥体 LED 驱动代码(mutex.c) 3.2.1、设备结构体定义(28-39

By Ne0inhk
Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 后端工程师扔给你一个 Swagger (OpenAPI) 文档地址,你会怎么做? 1. 对着文档,手写 Dart Model 类(容易写错字段类型)。 2. 手写 Retrofit/Dio 的 API 接口定义(容易拼错 URL)。 3. 当后端修改了字段名,你对着报错修半天。 这是重复劳动的地狱。 swagger_dart_code_generator 可以将 Swagger (JSON/YAML) 文件直接转换为高质量的 Dart 代码,包括: * Model 类:支持 json_serializable,带 fromJson/

By Ne0inhk
Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

文章目录 * 前言 * make/makefile * 文件的三个时间 * Linux第一个小程序-进度条 * 回车和换行 * 缓冲区 * 程序的代码展示 * git指令 * 关于gitee * Linux调试器-gdb使用 * 作业部分 前言 做 Linux 开发时,你是不是也遇到过这些 “卡脖子” 时刻?写 makefile 时,明明语法没错却报错,最后发现是依赖方法行没加 Tab;想提交代码到 gitee,记不清 git add/commit/push 的 “三板斧”,还得反复搜教程;用 gdb 调试程序,输了命令没反应,才想起编译时没加-g生成 debug 版本;甚至连写个进度条,都搞不懂\r和\n的区别,导致进度条乱跳…… 其实这些问题,

By Ne0inhk