当前位置: 首页 > news >正文

从零开始实现 C++ TinyWebServer 异步日志系统 Log类详解

文章目录

  • 日志的运行流程
  • Log 成员变量
  • 实现构造和析构
  • 实现 Init() 函数
  • 实现 Write() 函数
  • 实现宏定义函数
  • Log 代码
  • Log 测试

从零开始实现 C++ TinyWebServer 项目总览

项目源码

对于一个服务器而言,不论是在调试中还是在运行中,都需要通过打日志的方式来记录程序的运行情况。

  • 同步日志:日志写入函数与工作线程串行执行,由于涉及到I/O操作,当单条日志比较大的时候,同步模式会阻塞整个处理流程,服务器所能处理的并发能力将有所下降,尤其是在峰值的时候,写日志可能成为系统的瓶颈。
  • 异步日志:将所写的日志内容先存入阻塞队列中,写线程从阻塞队列中取出内容,写入日志。

日志的运行流程

  1. 获取日志实例
    使用单例模式(具体采用局部静态变量的实现方法)来获取 Log 类的唯一实例,调用方式为 Log::GetInstance()。单例模式确保了在整个程序运行期间,Log 类只有一个实例存在,避免了多个实例可能带来的资源浪费和数据不一致问题。
  2. 初始化日志系统
    通过获取的 Log 实例调用 init() 函数来完成日志系统的初始化工作。在初始化过程中,会根据设置的阻塞队列大小来决定采用同步日志还是异步日志。如果阻塞队列的大小大于 0,就会选择异步日志模式;如果等于 0,则选择同步日志模式。同时,会更新 is_async 变量来标记当前的日志模式。
  3. 写入日志
    当需要记录日志时,通过 Log 实例调用 Write() 函数。在写入日志之前,会根据当前的时间信息创建一个新的日志文件,日志文件的命名规则是:前缀为当前时间,后缀为 .log。同时,会更新记录当前日期的 today_ 变量和记录当前日志行数的 line_count 变量。
  4. 判断日志写入方式
    Write() 函数内部,会根据 is_async 变量的值来决定具体的日志写入方式。如果 is_asynctrue,表示当前处于异步日志模式,工作线程会将需要写入的日志内容放入阻塞队列中,然后由专门的写线程从阻塞队列中取出数据并写入日志文件;如果 is_asyncfalse,则表示处于同步日志模式,日志内容会直接写入到日志文件中。

Log 成员变量

static const int Log_NAME_LENGTH = 256; // 最长文件名
static const int MAX_LINES = 50000;     // 最长日志条数

const char* path_;      // 路径名
const char* suffix_;    // 后缀名

bool is_open_;  // 是否开启
int level_;     // 日志等级
bool is_async_; // 是否开启异步日志

int today_;     // 当天日期
int line_count_;// 日志行数

Buffer buff_;   // 输出缓冲区
FILE* fp_;      // 文件指针

std::mutex mtx_;
std::unique_ptr<BlockQueue<string>> deque_; // 阻塞队列
std::unique_ptr<thread> write_thread_;		// 写线程

实现构造和析构

构造函数:初始化成员变量,默认不启用异步日志记录。

Log::Log() 
    : is_async_(false), today_(0),
      line_count_(0), fp_(nullptr),
      deque_(nullptr), write_thread_(nullptr) {}

析构函数:在对象销毁时,确保所有待写入的日志信息都被处理,关闭阻塞队列,等待写线程退出,最后关闭文件指针。

Log::~Log() {
    while (!deque_->empty())
        deque_->flush(); // 唤醒消费者,处理剩下数据
    deque_->close();
    write_thread_->join(); // 等待线程退出
    if (fp_) {
        std::lock_guard<std::mutex> locker(mtx_);
        Flush();
        fclose(fp_);
    }
}

实现 Init() 函数

  • 初始化日志系统,阻塞队列,写线程,设置日志级别、日志文件路径和后缀等。
  • 根据 max_capacity 的值决定是否启用异步日志记录。
  • 生成日志文件名,并打开日志文件。如果文件打开失败,尝试创建目录并再次打开。
