tinywebserver log模块 日志

项目源网站tinywebserver。这个模块是日志模块,开了另一个线程,往fd里写日志。

blockqueue 双端队列

这个双端队列用于存续需要写入的数据,使用信号量和锁保证线程安全,为啥用双端队列而不用队列呢?我也不知道,log模块里也只调用了push_back的接口。

代码

/*
 * @Author       : mark
 * @Date         : 2020-06-16
 * @copyleft Apache 2.0
 */
#ifndef BLOCKQUEUE_H
#define BLOCKQUEUE_H

#include <mutex>
#include <deque>
#include <condition_variable>
#include <sys/time.h>

// 线程安全的阻塞双端队列
template <class T>
class BlockDeque
{
public:
    explicit BlockDeque(size_t MaxCapacity = 1000);

    ~BlockDeque();

    void clear();

    bool empty();

    bool full();

    void Close();

    size_t size();

    size_t capacity();

    T front();

    T back();

    void push_back(const T &item);

    void push_front(const T &item);

    // 都是弹出队首元素
    bool pop(T &item); // 返回值表示是否成功弹出元素,item就是弹出的元素

    bool pop(T &item, int timeout); // 如果队列有元素,返回,没有元素,等待timeout秒,还是没有元素返回false

    void flush();

private:
    std::deque<T> deq_; // 双端队列

    size_t capacity_;

    std::mutex mtx_;

    bool isClose_;

    std::condition_variable condConsumer_;

    std::condition_variable condProducer_;
};

template <class T>
BlockDeque<T>::BlockDeque(size_t MaxCapacity) : capacity_(MaxCapacity)
{
    assert(MaxCapacity > 0);
    isClose_ = false;
}

template <class T>
BlockDeque<T>::~BlockDeque()
{
    Close();
};

template <class T>
void BlockDeque<T>::Close()
{
    {
        std::lock_guard<std::mutex> locker(mtx_);
        deq_.clear();
        isClose_ = true;
    }
    condProducer_.notify_all();
    condConsumer_.notify_all();
};

template <class T>
void BlockDeque<T>::flush()
{
    condConsumer_.notify_one();
};

template <class T>
void BlockDeque<T>::clear()
{
    std::lock_guard<std::mutex> locker(mtx_);
    deq_.clear();
}

template <class T>
T BlockDeque<T>::front()
{
    std::lock_guard<std::mutex> locker(mtx_);
    return deq_.front();
}

template <class T>
T BlockDeque<T>::back()
{
    std::lock_guard<std::mutex> locker(mtx_);
    return deq_.back();
}

template <class T>
size_t BlockDeque<T>::size()
{
    std::lock_guard<std::mutex> locker(mtx_);
    return deq_.size();
}

template <class T>
size_t BlockDeque<T>::capacity()
{
    std::lock_guard<std::mutex> locker(mtx_);
    return capacity_;
}

template <class T>
void BlockDeque<T>::push_back(const T &item)
{
    std::unique_lock<std::mutex> locker(mtx_);
    while (deq_.size() >= capacity_)
    {
        condProducer_.wait(locker);
    }
    deq_.push_back(item);
    condConsumer_.notify_one();
}

template <class T>
void BlockDeque<T>::push_front(const T &item)
{
    std::unique_lock<std::mutex> locker(mtx_);
    while (deq_.size() >= capacity_)
    {
        condProducer_.wait(locker);
    }
    deq_.push_front(item);
    condConsumer_.notify_one();
}

template <class T>
bool BlockDeque<T>::empty()
{
    std::lock_guard<std::mutex> locker(mtx_);
    return deq_.empty();
}

template <class T>
bool BlockDeque<T>::full()
{
    std::lock_guard<std::mutex> locker(mtx_);
    return deq_.size() >= capacity_;
}

template <class T>
bool BlockDeque<T>::pop(T &item)
{
    std::unique_lock<std::mutex> locker(mtx_);
    while (deq_.empty())
    {
        condConsumer_.wait(locker);
        if (isClose_)
        {
            return false;
        }
    }
    item = deq_.front();
    deq_.pop_front();
    condProducer_.notify_one();
    return true;
}

