C++ 日志系统实战第四步:设计与代码实现详解
📖本项目设计多日志器日志系统,涵盖日志等级、消息等模块,支持同步异步落地,还展示了部分代码设计~
全是通俗易懂的讲解,如果你本节之前的知识都掌握清楚,那就速速来看我的项目笔记吧~
本文将加入项目代码编写!
目录
日志系统框架设计
本项目实现的是一个多日志器日志系统,主要实现的功能是让程序员能够轻松的将程序运行日志信息落地到指定的位置,且支持同步与异步两种方式的日志落地方式。
项目的框架设计将项目分为以下几个模块来实现。
模块划分
- 日志等级模块:对输出日志的等级进行划分,以便于控制日志的输出,并提供等级枚举转字符串功能。
- OFF:关闭
- DEBUG:调试,调试时的关键信息输出。
- INFO:提示,普通的提示型日志信息。
- WARN:警告,不影响运行,但是需要注意一下的日志。
- ERROR:错误,程序运行出现错误的日志
- FATAL:致命,一般是代码异常导致程序无法继续推进运行的日志
- 日志消息模块:中间存储日志输出所需的各项要素信息
- 时间:描述本条日志的输出时间。
- 线程 ID:描述本条日志是哪个线程输出的。
- 日志等级:描述本条日志的等级。
- 日志数据:本条日志的有效载荷数据。
- 日志文件名:描述本条日志在哪个源码文件中输出的。
- 日志行号:描述本条日志在源码文件的哪一行输出的。
- 日志消息格式化模块:设置日志输出格式,并提供对日志消息进行格式化功能。
- 系统的默认日志输出格式:% d {% H:% M:% S}% T% t% T% p% T% c% T% f:% l% T% m% n
- 示例:-> 13:26:32 [2343223321] [FATAL] [root] main.c:76 套接字创建失败 \n
- % d {% H:% M:% S}:表示日期时间,花括号中的内容表示日期时间的格式。
- % T:表示制表符缩进。
- % t:表示线程 ID
- % p:表示日志级别
- % c:表示日志器名称,不同的开发组可以创建自己的日志器进行日志输出,小组之间互不影响。
- % f:表示日志输出时的源代码文件名。
- % l:表示日志输出时的源代码行号。
- % m:表示给与的日志有效载荷数据
- % n:表示换行
- 设计思想:设计不同的子类,不同的子类从日志消息中取出不同的数据进行处理。
- 日志消息落地模块:决定了日志的落地方向,可以是标准输出,也可以是日志文件,也可以滚动文件输出……
- 标准输出:表示将日志进行标准输出的打印。
- 日志文件输出:表示将日志写入指定的文件末尾。
- 滚动文件输出:当前以文件大小进行控制,当一个日志文件大小达到指定大小,则切换下一个文件进行输出
- 后期,也可以扩展远程日志输出,创建客户端,将日志消息发送给远程的日志分析服务器。
- 设计思想:设计不同的子类,不同的子类控制不同的日志落地方向。
- 日志器模块:
- 此模块是对以上几个模块的整合模块,用户通过日志器进行日志的输出,有效降低用户的使用难度。
- 包含有:日志消息落地模块对象,日志消息格式化模块对象,日志输出等级
- 日志器管理模块:
- 为了降低项目开发的日志耦合,不同的项目组可以有自己的日志器来控制输出格式以及落地方向,因此本项目是一个多日志器的日志系统。
- 管理模块就是对创建的所有日志器进行统一管理。并提供一个默认日志器提供标准输出的日志输出。
- 异步线程模块:
- 实现对日志的异步输出功能,用户只需要将输出日志任务放入任务池,异步线程负责日志的落地输出功能,以此提供更加高效的非阻塞日志输出。
模块关系图
代码设计
实用类设计
/*
实用工具类的实现
1.获取系统时间
2.判断文件是否存在
3.获取文件所在路径
4.创建目录
*/
#include <iostream>
#include <ctime>
namespace mylog
{
namespace util
{
// 获取系统时间
class Date
{
public:
static size_t getTime()//返回当前时间戳
{
return (size_t)time(nullptr);
}
};
// 判断文件是否存在
class File
{
public:
//静态接口,直接使用,不用实例化对象
static bool exists(const std::string& pathname);
static std::string path(const std::string& filename);
static bool createDir(const std::string& dirname);
};
}
}
判断文件是否存在函数(exists):
access
函数是 C 语言中的一个文件访问权限检测函数,定义在 <unistd.h>
头文件中 。
- 函数原型:
int access(const char *pathname, int mode);
- 参数
pathname
:要检测的文件或目录的路径名。mode
:指定访问模式,常用取值有:R_OK
:检查是否有读权限。W_OK
:检查是否有写权限。X_OK
:检查是否有执行权限。F_OK
:检查文件是否存在。
- 返回值:调用成功时返回 0 ;调用失败时返回 -1 ,同时会设置
errno
来指示错误原因,比如ENOENT
表示文件不存在等。
这是Linux下的系统调用,为了项目在各个操作系统都支持。我们可以使用stat函数
stat
函数是 C 语言中用于获取文件状态信息的函数 ,定义在<sys/types.h>
、<sys/stat.h>
和<unistd.h>
头文件中。
函数原型
int stat(const char *pathname, struct stat *buf);
参数
pathname
:指向要获取状态信息的文件路径名。buf
:是一个struct stat
结构体指针,用于存储获取到的文件状态信息。struct stat
包含文件的 inode 编号、文件类型、权限、大小、修改时间等众多属性。
返回值
成功调用时返回 0 ;失败时返回 -1 ,并设置errno
来指示错误类型,如ENOENT
表示文件不存在。
#include <sys/stat.h>
static bool exists(const std::string& pathname)
{
// return access(pathname.c_str(), F_OK) != -1; Linux下可用
struct stat buffer;
if (stat(pathname.c_str(), &buffer) == 0)
{
return true;
}
else
{
return false;
}
}
获取文件的所在路径函数(path):
就是查找最后一个‘/’
static std::string path(const std::string& filename)
{
size_t pos = filename.find_last_of("/\\");
if (pos == std::string::npos)
{
return "."; // 当前目录下
}
else
{
return filename.substr(0, pos);
}
}
创建目录(creatdir):
对于.abc/b/a.text 要找到前面的才可以创建后面的
mkdir
函数用于创建一个新的目录,在不同操作系统的 C 语言标准库中均有提供。
函数原型
在 POSIX 系统(如 Linux、macOS)中,其原型定义在 <sys/stat.h>
和 <sys/types.h>
头文件中:
int mkdir(const char *pathname, mode_t mode);
参数说明
pathname
:要创建的目录的路径名,可以是绝对路径或相对路径。mode
:指定新目录的访问权限,是一个八进制数,如0777
表示所有用户都有读、写、执行权限。
返回值
- 成功时返回 0。
- 失败时返回 -1,并设置
errno
以指示错误原因,常见的错误包括:
EEXIST
:指定路径的目录已存在。EACCES
:没有足够权限创建目录。ENOENT
:路径中的父目录不存在。
static bool createDir(const std::string &dirname)
{
if (dirname.empty())
{
return false; // 空字符串无法创建目录
}
int pos = 0, index = 0;
while (index <= dirname.size())
{
pos = dirname.find_first_of("/\\", index);
if (pos == std::string::npos)
{
// 没有/或\,说明是当前目录
if (mkdir(dirname.c_str(), 0777) != 0)
{
std::cerr << "Failed to create directory: " << dirname << std::endl;
return false;
}
index = dirname.size() + 1;
}
else
{
std::string parent_str = dirname.substr(0, pos + 1);
if (!exists(parent_str))
{
if (mkdir(parent_str.c_str(), 0777) != 0)
{
std::cerr << "Failed to create directory: " << parent_str << std::endl;
return false;
}
}
index = pos + 1;
}
}
return true;
}
日志等级设计
#pragma once
namespace logs {
enum class Level
{
Unknown=0,
Debug=1,
Info=2,
Warning=3,
Error=4,
Fatal=5,
Off=6
};
class LevelHelper
{
public:
static const char* ToString(Level level)
{
switch(level)
{
case Level::Debug:
return "Debug";
case Level::Info:
return "Info";
case Level::Warning:
return "Warning";
case Level::Error:
return "Error";
case Level::Fatal:
return "Fatal";
case Level::Off:
return "Off";
default:
return "Unknown";
}
}
};
} // namespace logs
日志消息类
message.hpp
#pragma once
#include<iostream>
#include<string>
#include"util.hpp"
#include"level.hpp"
#include<thread>
namespace mylog {
struct LogMessage {
size_t ctime_;// creation time
Level level_;// log level
std::thread::id tid_;// process id
std::string file_;// source file name
size_t line_;// source line number
std::string message_;// log message
std::string logger_name_;// logger name
LogMessage(Level level, const char* file, size_t line, const char* message, const char* logger_name)
: ctime_(util::Date::getTime()), level_(level), tid_(std::this_thread::get_id()), file_(file), line_(line), message_(message), logger_name_(logger_name)
{}
};
}
日志输出格式
格式化子串实现:
format.hpp:
#pragma once
#include "message.hpp"
#include <memory>
#include <ctime>
#include <cassert>
#include <unordered_set>
#include <sstream>
#include <vector>
namespace mylog
{
// 基类用来定义日志格式
class Format
{
public:
using ptr = std::shared_ptr<Format>;
virtual void format(std::ostream &os, const LogMessage &msg) = 0;
};
// 具体的日志格式
class MsgFormat : public Format
{
public:
void format(std::ostream &os, const LogMessage &msg) override
{
os << msg.message_;
}
};
class LevelFormat : public Format
{
public:
void format(std::ostream &os, const LogMessage &msg) override
{
os << LevelHelper::ToString(msg.level_);
}
};
class TimeFormat : public Format
{
public:
TimeFormat(const std::string &format = "%H:%M:%S") : formatTime(format)
{
}
void format(std::ostream &os, const LogMessage &msg) override
{
struct tm t;
localtime_r(&msg.ctime_, &t);
char buf[64] = {0};
strftime(buf, sizeof(buf), formatTime.c_str(), &t);
os << buf;
}
private:
std::string formatTime;
};
class FileFormat : public Format
{
public:
void format(std::ostream &os, const LogMessage &msg) override
{
os << msg.file_;
}
};
class LineFormat : public Format
{
public:
void format(std::ostream &os, const LogMessage &msg) override
{
os << msg.line_;
}
};
class LoggerFormat : public Format
{
public:
void format(std::ostream &os, const LogMessage &msg) override
{
os << msg.logger_;
}
};
class ThreadIdFormat : public Format
{
public:
void format(std::ostream &os, const LogMessage &msg) override
{
os << msg.tid_;
}
};
class TableFormat : public Format
{
public:
void format(std::ostream &os, const LogMessage &msg) override
{
os << "\t";
}
};
class NewLineFormat : public Format
{
public:
void format(std::ostream &os, const LogMessage &msg) override
{
os << "\n";
}
};
class OtherFormat : public Format
{
public:
OtherFormat(const std::string &other = "") : other(other)
{
}
void format(std::ostream &os, const LogMessage &msg) override
{
os << other;
}
private:
std::string other;
};
/*
%d:表示日期 ,包含子格式 {%H:%M:%S}
%t:表示线程 ID
%c:表示日志器名称
%f:表示源码文件名
%l:表示源码行号
%p:表示日志级别
%T:表示制表符缩进
%m:表示主体消息
%n:表示换行
*/
class FormatBuilder // 日志格式构造器
{
public:
FormatBuilder(const std::string &pattern = "[%d{%H:%M:%S}][%t][%c][%f:%l][%p]%T%m%n") : pattern_(pattern)
{
assert(parsePattern());
}
// 对msg进行格式化
void format(std::ostream &os, const LogMessage &msg)
{
for (auto &f : formats)
{
f->format(os, msg);
}
}
std::string format(const LogMessage &msg)
{
std::ostringstream oss;
format(oss, msg);
return oss.str();
}
private:
// 假设此函数在 FormatBuilder 类中
bool parsePattern()
{
size_t pos = 0; // 表示处理到的位置
std::string key; // 表示格式化关键字
std::string value; // 表示格式化值
std::vector<std::pair<std::string, std::string>> items; // 表示格式化项
while (pos < pattern_.size())
{
// 1. 如果不是%,说明是普通字符
while (pos < pattern_.size() && pattern_[pos] != '%')
{
value += pattern_[pos];
pos++;
}
// 2. 如果是%,说明是格式化字符
if (pos + 1 < pattern_.size() && pattern_[pos + 1] == '%')
{
// 2.1 如果是%%,说明是%
value += pattern_[pos];
pos += 2;
}
if (!value.empty())
{
items.emplace_back(std::make_pair("", value));
value.clear();
continue;
}
// 3. 如果是格式化字符
if (pos < pattern_.size() && pattern_[pos] == '%')
{
pos++; // 跳过%
// 合并检查:跳过 '%' 后,统一检查是否超出字符串范围
if (pos >= pattern_.size())
{
std::cerr << "Error: Unexpected end of pattern after % at position " << pos << "." << std::endl;
return false;
}
key = pattern_[pos];
pos++;
if (pos < pattern_.size() && pattern_[pos] == '{')
{
pos++; // 跳过{
// 3.1 如果是{,说明是子格式化
while (pos < pattern_.size() && pattern_[pos] != '}')
{
value += pattern_[pos];
pos++;
}
if (pos >= pattern_.size() || pattern_[pos] != '}')
{
std::cerr << "Error: Missing closing brace for sub - pattern starting at position " << pos - value.length() - 1 << "." << std::endl;
return false;
}
pos++; // 跳过}
}
// 检查是否为不认识的格式化关键字
static const std::unordered_set<char> validKeys = {'d', 't', 'c', 'f', 'l', 'p', 'T', 'm', '%', 'n'};
if (validKeys.find(key[0]) == validKeys.end())
{
std::cerr << "Error: Unknown format keyword '" << key << "' at position " << pos - key.length() - 1 << "." << std::endl;
return false;
}
items.emplace_back(std::make_pair(key, value));
key.clear();
value.clear();
}
}
// 4. 创建格式化对象
for (auto &item : items)
{
formats.push_back(createItem(item.first, item.second));
}
return true;
}
// 创建格式化对象
Format::ptr createItem(const std::string &key, const std::string &value)
{
if (key == "d")
{
return std::make_shared<TimeFormat>(value);
}
else if (key == "t")
{
return std::make_shared<ThreadIdFormat>();
}
else if (key == "c")
{
return std::make_shared<LoggerFormat>();
}
else if (key == "f")
{
return std::make_shared<FileFormat>();
}
else if (key == "l")
{
return std::make_shared<LineFormat>();
}
else if (key == "p")
{
return std::make_shared<LevelFormat>();
}
else if (key == "T")
{
return std::make_shared<TableFormat>();
}
else if (key == "m")
{
return std::make_shared<MsgFormat>();
}
else if (key == "n")
{
return std::make_shared<NewLineFormat>();
}
else if (key.empty())
{
return std::make_shared<OtherFormat>(value);
}
else
{
return std::make_shared<OtherFormat>(key);
}
}
private:
std::string pattern_; // 格式化字符串
std::vector<Format::ptr> formats;
};
}
// 解析格式化字符串:
日志落地(LogSink)类设计(简单工厂模式)
sink.hpp
#pragma once
#include "util.hpp"
#include <memory>
#include <fstream>
#include <sstream>
#include<assert.h>
namespace mylog
{
/*
日志落地实现
1.抽象落地基类
2.根据不同的落地方式派生出不同的落地类
3.简单工厂模式
*/
class LogSink
{
public:
using ptr = std::shared_ptr<LogSink>;
LogSink() {}
virtual void log(const char *msg, size_t len) = 0;
virtual ~LogSink() {}
};
// 向标准输出流输出日志
class StdoutSink : public LogSink
{
public:
void log(const char *msg, size_t len) override
{
std::cout.write(msg, len);
}
};
// 向文件中输出日志
class FileSink : public LogSink
{
public:
FileSink(std::string pathname) : pathname_(pathname)
{
// 1.创建文件所在目录
if (!util::File::createDir(util::File::path(pathname_)))
{
std::cerr << "create dir failed" << std::endl;
}
// 2.打开文件
ofs_.open(pathname_.c_str(), std::ios::binary | std::ios::app); // 以二进制,追加方式打开文件
assert(ofs_.is_open());
}
void log(const char *msg, size_t len) override
{
ofs_.write(msg, len);
}
private:
std::string pathname_;
std::ofstream ofs_; // ofstream对象,用于输出日志到文件
};
// 向滚动文件中输出日志(按文件大小)
class RollSinkbySize : public LogSink
{
public:
RollSinkbySize(std::string basename, size_t max_size) : basename_(basename), max_size_(max_size)
{
std::string pathname = create_new_file();
// 1.创建文件所在目录
util::File::createDir(util::File::path(pathname));
// 2.打开文件
ofs_.open(pathname.c_str(), std::ios::binary | std::ios::app); // 以二进制,追加方式打开文件
assert(ofs_.is_open());
cur_size_ = ofs_.tellp(); // 获取当前文件大小
}
void log(const char *msg, size_t len) override
{
if(cur_size_ > max_size_) // 当前文件大小 + 日志大小 > 最大文件大小
{
ofs_.close(); // 关闭当前文件
std::string pathname = create_new_file();
// 1.创建文件所在目录
util::File::createDir(util::File::path(pathname));
// 2.打开文件
ofs_.open(pathname.c_str(), std::ios::binary | std::ios::app); // 以二进制,追加方式打开文件
assert(ofs_.is_open());
cur_size_ = 0; // 重置当前文件大小
}
ofs_.write(msg, len);
assert(ofs_.good());
cur_size_ += len; // 更新当前文件大小
}
private:
std::string create_new_file() // 创建新的日志文件
{
time_t t = util::Date::getTime();
struct tm lt;
localtime_r(&t, <);
std::stringstream filename;
filename << basename_ << "-" << lt.tm_year + 1900 << "-" << lt.tm_mon + 1 << "-" << lt.tm_mday << "-" << lt.tm_hour << lt.tm_min << lt.tm_sec <<"-"<<name_count_++ << ".log";
return filename.str();
}
size_t name_count_ = 0; // 日志文件名计数器
std::string basename_; // 通过基础文件名 + 时间戳生成新的日志文件名
size_t max_size_;
std::ofstream ofs_; // ofstream对象,用于输出日志到文件
size_t cur_size_; // 当前日志文件大小
};
//工厂模式
class SinkFactory
{
public:
template<typename SinkType, typename... Args>
static LogSink::ptr create(Args &&... args)
{
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
};
}
后续还有代码,期待共同实现~
如果你对日志系统感到兴趣,欢迎关注我👉【A charmer】
更多推荐
所有评论(0)