【Linux】简易日志工具项目

news2024/11/15 12:55:54

在这里插入图片描述

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

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

  • 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。
  • 来源:指出产生日志的文件行数(可以快速找到对应模块)。
  • 消息内容:具体描述事件或错误信息。

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

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

1.2 日志的意义

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

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

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

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

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

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

  1. 深入理解日志原理
    通过自己实现日志工具,可以更深入地理解日志记录的基本原理,包括日志的格式化、写入、级别控制等。对以后使用第三方日志库有很大帮助
  2. 掌握核心编程技能
    在开发过程中,可以锻炼和提升核心编程技能,如文件操作、字符串处理、时间管理、异常处理等。这是一笔很重要的经验!
  3. 模块化和抽象思维
    日志工具的开发需要良好的模块化和抽象思维能力,这有助于在未来的项目中更好地组织代码。
  4. 错误处理和调试
    在开发过程中,不可避免地会遇到错误和调试问题,这提供了实践错误处理和调试技巧的机会。
  5. 理解日志在系统中的作用
    通过实现日志工具,可以更深刻地理解日志在系统监控、问题排查、性能分析等方面的重要性。
  6. 增强项目经验
    开发日志工具可以作为一个独立的项目经验,有助于在简历上展示实际编程能力和解决问题的能力。

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

2 构建自己的日志工具

2.1 框架搭建

设计一个日志系统首先要明确我们希望打印出什么格式的日志信息:
在这里插入图片描述
我们想要呈现出上面这样的日志信息,就需要设置一个信息类logmessage来储存信息,类内需要这些信息:

  • int _level : 日志等级,通过枚举变量来快速通过数字对应等级
  • pid_t _id : 进程ID
  • std::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、文件名、行号、当前时间、日志信息:

  1. _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";
        }
    }
    
  2. 获取 pid + 文件名 + 行号:这个很简单!
    // 处理文件名 行号
        lm._filename = filename;
        lm._filenumber = filenumber;
        // 获取进程ID
        lm._id = getpid();
    
  3. _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;
    }
    
  4. _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;
        }
    }

打印方式有两种:

  1. 向显示器打印:这个很好写,直接使用printf打印特定格式就好
  2. 向文件打印:使用文件流操作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 线程安全优化

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

编程技巧与学习点:

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2067689.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

带有限制编辑的PDF文件怎么取消编辑限制

在日常工作和学习中&#xff0c;我们经常会遇到一些带有“限制编辑”的PDF文件。这些文件由于设置了密码保护&#xff0c;使得我们无法直接编辑、复制或打印其中的内容&#xff0c;给信息的处理和利用带来了诸多不便。然而&#xff0c;通过一些有效的方法和工具&#xff0c;我们…

C++—八股文总结(25秋招期间一直更新)

1、const 1.1 指针常量和常量指针 说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么&#xff0c;有什么特点。 const int *aint const *a; //可以通过 a 访问整数值&#xff0c;但不能通过 a 修改该整数的值&#xff0c;指针本身是…

【AI赋能游戏】《黑神话:悟空》专属黑悟空无限创意生成器!(整合包分享)

最近最火的话题&#xff0c;肯定就是《黑神话&#xff1a;悟空》了&#xff01;这游戏实在是忒火火火了。。。全网破圈霸屏&#xff0c;连官媒央视都给了无死角宣传&#xff01; 《黑神话&#xff1a;悟空》登顶Steam历史售卖榜&#xff0c;同时在线玩家冲到了最高**222**万人&…

8.22-docker的部署及其使用

docker 1.docker环境部署以及语法 [rootdocker ~]# cat << EOF | tee /etc/modules-load.d/k8s.conf> overlay> br_netfilter> EOFoverlaybr_netfilter[rootdocker ~]# modprobe overlay[rootdocker ~]# modprobe br_netfilter[rootdocker ~]# cat /etc/module…

【个人学习】JVM(8): 对象的实例化、内存布局、访问定位

对象的实例化内存布局与访问定位 对象的实例化 对象创建的方式 new&#xff1a;最常见的方式、单例类中调用getInstance的静态类方法&#xff0c;XXXFactory的静态方法Class的newInstance方法&#xff1a;在JDK9里面被标记为过时的方法&#xff0c;因为只能调用空参构造器&am…

Ignition Gateway配置

Config-System backup和restore&#xff1a; backup可以直接备份整个gateway配置&#xff0c;包括所有项目。 restore可以恢复gateway配置&#xff0c;包括所有项目。

pytorch基础学习

环境安装 mac安装conda&#xff08;为什么安装conda? conda类似沙箱&#xff0c;将一个一个环境隔离起来&#xff0c;解决Python工程之前的包冲突问题&#xff09; 下载Miniconda安装器:https://docs.conda.io/en/latest/miniconda.html 执行dmg安装。 安装完成后&#xff0c…

C++对象初始化

