C++ 实现日志系统

Posted shmilyt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 实现日志系统相关的知识,希望对你有一定的参考价值。

自实现日志

全局宏定义

common.h

#pragma once

#if !defined(__PRETTY_FUNCTION__) && !defined(__GNUC__)
#define __PRETTY_FUNCTION__ __FUNCSIG__
#endif


// 允许打印在控制台的单条日志的最大长度
const int MAX_CONSOLE_LOG_LENGTH = 512;

注意common.h被多个文件包含时, MAX_CONSOLE_LOG_LENGTH会被编译器替换为512, 所以不会造成重复定义

头文件

#pragma once

#include "common.h"
#include <queue>
#include <string>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <functional>
#include <ctime>

#define LogInfo(...) Logger::GetInstance().AddToQueue("info", __FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__)
#define LogError(...) Logger::GetInstance().AddToQueue("error", __FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__)
#define LogWarning(...) Logger::GetInstance().AddToQueue("warning", __FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__)

class Logger 
public:
	Logger();
	~Logger();
	static Logger& GetInstance();
	void AddToQueue(const char* logLevel, const char* fileName, int lineNo, const char* funcName, const char* msg);
	void WriteLog();
private:
	std::queue<std::string> m_queue;
	FILE* m_fp;
	std::mutex   m_mutex;
	std::atomic<bool> m_isRunning;
	std::shared_ptr<std::thread> m_threadPtr;
    std::condition_variable m_cv;
;

实现

#include "Logger.h"

Logger::Logger()

   // 创建日志文件并启动写线程
	time_t now = time(nullptr);
	struct tm* tp = localtime(&now);
	char logFileName[128] =  0 ;
	sprintf(logFileName, "D:\\\\mlog_%04d%02d%02d.log", tp->tm_year + 1900, tp->tm_mon + 1, tp->tm_mday);

	// 以只写模式打开文本文件
	m_fp = fopen(logFileName, "at+");
	if (m_fp == nullptr) 
		return;
	
	m_isRunning = true;
	m_threadPtr.reset(new std::thread(std::bind(&Logger::WriteLog, this)));


Logger::~Logger()

	m_isRunning = false;
	// 等待写线程结束
	m_cv.notify_one();
	m_threadPtr->join();
	fclose(m_fp);
	m_fp = nullptr;
	printf("析构函数执行完毕");


Logger& Logger::GetInstance()

	static Logger logger;
	return logger;


void Logger::AddToQueue(const char* logLevel, const char* fname, int lineNo, const char* funcName, const char* msg)

	time_t now = time(NULL);
	struct tm* tp = localtime(&now);
	char formatStr[512] =  0 ;
	// [年-月-日 时:分:秒][日志等级][文件:行号 函数名] 信息
	sprintf(formatStr, "[%04d-%02d-%02d %02d:%02d:%02d][%s][%s:%d %s]%s\\n",
		tp->tm_year+ 1900, tp->tm_mon+1, tp->tm_mday, tp->tm_hour, tp->tm_min, tp->tm_sec,
		logLevel, fname, lineNo, funcName, msg);
	
		std::lock_guard<std::mutex> lock(m_mutex);
		m_queue.emplace(formatStr);
	
	m_cv.notify_one();


void Logger::WriteLog()

	if (m_fp == nullptr) 
		return;
	

	while (m_isRunning) 
		std::unique_lock<std::mutex> lock(m_mutex);
		while (m_queue.empty()) 
			// 阻塞等待
			m_cv.wait(lock);
		

		// 向控制台和文件文件写日志
		const std::string &msg = m_queue.front();
		size_t len = msg.size();
		if (len < MAX_CONSOLE_LOG_LENGTH) 
			printf(msg.c_str());
		
		fwrite(msg.c_str(), len, 1, m_fp);
		fflush(m_fp);
		m_queue.pop();
	

基于c++的日志文件实现

概述

