從零實(shí)現(xiàn)一個(gè) C++ 輕量級(jí)日志系統(tǒng)原理與實(shí)踐指南
前言:
日志系統(tǒng)可以說(shuō)是每個(gè)程序員都繞不開的話題。在大型C++項(xiàng)目中,日志打印和記錄幾乎是日常開發(fā)中最常用的功能之一 —— 排查Bug、追蹤調(diào)用鏈、監(jiān)控線上服務(wù)狀態(tài),都離不開一套好用的日志工具。
提到C++日志庫(kù),有一些人會(huì)想:日志系統(tǒng)到底是怎么實(shí)現(xiàn)的? 拋開spdlog這些現(xiàn)成的庫(kù),我們自己能不能從零手搓一個(gè)可用的日志組件出來(lái)?
其實(shí)手搓一個(gè)簡(jiǎn)易日志工具并不復(fù)雜,核心思路就是 將格式化后的信息輸出到不同的目的地,比如控制臺(tái)、文件等。在這個(gè)思路的指引下,我們可以一步步搭建自己的日志系統(tǒng),并順便搞懂:
- 日志級(jí)別是怎么分類和管理的;
- 日志消息如何格式化并分派到不同的輸出端;
- 異步日志如何做到高性能;
- RAII機(jī)制在日志系統(tǒng)中的應(yīng)用。
如果你曾對(duì)這些問題感到好奇,那這篇文章就是寫給你的。下面,我們就從零開始,手搓一個(gè)實(shí)用且可擴(kuò)展的C++日志工具。
一. 日志系統(tǒng)的設(shè)計(jì)理念
1.1 日志的核心組成要素
一條合格的工業(yè)級(jí)日志,必須包含必選字段和可選擴(kuò)展字段,確保問題可追溯、狀態(tài)可監(jiān)控:
- 必選核心字段:
- 時(shí)間戳:可讀性強(qiáng)的年月日時(shí)分秒格式,精準(zhǔn)定位事件發(fā)生時(shí)間
- 日志等級(jí):區(qū)分事件嚴(yán)重程度,支持分級(jí)過濾與告警
- 日志內(nèi)容:用戶自定義的業(yè)務(wù) / 調(diào)試信息
- 可選擴(kuò)展字段:
- 進(jìn)程 PID / 線程 ID:多進(jìn)程 / 多線程環(huán)境下定位執(zhí)行流
- 文件名與行號(hào):精準(zhǔn)定位日志打印的代碼位置
- 自定義擴(kuò)展字段:如模塊名、用戶 ID 等業(yè)務(wù)信息
本文實(shí)現(xiàn)的日志格式如下,完全兼容主流日志庫(kù)的規(guī)范:
[2026-04-16 21:33:18] [DEBUG] [1030871] [Main.cc] [10] - hello world hello Cx330
日期 日志等級(jí) pid 源文件 行號(hào) - 內(nèi)容 root 
1.2 日志系統(tǒng)的兩大核心階段
日志的生命周期可拆分為兩個(gè)完全解耦的階段,這是我們?cè)O(shè)計(jì)的核心依據(jù):
- 日志形成階段:將時(shí)間戳、等級(jí)、文件名、行號(hào)、用戶內(nèi)容等信息,拼接成一條完整的格式化字符串,與日志輸出目的地?zé)o關(guān)
- 日志刷新階段:將格式化完成的日志字符串,寫入到指定目的地(控制臺(tái)、文件、數(shù)據(jù)庫(kù)、網(wǎng)絡(luò)等),僅關(guān)注寫入邏輯
兩個(gè)階段解耦后,我們可以獨(dú)立擴(kuò)展刷新邏輯,而無(wú)需修改日志格式化的核心代碼,這正是策略模式的最佳應(yīng)用場(chǎng)景。
1.3 為什么選擇策略模式?
- 分離關(guān)注點(diǎn):日志類(上下文)只負(fù)責(zé)構(gòu)建格式化后的日志消息,具體的“寫到哪里”交由獨(dú)立的對(duì)象完成。
- 運(yùn)行時(shí)切換:程序運(yùn)行時(shí)可隨時(shí)切換日志策略,比如調(diào)試階段用控制臺(tái)輸出,生產(chǎn)環(huán)境用文件持久化。
- 擴(kuò)展性極佳:新增一種輸出方式(比如 UDP 網(wǎng)絡(luò)發(fā)送),只需實(shí)現(xiàn)一個(gè)新的類,日志類完全不需要改動(dòng)。
- 支持組合:日志類可以持有多個(gè)對(duì)象,一條日志同時(shí)送給控制臺(tái)、文件、遠(yuǎn)程服務(wù)器,這正是策略模式在集合層面的靈活運(yùn)用。
二、代碼設(shè)計(jì):實(shí)現(xiàn)一個(gè)完整的日志庫(kù)
日志系統(tǒng)的核心前提是線程安全,同時(shí)需要時(shí)間戳、日志等級(jí)等基礎(chǔ)能力支撐,我們先實(shí)現(xiàn)這些底層模塊。
2.1 RAII 風(fēng)格互斥鎖封裝(線程安全基石)
多線程環(huán)境下,控制臺(tái)、日志文件都是臨界資源,多個(gè)線程同時(shí)寫入會(huì)導(dǎo)致內(nèi)容交錯(cuò)、亂序,必須通過互斥量保證臨界區(qū)的原子性。我們基于 Linux 原生的pthread_mutex封裝互斥鎖,并通過 RAII 機(jī)制管理鎖的生命周期,避免手動(dòng)解鎖導(dǎo)致的死鎖、內(nèi)存泄漏問題,這也是 C++11 std::lock_guard的核心實(shí)現(xiàn)原理。
Mutex.hpp
#ifndef __MUTEX_HPP
#define __MUTEX_HPP
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock,nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Orgin()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
// 鎖的開關(guān)
class LockGuard
{
public:
LockGuard(Mutex *lockp):_lockp(lockp)
{
_lockp->Lock();
}
~LockGuard()
{
_lockp->Unlock();
}
private:
Mutex *_lockp;
};
#endif核心設(shè)計(jì)解析:
- 禁用拷貝:互斥量是系統(tǒng)資源,不允許拷貝和賦值,避免重復(fù)釋放、死鎖等問題
- RAII 機(jī)制:
LockGuard在對(duì)象構(gòu)造時(shí)加鎖,析構(gòu)時(shí)自動(dòng)解鎖,即使代碼中途拋出異常,也能保證鎖被釋放,徹底避免手動(dòng)解鎖的遺漏 - 接口封裝:屏蔽原生
pthread庫(kù)的接口細(xì)節(jié),提供更符合 C++ 面向?qū)ο蟮氖褂梅绞?/li>
2.2 格式化時(shí)間戳模塊
時(shí)間戳是日志的核心字段,我們需要實(shí)現(xiàn)秒級(jí)、可重入、格式化的時(shí)間戳獲取功能。
重點(diǎn)注意:C 標(biāo)準(zhǔn)庫(kù)的localtime函數(shù)是不可重入的,多線程環(huán)境下會(huì)出現(xiàn)數(shù)據(jù)錯(cuò)亂,因此必須使用可重入版本localtime_r,它由調(diào)用者提供結(jié)構(gòu)體緩沖區(qū),避免了全局靜態(tài)變量的競(jìng)態(tài)問題。
時(shí)間戳實(shí)現(xiàn)代碼(在命名空間里面)
// 獲取當(dāng)前時(shí)間的字符串表示(格式:YYYY-MM-DD HH:MM:SS)
std::string GetTimeStamp()
{
// 獲取從1970-01-01 UTC到當(dāng)前時(shí)刻的秒數(shù)(Unix時(shí)間戳)
time_t timestamp = time(nullptr);
// 定義tm結(jié)構(gòu)體用于存儲(chǔ)分解后的本地時(shí)間
struct tm data_time;
// 將時(shí)間戳轉(zhuǎn)換為本地時(shí)間(線程安全版本,localtime_r是POSIX標(biāo)準(zhǔn))
localtime_r(×tamp, &data_time);
// 緩沖區(qū),用于存放格式化后的時(shí)間字符串
char data_time_str[128];
// 使用snprintf進(jìn)行格式化,限制最大寫入長(zhǎng)度,防止溢出
// 格式:年-月-日 時(shí):分:秒,各部分不足兩位時(shí)補(bǔ)零
snprintf(data_time_str, sizeof(data_time_str), "%4d-%02d-%02d %02d:%02d:%02d",
// tm_year 從1900年開始計(jì)數(shù),需要加1900得到實(shí)際年份
data_time.tm_year + 1900,
// tm_mon 范圍0~11,需要加1轉(zhuǎn)換為實(shí)際月份
data_time.tm_mon + 1,
data_time.tm_mday, // 日(1~31)
data_time.tm_hour, // 小時(shí)(0~23)
data_time.tm_min, // 分鐘(0~59)
data_time.tm_sec); // 秒(0~60,閏秒時(shí)可達(dá)60)
// 返回std::string對(duì)象,自動(dòng)拷貝緩沖區(qū)內(nèi)容
return data_time_str;
}細(xì)節(jié)解析:
- 可重入性保障:使用
localtime_r替代localtime,確保多線程環(huán)境下時(shí)間轉(zhuǎn)換不會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng) - 格式化補(bǔ)零:通過
%02d確保月、日、時(shí)、分、秒始終是兩位數(shù)字,保證日志格式的一致性 - 時(shí)間偏移修正:
tm_year需要 + 1900 得到真實(shí)年份,tm_mon需要 + 1 得到真實(shí)月份,這是tm結(jié)構(gòu)體的標(biāo)準(zhǔn)規(guī)范