template <class T>
bool BlockDeque<T>::pop(T &item, int timeout)
{
    std::unique_lock<std::mutex> locker(mtx_);
    while (deq_.empty())
    {
        if (condConsumer_.wait_for(locker, std::chrono::seconds(timeout)) == std::cv_status::timeout)
        {
            return false;
        }
        if (isClose_)
        {
            return false;
        }
    }
    item = deq_.front();
    deq_.pop_front();
    condProducer_.notify_one();
    return true;
}

#endif // BLOCKQUEUE_H

组成

  • deque 标准库里的双端队列
  • mutex 锁 保证只有一个线程访问这个deq_
  • condProducer_ 信号量 表示生产者,当需要调用push的时候
    • deque没满,直接push,唤醒一个消费者
    • deque满了,等待
  • condConsumer_信号量 表示消费者,当需要pop的时候
    • deque为空,等待
    • deque不为空,直接pop,唤醒一个生产者

关闭循环队列

template <class T>
void BlockDeque<T>::Close()
{
    {
        std::lock_guard<std::mutex> locker(mtx_);
        deq_.clear();
        isClose_ = true;
    }
    condProducer_.notify_all();
    condConsumer_.notify_all();
};

清空deque,唤醒所有的等待进程。

log 日志模块

代码

头文件

/*
 * @Author       : mark
 * @Date         : 2020-06-16
 * @copyleft Apache 2.0
 */
#ifndef LOG_H
#define LOG_H

#include <mutex>
#include <string>
#include <thread>
#include <sys/time.h>
#include <string.h>
#include <stdarg.h> // vastart va_end
#include <assert.h>
#include <sys/stat.h> //mkdir
#include "blockqueue.h"
#include "../buffer/buffer.h"

class Log
{
public:
    void init(int level, const char *path = "./log",
              const char *suffix = ".log",
              int maxQueueCapacity = 1024);
    // 单例模式
    static Log *Instance();
    static void FlushLogThread();

    void write(int level, const char *format, ...);
    void flush();

    int GetLevel();
    void SetLevel(int level);
    bool IsOpen() { return isOpen_; }

private:
    Log();
    void AppendLogLevelTitle_(int level);
    virtual ~Log();
    void AsyncWrite_();

private:
    static const int LOG_PATH_LEN = 256;
    static const int LOG_NAME_LEN = 256;
    static const int MAX_LINES = 50000;

    const char *path_;
    const char *suffix_;

    int MAX_LINES_;

    int lineCount_;
    int toDay_;

    bool isOpen_;

    Buffer buff_;
    int level_;
    bool isAsync_;

    FILE *fp_;
    std::unique_ptr<BlockDeque<std::string>> deque_;
    std::unique_ptr<std::thread> writeThread_;
    std::mutex mtx_;
};

// do {}while(0) 括起来,确保宏展开后的内容被视为一个语句块,避免多次展开
#define LOG_BASE(level, format, ...)                   \
    do                                                 \
    {                                                  \
        Log *log = Log::Instance();                    \
        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);

#endif // LOG_H

源文件

/*
 * @Author       : mark
 * @Date         : 2020-06-16
 * @copyleft Apache 2.0
 */ 
#include "log.h"

using namespace std;

Log::Log() {
    lineCount_ = 0;
    isAsync_ = false;
    writeThread_ = nullptr;
    deque_ = nullptr;
    toDay_ = 0;
    fp_ = nullptr;
}

Log::~Log() {
    if(writeThread_ && writeThread_->joinable()) {
        while(!deque_->empty()) {
            deque_->flush();
        };
        deque_->Close();
        writeThread_->join();
    }
    if(fp_) {
        lock_guard<mutex> locker(mtx_);
        flush();
        fclose(fp_);
    }
}

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

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

