深入了解linux系统—— 日志

发布于:2025-09-09 ⋅ 阅读:(24) ⋅ 点赞:(0)

日志

在之前写代码的过程中,测试代码都是之间像显示器上输出内容;

当多线程像显示器文件输出时,由于没有做任何的防护,就有可能导致多线程输出信息混在一起,不方便观察。

而计算机中的日志记录系统和软件运行中发生事件的文件,作用就是:监控运行状态,记录异常信息,帮助快速定位问题并支持程序员进行问题修复。

是系统维护、故障排查和安全管理的重要工具。

简单来说:日志就像生活中的日记一样,记录程序运行时运行状态、异常信息等等。

对于一个合格的日志,要具有以下指标:

时间戳、日志等级、日志内容

文件名、行号、进程/线程id

最主要的就是时间戳、日志等级和日志内容。

一般来说,日志等级可以分为:

​ DEBUG:调试信息

​ INFO:正常输出

​ WARNING:告警信息

​ ERROR:错误信息(能够运行结束)

​ FATAL:错误(不能运行:打开文件失败等等)

日志有现成的解决方案,spdlogglogBoost.Log等等;

这里自定义实现一个日志(采用设计模式 - 策略模式)。

刷新策略

要自定义实现一个日志,这里首先来实现一种刷新策略;

假设现在存在一条日志信息,可以刷新到显示器文件中(显示器策略)、也可以刷新到指定文件中(文件策略)

这里,我们就可以设计一个基类:logflush,其中存在一个虚函数flush

对于一种刷新策略,就要继承基类logflush并重写flush方法实现自己的刷新策略。

    class logflush
    {
        virtual void flush(std::string massage) = 0;
        ~logflush() = delete;
    };

显示器刷新策略

向显示器文件中刷新,这里直接使用std::cout即可