測(cè)試代碼
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 測(cè)試時(shí)間戳模塊
void testTime()
{
for(int i = 0; i < 5; i++)
{
std::cout << GetTimeStamp() << std::endl;
sleep(1);
}
}
int main()
{
// 1. 測(cè)試時(shí)間
testTime();
return 0;
}
2.3 類型安全的日志等級(jí)模塊
日志等級(jí)用于區(qū)分事件的嚴(yán)重程度,我們使用 C++11 的enum class實(shí)現(xiàn)類型安全的日志等級(jí),避免普通枚舉的隱式類型轉(zhuǎn)換問題,同時(shí)提供枚舉到字符串的轉(zhuǎn)換能力。
日志等級(jí)實(shí)現(xiàn)代碼(在命名空間里面,我沒帶上)
// 日志等級(jí)枚舉,用于區(qū)分事件的嚴(yán)重程度
enum LogLevel
{
DEBUG, // 調(diào)試信息,僅開發(fā)階段使用,生產(chǎn)環(huán)境通常關(guān)閉
INFO, // 常規(guī)信息,如服務(wù)啟動(dòng)、正常業(yè)務(wù)流轉(zhuǎn)
WARNING, // 警告信息,表示潛在問題,但系統(tǒng)仍可正常運(yùn)行
ERROR, // 運(yùn)行時(shí)錯(cuò)誤,某個(gè)功能可能受損,需要關(guān)注
FATAL // 致命錯(cuò)誤,程序即將終止(如內(nèi)存分配失?。?
};
// 將日志等級(jí)枚舉轉(zhuǎn)換為對(duì)應(yīng)的字符串(用于日志輸出)
std::string LogLevel2String(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG"; // 調(diào)試等級(jí)
case LogLevel::INFO:
return "INFO"; // 信息等級(jí)
case LogLevel::WARNING:
return "WARNING"; // 警告等級(jí)
case LogLevel::ERROR:
return "ERROR"; // 錯(cuò)誤等級(jí)
case LogLevel::FATAL:
return "FATAL"; // 致命等級(jí)
default:
return "UNKNOWN"; // 未匹配到的等級(jí)(防護(hù)性代碼)
}
}核心設(shè)計(jì)解析:
- 類型安全:
enum class不會(huì)隱式轉(zhuǎn)換為整型,避免了錯(cuò)誤的等級(jí)賦值,編譯期即可發(fā)現(xiàn)類型問題 - 等級(jí)分層:遵循業(yè)界通用的 5 級(jí)日志規(guī)范,覆蓋從調(diào)試到致命錯(cuò)誤的全場(chǎng)景
- 字符串轉(zhuǎn)換:通過
switch語(yǔ)句實(shí)現(xiàn)枚舉到字符串的映射,確保日志中輸出可讀性強(qiáng)的等級(jí)名稱,而非整型數(shù)字