所有的商业软件或线上系统都具有日志功能,因为日志信息提供了系统启动以来的重要的操作或状态迁移记录,是追踪各种异常错误的第一手资料。绝大部分系统的日志模块会自动保留历史日志文件,即:日志文件大小达到约定上限时,自动转储到一个新的历史文件,当前文件清空并继续记录新的日志信息,例如:假设当前日志文件名为test.log, 当它的大小到达上限(例如10MB)时,就把其文件内容转储到新文件test.log.1, 然后test.log清空并继续记录新信息。根据配置不同,我们可以保留1到N份历史日志文件。当历史日志文件数目达到上限时,我们可以采用round-robin策略(或其他策略)依据文件生成时间依次覆盖老的文件。本文尝试用C++实现上述日志文件功能,其功能归纳总结如下:

1) 日志文件提供接口让用户配置日志文件名、日志大小上限、历史日志文件数目上限;

2) 日志文件提供Append()接口,让用户向文件追加日志消息;

3) 日志文件在执行Append()接口过程中,自动检测当前日志文件大小:如果追加当前消息后,文件大小超过约定上限,则记录当前消息前,将已有消息转储到历史文件并保证历史日志文件数不超过约定上限;否者,直接记录当前消息;

4) 日志文件提供接口让用户配置是否对历史日志文件进行压缩;

实现

LogFile类实现了上述功能,先看一下其接口定义:

 1 #ifndef _LOGFILE_H
 2 #define _LOGFILE_H
 3 
 4 #include <fstream>
 5 #include <iostream>
 6 
 7 
 8 class LogFile 
 9 public:
10     LogFile(const LogFile &) = delete;
11     LogFile& operator=(const LogFile &) = delete;
12     LogFile(const std::string&, double, unsigned int);
13 
14     ~LogFile();
15     void Append(std::string &&msg);
16 
17 private:
18     void Rotate();
19     double GetFileSize();
20     std::string NextHistoryFile();
21 
22 private:
23     std::ofstream ofs_;
24     std::string file_name_;
25     double cur_size_;
26     double max_size_;
27     unsigned int max_file_num_;
28 ;
29 
30 #endif //_LOGFILE_H

 

LogFile成员变量说明

ofs_: c++ std::ofstream类型对象,通过其操作符"<<"把消息写入日志文件

file_name_: 日志文件名;

cur_size_: 实时记录当前日志文件大小,避免每次执行Append()操作时调用系统函数获取文件大小;

max_size_: 用户指定的日志文件上限;

max_file_num_: 用户指定的最大历史文件数;

 

LogFile成员函数说明

LogFile(const std::string&, double, unsigned int): LogFile类的唯一构造函数,可以指定日志文件名,文件大小上限,历史文件数量上限,其实现也非常简单: 

LogFile::LogFile(const std::string& file_name,
                 double max_size,
                 unsigned int max_file_num)
    : file_name_(file_name), max_size_(max_size), max_file_num_(max_file_num)

    assert(max_file_num_ > 0);
    cur_size_ = GetFileSize();
    ofs_.open(file_name_, std::ofstream::out|std::ofstream::app);

 

~LogFile(): 析构函数,主要功能是关闭在构造函数中打开的ofstream对象.

LogFile::~LogFile()

    if (ofs_.is_open()) 
        ofs_.close();
    

 

void Append(std::string &&msg):LogFile类最重要的接口,让用户向日志文件追加新的消息。其实现逻辑为:在写文件前,先检查当前日志文件大小加上当前消息的长度之和是否会超过创建LogFile对象时指定的日志文件大小上限:如果超过,就将当前文件内容转存到历史文件,并清空当前日志文件(通过Rotate函数),然后,继续把当前消息写入日志文件。 有一点值得说明:获取当前日志文件大小不是通过调用系统函数,而是通过类成员变量cur_size_(构造函数调用一次系统函数为cur_size_赋初值,之后,每次执行Append(), cur_size_都累加消息长度,从而实时追踪日志文件长度), 这样避免了每次执行Append()时都调用系统函数检查文件长度,从而提高了效率。

