目录
[2.1 刷新到显示器](#2.1 刷新到显示器)
[2.2 刷新到文件](#2.2 刷新到文件)
[3.1 Log.hpp](#3.1 Log.hpp)
[3.2 Mutex.hpp](#3.2 Mutex.hpp)
[4.2 日志等级](#4.2 日志等级)
[4.3 时间工具函数 GetTimeStamp()](#4.3 时间工具函数 GetTimeStamp())
[4.4 主日志类 Logger](#4.4 主日志类 Logger)
[4.5 内部类 logMessage(核心:RAII 日志落盘)](#4.5 内部类 logMessage(核心:RAII 日志落盘))
[4.6 全局对象 + 宏封装](#4.6 全局对象 + 宏封装)
[4.7 行时序示例(单条 LOG)](#4.7 行时序示例(单条 LOG))
一、日志概念
计算机中的日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题并支持程序员进行问题修复。它是系统维护、故障排查和安全管理的重要工具。
日志格式以下几个指标是必须得有的
- 时间戳
- 日志等级
- 日志内容
以下几个指标是可选的
- 文件名行号
- 进程,线程相关id信息等
日志有现成的解决方案,如:spdlog、glog、Boost.Log、Log4cxx等等,我们依旧采用自定义日志的方式。
这里我们采用设计模式-策略模式来进行日志的设计。
我们想要的日志格式如下:
可读性很好的时间 ⽇志等级 进程 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
常见的日志等级如下:
- DEBUG:测试信息
- INFO:常规消息
- WARNING:告警消息
- ERROR:错误消息
- FATAL:致命消息
日志的功能:
- 形成完整日志
- 刷新到目标文件(显示器,指定文件打印日志)
我们可以定制一个策略来让日志信息往哪里刷新。这就叫做策略模式
二、策略模式
我们可以通过定义基类的方式,来规定统一接口,然后由不同的子类,来充当一种不同的某种形式的策略,我们把这种设计模式叫做策略模式,策略模式利用的就是C++的多态特性。
策略模式的代码如下:
cpp
#ifndef __LOG_HPP
#define __LOG_HPP
#include <iostream>
#include <filesystem>
#include <string>
#include <fstream>
#include "Mutex.hpp"
namespace LogModule
{
using namespace MutexModule;
const std::string gsep = "\r\n";
// 刷新策略基类
class LogStrategy
{
public:
~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 显示器打印日志的策略
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy() {}
~ConsoleLogStrategy() {}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cout << message << gsep;
}
private:
Mutex _mutex;
};
// 文件打印日志的策略
const std::string defaultpath = "./log";
const std::string defaultfile = "my.log";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
: _path(path),
_file(file)
{
LockGuard lockguard(_mutex);
// 来判断文件路径是否存在
if (std::filesystem::exists(_path))
{
return;
}
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << "\n";
}
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
std::ofstream out(filename, std::ios::app);
if (!out.is_open())
{
return;
}
out << message << gsep;
out.close();
}
~FileLogStrategy() {}
private:
std::string _path; // 日志文件所在路径
std::string _file; // 日志文件本身
Mutex _mutex;
};
}
#endif
2.1 刷新到显示器
cpp
#include "Log.hpp"
#include <memory>
using namespace LogModule;
int main()
{
std::unique_ptr<LogStrategy> strategy = std::make_unique<ConsoleLogStrategy>();
strategy->SyncLog("hello log1");
strategy->SyncLog("hello log2");
strategy->SyncLog("hello log3");
return 0;
}

2.2 刷新到文件
cpp
#include "Log.hpp"
#include <memory>
using namespace LogModule;
int main()
{
std::unique_ptr<LogStrategy> strategy = std::make_unique<FileLogStrategy>();
strategy->SyncLog("hello log1");
strategy->SyncLog("hello log2");
strategy->SyncLog("hello log3");
return 0;
}

三、形成完整的日志
上面策略模式我们可以选择将日志刷新到哪里,下面我们就来形成一个完整的日志。
3.1 Log.hpp
cpp
#ifndef __LOG_HPP
#define __LOG_HPP
#include <iostream>
#include <filesystem>
#include <string>
#include <memory>
#include <unistd.h>
#include <fstream>
#include <ctime>
#include <cstdio>
#include "Mutex.hpp"
namespace LogModule
{
using namespace MutexModule;
const std::string gsep = "\r\n";
// 刷新策略基类
class LogStrategy
{
public:
~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 显示器打印日志的策略
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy() {}
~ConsoleLogStrategy() {}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::cout << message << gsep;
}
private:
Mutex _mutex;
};
// 文件打印日志的策略
const std::string defaultpath = "./log";
const std::string defaultfile = "my.log";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
: _path(path),
_file(file)
{
LockGuard lockguard(_mutex);
// 来判断文件路径是否存在
if (std::filesystem::exists(_path))
{
return;
}
try
{
std::filesystem::create_directories(_path);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << "\n";
}
}
void SyncLog(const std::string &message) override
{
LockGuard lockguard(_mutex);
std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file;
std::ofstream out(filename, std::ios::app);
if (!out.is_open())
{
return;
}
out << message << gsep;
out.close();
}
~FileLogStrategy() {}
private:
std::string _path; // 日志文件所在路径
std::string _file; // 日志文件本身
Mutex _mutex;
};
// 1:形成日志等级
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string Level2Str(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
// 获得时间
std::string GetTimeStamp()
{
time_t curr = time(nullptr);
struct tm curr_tm;
localtime_r(&curr, &curr_tm);
char timebuffer[128];
snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
curr_tm.tm_year + 1900,
curr_tm.tm_mon + 1,
curr_tm.tm_mday,
curr_tm.tm_hour,
curr_tm.tm_min,
curr_tm.tm_sec);
return timebuffer;
}
// 1:形成日志 2根据不同的策略,完成刷新
class Logger
{
public:
Logger()
{
EnableConsoleLogStrategy();
}
void EnableFileLogStrategy()
{
_fflush_strategy = std::make_unique<FileLogStrategy>();
}
void EnableConsoleLogStrategy()
{
_fflush_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 内部类,表示的是未来的一条日志
class logMessage
{
public:
logMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
: _curr_time(GetTimeStamp()),
_level(level),
_pid(getpid()),
_src_name(src_name),
_line_number(line_number),
_logger(logger)
{
// 日志的左半部分拼进来
std::stringstream ss;
ss << "[" << _curr_time << "]"
<< "[" << Level2Str(_level) << "]"
<< "[" << _pid << "]"
<< "[" << _src_name << "]"
<< "[" << _line_number << "]"
<< "-";
_loginfo = ss.str();
}
template <typename T>
logMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~logMessage()
{
if (_logger._fflush_strategy)
{
_logger._fflush_strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time;
LogLevel _level;
pid_t _pid;
std::string _src_name;
int _line_number;
std::string _loginfo; // 合并之后一条完整的信息
Logger &_logger;
};
logMessage operator()(LogLevel level, std::string name, int line)
{
return logMessage(level, name, line, *this);
}
~Logger() {}
private:
std::unique_ptr<LogStrategy> _fflush_strategy;
};
// 定义全局日志对象
Logger logger;
// 使用宏,简化用户操作
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}
#endif
3.2 Mutex.hpp
cpp
#pragma once
#include <iostream>
#include <pthread.h>
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_mutex);
}
void UnLock()
{
pthread_mutex_unlock(&_mutex);
}
pthread_mutex_t *Get()
{
return &_mutex;
}
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}
private:
pthread_mutex_t _mutex;
};
class LockGuard
{
public:
LockGuard(Mutex &mutex) : _mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.UnLock();
}
private:
Mutex &_mutex;
};
}
3.3 main.cc
cpp
#include "Log.hpp"
#include <memory>
using namespace LogModule;
int main()
{
Enable_Console_Log_Strategy();
LOG(LogLevel::DEBUG) << "hello world " << 3.14;
LOG(LogLevel::DEBUG) << "hello world " << 3.14;
LOG(LogLevel::DEBUG) << "hello world " << 3.14;
LOG(LogLevel::DEBUG) << "hello world " << 3.14;
LOG(LogLevel::DEBUG) << "hello world " << 3.14;
LOG(LogLevel::DEBUG) << "hello world " << 3.14;
LOG(LogLevel::DEBUG) << "hello world " << 3.14;
Enable_File_Log_Strategy();
LOG(LogLevel::DEBUG) << "hello file " << 3.14;
LOG(LogLevel::DEBUG) << "hello file " << 3.14;
LOG(LogLevel::DEBUG) << "hello file " << 3.14;
return 0;
}
运行如下所示:

四、核心模块拆解
4.1.策略模式:日志输出策略抽象
基类 LogStrategy,定义纯虚接口 SyncLog(message):把完整字符串输出。
ConsoleLogStrategy:控制台打印,内部自带互斥锁,多线程保护std::cout。FileLogStrategy:文件写日志- 构造:自动检查日志目录,不存在则创建目录
./log SyncLog:加锁,以追加模式打开文件./log/my.log,写入日志,写完关闭文件- 自带互斥锁,防止多线程写文件错乱
- 构造:自动检查日志目录,不存在则创建目录
策略可以动态切换:运行时选择打印到控制台,还是写入文件。
4.2 日志等级
enum class LogLevel:DEBUG / INFO / WARNING / ERROR / FATAL,Level2Str 枚举转字符串,用于日志打印。
4.3 时间工具函数 GetTimeStamp()
获取本地时间,格式化输出 yyyy-mm-dd hh:mm:ss 时间戳,使用localtime_r线程安全版本。
4.4 主日志类 Logger
- 成员:
unique_ptr<LogStrategy>,持有当前选中的输出策略 - 接口:
EnableConsoleLogStrategy():切换策略为控制台输出EnableFileLogStrategy():切换策略为文件输出operator():重载圆括号,返回临时对象 logMessage
4.5 内部类 logMessage(核心:RAII 日志落盘)
这是整个日志框架最关键的设计:临时对象生命周期控制日志输出
- 构造函数:接收日志等级、源码文件名、行号、Logger 引用; 预先拼接日志头部:
[时间][等级][pid][文件名][行号]-,存入_loginfo - 重载
operator<<:流式追加任意类型日志内容,拼接到_loginfo - 析构函数 :对象销毁时,调用
_logger所持策略的SyncLog(),把完整日志一次性输出。
调用逻辑:
LOG(xxx) << msg→ 创建临时logMessage对象,<<拼接内容;语句结束,临时对象析构 → 自动写日志。
4.6 全局对象 + 宏封装
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
__FILE__:当前源码文件名;__LINE__:当前代码行号,自动填入日志。- 用户使用:一行代码即可打印日志,不用手动构造对象。
4.7 行时序示例(单条 LOG)
LOG(LogLevel::DEBUG) << "hello world " << 3.14;
- 宏展开:
logger(LogLevel::DEBUG, __FILE__, __LINE__) logger.operator()创建临时 logMessage 对象- 构造:拼接时间、等级、pid、文件名、行号
<<依次追加字符串、浮点数,拼入_loginfo- 语句末尾,临时对象析构 → 调用策略 SyncLog 输出整条日志