注意:日志可以被多线程使用,显示器文件就是临界资源,要对临界区进行加锁(这里就使用之间封装的Mutexlockgroup

    // 显示器刷新
    class displayflush : public logflush
    {
        void flush(std::string massage) override
        {
            lockgroup(_mutex);
            std::cout << massage << std::endl;
        }

    private:
        Mutex _mutex;
    };

文件刷新策略

向文件中刷新,首先要先打开这个文件,我们就要知道该文件的路径、文件名。

要打开一个文件、如果该文件不存在,调用open时可以新建;但是,如果路径不存在,我们这里调用就会出错。

所以,我们首先要做的是:判断文件路径是否存在,如果该路径不存在,就要新建。

这里可以使用std::filesystem:exists来判断一个路径是否存在(路径存在返回true,不存在返回false

使用std::filesystem::create_directories来创建一个路径。

    static std::string default_path = "./log";
    static std::string default_name = "log.log";
    const std::string gsep = "\r\n";
    class fileflush : public logflush
    {
    public:
        fileflush(const std::string &path = default_path, const std::string &name = default_name)
            : _path(path), _name(name)
        {
            if (std::filesystem::exists(path))
            {
                return;
            }
            // 路径不存在,创建
            try
            {
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << gsep;
            }
        }
    private:
        std::string _path;
        std::string _name;
        std::string _pathname;
        Mutex _mutex;
    };

然后就是重写flush方法:

在打开目标文件之前,要对文件路径和名称进行合并,生成文件绝对路径。

要打开目标文件(以追加方式打开,文件不存在就创建)(这里打开文件可以使用:std::ofstream out(_pathname, std::ios::app);C++17支持,std::ios::app表示以追加方式打开文件)

然后就是将日志信息输出到目标文件中,为了方便输出,这里定义一个结尾符:const std::string gsep = "\r\n";

最后关闭文件。

为了保证线程安全,要进行加锁

        void flush(std::string massage) override
        {
            lockgroup lgp(_mutex);
            _pathname = _path + (_path.back() == '/' ? ' ' : '/') + _name;
            std::ofstream out(_pathname, std::ios::app);
            if (!out.is_open())
            {
                // 打开文件失败
                return;
            }
            out << massage << gsep;
        }

日志信息

1. 构建日志

有了日志刷新策略,现在来实现日志log

要实现日志,首先就要有上述的刷新策略,这里默认使用显示器刷新策略:

    class Log
    {
        public:
        Log()
        {
            _log = std::make_unique<displayflush>();
        }
        void EnableDisplayFlush()
        {
            _log = std::make_unique<displayflush>();
        }
        void EnableFileFlush()
        {
            _log = std::make_unique<fileflush>();
        }
        private:
        std::unique_ptr<logflush> _logflush;
    }

有了上述刷新策略,现在来看日志信息:

[2025-8-24 22:42:35] [DEBUG] [641189] [test.cc] [12] log.txt 2015-8-24
[2025-8-24 22:42:35] [DEBUG] [641189] [test.cc] [13] hello

这里预期的日志信息如上,在一条日志中有存在时间,日志等级,进程pid,文件名,行号,信息等。

所以,我们就要实现获取时间的接口函数GetTime,以及日志等级enum class Level

这里,Level枚举类型默认输出是整型,我们想要以DEBUGINFO这样的形式输出,就需要提供一个方法根据日志等级获取相对对应字符串。

    std::string GetTime()
    {
        time_t tm = time(nullptr);
        struct tm curr;
        localtime_r(&tm, &curr);
        std::stringstream ss;
        ss << curr.tm_year + 1900 << "-"
           << curr.tm_mon + 1 << "-"
           << curr.tm_mday << " "
           << curr.tm_hour << ":"
           << curr.tm_min << ":"
           << curr.tm_sec;
        return ss.str();
    }
    enum class Level
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
    std::string GetLevel(Level level)
    {
        switch (level)
        {
        case Level::DEBUG:
            return "DEBUG";
        case Level::INFO:
            return "INFO";
        case Level::WARNING:
            return "WARNING";
        case Level::ERROR:
            return "ERROR";
        case Level::FATAL:
            return "FATAL";
        default:
            return "UNKONW";
        }
    }

有了上述这些内容,现在来实现一条日志信息logmassage将其设计成Log内部类

一条日志,要具有 时间、日志等级、进程id、文件名、行号,日志信息;

        class Logmassage
        {
        public:
        private:
            std::string _time;       // 日志时间
            Level _level;            // 日志等级
            pid_t _pid;              // 进程id
            std::string _filename;   // 文件名
            int _line;               // 行号
            std::string _logmassage; // 完整的日志信息
            Log *_log;               // log指针,方便刷新日志信息
        };

这里像日志等级,文件名、行号等不能自行获取的,就要通过构造函数参数传递进来;

然后根据这些信息,构建出来完整的日志信息。

构建完整的日志信息,可以使用C语言中的sprintf/snprintf来实现;

这里使用C++中的stringstream类。

        class Logmassage
        {
        public:
            Logmassage(const std::string &time, Level level, const std::string &filename, int line, Log *plog)
                : _time(time), _level(level), _pid(getpid()), _filename(filename), _line(line), _log(log)
            {
                std::stringstream ss;
                ss << '[' << _time << ']'
                   << '[' << GetLevel(_level) << ']' /*枚举类型,默认是整型*/
                   << '[' << _pid << ']'
                   << '[' << _filename << ']'
                   << '[' << _line << "] : ";
                _logmassage = ss.str();
            }
        private:
            std::string _time;       // 日志时间
            Level _level;            // 日志等级
            pid_t _pid;              // 进程id
            std::string _filename;   // 文件名
            int _line;               // 行号
            std::string _logmassage; // 完整的日志信息
            Log *_log;               // log指针,方便刷新日志信息
        };

这里在Logmassage类中存在Log* _log的指针,方便进行日志信息刷新

2. 输入日志信息

上述已经完成了整个日志的框架,但是还缺少信息;

这里想要实现的使用日志的方式,就像cout <<这样使用<<来输入日志信息,所以就要实现operator<<方法;

并且,可以连续使用多个<<,在使用operator时,要将返回值设置成Logmassage&

            template <typename T>
            Logmassage &operator<<(const T &data)
            {
                std::stringstream ss;
                ss << data;
                _logmassage += ss.str();
                return *this;
            }

最后,为了方便使用在Loamassage析构方法中,刷新该日志信息;

            ~Logmassage()
            {
                if (_log->_logflush)
                {
                    _log->_logflush->flush(_logmassage);
                }
            }

3. 使用日志

到现在,就已经将日志大概实现了出来;

但是,按照现在实现的日志,我们使用起来存在问题:

创建Logmassage就需要存在一个已经有的Log指针,而我们使用日志就要先创建Log对象。

所以这里就要实现一个仿函数,在调用Log()时,用来构建Logmassage对象并返回。

        Logmassage operator()(const std::string &time, Level level, const std::string &filename, int line)
        {
            return Logmassage(time, level, filename, line, this);
        }

但是,就算实现了仿函数,我们要使用该日志时,还是非常麻烦的,需要传递什么时间,日志等级,文件名,行号,有没有更加简单粗暴的,就想要只传递时间,后面跟上<< 信息就可以使用日志的?

LOG(Level::DEBUG) << "hello log";

当然是可以实现的,时间需要调用GetTime方法;文件名和行号,我们知道宏__FILE____LINE__指的就是文件名和行号;

所以,我们就可以实现一个宏,调用是只需传递日志等级,就可以使用日志。

    Log log;
#define LOG(level) Log(GetTime(), level, __FILE__,__LINE__)

并且将log 定义成全局的,在使用时只需使用即可。


网站公告

今日签到

点亮在社区的每一天
去签到