測(cè)試代碼
#include <iostream>
#include <memory>
#include <unistd.h>
#include "Logger.hpp"
using namespace LogModule;
// 測(cè)試日志類枚舉類型轉(zhuǎn)字符類型模塊
void testEnum()
{
std::cout << LogLevel2String(LogLevel::DEBUG) << std::endl;
std::cout << LogLevel2String(LogLevel::INFO) << std::endl;
std::cout << LogLevel2String(LogLevel::WARNING) << std::endl;
std::cout << LogLevel2String(LogLevel::ERROR) << std::endl;
std::cout << LogLevel2String(LogLevel::FATAL) << std::endl;
}
int main()
{
// 2. 測(cè)試枚舉類轉(zhuǎn)字符串類型
testEnum();
return 0;
}
三. 基于策略模式的日志刷新核心實(shí)現(xiàn)
基于策略模式的設(shè)計(jì),我們先定義抽象的刷新策略基類,再分別實(shí)現(xiàn)控制臺(tái)和文件兩種具體的刷新策略,后續(xù)可無(wú)限擴(kuò)展其他策略。
3.1 抽象策略基類 LogStrategy
抽象基類定義了所有刷新策略必須實(shí)現(xiàn)的純虛接口,同時(shí)使用虛析構(gòu)函數(shù)確保子類對(duì)象能正確析構(gòu)。
namespace LogModule
{
// 3. 刷新策略
// 基類: 策略模式 (Strategy Pattern)
// 核心思想:將“日志的產(chǎn)生”與“日志的刷新目的地”解耦。
// 通過定義統(tǒng)一的接口,使得程序可以在運(yùn)行時(shí)動(dòng)態(tài)決定將日志輸出到控制臺(tái)、文件、數(shù)據(jù)庫(kù)或網(wǎng)絡(luò)。
class LogStrategy
{
public:
// 虛析構(gòu)函數(shù):在多態(tài)體系中,基類必須擁有虛析構(gòu)函數(shù)。
// 這樣當(dāng)我們通過基類指針刪除派生類對(duì)象時(shí),才能確保調(diào)用到子類的析構(gòu)函數(shù),防止內(nèi)存泄漏。
virtual ~LogStrategy() = default; // 不在這里析構(gòu)
// 核心刷新接口:這是一個(gè)純虛函數(shù)。
// 純虛函數(shù)的核心作用是定義一種“契約”,強(qiáng)制派生類(子類)必須實(shí)現(xiàn)具體的邏輯。
// 不同的子類可以根據(jù)自己的策略(如 ConsoleStrategy 或 FileStrategy)來(lái)實(shí)現(xiàn)不同的刷新行為。
virtual void SyncLog(const std::string &message) = 0; // 強(qiáng)制子類對(duì)其進(jìn)行重寫
};
}設(shè)計(jì)說(shuō)明:
- 純虛函數(shù)
SyncLog定義了策略的統(tǒng)一接口,入?yún)⑹歉袷交瓿傻娜罩咀址宇愔恍桕P(guān)注具體的寫入邏輯 - 虛析構(gòu)函數(shù)是 C++ 多態(tài)的基礎(chǔ)規(guī)范,避免通過基類指針釋放子類對(duì)象時(shí)出現(xiàn)內(nèi)存泄漏

