【Linux】浅易日志工具项目

打印 上一主题 下一主题

主题 1643|帖子 1643|积分 4929


   有些鸟儿是不应该被关在笼子里的,      由于他们的羽毛太丰润了。        当他们飞走,你会由衷地庆贺他获得自由。           --- 肖申克的救赎》---            

  
1 日志

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

界说:日志是一种按时间次序排列的记载,用于记载软件在运行过程中产生的各种信息,包括操纵行为、系统状态、错误告诫等。就像日志一样,程序每进行一个任务操纵都要留下信息,方便他人查看。
日志通常包含以下几种信息:


  • 时间戳:记载变乱发生的时间。
  • 日志级别:表示日志信息的严重程度,如DEBUG、INFO、WARNING、ERROR、FATAL。
  • 来源:指出产生日志的文件行数(可以快速找到对应模块)。
  • 消息内容:具体描述变乱或错误信息。
目前,在实际开辟中我们有非常丰富的日志库可以选择:

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

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

  • 追踪问题:通过日志,开辟者可以相识软件在运行过程中的状态,快速定位问题所在。
  • 分析原因:日志记载了软件运行过程中的详细信息,有助于分析问题产生的原因。
  • 优化性能:通过分析日志,可以发现软件的性能瓶颈,从而进行优化。
  • 安全审计:日志记载了软件的操纵行为,有助于审计和监控系统的安全性。
  • 数据挖掘:在某些场景下,日志数据可以用于数据挖掘,为业务分析和决议提供支持。
  同样日志在项目开辟中至关重要,从开辟调试阶段 - 测试阶段 - 部署阶段 - 运行维护阶段…都具有相称重要的作用!而且一个优雅的日志系统是可以让开辟者赏心悦目标进行项目开辟,优雅!
总之,日志在项目开辟中具有举足轻重的作用。一个美满的日志系统可以提高软件的可靠性、稳固性和可维护性,为软件开辟和运维提供有力支持。
1.3 为什么要构建自己的日志工具

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

  • 深入明白日志原理
    通过自己实现日志工具,可以更深入地明白日志记载的根本原理,包括日志的格式化、写入、级别控制等。对以后使用第三方日志库有很大帮助
  • 把握核心编程技能
    在开辟过程中,可以锻炼和提升核心编程技能,如文件操纵、字符串处理惩罚、时间管理、非常处理惩罚等。这是一笔很重要的履历!
  • 模块化和抽象思维
    日志工具的开辟需要良好的模块化和抽象思维能力,这有助于在未来的项目中更好地组织代码。
  • 错误处理惩罚和调试
    在开辟过程中,不可制止地会碰到错误和调试问题,这提供了实践错误处理惩罚和调试技巧的机会。
  • 明白日志在系统中的作用
    通过实现日志工具,可以更深刻地明白日志在系统监控、问题排查、性能分析等方面的重要性。
  • 增强项目履历
    开辟日志工具可以作为一个独立的项目履历,有助于在简历上展示实际编程能力和解决问题的能力。
  总之,开辟一个自己的浅易日志工具是一个综合性的学习过程,下面我们来开辟一个自己的日志工具!
2 构建自己的日志工具

2.1 框架搭建

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

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


  • int _level : 日志品级,通过枚举变量来快速通过数字对应品级
  • pid_t _id : 进程ID
  • std::string _filename : 文件名
  • int _filenumber : 行号
  • std::string _curr_time : 当前时间
  • std::string _message_info : 日志信息
然后我们在计划一个初步的日志类Log,我们希望的是通过:
  1. Log lg;
  2. lg.LogMessage(__FILE__ , __LINE__ , DEBUG , "%d %s %f" , 1 , "你好" , 3.14);
复制代码
这样的上层调用来实现日志信息的打印,以是Log内部不需要设置信息类logmessage。只需要在LogMessage函数中设置一个暂时变量,包管每次调用都会通过这个暂时来储存信息。为了可以区分是向显示器打印还是向文件打印,我们添加一个成员变量_type来方便后期确认打印方式!
  1. #pragma once
  2. #include <string>
  3. #include <sys/types.h>
  4. #include <unistd.h>
  5. #include <aio.h>
  6. #include <stdarg.h>
  7. #include <fstream>
  8. #include <cstring>
  9. //打印方式
  10. #define SCREEN_TYPE 1
  11. #define FILE_TYPE 2
  12. const std::string file = "log.txt";
  13. // 等级划分
  14. enum
  15. {
  16.     DEBUG = 1,
  17.     INFO,
  18.     WARNING,
  19.     ERROR,
  20.     FATAL,
  21. };
  22. // 信息类
  23. class logmessage
  24. {
  25. public:
  26.     std::string _level;        // 日志信息等级
  27.     int _id;                   // 进程ID
  28.     std::string _curr_time;    // 当前时间
  29.     std::string _filename;     // 文件名
  30.     int _filenumber;           // 行号
  31.     std::string _message_info; // 日志信息
  32. };
  33. // 日志类
  34. class Log
  35. {
  36. private:
  37.     std::string LevelToString(int level)
  38.     {
  39.         switch (level)
  40.         {
  41.         case 1:
  42.             return "DEBUG";
  43.         case 2:
  44.             return "INFO";
  45.         case 3:
  46.             return "WARNING";
  47.         case 4:
  48.             return "ERROR";
  49.         case 5:
  50.             return "FATAL";
  51.         default:
  52.             return "UNKNOW";
  53.         }
  54.     }
  55. public:
  56.     // 空的构造函数
  57.     Log() : _type(SCREEN_TYPE)
  58.     {
  59.     }
  60.     // 处理数据
  61.     void LogMessage(std::string filename, int filenumber, int level, const char *format, ...)
  62.     {      
  63.     }
  64.     ~Log()
  65.     {
  66.     }
  67. private:
  68.     int _type;
  69. };