// 初始化
void Log::Init(int level, const char* path,
              const char* suffix, 
              int max_capacity) {
    is_open_ = true;
    level_ = level; 
    path_ = path;
    suffix_ = suffix;

    if (max_capacity) { // 异步
        is_async_ = true;
        if (!deque_) {
            std::unique_ptr<BlockQueue<string>> new_deque(new BlockQueue<string>);
            deque_ = std::move(new_deque); // 所有权转移
            std::unique_ptr<thread> new_thread(new thread(FLushLogThread));
            write_thread_ = std::move(new_thread);
        }
    } else { // 同步
        is_async_ = false;
    }

    line_count_ = 0;

    path_ = path;
    suffix_ = suffix;
    time_t timer = time(nullptr);
    struct tm* sys_time = localtime(&timer);
    struct tm t = *sys_time;
    char filename[Log_NAME_LENGTH] = {0};
    snprintf(filename, Log_NAME_LENGTH - 1, "%s%04d_%02d_%0d%s",
             path_, t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, suffix_);
    today_ = t.tm_mday;

    {
        std::lock_guard<std::mutex> locker(mtx_);
        if (fp_) {
            Flush(); 
            fclose(fp_);
        }
        fp_ = fopen(filename, "a");
        if (fp_ == nullptr) {
            mkdir(path_, 0777); // 777最大权限
            fp_ = fopen(filename, "a");
        }
        assert(fp_ != nullptr);
    }
}

实现 Write() 函数

  • 获取当前时间,判断是否需要创建新的日志文件(日期改变或日志行数达到最大值)。
  • 格式化日志信息,包括时间戳、日志级别等。
  • 根据是否启用异步日志记录,将日志信息写入阻塞队列或直接写入文件。