3.2 控制臺(tái)日志策略 ConsoleLogStrategy
控制臺(tái)策略負(fù)責(zé)將日志輸出到標(biāo)準(zhǔn)錯(cuò)誤流(stderr),核心是保證多線程環(huán)境下的輸出原子性,避免日志交錯(cuò)。用到了我們自己的互斥鎖記得包含對(duì)應(yīng)頭文件,我這里就不寫了
namespace LogModule
{
// 策略1: 控制臺(tái)日志策略
// 子類:繼承自策略基類,用于將日志直接刷新到標(biāo)準(zhǔn)輸出(顯示器),常用于本地開發(fā)與調(diào)試
class ConsoleLogStrategy: public LogStrategy
{
public:
// 構(gòu)造函數(shù)與析構(gòu)函數(shù):當(dāng)前策略不涉及復(fù)雜資源申請(qǐng),故使用默認(rèn)實(shí)現(xiàn)即可
ConsoleLogStrategy(){}
~ConsoleLogStrategy(){}
/**
* @brief 實(shí)現(xiàn)具體的日志同步邏輯——刷新到控制臺(tái)
* @param message 組裝好的完整日志字符串
*/
void SyncLog(const std::string &message) override // 檢查重寫的錯(cuò)誤
{
// 【核心原理】顯示器(stdout)在多線程環(huán)境下屬于“臨界資源”。
// 如果不加保護(hù),多個(gè)線程同時(shí)調(diào)用 std::cout 會(huì)導(dǎo)致各條日志的字符在屏幕上發(fā)生“交織”或亂碼 。
// 使用自定義的 LockGuard 配合互斥鎖,確保這一系列操作的原子性 。
LockGuard logGuard(&_mutex);
std::cout << message << std::endl;
}
private:
// 互斥鎖:專門用于保護(hù)當(dāng)前控制臺(tái)輸出的原子性,防止并發(fā)打印時(shí)消息錯(cuò)亂
Mutex _mutex;
};
}核心細(xì)節(jié)解析:
- 線程安全保障:控制臺(tái)是全局臨界資源,通過互斥鎖保證同一時(shí)刻只有一個(gè)線程能執(zhí)行輸出操作,徹底避免多線程日志交錯(cuò)
- stderr 輸出:使用
std::cerr而非std::cout,因?yàn)?stderr 無(wú)緩沖區(qū),日志會(huì)實(shí)時(shí)輸出,避免程序崩潰時(shí)緩沖區(qū)日志丟失 - RAII 鎖管理:使用
LockGuard自動(dòng)管理鎖,無(wú)需手動(dòng)解鎖,代碼更健壯
3.3 文件日志策略 FileLogStrategy
文件策略負(fù)責(zé)將日志持久化到磁盤文件,核心功能包括:自動(dòng)創(chuàng)建日志目錄、追加模式寫入、線程安全保障,使用 C++17 的filesystem庫(kù)處理目錄和文件操作(記得帶上對(duì)應(yīng)頭文件)。
#include <fstream> #include <filesystem>
namespace LogModule
{
// 定義全局默認(rèn)路徑與文件名常量
const static std::string gdefaultlogdir = "./log/";
const static std::string gdefaultlogfilename = "log.txt";
// 策略2:文件類日志策略
// 子類:繼承自策略基類,實(shí)現(xiàn)將日志持久化到磁盤文件的邏輯
class FileLogStrategy: public LogStrategy
{
public:
/**
* @brief 構(gòu)造函數(shù):初始化日志路徑并確保目錄環(huán)境就緒
* @param logdir 日志存儲(chǔ)目錄
* @param logfilename 日志文件名稱
*/
FileLogStrategy(const std::string &logdir = gdefaultlogdir, const std::string &logfilename = gdefaultlogfilename)
:_logdir(logdir),
_logfilename(logfilename)
{
// 【重點(diǎn)】構(gòu)造階段即進(jìn)行加鎖保護(hù)。
// 理由:判斷目錄是否存在并創(chuàng)建目錄屬于“先檢查再執(zhí)行(Check-Then-Act)”模式,
// 必須保證這一系列操作的原子性,防止多線程同時(shí)創(chuàng)建導(dǎo)致競(jìng)態(tài)沖突 。
LockGuard lockGuard(&_mutex);
// 使用 C++17 的 <filesystem> 庫(kù)進(jìn)行跨平臺(tái)路徑檢查
if(std::filesystem::exists(_logdir))
{
return;
}
else
{
try
{
// 遞歸創(chuàng)建目錄(類似于 Linux 命令 mkdir -p),如果路徑中包含多級(jí)不存在的目錄會(huì)一并創(chuàng)建
std::filesystem::create_directories(_logdir);
}
catch (std::filesystem::filesystem_error &e)
{
// 捕獲文件系統(tǒng)異常(如權(quán)限不足、磁盤空間不足等)并輸出錯(cuò)誤信息
std::cerr << e.what() << std::endl;
}
}
}
// 析構(gòu)函數(shù):由于不涉及手動(dòng)管理的堆內(nèi)存或特殊文件句柄(使用局部變量流管理),故使用默認(rèn)實(shí)現(xiàn)
~FileLogStrategy(){}
/**
* @brief 執(zhí)行具體的日志落盤操作
* @param message 待寫入的完整日志字符串
*/
void SyncLog(const std::string &message) override
{
// 加鎖保護(hù):防止多線程同時(shí)寫入同一文件導(dǎo)致內(nèi)容交織(Interleaving)亂碼
LockGuard logGuard(&_mutex);
// 構(gòu)造完整的目標(biāo)文件路徑
std::string target = _logdir + _logfilename;
// 以追加模式(std::ios::app)打開文件流:
// 核心邏輯:保證每條新日志都寫在文件末尾,不會(huì)覆蓋已有日志內(nèi)容 。
std::ofstream out(target, std::ios::app); // 追加
if(!out.is_open()) // 打開文件檢查
{
return; // 如果因權(quán)限或路徑問題打開失敗,則放棄本次寫入,防止程序崩潰
}
// 將消息流式寫入文件,并手動(dòng)添加換行符以符合日志排版規(guī)范
out << message << "\n"; // 流式寫入
// 文件流離開作用域或顯式調(diào)用 close 會(huì)自動(dòng)觸發(fā)刷新并關(guān)閉文件
out.close();
}
private:
std::string _logdir; // 存儲(chǔ)目錄路徑
std::string _logfilename; // 存儲(chǔ)文件名稱
Mutex _mutex; // 用于保障當(dāng)前策略類實(shí)例在多線程環(huán)境下的線程安全
};
}
核心設(shè)計(jì)解析:
- 自動(dòng)目錄創(chuàng)建:構(gòu)造函數(shù)中檢查日志目錄是否存在,不存在則通過
create_directories遞歸創(chuàng)建,避免手動(dòng)創(chuàng)建目錄的繁瑣 - 追加模式寫入:使用
std::ios::app打開文件,所有日志都會(huì)追加到文件末尾,不會(huì)覆蓋歷史日志,符合日志系統(tǒng)的通用規(guī)范 - 異常處理:目錄創(chuàng)建時(shí)捕獲
filesystem的異常,避免目錄創(chuàng)建失敗導(dǎo)致程序崩潰 - 線程安全:文件寫入全程加鎖,保證多線程環(huán)境下不會(huì)出現(xiàn)半行日志、內(nèi)容交錯(cuò)的問題