void Log::init(int level = 1, const char* path, const char* suffix,
    int maxQueueSize) {
    isOpen_ = true;
    level_ = level;
    if(maxQueueSize > 0) {
        isAsync_ = true;
        if(!deque_) {
            unique_ptr<BlockDeque<std::string>> newDeque(new BlockDeque<std::string>);
            deque_ = move(newDeque);
            
            std::unique_ptr<std::thread> NewThread(new thread(FlushLogThread));
            writeThread_ = move(NewThread);
        }
    } else {
        isAsync_ = false;
    }

    lineCount_ = 0;

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

    {
        lock_guard<mutex> locker(mtx_);
        buff_.RetrieveAll();
        if(fp_) { 
            flush();
            fclose(fp_); 
        }

        fp_ = fopen(fileName, "a");
        if(fp_ == nullptr) {
            mkdir(path_, 0777);
            fp_ = fopen(fileName, "a");
        } 
        assert(fp_ != nullptr);
    }
}

void Log::write(int level, const char *format, ...) {
    struct timeval now = {0, 0};
    gettimeofday(&now, nullptr);
    time_t tSec = now.tv_sec;
    struct tm *sysTime = localtime(&tSec);
    struct tm t = *sysTime;
    va_list vaList;

    /* 日志日期 日志行数 */
    if (toDay_ != t.tm_mday || (lineCount_ && (lineCount_  %  MAX_LINES == 0)))
    {
        unique_lock<mutex> locker(mtx_);
        locker.unlock();
        
        char newFile[LOG_NAME_LEN];
        char tail[36] = {0};
        snprintf(tail, 36, "%04d_%02d_%02d", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);

        if (toDay_ != t.tm_mday)
        {
            snprintf(newFile, LOG_NAME_LEN - 72, "%s/%s%s", path_, tail, suffix_);
            toDay_ = t.tm_mday;
            lineCount_ = 0;
        }
        else {
            snprintf(newFile, LOG_NAME_LEN - 72, "%s/%s-%d%s", path_, tail, (lineCount_  / MAX_LINES), suffix_);
        }
        
        locker.lock();
        flush();
        fclose(fp_);
        fp_ = fopen(newFile, "a");
        assert(fp_ != nullptr);
    }

    {
        unique_lock<mutex> locker(mtx_);
        lineCount_++;
        int n = snprintf(buff_.BeginWrite(), 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);
        AppendLogLevelTitle_(level);

        va_start(vaList, format);
        int m = vsnprintf(buff_.BeginWrite(), buff_.WritableBytes(), format, vaList);
        va_end(vaList);

        buff_.HasWritten(m);
        buff_.Append("\n\0", 2);

        if(isAsync_ && deque_ && !deque_->full()) {
            deque_->push_back(buff_.RetrieveAllToStr());
        } else {
            fputs(buff_.Peek(), fp_);
        }
        buff_.RetrieveAll();
    }
}

void Log::AppendLogLevelTitle_(int level) {
    switch(level) {
    case 0:
        buff_.Append("[debug]: ", 9);
        break;
    case 1:
        buff_.Append("[info] : ", 9);
        break;
    case 2:
        buff_.Append("[warn] : ", 9);
        break;
    case 3:
        buff_.Append("[error]: ", 9);
        break;
    default:
        buff_.Append("[info] : ", 9);
        break;
    }
}

void Log::flush() {
    if(isAsync_) { 
        deque_->flush(); 
    }
    fflush(fp_);
}

void Log::AsyncWrite_() {
    // 写日志线程
    // 循环从队列中取出日志,写到文件中
    string str = "";
    while(deque_->pop(str)) {
        lock_guard<mutex> locker(mtx_);
        fputs(str.c_str(), fp_);
    }
}

Log* Log::Instance() {
    static Log inst;
    return &inst;
}

void Log::FlushLogThread() {
    Log::Instance()->AsyncWrite_();
}

使用了c++的单例模式,需要刚开始的时候对log进行初始化。

成员

  • std::unique_ptr<BlockDequestd::string> 指向BlockDeque的指针,用于存储需要写入的数据
  • std::unique_ptrstd::thread 指向写入数据的线程,初始化的时候调用写线程,BlockDeque一有数据,就往文件里写