复制代码
接下来我们来处理惩罚最重要的LogMessage函数。
2.2 LogMessage函数

在LogMessage函数中我们需要依次处理惩罚传入的信息,并储存在logmessage类中。函数一定要支持可变参数,才气更好的支持外部调用的功能性!
  1. logMessage(std::string filename , int level , int filenumber , const char* format , ...)
复制代码
接下来我们进行信息类的处理惩罚,依次处理惩罚 日志品级、进程ID、文件名、行号、当前时间、日志信息:

  • _level :通过公共方法LevelToString()将 品级 转换为 字符串:简朴的通过switch语句实现
    1. std::string LevelToString(int level)
    2. {
    3.     switch (level)
    4.     {
    5.     case 1:
    6.         return "DEBUG";
    7.     case 2:
    8.         return "INFO";
    9.     case 3:
    10.         return "WARNING";
    11.     case 4:
    12.         return "ERROR";
    13.     case 5:
    14.         return "FATAL";
    15.     default:
    16.         return "UNKNOW";
    17.     }
    18. }
    复制代码
  • 获取 pid + 文件名 + 行号:这个很简朴!
    1. // 处理文件名 行号
    2.     lm._filename = filename;
    3.     lm._filenumber = filenumber;
    4.     // 获取进程ID
    5.     lm._id = getpid();
    复制代码
  • _curr_time: 获取时间 time() ,再通过localtime()得到当前时间的结构体,然后通过方法 TimeToString() 转换为字符串就可以了,需要注意的是,获取的时间结构体内的时间和原本时间有出入,需要进行一些处理惩罚:
    1. std::string TimeToString()
    2. {
    3.     time_t now = time(nullptr);
    4.     struct tm *t = localtime(&now);
    5.    
    6.     int year = t->tm_year;
    7.     int mon = t->tm_mon;
    8.     int day = t->tm_mday;
    9.     int hour = t->tm_hour;
    10.     int min = t->tm_min;
    11.     int sec = t->tm_sec;
    12.     char buffer[1024];
    13.     snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",
    14.              year + 1900,
    15.              mon + 1,
    16.              day,
    17.              hour,
    18.              min,
    19.              sec);
    20.     return buffer;
    21. }
    复制代码
  • _message_info:日志信息是一段带有可变参数的字符串,使用vsnprintf可以简朴解决。首先辈行va_list 的初始化,然后 vsnprintf() 可以直接将可变参数中进行提取 ,(va_start标定开始位置 , va_end竣事)
    1. // 日志信息
    2.     char buffer[1024];
    3.     va_list ap;
    4.     va_start(ap, format);
    5.     vsnprintf(buffer, sizeof(buffer), format, ap);
    6.     va_end(ap);
    7.     lm._message_info = buffer;
    复制代码
这样最重要的数据转换我们就完成了,接下来就是打印的问题了,我们计划一个FlushLog刷新日志信息的函数,在里面进行打印的处理惩罚,根据打印格式打印对应信息:
  1. // 刷新数据
  2.     void FlushLog(const logmessage &lg)
  3.     {
  4.         switch (_type)
  5.         {
  6.         case 1:
  7.             FlushToScreen(lg);
  8.             break;
  9.         case 2:
  10.             FlushToFile(lg);
  11.             break;
  12.         }
  13.     }