測(cè)試代碼


四. 日志主體類與流式輸出設(shè)計(jì)
完成基礎(chǔ)模塊和策略模式的實(shí)現(xiàn)后,我們來(lái)實(shí)現(xiàn)日志系統(tǒng)的主體類,核心目標(biāo)是:兼容 glog 的流式調(diào)用風(fēng)格、自動(dòng)拼接日志元信息、RAII 自動(dòng)觸發(fā)日志刷新。
4.1 Logger 主類的整體架構(gòu)
Logger類是日志系統(tǒng)的對(duì)外入口,核心職責(zé)包括:
- 管理當(dāng)前使用的日志刷新策略,支持動(dòng)態(tài)切換
- 提供仿函數(shù)接口,生成日志消息對(duì)象
- 封裝策略切換的便捷接口
4.2 LogMessage 內(nèi)部類:RAII 實(shí)現(xiàn)日志自動(dòng)刷新
LogMessage是Logger的內(nèi)部類,是整個(gè)日志系統(tǒng)最巧妙的設(shè)計(jì):
- 構(gòu)造函數(shù)中完成日志元信息(時(shí)間、等級(jí)、PID、文件名、行號(hào))的拼接
- 重載
<<運(yùn)算符,支持流式拼接任意類型的日志內(nèi)容 - 析構(gòu)函數(shù)中自動(dòng)觸發(fā)日志刷新,利用臨時(shí)對(duì)象的生命周期實(shí)現(xiàn) “寫完即刷新”
4.3 完整的 Logger 類實(shí)現(xiàn)
#include <memory> #include <sstream> #include <unistd.h>
namespace LogModule
{
// 真正要的日志類
class Logger
{
public:
Logger()
{
UseConsoleLogStrategy();
}
~Logger(){}
// 顯示器的刷新策略
void UseConsoleLogStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 文件的刷新策略
void UseFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
// 內(nèi)部類:一條日志
// 目標(biāo)是把一個(gè)類對(duì)象,變成一個(gè)string
class LogMessage
{
public:
LogMessage(LogLevel level,std::string &filename,int line,Logger &self)
:_level(level),
_curr_time(GetTimeStamp()),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(self)
{
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LogLevel2String(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< "- ";
_loginfo = ss.str();
}
template<typename T>
LogMessage &operator << (const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage() // RAII風(fēng)格的日志刷新
{
if(_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
LogLevel _level; // 日志等級(jí)
std::string _curr_time; // 當(dāng)前時(shí)間
pid_t _pid; // 進(jìn)程pid
std::string _filename; // 文件名
int _line; // 行號(hào)
std::string _loginfo; // 一條完整的日志
Logger &_logger; // 外部類的引用
};
// LogMessage 對(duì)象打印日志的時(shí)候,故意返回一個(gè)臨時(shí)的 LogMessage對(duì)象
// 為什么要返回臨時(shí)內(nèi)部類對(duì)象?
LogMessage operator()(LogLevel level,std::string filename, int line)
{
return LogMessage(level,filename,line,*this);
}
private:
std::unique_ptr<LogStrategy> _strategy; // 刷新日志的策略
};
Logger logger;
// 使用宏,包裝我們的日志打印過程,宏有一個(gè)特點(diǎn),#define A B,B替換成A
#define LOG(level) logger(level,__FILE__,__LINE__)
// 動(dòng)態(tài)調(diào)整日志策略
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()
}
核心設(shè)計(jì)深度解析:
- 仿函數(shù)機(jī)制:重載
operator()讓Logger對(duì)象可以像函數(shù)一樣調(diào)用,返回一個(gè)LogMessage臨時(shí)對(duì)象,這是實(shí)現(xiàn)流式調(diào)用的核心 - RAII 自動(dòng)刷新:
LogMessage是臨時(shí)對(duì)象,當(dāng)整條LOG(xxx) << "xxx"語(yǔ)句執(zhí)行完畢后,臨時(shí)對(duì)象會(huì)被析構(gòu),析構(gòu)函數(shù)中自動(dòng)調(diào)用策略的刷新接口,無(wú)需用戶手動(dòng)觸發(fā)刷新 - 模板化流式運(yùn)算符:通過模板重載
<<運(yùn)算符,支持 int、double、string、char 等任意可流輸出的類型,實(shí)現(xiàn)和std::cout一致的使用體驗(yàn)
預(yù)定義宏封裝:
__FILE__:編譯期自動(dòng)替換為當(dāng)前源文件名__FILE__:編譯期自動(dòng)替換為當(dāng)前代碼行號(hào)LOG宏將繁瑣的參數(shù)傳遞封裝為極簡(jiǎn)的調(diào)用方式,完全對(duì)齊 glog 的使用風(fēng)格
全局單例:定義全局的logger對(duì)象,整個(gè)程序共用一個(gè)日志實(shí)例,避免重復(fù)創(chuàng)建,同時(shí)保證策略切換全局生效
五. 日志系統(tǒng)的線程安全與可重入性深度解析

六、日志系統(tǒng)源碼
6.1 完整Logger.hpp代碼
#ifndef __LOGGER_HPP
#define __LOGGER_HPP
#include <iostream>
#include <cstdio>
#include <string>
#include <ctime>
#include <filesystem> // C++17
#include "Mutex.hpp"
#include <fstream>
#include <sstream>
#include <memory>
#include <unistd.h>
namespace LogModule
{
// 1.獲取時(shí)間
std::string GetTimeStamp()
{
time_t timestamp = time(nullptr);
struct tm data_time;
localtime_r(×tamp, &data_time);
char data_time_str[128];
snprintf(data_time_str, sizeof(data_time_str), "%4d-%02d-%02d %02d:%02d:%02d",
data_time.tm_year + 1900, // 從1900開始記的
data_time.tm_mon + 1, // 默認(rèn)月份從0開始記的
data_time.tm_mday,
data_time.tm_hour,
data_time.tm_min,
data_time.tm_sec);
return data_time_str;
}
enum LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// 2.日志等級(jí)
std::string LogLevel2String(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";
}
}
// 3.日志刷新
// 基類:策略基類,設(shè)置刷新策略的
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &logmessage) = 0;
};
// 子類:繼承純虛接口類
// 策略1
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy(){}
~ConsoleLogStrategy(){}
virtual void SyncLog(const std::string &logmessage) override
{
LockGuard lockguard(&_mutex);
std::cout<<logmessage<<std::endl;
}
private:
Mutex _mutex;
};
static const std::string glogdir = "./log/";
static const std::string glogfilename = "log.log";
// 子類:繼承純虛接口類
// 策略2
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &dir = glogdir,const std::string &filename = glogfilename)
:_logdir(dir),_logfilename(filename)
{
// log/log.txt
LockGuard lockguard(&_mutex);
if(std::filesystem::exists(_logdir))
{
return;
}
else
{
try
{
std::filesystem::create_directories(_logdir);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr<< e.what() <<std::endl;
}
}
}
~FileLogStrategy()
{}
void SyncLog(const std::string &logmessage) override
{
std::string target = _logdir + _logfilename;
std::ofstream out(target,std::ios::app); // 追加寫入文件
if(!out.is_open())
{
return;
}
// 方法1:
// out.write(logmessage.c_str(), logmessage.size());
// out.write("\n", 1); // 寫入換行符
// 方法2:
// std::string line = logmessage + '\n';
// out.write(line.c_str(), line.size());
// 方法3:
out << logmessage << '\n';
out.close();
}
private:
std::string _logdir;
std::string _logfilename; // ./log/XXX.log
Mutex _mutex;
};
// 真正要的日志類
class Logger
{
public:
Logger()
{
UseConsoleLogStrategy();
}
~Logger(){}
// 顯示器的刷新策略
void UseConsoleLogStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
// 文件的刷新策略
void UseFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
// 內(nèi)部類:一條日志
// 目標(biāo)是把一個(gè)類對(duì)象,變成一個(gè)string
class LogMessage
{
public:
LogMessage(LogLevel level,std::string &filename,int line,Logger &self)
:_level(level),
_curr_time(GetTimeStamp()),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(self)
{
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LogLevel2String(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] "
<< "- ";
_loginfo = ss.str();
}
template<typename T>
LogMessage &operator << (const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage() // RAII風(fēng)格的日志刷新
{
if(_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
LogLevel _level; // 日志等級(jí)
std::string _curr_time; // 當(dāng)前時(shí)間
pid_t _pid; // 進(jìn)程pid
std::string _filename; // 文件名
int _line; // 行號(hào)
std::string _loginfo; // 一條完整的日志
Logger &_logger; // 外部類的引用
};
// LogMessage 對(duì)象打印日志的時(shí)候,故意返回一個(gè)臨時(shí)的 LogMessage對(duì)象
// 為什么要返回臨時(shí)內(nèi)部類對(duì)象?
LogMessage operator()(LogLevel level,std::string filename, int line)
{
return LogMessage(level,filename,line,*this);
}
private:
std::unique_ptr<LogStrategy> _strategy; // 刷新日志的策略
};
Logger logger;
// 使用宏,包裝我們的日志打印過程,宏有一個(gè)特點(diǎn),#define A B,B替換成A
#define LOG(level) logger(level,__FILE__,__LINE__)
// 動(dòng)態(tài)調(diào)整日志策略
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleLogStrategy()
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileLogStrategy()
}
#endif6.2 完整測(cè)試代碼
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include "Logger.hpp" // 我們實(shí)現(xiàn)的日志頭文件
using namespace LogModule;
// 多線程測(cè)試函數(shù):10個(gè)線程同時(shí)打印日志
void *thread_log_test(void *arg)
{
char *thread_name = (char *)arg;
for (int i = 0; i < 5; i++)
{
LOG(LogLevel::INFO) << thread_name << " 執(zhí)行日志打印, 循環(huán)次數(shù): " << i;
usleep(1000);
}
return nullptr;
}
int main()
{
// 1. 基礎(chǔ)控制臺(tái)日志輸出
std::cout << "===== 控制臺(tái)日志測(cè)試 =====" << std::endl;
ENABLE_CONSOLE_LOG_STRATEGY();
LOG(LogLevel::DEBUG) << "這是DEBUG調(diào)試日志, 數(shù)值: " << 3.14159;
LOG(LogLevel::INFO) << "這是INFO常規(guī)日志, 服務(wù)啟動(dòng)成功";
LOG(LogLevel::WARNING) << "這是WARNING警告日志, 配置缺失, 使用默認(rèn)值";
LOG(LogLevel::ERROR) << "這是ERROR錯(cuò)誤日志, 文件讀取失敗";
LOG(LogLevel::FATAL) << "這是FATAL致命日志, 內(nèi)存耗盡, 服務(wù)退出";
// 2. 切換為文件日志策略
std::cout << "\n===== 文件日志測(cè)試 =====" << std::endl;
ENABLE_FILE_LOG_STRATEGY();
LOG(LogLevel::INFO) << "切換為文件日志策略, 日志將持久化到./log/log.txt";
LOG(LogLevel::DEBUG) << "文件日志測(cè)試, 支持鏈?zhǔn)狡唇? " << "字符串 " << 1234 << " 浮點(diǎn)數(shù) " << 2.71828;
// 3. 多線程線程安全測(cè)試
std::cout << "\n===== 多線程日志測(cè)試 =====" << std::endl;
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, thread_log_test, (void *)"thread-1");
pthread_create(&t2, nullptr, thread_log_test, (void *)"thread-2");
pthread_create(&t3, nullptr, thread_log_test, (void *)"thread-3");
pthread_create(&t4, nullptr, thread_log_test, (void *)"thread-4");
// 等待所有線程執(zhí)行完畢
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
LOG(LogLevel::INFO) << "多線程日志測(cè)試完成, 無(wú)亂序、無(wú)交錯(cuò)";
return 0;
}6.3 優(yōu)化方向
- 修復(fù)策略切換與日志記錄的數(shù)據(jù)競(jìng)爭(zhēng) – 對(duì)
Logger::_strategy的讀寫加鎖(如shared_mutex+shared_ptr),避免多線程切換策略時(shí)崩潰。 - 文件策略避免每次寫入打開/關(guān)閉文件 – 在
FileLogStrategy構(gòu)造時(shí)持有一個(gè)ofstream對(duì)象,減少系統(tǒng)調(diào)用開銷。 - 增加日志等級(jí)過濾 – 在
LOG宏或LogMessage構(gòu)造前檢查等級(jí)閾值,避免無(wú)效的字符串構(gòu)造。 - 優(yōu)化字符串拼接效率 – 使用
ostringstream成員變量或fmt庫(kù),減少臨時(shí)對(duì)象與內(nèi)存分配。 - 單例化全局 Logger – 采用 Meyers Singleton,解決靜態(tài)初始化順序問題。
- 完善文件策略的目錄創(chuàng)建 – 使用
std::call_once或程序啟動(dòng)時(shí)統(tǒng)一創(chuàng)建,避免多實(shí)例并發(fā)創(chuàng)建目錄的競(jìng)態(tài)。 - 規(guī)范異常處理與降級(jí)策略 – 文件打開失敗時(shí)記錄到
stderr或拋異常,避免靜默丟失日志。 - 支持異步日志(高吞吐場(chǎng)景) – 引入后臺(tái)線程與無(wú)鎖隊(duì)列,解耦業(yè)務(wù)線程與 I/O 操作。
- 增強(qiáng)可重入性與信號(hào)安全 – 提供信號(hào)安全的專用接口(如
write系統(tǒng)調(diào)用),或明確禁止在信號(hào)處理函數(shù)中使用。 - 支持自定義日志格式 – 抽象
Formatter接口,允許用戶定制輸出布局。

寫在最后
至此,我們完整剖析了一個(gè)現(xiàn)代 C++ 流式日志系統(tǒng)的核心設(shè)計(jì):從日志等級(jí)的枚舉定義,到策略模式解耦輸出目標(biāo),再到 RAII + 臨時(shí)對(duì)象實(shí)現(xiàn)的自動(dòng)刷新,以及線程安全與可重入性的深度考量。
這個(gè)日志庫(kù)雖然只有短短幾百行代碼,卻凝聚了策略模式、RAII、智能指針、臨時(shí)對(duì)象生命周期、流式接口、宏與預(yù)定義標(biāo)識(shí)符等 C++ 關(guān)鍵思想。更重要的是,通過分析它的線程安全漏洞和性能瓶頸,我們更能理解并發(fā)編程的復(fù)雜性以及工程落地必須權(quán)衡的取舍。
當(dāng)然,任何代碼都不是完美的。你已看到它的十項(xiàng)優(yōu)化方向——從修復(fù)數(shù)據(jù)競(jìng)爭(zhēng)到支持異步日志,每一點(diǎn)改進(jìn)都能讓這個(gè)輪子更滾得更遠(yuǎn)。如果你正在為自己項(xiàng)目的日志組件苦惱,不妨從這份代碼出發(fā),增加等級(jí)過濾、持久的文件流、可配置格式等特性,打造一個(gè)真正生產(chǎn)就緒的日志庫(kù)。
最后,感謝你跟隨本文深入到這個(gè)看似簡(jiǎn)單卻暗藏玄機(jī)的模塊中。日志是系統(tǒng)的“黑匣子”,好的日志設(shè)計(jì)能讓你在排查問題時(shí)事半功倍。希望這份解析能為你帶來(lái)實(shí)實(shí)在在的啟發(fā),也歡迎在評(píng)論區(qū)分享你的日志系統(tǒng)實(shí)踐或疑問。
到此這篇關(guān)于從零實(shí)現(xiàn)一個(gè) C++ 輕量級(jí)日志系統(tǒng)原理與實(shí)踐指南的文章就介紹到這了,更多相關(guān) C++ 輕量級(jí)日志系統(tǒng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C++編程中使用設(shè)計(jì)模式中的policy策略模式的實(shí)例講解
這篇文章主要介紹了C++編程中使用設(shè)計(jì)模式中的policy策略模式的實(shí)例講解,文章最后對(duì)策略模式的優(yōu)缺點(diǎn)有一個(gè)簡(jiǎn)單的總結(jié),需要的朋友可以參考下2016-03-03
C++之構(gòu)造函數(shù)默認(rèn)值設(shè)置方式
這篇文章主要介紹了C++之構(gòu)造函數(shù)默認(rèn)值設(shè)置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
模擬實(shí)現(xiàn)C語(yǔ)言中的內(nèi)存管理
這篇文章主要內(nèi)容是模擬C語(yǔ)言中的內(nèi)存管理,需要的朋友可以參考下2015-07-07
C++ clock()解析如何使用時(shí)鐘計(jì)時(shí)的應(yīng)用
本篇文章是對(duì)c++中的clock()函數(shù)進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06
C語(yǔ)言實(shí)現(xiàn)直角坐標(biāo)轉(zhuǎn)換為極坐標(biāo)的方法
這篇文章主要介紹了C語(yǔ)言實(shí)現(xiàn)直角坐標(biāo)轉(zhuǎn)換為極坐標(biāo)的方法,涉及C語(yǔ)言進(jìn)行三角函數(shù)與數(shù)值運(yùn)算相關(guān)操作技巧,需要的朋友可以參考下2017-09-09
C++vector的insert函數(shù)用法小結(jié)
std::vector::insert是C++中用于在指定位置插入元素的函數(shù),支持插入單個(gè)元素、多個(gè)相同元素、一個(gè)范圍的元素或初始化列表中的元素,插入操作可能會(huì)使插入點(diǎn)之后的迭代器失效,并且時(shí)間復(fù)雜度為O(n),本文介紹C++vector的insert函數(shù)用法小結(jié),感興趣的朋友一起看看吧2025-03-03
win10+VS2017+Cuda10.0環(huán)境配置詳解
這篇文章主要介紹了win10+VS2017+Cuda10.0環(huán)境配置詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
C語(yǔ)言使用sizeof和strlen計(jì)算數(shù)組和指針大小
sizeof()一般是用來(lái)求取?變量?或者?類型?所占內(nèi)存空間的大小,strlen()是一個(gè)庫(kù)函數(shù)是專門用來(lái)計(jì)算?字符串?長(zhǎng)度的,下面我們就來(lái)看看C語(yǔ)言如何使用sizeof和strlen計(jì)算數(shù)組和指針大小吧2023-11-11