void Log::AsyncWrite_() {
    // 写日志线程
    // 循环从队列中取出日志,写到文件中
    string str = "";
    while(deque_->pop(str)) {
        lock_guard<mutex> locker(mtx_);
        fputs(str.c_str(), fp_);
    }
}
  • LOG_INFO LOG_WARN LOG_ERROR是日志的宏,本质上就是调用单例模式,往队列里添加数据,加了do{}while(0)是防止循环展开之后的bug问题

测试代码

#include <gtest/gtest.h>
#include <iostream>
#include <string>
#include <ctime>
#include <fstream>
#include "log.h"
#include "blockqueue.h"

namespace WebServerTest
{
    using namespace std;
    class LogTest : public ::testing::Test
    {
    protected:
        void SetUp() override
        {
            Log::Instance()->init(1, "./log", ".log", 1024);
        }

        void TearDown() override
        {
            // Log::Instance()->flush();
        }
    };

    TEST_F(LogTest, LogInfo)
    {
        // 因为是另一个线程在写入程序,可能出现还没有开始写,就读去文件,那个时候啥也读不到
        LOG_INFO("This is a log info message");
        LOG_INFO("This is a log info message with %d", 10);
        LOG_INFO("This is a log info message with %s", "string");
        sleep(1);
        Log::Instance()->flush();

        time_t timer = time(nullptr);
        struct tm *sysTime = localtime(&timer);
        struct tm t = *sysTime;
        char fileName[256] = {0};
        snprintf(fileName, 256 - 1, "%s/%04d_%02d_%02d%s",
                 "./log", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, ".log");
        ifstream ifs(fileName);

        string last_one, curr_line, last_two, last_three;
        getline(ifs, last_three);
        getline(ifs, last_two);
        getline(ifs, last_one);
        while (getline(ifs, curr_line))
        {
            last_three = move(last_two);
            last_two = move(last_one);
            last_one = move(curr_line);
        }
        ifs.close();
        // cout << last_one << endl;
        // cout << last_two << endl;
        // cout << last_three << endl;
        EXPECT_NE(last_one.find("This is a log info message with string"), std::string::npos);
        EXPECT_NE(last_two.find("This is a log info message with 10"), std::string::npos);
        EXPECT_NE(last_three.find("This is a log info message"), std::string::npos);
    }

    TEST_F(LogTest, LogWarn)
    {
        LOG_WARN("This is a log warn message");
        sleep(1);
        Log::Instance()->flush();
        time_t timer = time(nullptr);
        struct tm *sysTime = localtime(&timer);
        struct tm t = *sysTime;
        char fileName[256] = {0};
        snprintf(fileName, 256 - 1, "%s/%04d_%02d_%02d%s",
                 "./log", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, ".log");
        ifstream ifs(fileName);
        string curr_line, last_one;
        getline(ifs, last_one);
        while (getline(ifs, curr_line))
        {
            last_one = move(curr_line);
        }
        ifs.close();
        EXPECT_NE(last_one.find("This is a log warn message"), std::string::npos);
        EXPECT_NE(last_one.find("warn"), std::string::npos);
    }

    TEST_F(LogTest, LogError)
    {
        LOG_ERROR("This is a log error message");
        sleep(1);
        Log::Instance()->flush();
        time_t timer = time(nullptr);
        struct tm *sysTime = localtime(&timer);
        struct tm t = *sysTime;
        char fileName[256] = {0};
        snprintf(fileName, 256 - 1, "%s/%04d_%02d_%02d%s",
                 "./log", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, ".log");
        ifstream ifs(fileName);
        string curr_line, last_one;
        getline(ifs, last_one);
        while (getline(ifs, curr_line))
        {
            last_one = move(curr_line);
        }
        ifs.close();
        EXPECT_NE(last_one.find("This is a log error message"), std::string::npos);
        EXPECT_NE(last_one.find("error"), std::string::npos);

    }
};

这里有一个bug,就是你初始化了之后,往BlockQueue里写数据,操作系统可能并不一定会执行你写入文件的那个线程,导致LOG_INFO之后,文件里还是没有数据。
解决方法是让测试的线程sleep,操作系统就会调用那个线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值