概述
该模块主要实现了一个日志系统的输出模块,通过多态、工厂模式等设计模式,构建灵活的日志输出架构。
- **功能:**格式化完成的标准日志消息,输出到指定为止
- **拓展:**支持同时将日志落地到不同的位置,也就是输出日志到不同位置
- 输出位置
- **标准输出:**输出到屏幕上,主要用于测试日志是否正常运行
- **指定文件:**将日志信息输出到指定文件中,便于事后进行日志分析
- **滚动文件:**文件按照大小或者时间输出到指定文件中,方便按日期或者大小删除日志消息
- 实现思想
- 首先抽象出落地模块类
- 根据不同落地方向,从基类派生出来即可
- 其次工厂模式,创建与表示分离
架构设计
整体架构分析
- 主框架定义了一个日志输出系统,包含多种日志输出方式,例如标准输出、文件输出、滚动文件输出
- 模块代码通过基类 LogSink定义了一个统一的接口,并且通过派生类 StdoutSink 、 FileSink 、RollSink实现具体的日志输出方式
- SinkFactory工厂类则是通过模版方法提供创建不同类型日志输出对象的功能
- 保证单一职责原则,一个类派生类只负责一种日志输出方式
具体实现
LogSink基类
- 定义所有日志输出的基类,参数分别接收日志的内容以及长度
cpp
class LogSink {
public:
using ptr = std::shared_ptr<LogSink>;
LogSink() {}
virtual ~LogSink() {}
virtual void log(const char *data, size_t len) = 0;
};
StdoutSink
- **作用:**实现将日志输出到显示器(标准输出)的功能
- 实现逻辑: 使用std::cout::write标准库函数,输出一段指定长度的字符数据,一般用于二进制数据或者特定格式的字符串
cpp
class StdoutSink : public LogSink {
public:
using ptr = std::shared_ptr<StdoutSink>;
StdoutSink() = default;
void log(const char *data, size_t len) {
std::cout.write(data, len);
}
};
FileSink
- **功能:**实现将日志写入到指定文件中
- 实现逻辑
- 构造函数接收一个文件名filename ,用于指定日志输出的指定文件
- util::file::create_directory函数则是确保日志文件目录是否存在,如果不存在则对该目录进行创建
- **_ofs.oen:**表示以二进制追加模式打开文件,如果文件不存在则会创建它
- log函数则是通过write逻辑实现数据写入
- **_ofs.good():**检查写操作是否成功,如果失败则输出错误信息
cpp
class FileSink : public LogSink {
public:
using ptr = std::shared_ptr<FileSink>;
FileSink(const std::string &filename): _filename(filename) {
util::file::create_directory(util::file::path(filename));
_ofs.open(_filename, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
}
const std::string &file() { return _filename; }
void log(const char *data, size_t len) {
_ofs.write((const char*)data, len);
if (_ofs.good() == false) {
std::cout << "日志输出文件失败!\n";
}
}
private:
std::string _filename;
std::ofstream _ofs;
};
RollSink
- 功能:滚动日志功能的实现,也就是当日志文件达到一定大小的时候,自动生成新的日志文件继续写入
- 实现逻辑
- **构造函数:**初始化日志文件基本信息,_basename文件名(基础文件名,后面可以加上拓展文件名),_max_fsize最大文件大小
- **initLogFile:**检查当前日志文件的大小,如果文件已经达到设定的最大文件大小或是文件没有打开,则关闭当前文件并创建新的日志文件
- **createFilename函数:**根据当前的时间,生成新的日志文件名,文件名中包含的有日期和时间,同时于要保证每个文件名的唯一性
- **Log函数;**首先初始化日志文件(调用initLogFile函数)确保日志已经准备好,然后将日志数据写入到文件中,最后更新当前文件大小_cur_fsize
cpp
class RollSink : public LogSink {
public:
using ptr = std::shared_ptr<RollSink>;
RollSink(const std::string &basename, size_t max_fsize):
_basename(basename), _max_fsize(max_fsize), _cur_fsize(0) {
util::file::create_directory(util::file::path(basename));
}
void log(const char *data, size_t len) {
initLogFile();
_ofs.write(data, len);
if (_ofs.good() == false) {
std::cout << "日志输出文件失败!\n";
}
_cur_fsize += len;
}
private:
void initLogFile() {
if (_ofs.is_open() == false || _cur_fsize >= _max_fsize) {
_ofs.close();
std::string name = createFilename();
_ofs.open(name, std::ios::binary | std::ios::app);
assert(_ofs.is_open());
_cur_fsize = 0;
return;
}
return;
}
std::string createFilename() {
time_t t = time(NULL);
struct tm lt;
localtime_r(&t, <);
std::stringstream ss;
ss << _basename;
ss << lt.tm_year + 1900;
ss << lt.tm_mon + 1;
ss << lt.tm_mday;
ss << lt.tm_hour;
ss << lt.tm_min;
ss << lt.tm_sec;
ss << ".log";
return ss.str();
}
private:
std::string _basename;
std::ofstream _ofs;
size_t _max_fsize;
size_t _cur_fsize;
};
SinkFactory
- **作用:**工厂类,主要用于创建各种LogSInk对象
- 实现逻辑
- 通过模板函数
create
,SinkFactory
可以创建任意类型的LogSink
派生类对象- 使用
std::make_shared<SinkType>
创建对象,std::forward<Args>(args)...
用于完美转发构造函数的参数,使得对象的创建既高效又灵活
cpp
class SinkFactory {
public:
template<typename SinkType, typename ...Args>
static LogSink::ptr create(Args &&...args) {
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
单元测试
功能测试1:简单测试其功能
cpp
#include "sink.hpp"
#include <iostream>
#include <string>
#include <cstring>
int main() {
// 创建并测试 StdoutSink(标准输出日志)
{
bitlog::LogSink::ptr stdoutSink = bitlog::SinkFactory::create<bitlog::StdoutSink>();
const char* message = "这是一个用于测试 StdoutSink 的消息。\n";
stdoutSink->log(message, strlen(message));
}
// 创建并测试 FileSink(文件输出日志)
{
std::string filename = "test_log.txt";
bitlog::LogSink::ptr fileSink = bitlog::SinkFactory::create<bitlog::FileSink>(filename);
const char* message = "这是一个用于测试 FileSink 的消息。\n";
fileSink->log(message, strlen(message));
// 检查文件是否生成并输出文件内容
std::ifstream ifs(filename);
if (ifs.is_open()) {
std::cout << "FileSink 测试通过。日志文件内容:\n";
std::string line;
while (std::getline(ifs, line)) {
std::cout << line << std::endl;
}
ifs.close();
} else {
std::cout << "FileSink 测试失败。无法打开日志文件。\n";
}
}
// 创建并测试 RollSink(滚动文件日志)
{
std::string basename = "test_roll_log";
size_t max_fsize = 50; // 小文件大小以便快速测试滚动
bitlog::LogSink::ptr rollSink = bitlog::SinkFactory::create<bitlog::RollSink>(basename, max_fsize);
const char* message1 = "这是用于测试 RollSink 的第一条日志消息。\n";
const char* message2 = "这是用于测试 RollSink 的第二条日志消息。这条消息较长,用于触发滚动功能。\n";
rollSink->log(message1, strlen(message1));
rollSink->log(message2, strlen(message2));
// 检查是否生成多个日志文件
for (int i = 0; i < 2; ++i) {
std::string rollFilename = basename + std::to_string(1900 + localtime(nullptr)->tm_year)
+ std::to_string(1 + localtime(nullptr)->tm_mon)
+ std::to_string(localtime(nullptr)->tm_mday)
+ std::to_string(localtime(nullptr)->tm_hour)
+ std::to_string(localtime(nullptr)->tm_min)
+ std::to_string(localtime(nullptr)->tm_sec + i)
+ ".log";
std::ifstream ifs(rollFilename);
if (ifs.is_open()) {
std::cout << "RollSink 测试通过。日志文件 " << rollFilename << " 内容:\n";
std::string line;
while (std::getline(ifs, line)) {
std::cout << line << std::endl;
}
ifs.close();
} else {
std::cout << "RollSink 测试失败。无法打开日志文件 " << rollFilename << "。\n";
}
}
}
return 0;
}
复杂测试,并发场景下多种日志格式以及滚动日志
cpp
#include "sink.hpp"
#include <iostream>
#include <string>
#include <cstring>
#include <thread>
#include <vector>
#include <chrono>
#include <atomic>
// 模拟多线程日志写入的函数
void thread_log(bitlog::LogSink::ptr sink, const std::string& prefix, int num_messages, std::atomic<int>& counter) {
for (int i = 0; i < num_messages; ++i) {
std::string message = prefix + " 线程 " + std::to_string(counter++) + ": 日志消息 " + std::to_string(i + 1) + "\n";
sink->log(message.c_str(), message.size());
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 模拟写入间隔
}
}
int main() {
std::atomic<int> counter(1); // 用于标记每条日志的编号
// 测试并发写入到 StdoutSink
{
bitlog::LogSink::ptr stdoutSink = bitlog::SinkFactory::create<bitlog::StdoutSink>();
std::vector<std::thread> threads;
// 启动多个线程进行并发日志写入
for (int i = 0; i < 5; ++i) {
threads.emplace_back(thread_log, stdoutSink, "StdoutSink", 10, std::ref(counter));
}
for (auto& t : threads) {
t.join(); // 等待所有线程完成
}
std::cout << "StdoutSink 并发测试完成。\n";
}
// 测试多线程写入到 FileSink
{
std::string filename = "complex_test_log.txt";
bitlog::LogSink::ptr fileSink = bitlog::SinkFactory::create<bitlog::FileSink>(filename);
std::vector<std::thread> threads;
// 启动多个线程进行并发日志写入
for (int i = 0; i < 5; ++i) {
threads.emplace_back(thread_log, fileSink, "FileSink", 10, std::ref(counter));
}
for (auto& t : threads) {
t.join(); // 等待所有线程完成
}
std::cout << "FileSink 并发测试完成。日志文件:" << filename << "\n";
}
// 测试复杂滚动日志
{
std::string basename = "complex_roll_test_log";
size_t max_fsize = 100; // 设置较小的文件大小以触发滚动
bitlog::LogSink::ptr rollSink = bitlog::SinkFactory::create<bitlog::RollSink>(basename, max_fsize);
std::vector<std::thread> threads;
// 启动多个线程进行并发日志写入
for (int i = 0; i < 5; ++i) {
threads.emplace_back(thread_log, rollSink, "RollSink", 10, std::ref(counter));
}
for (auto& t : threads) {
t.join(); // 等待所有线程完成
}
std::cout << "RollSink 并发测试完成。\n";
// 检查生成的滚动日志文件
for (int i = 0; i < 10; ++i) {
std::string rollFilename = basename + std::to_string(1900 + localtime(nullptr)->tm_year)
+ std::to_string(1 + localtime(nullptr)->tm_mon)
+ std::to_string(localtime(nullptr)->tm_mday)
+ std::to_string(localtime(nullptr)->tm_hour)
+ std::to_string(localtime(nullptr)->tm_min)
+ std::to_string(localtime(nullptr)->tm_sec + i)
+ ".log";
std::ifstream ifs(rollFilename);
if (ifs.is_open()) {
std::cout << "RollSink 测试通过。日志文件 " << rollFilename << " 内容:\n";
std::string line;
while (std::getline(ifs, line)) {
std::cout << line << std::endl;
}
ifs.close();
} else {
std::cout << "无法打开日志文件 " << rollFilename << "(可能文件数过多已删除)。\n";
}
}
}
return 0;
}