void Log::Write(int level, const char* format, ...) {
    struct timeval now = {0, 0};
    gettimeofday(&now, nullptr);
    time_t time_second =  now.tv_sec;
    struct tm* sys_time = localtime(&time_second);
    struct tm t = *sys_time;

    // 日期不对或行数满了
    if (today_ != t.tm_mday || (line_count_ && (line_count_ % MAX_LINES == 0))) {
        std::unique_lock<std::mutex> locker(mtx_);
        locker.unlock();

        char new_file[Log_NAME_LENGTH];
        char tail[36];
        snprintf(tail, 36, "%04d_%02d_%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);

        if (today_ != t.tm_mday) {
            snprintf(new_file, Log_NAME_LENGTH - 72, "%s%s%s", path_, tail, suffix_);
            today_ = t.tm_mday;
        } else {
            int num = line_count_ / MAX_LINES;
            snprintf(new_file, Log_NAME_LENGTH, "%s%s-%d%s", path_, tail, num, suffix_);
        }

        {
            std::lock_guard<std::mutex> locker(mtx_);
            Flush();
            fclose(fp_);
            fp_ = fopen(new_file, "a");
            assert(fp_ != nullptr);
        }
    }

    {
        std::lock_guard<std::mutex> locker(mtx_);
        line_count_++;

        int n = snprintf(buff_.WriteBegin(), 128, "%d-%02d-%02d %02d:%02d:%02d.%06ld ",     
                         t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, 
                         t.tm_hour, t.tm_min, t.tm_sec, now.tv_usec);
        buff_.HasWritten(n);
        AppendLogLevel(level);

        va_list vaList;
        va_start(vaList, format);
        int m = vsnprintf(buff_.WriteBegin(), buff_.WritableBytes(), format, vaList);
        va_end(vaList);
        buff_.HasWritten(m);
        buff_.Append("\n\0", 2);

        if (is_async_ && deque_) // 异步模式-生产者
            deque_->push_back(buff_.RetrieveAllAsString());
        else // 同步模式-直接写入
            fputs(buff_.ReadBegin(), fp_);    
        buff_.RetrieveAll(); 
    }
}

实现宏定义函数

  • 简化日志类的使用,方便日志的写入操作,根据日志级别判断是否需要写入日志。
  • 如果日志级别小于等于当前的日志级别,则调用Log::Write函数写入日志,并调用Log::Flush函数刷新缓冲区。
// 小于等于当前level才输出
#define LOG_BASE(level, format, ...) \
    do { \
        Log* log = Log::GetInstance(); \
        if (log->IsOpen() && log->GetLevel() <= level) { \
            log->Write(level, format, ##__VA_ARGS__); \
            log->Flush(); \
        } \
    }while(0);

#define LOG_DEBUG(format, ...) do {LOG_BASE(0, format, ##__VA_ARGS__)} while(0);
#define LOG_INFO(format, ...) do {LOG_BASE(1, format, ##__VA_ARGS__)} while(0);
#define LOG_WARN(format, ...) do {LOG_BASE(2, format, ##__VA_ARGS__)} while(0);
#define LOG_ERROR(format, ...) do {LOG_BASE(3, format, ##__VA_ARGS__)} while(0);
#define LOG_FATAL(format, ...) do {LOG_BASE(4, format, ##__VA_ARGS__)} while(0);

Log 代码

log.h

#ifndef Log_H
#define Log_H

#include <sys/stat.h> // mkdir
#include <sys/time.h> // gettimeofday
#include <cstdio>   // FILE
#include <cstdarg>  // va_start
#include <ctime>
#include <cassert>
#include <string>
#include <utility>  // move
#include <memory>   // unique_ptr
#include <thread>

#include "blockqueue.h"
#include "../buffer/buffer.h"

using std::string;
using std::thread;

class Log {
public:
    void Init(int level, const char* path = "./log",
              const char* suffix = ".log", 
              int max_capacity = 1024);    

    static Log* GetInstance();
    static void FLushLogThread();

    void Flush();
    void Write(int level, const char* format, ...);

    int GetLevel();
    void SetLevel(int level);
    bool IsOpen();

private:
    Log();
    ~Log();

    void AppendLogLevel(int level);
    void AsyncWrite();

    static const int Log_NAME_LENGTH = 256; // 最长文件名
    static const int MAX_LINES = 50000;     // 最长日志条数

    const char* path_;      // 路径名
    const char* suffix_;    // 后缀名

    bool is_open_;  // 是否开启
    int level_;     // 日志等级
    bool is_async_; // 是否开启异步日志

    int today_;     // 当天日期
    int line_count_;// 日志行数

    Buffer buff_;   // 输出缓冲区
    FILE* fp_;      // 文件指针

    std::mutex mtx_;
    std::unique_ptr<BlockQueue<string>> deque_;
    std::unique_ptr<thread> write_thread_;
};

// 小于等于当前level才输出
#define LOG_BASE(level, format, ...) \
    do { \
        Log* log = Log::GetInstance(); \
        if (log->IsOpen() && log->GetLevel() <= level) { \
            log->Write(level, format, ##__VA_ARGS__); \
            log->Flush(); \
        } \
    }while(0);

#define LOG_DEBUG(format, ...) do {LOG_BASE(0, format, ##__VA_ARGS__)} while(0);
#define LOG_INFO(format, ...) do {LOG_BASE(1, format, ##__VA_ARGS__)} while(0);
#define LOG_WARN(format, ...) do {LOG_BASE(2, format, ##__VA_ARGS__)} while(0);
#define LOG_ERROR(format, ...) do {LOG_BASE(3, format, ##__VA_ARGS__)} while(0);
#define LOG_FATAL(format, ...) do {LOG_BASE(4, format, ##__VA_ARGS__)} while(0);

#endif // Log_H

log.cc

#include "log.h"

Log::Log() 
    : is_async_(false), today_(0),
      line_count_(0), fp_(nullptr),
      deque_(nullptr), write_thread_(nullptr) {}

Log::~Log() {
    while (!deque_->empty())
        deque_->flush(); // 唤醒消费者,处理剩下数据
    deque_->close();
    write_thread_->join(); // 等待线程退出
    if (fp_) {
        std::lock_guard<std::mutex> locker(mtx_);
        Flush();
        fclose(fp_);
    }
}

// 初始化
void Log::Init(int level, const char* path,
              const char* suffix, 
              int max_capacity) {
    is_open_ = true;
    level_ = level; 
    path_ = path;
    suffix_ = suffix;

    if (max_capacity) { // 异步
        is_async_ = true;
        if (!deque_) {
            std::unique_ptr<BlockQueue<string>> new_deque(new BlockQueue<string>);
            deque_ = std::move(new_deque); // 所有权转移
            std::unique_ptr<thread> new_thread(new thread(FLushLogThread));
            write_thread_ = std::move(new_thread);
        }
    } else { // 同步
        is_async_ = false;
    }

    line_count_ = 0;

    path_ = path;
    suffix_ = suffix;
    time_t timer = time(nullptr);
    struct tm* sys_time = localtime(&timer);
    struct tm t = *sys_time;
    char filename[Log_NAME_LENGTH] = {0};
    snprintf(filename, Log_NAME_LENGTH - 1, "%s%04d_%02d_%0d%s",
             path_, t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, suffix_);
    today_ = t.tm_mday;

    {
        std::lock_guard<std::mutex> locker(mtx_);
        if (fp_) {
            Flush(); 
            fclose(fp_);
        }
        fp_ = fopen(filename, "a");
        if (fp_ == nullptr) {
            mkdir(path_, 0777); // 777最大权限
            fp_ = fopen(filename, "a");
        }
        assert(fp_ != nullptr);
    }
}

void Log::AppendLogLevel(int level) {
    const char* level_title[] = {"[DEBUG]: ", "[INFO] : ", "[WARN] : ",
                                "[ERROR]: ", "[FATAL]: "};
    int valid_level = (level >= 0 && level <= 4) ? level : 1;
    buff_.Append(level_title[valid_level], 9);
}

void Log::Write(int level, const char* format, ...) {
    struct timeval now = {0, 0};
    gettimeofday(&now, nullptr);
    time_t time_second =  now.tv_sec;
    struct tm* sys_time = localtime(&time_second);
    struct tm t = *sys_time;

    // 日期不对或行数满了
    if (today_ != t.tm_mday || (line_count_ && (line_count_ % MAX_LINES == 0))) {
        std::unique_lock<std::mutex> locker(mtx_);
        locker.unlock();

        char new_file[Log_NAME_LENGTH];
        char tail[36];
        snprintf(tail, 36, "%04d_%02d_%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);

        if (today_ != t.tm_mday) {
            snprintf(new_file, Log_NAME_LENGTH - 72, "%s%s%s", path_, tail, suffix_);
            today_ = t.tm_mday;
        } else {
            int num = line_count_ / MAX_LINES;
            snprintf(new_file, Log_NAME_LENGTH, "%s%s-%d%s", path_, tail, num, suffix_);
        }

        {
            std::lock_guard<std::mutex> locker(mtx_);
            Flush();
            fclose(fp_);
            fp_ = fopen(new_file, "a");
            assert(fp_ != nullptr);
        }
    }

    {
        std::lock_guard<std::mutex> locker(mtx_);
        line_count_++;

        int n = snprintf(buff_.WriteBegin(), 128, "%d-%02d-%02d %02d:%02d:%02d.%06ld ",     
                         t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, 
                         t.tm_hour, t.tm_min, t.tm_sec, now.tv_usec);
        buff_.HasWritten(n);
        AppendLogLevel(level);

        va_list vaList;
        va_start(vaList, format);
        int m = vsnprintf(buff_.WriteBegin(), buff_.WritableBytes(), format, vaList);
        va_end(vaList);
        buff_.HasWritten(m);
        buff_.Append("\n\0", 2);

        if (is_async_ && deque_) // 异步模式-生产者
            deque_->push_back(buff_.RetrieveAllAsString());
        else // 同步模式-直接写入
            fputs(buff_.ReadBegin(), fp_);    
        buff_.RetrieveAll(); 
    }
}

// 单例模式之饿汉模式
Log* Log::GetInstance() {
    // 静态局部变量的初始化是线程安全的
    static Log log;
    return &log;
}

// 异步日志的写线程函数
void Log::FLushLogThread() {
    Log::GetInstance()->AsyncWrite();
}

// 写线程真正的执行函数
void Log::AsyncWrite() {
    string str = "";
    while (deque_->pop(str)) { // 异步模式-消费者
        std::lock_guard<std::mutex> locker(mtx_);
        fputs(str.c_str(), fp_);
    }
}

// 唤醒消费者,开始写日志
void Log::Flush() {
    if (is_async_)
        deque_->flush();
    fflush(fp_);
}

int Log::GetLevel() {
    std::lock_guard<std::mutex> lock(mtx_);
    return level_;
}

void Log::SetLevel(int level) {
    std::lock_guard<std::mutex> lock(mtx_);
    level_ = level;
}

bool Log::IsOpen() {
    return is_open_;
}

Log 测试

测试 log 功能

#include "../code/log/log.h"
#include <iostream>

int main() {
    // 获取 Log 类的单例实例
    Log* logger = Log::GetInstance();

    // 初始化日志系统
    logger->Init(0, "./logs/", ".log", 1024);

    // 输出不同级别的日志信息
    LOG_DEBUG("This is a debug message.");
    LOG_INFO("This is an info message.");
    LOG_WARN("This is a warning message.");
    LOG_ERROR("This is an error message.");
    LOG_FATAL("This is anfatal message.");

    // 输出日志级别
    std::cout << "Current log level: " << logger->GetLevel() << std::endl;

    // 修改日志级别
    logger->SetLevel(2);
    std::cout << "New log level: " << logger->GetLevel() << std::endl;

    // 再次输出不同级别的日志信息
    LOG_DEBUG("This debug message should not be logged.");
    LOG_INFO("This info message should not be logged.");
    LOG_WARN("This is a new warning message.");
    LOG_ERROR("This is a new error message.");
    LOG_FATAL("This is a new fatal message.");


    // 可变参数
    logger->SetLevel(0);
    LOG_DEBUG("%s %d %d", "log info", 123, 666);

    // 大量日志
    for (int i = 0; i < 100000; ++i)
        LOG_DEBUG("This is the %d-th error message.", i);

    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(buffer_unit_test)

# 设置 cpp 标准和编译器选项
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")


# 添加可执行文件
add_executable(log_unit_test log_unit_test.cc ../code/log/log.cc ../code/buffer/buffer.cc)

相关文章:

  • MySQL数据库入门到大蛇尚硅谷宋红康老师笔记 高级篇 part10
  • LIMS系统在纸制品制造的应用 内检实验室LIMS系统提升纸制品质控
  • JavaScript 数组方法大全:从基础到高级
  • IO(Input/Output)
  • GStreamer —— 3.1、Qt+GStreamer制作多功能播放器,支持本地mp4文件、rtsp流、usb摄像头等(可跨平台,附源码)
  • 新增菜品-02.代码开发2
  • 【后端】【Djagno】【ORM】models.ManyToManyField 多对多字段类型全解
  • 【设计模式】策略模式
  • EasyExcel--导入和导出Excel的方法
  • 面向机器人领域 | AKM Delta-Sigma数字输出无磁芯电流传感器
  • turnjs图册翻书效果
  • 数据仓库是什么,跟数据集成有什么关系
  • Web3 时代数据保护的关键挑战与应对策略
  • PostgreSQL 触发器
  • 电机控制常见面试问题(十四)
  • gralloc1_perform具体在干什么
  • 从两指到三指:Robotiq机器人自适应夹持器技术解析
  • MySQL InnoDB 事务隔离级别和锁
  • git 命令回退版本
  • nodejs - 基础知识
  • 国家统计局:要持续加大好房子建设供应力度,积极推动城市更新行动和保障房建设
  • 哪条线路客流最大?哪个站点早高峰人最多?上海地铁一季度客流报告出炉
  • 阳光保险拟设立私募证券投资基金,总规模200亿元
  • 词条数量大幅扩充,《辞海》第八版启动编纂
  • 再现五千多年前“古国时代”:凌家滩遗址博物馆今开馆
  • 长期吃太饱,身体会发生什么变化?