首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【Linux 实战】从0到1手搓日志系统:附完整代码

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

作者头像
Yuzuriha
发布2026-01-14 19:22:01
发布2026-01-14 19:22:01
1200
举报
文章被收录于专栏:Linux网络Linux网络

前言: 上文我们讲了线程的同步以及理解并实现生产者消费者模式【Linux系统】深入理解线程同步,实现生产消费模型-CSDN博客 本文我们来讲一下如何手搓日志库,为下一篇文件:线程池的实现做铺垫!

日志与策略模式

什么是设计模式

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

认识日志

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

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

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

1.时间戳 2.日志的等级 3.日志的内容 还可以加入: 1.文件名 2.行号 3.线程or进程的id

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

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

代码语言:javascript
复制
[可读性很好的时间] [⽇志等级] [进程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系统】深入理解线程同步,实现生产消费模型-CSDN博客

代码语言:javascript
复制
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++中的继承与多态来实现日志选择不同的刷新策略。

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

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

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

代码语言:javascript
复制
#include <filesystem>    //C++17标准
命名空间:std::filesystem 位于filesystem命名空间中

判断路径p是否存在,返回bool:exists(const path& p)
创建对应的路径:            create_directories(const path& p)

打开对应的路径:

代码语言:javascript
复制
#include <fstream>
命名空间:std 位于std命名空间中

创建对象时打开文件: ofstream ofs(path,openmode)

打开模式设置为追加模式:ios::app

具体实现:

代码语言:javascript
复制
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;
};

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

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

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

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

代码语言:javascript
复制
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类的方式,来实现日志信息。

补充:

外部类只能通过内部类的实例化对象,来访问内部类中的方法与成员,且受修饰符限制。 内部类可以直接访问外部类的方法以及成员,没有限制

代码语言:javascript
复制
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:

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

代码语言:javascript
复制
[可读性很好的时间] [⽇志等级] [进程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

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

代码语言:javascript
复制
// 日志等级
// 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结构体:转化后的本地时间存储放在这个结构体中。

代码语言:javascript
复制
// 获取时间
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()直接获得,而行号、名称、信息等可由外部作为参数传入。

实现:

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

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

代码语言:javascript
复制
// 日志信息
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对象的快速创建。

代码语言:javascript
复制
// 重载运算符(),便于创建LogMessage对象
// 这里返回临时对象:当临时对象销毁时,调用对应的析构函数,自动对象中创建好的日志信息进行刷新!
// 其次局部对象也不能传引用返回!

LogMessage operator()(const LogLevel& level, const string& name, const int& line)
{
    return LogMessage(level, name, line, *this);
}

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

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

代码语言:javascript
复制
// 创建日志,并刷新
//__FILE__ 和 __LINE__ 是编译器预定义的宏,作用是获取当前代码所在的文件名、行号
#define LOG(level) logger(level, __FILE__, __LINE__)         // 细节:不加;


// 切换刷新策略
#define Enable_Console_LogStrategy() logger.EnableConsoleLogStrategy();
#define Enable_File_LogStrategy() logger.EnableFileLogStrategy();
接口调用演示:

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

代码语言:javascript
复制
#include "Log.hpp"
using namespace LogModule;

int main()
{
    // 测试日志文件的刷新

    // 文件刷新
    Enable_File_LogStrategy();
    LOG(LogLevel::DEBUG) << "调试" << 1;
    LOG(LogLevel::ERROR) << "错误" << 2;
    LOG(LogLevel::WARNING) << "警告" << 3;
}

完整代码

代码语言:javascript
复制
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__) // 细节:不加;
};
代码语言:javascript
复制
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;
};
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-09-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 日志与策略模式
  • 前提
  • 日志刷新策略实现
  • 日志文件生成与选择刷新策略实现
    • LogMessage:
    • 实现:
    • 接口调用演示:
  • 完整代码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档