复制代码
打印方式有两种:

  • 向显示器打印:这个很好写,直接使用printf打印特定格式就好
  • 向文件打印:使用文件流操纵fstream快速进行写入处理惩罚(非常好用!)
  1. void FlushToScreen(const logmessage &lg)
  2.     {
  3.         printf("[%s][%d][%s][%d][%s] %s \n",
  4.                lg._level.c_str(),
  5.                lg._id,
  6.                lg._filename.c_str(),
  7.                lg._filenumber,
  8.                lg._curr_time.c_str(),
  9.                lg._message_info.c_str());
  10.     }
  11.     void FlushToFile(const logmessage &lg)
  12.     {
  13.         std::fstream out(_logfile.c_str(), std::ios_base::out | std::ios_base::app );
  14.         char buffer[1024];
  15.         snprintf(buffer, sizeof(buffer), "[%s][%d][%s][%d][%s] %s \n",
  16.                lg._level.c_str(),
  17.                lg._id,
  18.                lg._filename.c_str(),
  19.                lg._filenumber,
  20.                lg._curr_time.c_str(),
  21.                lg._message_info.c_str());
  22.         out.write(buffer, strlen(buffer));
  23.         out.close();
  24.     }
复制代码
现在我们 运行测试一下:

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

单线程的情况,我们的日志工具肯定是没有问题的!假如是多线程呢?我们来看看我们有哪些是全局的变量需要互斥锁保护:只有显示器打印和文件打印是对全局的资源进行操纵,以是我们只需要对FlushLog中进行线程保护即可!
  1. //全局锁
  2. pthread_mutex_t _mtx = PTHREAD_MUTEX_INITIALIZER;
复制代码
为了更加优雅的进行操纵,我们使用之前编写的RAII规则的锁守卫LockGuard进行保护:
  1. // 刷新数据
  2.     void FlushLog(const logmessage &lg)
  3.     {
  4.         LockGuard lock(&_mtx);
  5.         switch (_type)
  6.         {
  7.         case 1:
  8.             FlushToScreen(lg);
  9.             break;
  10.         case 2:
  11.             FlushToFile(lg);
  12.             break;
  13.         }
  14.     }
复制代码
这样我们的日志类就可以包管多线程下的安全运行了!
2.4 宏界说优化

上面的代码已经可以满意日志的誊写的工作了,但是假如还想要更加的优雅的操纵,我们可以使用宏界说来免去誊写文件名和行号的操纵,而且不在需要手动创建类,可以直接调用宏界说来进行日志的誊写!
  1. Log lg;
  2. #define Log(Level, Format, ...)                                \
  3.     do                                                         \
  4.     {                                                          \
  5.         lg.LogMessage(__FILE__, __LINE__, Level , Format, ##__VA_ARGS__); \
  6.     } while (0)
  7.    
  8. #define EnableScreen()          \
  9.     do                          \
  10.     {                           \
  11.         lg.Enable(SCREEN_TYPE); \
  12.     } while (0)
  13.    
  14. #define EnableFile()          \
  15.     do                        \
  16.     {                         \
  17.         lg.Enable(FILE_TYPE); \
  18.     } while (0)
复制代码
宏界说会在调用位置直接进行打开,以是__FILE__, __LINE__,就直接可以传入文件和行数了,不在需要我们誊写:
  1. int main()
  2. {
  3.     int cnt = 5;
  4.     while (cnt--)
  5.     {
  6.         Log(DEBUG, "%d %s %f", cnt, "你好", 3.1415);
  7.         sleep(1);
  8.     }
  9.    
  10.     EnableFile();
  11.    
  12.     cnt = 5;
  13.     while (cnt--)
  14.     {
  15.         Log(DEBUG, "%d %s %f", cnt, "你好", 3.1415);
  16.         sleep(1);
  17.     }
  18.     return 0;
  19. }
复制代码
这样是在是优雅:

这样我们就完成了日志工具项目标构建!!!
3 总结

项目技能栈:
   编程语言:C++
操纵系统相关:POSIX线程(pthread)、文件操纵、时间处理惩罚
编程技巧:面向对象编程、计划模式(单例模式、工厂模式)、RAII(资源获取即初始化)
  编程技巧与学习点:
   

  • 日志原理与计划 :文章深入探讨了日志的界说、构成和重要性,以及如何计划一个日志系统。
  • 核心编程技能:通过实现日志工具,锻炼了文件流操纵、字符串处理惩罚、时间管理等技能。
  • 错误处理惩罚与调试:在开辟过程中,实践了错误处理惩罚和调试技巧,特别是在多线程环境下的线程安全问题。
  • 线程安全 :通过引入互斥锁(mutex)和锁守卫(LockGuard),确保了日志工具在多线程环境下的安全使用。
  • 宏界说优化 :使用宏界说简化了日志记载的代码,提高了代码的轻巧性和易用性。
  我们通过构建一个浅易的日志工具,展示了从需求分析、系统计划到具体实现的完备过程。先容了如何使用C++构建一个具有根本功能的日志系统,包括日志消息的格式化、文件和屏幕输出、日志级别的控制等。实践了日志工具的线程安全优化,确保了其在多线程环境下的稳固性。
通过这个项目,可以学习到如何从零开始构建一个日志系统,把握相关的编程技能和计划理念,同时也能够加深对日志在软件开辟中作用的明白。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

卖不甜枣

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表