void LogFile::Append(std::string&& msg)

    double msg_size = (double)msg.size();
    if (cur_size_+ msg_size >= max_size_) 
        Rotate();
    

    ofs_ << std::forward<std::string>(msg) << std::endl;
    cur_size_ += msg_size;


void Rotate(): 转储函数,当前日志文件内容被转存到某个历史日志文件,当前日志文件被清空并被重新打开

void LogFile::Rotate()

    if (ofs_.is_open()) 
        ofs_.close();
    

    std::string history_file = NextHistoryFile();
    std::rename(file_name_.c_str(), history_file.c_str());
    ofs_.open(file_name_, std::ofstream::out|std::ofstream::app);
    cur_size_ = 0;

 

std::string NextHistoryFile(): 返回下一个历史日志文件名以用于转存日志文件信息

std::string LogFile::NextHistoryFile()

    static int next_file_no = 0;
    int file_num = (next_file_no++) % max_file_num_;
    return file_name_ + "." + std::to_string(file_num);

 

double LogFile::GetFileSize(): 返回当前日志文件大小(bytes),当前实现只针对Linux

double LogFile::GetFileSize()

    struct stat statbuf;
    if (stat(file_name_.c_str(), &statbuf) == 0) 
        return (double)statbuf.st_size;
     else 
        perror("Faild to get log file size");
        return 0;
    

测试

针对上述LogFile的实现,我们可以进行如下测试:

#include "logfile.h"


int main(void)

    LogFile lf("testlogfile.log", 1024, 8);
    for (auto i = 0; i < 200; i++) 
        lf.Append(std::to_string(i) + ": this is a very very very very very very very very very very very very very very vvery very very very very ery long log");
    

测试结果日下:

[email protected]:/local/project/logcpp$ ls -lart testlogfile.log*
-rw-rw-r-- 1 stephenw stephenw 992 6月  27 23:02 testlogfile.log.4
-rw-rw-r-- 1 stephenw stephenw 992 6月  27 23:02 testlogfile.log.3
-rw-rw-r-- 1 stephenw stephenw 992 6月  27 23:02 testlogfile.log.2
-rw-rw-r-- 1 stephenw stephenw 992 6月  27 23:02 testlogfile.log.1
-rw-rw-r-- 1 stephenw stephenw 992 6月  27 23:02 testlogfile.log.7
-rw-rw-r-- 1 stephenw stephenw 992 6月  27 23:02 testlogfile.log.6
-rw-rw-r-- 1 stephenw stephenw 992 6月  27 23:02 testlogfile.log.5
-rw-rw-r-- 1 stephenw stephenw 992 6月  27 23:02 testlogfile.log.0
-rw-rw-r-- 1 stephenw stephenw 124 6月  27 23:02 testlogfile.log

[email protected]:/local/project/logcpp$ tail -n5 testlogfile.log.0 testlogfile.log.1 testlogfile.log.2
==> testlogfile.log.0 <==
194: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
195: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
196: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
197: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
198: this is a very very very very very very very very very very very very very very vvery very very very very ery long log

==> testlogfile.log.1 <==
138: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
139: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
140: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
141: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
142: this is a very very very very very very very very very very very very very very vvery very very very very ery long log

==> testlogfile.log.2 <==
146: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
147: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
148: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
149: this is a very very very very very very very very very very very very very very vvery very very very very ery long log
150: this is a very very very very very very very very very very very very very very vvery very very very very ery long log

 

 

 

 

 

 

 

以上是关于C++ 实现日志系统的主要内容,如果未能解决你的问题,请参考以下文章

华为OD机试模拟题用 C++ 实现 - 日志采集系统(2023.Q1)

华为OD机试真题 C++ 实现日志限流2023 Q1 | 100分

华为机试真题 C++ 实现日志首次上报最多积分2022.11 Q4 新题

C++日志系统如何设计

华为OD机试真题 JavaScript 实现日志限流2023 Q1 | 100分

C++通用日志系统