背景 最近在编译一个库的时候发现有个编译错误&#xff0c;最后发现是初始化对象的时候出了问题&#xff0c;这里简单记录一下&#xff1a; #include <iostream>class A { public:int m_a; }; class C { public:int m_c;operator A(){}; }; class B { public:B(){};B (…

探寻孩子自闭症之因:为 “星星的孩子” 寻找答案

在这个丰富多彩的世界里&#xff0c;有一群特殊的孩子&#xff0c;他们仿佛来自遥远的星球&#xff0c;沉浸在自己的独特世界中&#xff0c;难以与外界进行有效的沟通和互动。他们是自闭症儿童&#xff0c;也被称为 “星星的孩子”。那么&#xff0c;为什么孩子会患上自闭症呢&…

大语言模型私有化部署和个性化调优的技术实践

简介 本文介绍如何在不依赖任何三方服务的情况下&#xff0c;私有化部署和使用大语言模型&#xff0c;以及如何以较低成本让大语言模型使用自己的数据来产生个性化输出。 本文偏技术向&#xff0c;读者需要具备一定技术背景&#xff0c;如有不懂之处&#xff0c;欢迎留言交流…

ARM——驱动——inmod加载内核模块

在上一篇文章的代码上添加出错处理 #include <linux/init.h> // 包含初始化宏和函数 #include <linux/kernel.h> // 包含内核函数和变量 #include <linux/fs.h> // 包含文件操作的结构和函数 #include <linux/kdev_t.h> /…

记忆化搜索与状态压缩:优化递归与动态规划的利器

记忆化搜索是解决递归和动态规划问题的一种高效优化技术。它结合了递归的灵活性和动态规划的缓存思想&#xff0c;通过保存已经计算过的子问题结果&#xff0c;避免了重复计算&#xff0c;大幅提升了算法的效率。当问题状态复杂时&#xff0c;状态压缩技术可以进一步优化空间使…

密码生成器(HTML+CSS+JavaScript)

&#x1f30f;个人博客主页&#xff1a;心.c ​ 前言&#xff1a;前两天写了密码生成器&#xff0c;现在跟大家分享一下&#xff0c;大家如果想使用随便拿&#xff0c;如果哪里有问题还请大佬们给我指出&#xff0c;感谢支持 &#x1f525;&#x1f525;&#x1f525;专题文章&…

《断点回归的非参数估计及 Stata 实现》

目录 一、引言 二、文献综述 三、理论原理 四、实证模型 五、稳健性检验 六、程序代码及解释 七、代码运行结果及解释 一、引言 断点回归&#xff08;Regression Discontinuity&#xff0c;RD&#xff09;设计是一种准实验方法&#xff0c;用于评估政策或干预措施的因果…

鉴权Cookies、授权session、token

2 鉴权、授权 区别&#xff1a;一个存在浏览器&#xff0c;一个存在服务器&#xff0c;session存到服务端当中 问1&#xff1a;http协议是一个典型的无状态协议&#xff0c;无记忆&#xff0c;那第一次登录下次是不是还要登录一次&#xff1f; ANS&#xff1a;不需要 因为Co…

C++设计模式3:工厂模式

工厂模式都是在那种有着大量类的程序里面使用的&#xff0c;比如我突然要产生许多的类&#xff0c;这时候就可以用工厂模式&#xff0c;工厂模式有如下三种类型。 简单工厂 用户输入实例化的信息(比如产品名称)&#xff0c;向工厂申请对象&#xff0c;工厂返回相应的对象&…

npm install报错,解决记录:11个步骤诊断和解决问题

在处理npm install报错时&#xff0c;可以遵循以下步骤来诊断和解决问题&#xff1a; 查看错误信息&#xff1a; 错误信息通常会给出问题的线索&#xff0c;例如依赖包版本冲突、网络问题、权限问题等。 更新npm和Node.js&#xff1a; 首先尝试更新npm和Node.js到最新版本&…

地平线—征程2(Journey 2-J2)芯片详解(16)—DDR系统

写在前面 本系列文章主要讲解地平线征程2(Journey 2-J2)芯片的相关知识,希望能帮助更多的同学认识和了解征程2(Journey 2-J2)芯片。 若有相关问题,欢迎评论沟通,共同进步。(*^▽^*) 错过其他章节的同学可以电梯直达目录↓↓↓ 地平线—征程2(Journey 2-J2)芯片详解…

新开发体育直播平台的创业指南:降低赛事版权成本方法

在全球化浪潮下&#xff0c;体育赛事已成为连接世界各地观众的情感纽带&#xff0c;其巨大的影响力不仅激发了全球观众的热情&#xff0c;也催生了体育赛事直播行业的蓬勃发展。然而&#xff0c;高昂的版权费用如同一道难以逾越的门槛&#xff0c;让众多新进入者和小型体育直播…

【与C++的邂逅】--- 类与对象(下)

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 与C的邂逅 本节我们继续学习类与对象下&#xff0c;下面给出本节内容大纲。 &#x1f3e0; 再谈构造 &#x1f4cc; 构造函数体赋值 在创建对象时&…