代码仓库:日志系统
目录
前言
在上一篇中,
我们明确了项目目标,
完成了模块的设计,
并画出了项目的结构图。
从项目结构中可以发现,
这个项目具有复杂的继承体系,
并且使用了多种设计模式。
如果直接按照模块一个类一个类的实现,
很有可能会在项目中后期出现混乱,
比如由于接口设计不当导致实现与设计不符。
确定核心功能
橙色:核心模块,需要完整实现,
绿色:基础框架,需要部分实现,
灰色:这次不用管。
基础框架的搭建
数据
LogLevel
提供 debug, info, warning, error, fatal 五个日志级别
#ifndef __LEVEL_HPP__
#define __LEVEL_HPP__
namespace ly
{
class LogLevel
{
public:
enum class Level
{
debug = 0,
info,
warning,
error,
fatal
};
static const char* levelToString(Level level)
{
if(level == Level::debug) return "debug";
if(level == Level::info) return "info";
if(level == Level::warning) return "warning";
if(level == Level::error) return "error";
if(level == Level::fatal) return "fatal";
return "unknown";
}
};
} // namespace ly
#endif // #define __LEVEL_HPP__
LogMessage
需存储:
- 日志级别
- 文件名
- 文件行号
- 消息内容
- 消息创建时间
- 线程id
- 日志器名
LogMessage 做为项目中最重要且最基础的数据结构,
只应该存储纯数据,
与上层的类不应该有任何的关系。
所以在设计接口时不能图方便直接传 Logger/Sink 等类。
我最开始想的是消息类存个 Sink::ptr 多好,
自己就能把自己落地了,
后来发现 logger.hpp 包含了 message.hpp,
于是就更正了这个错误的想法。
#ifndef __MESSAGE_HPP__
#define __MESSAGE_HPP__
#include "level.hpp"
#include <string>
#include <vector>
#include <thread>
#include <ctime>
namespace ly
{
struct LogMessage
{
using Level = LogLevel::Level;
LogMessage() = default;
// 等级, 行号, 文件名, 信息, 日志器名
LogMessage(Level level, size_t line, const std::string &file, const std::string &message, const std::string &logger_name)
: _level(level), _tid(std::this_thread::get_id()), _line(line), _file(file), _message(message), _logger_name(logger_name)
{
time_t t = time(0);
localtime_r(&t, &_time);
}
LogLevel::Level _level;
std::thread::id _tid;
tm _time;
size_t _line;
std::string _file;
std::string _message;
std::string _logger_name;
};
} // namespace ly
#endif // #define __MESSAGE_HPP__
线程 id 通过 std::this_thread::get_id() 获取,
时间通过 localtime_r 获取,
这两个不需要通过参数传递。
而其他信息,如行号,文件名必须由外部传入。
日志输出模块
日志输出模块作为一个独立的模块,
只需要负责将数据 按指定方式 输出到目标位置即可。
不应该知道项目中有哪些类,
不需要包含项目的其他头文件。
Sink
作为基类,规范了这个模块的输出接口,
也让用户知道如何扩展日志的输出方式。
class Sink
{
public:
using ptr = std::shared_ptr<Sink>;
virtual void log(const char* str, size_t len) = 0;
virtual ~Sink() = default;
};
StdoutSink
标准输出作为最简单的输出方式,
可以用来测试项目是否能跑。
class StdoutSink : public Sink
{
public:
virtual void log(const char* str, size_t len) override
{
fwrite(str, sizeof(char), len, stdout);
}
};
SinkFactory
为了适配不同 sink 的构造,
工厂需要使用可变参数模板,
其第一个参数用于确认 sink 的类型。
class SinkFactory
{
public:
template<typename T, typename ...Args>
static Sink::ptr create(Args &&...args)
{
return std::make_shared<T>(std::forward<Args>(args)...);
}
};
如果单独使用工厂,可能有点麻烦:
auto sink = SinkFactory::create<SizeRollSink>(1024 * 1024 * 1024);
对比直接用 make_shared:
auto sink = std::make_shared<SizeRollSink>(1024 * 1024 * 1024);
但是,在实际使用中 sink 并不需要像这样直接创建,
而是以日志器的建造者创建:
auto logger = ly::LocalBuilder()
.buildName("test")
.buildSink<ly::StdoutSink>()
.build();
如果去掉工厂,就会变成。。。
额
额。。
废物设计出现了!!
突然发现,如果建造者用模板方案,那工厂毫无价值啊:
// 有工厂:
template<typename SinkType, typename ...Args>
Builder &buildSink(Args &&...args) {
auto sink = SinkFactory::create<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(sink);
return static_cast<T &>(*this);
}
// 无工厂:
template<typename SinkType, typename ...Args>
Builder &buildSink(Args &&...args) {
auto sink = std::make_shared<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(sink);
return static_cast<T &>(*this);
}
那么暂时废除 SinkFactory 吧,
我们不能为了使用设计模式而使用设计模式。
SinkFactory扩展
现在这是个有用的工厂了,
毕竟能运行时动态创建的工厂才算真正的工厂:
static Sink::ptr create(const std::string& type, const std::unordered_map<std::string, std::string>& config)
{
if (type == "stdout") return std::make_shared<StdoutSink>();
if (type == "file") return std::make_shared<FileSink>(config.at("filename"));
if (type == "sizeroll") return std::make_shared<SizeRollSink>(config.at("filename"), std::stoi(config.at("limit_size")));
if (type == "timeroll") return std::make_shared<TimeRollSink>(config.at("filename"), std::stoi(config.at("limit_time")));
return nullptr;
}
由于用了哈希,
甚至还可以添加 json 格式的配置文件,
为复杂的 Sink 提供配置信息。
格式化模块
用于测试的格式化选项:
%s - 源文件名
%# - 行号
%v - 实际的日志消息
%% - '%'
FormatItem
格式化子项基类
每个格式化选项对应一个格式化子项派生类,
然后在格式化器中存储格式化子项指针数组,
格式化的过程就是遍历这个子项数组,
然后每个子项从 LogMessage 中提取出对应的信息,
最后组成一个完整的消息。
因此格式化子项基类提供一个纯虚函数 format,
作用是把 message 里的信息拼到 std::stringstream 里去。
class FormatItem
{
public:
using ptr = std::shared_ptr<FormatItem>;
virtual ~FormatItem() = default;
virtual void format(std::stringstream &out, const LogMessage &message) = 0;
};
FileNameFormatItem
%s - 源文件名
class FileNameFormatItem : public FormatItem
{
public:
virtual void format(std::stringstream &out, const LogMessage &message) override { out << message._file; }
};
LineFormatItem
%# - 行号
class LineFormatItem : public FormatItem
{
public:
virtual void format(std::stringstream &out, const LogMessage &message) override { out << message._line; }
};
MessageFormatItem
%v - 实际的日志消息
class MessageFormatItem : public FormatItem
{
public:
virtual void format(std::stringstream &out, const LogMessage &message) override { out << message._message; }
};
OtherFormatItem
解析不出来的信息直接按原样输出
class OtherFormatItem : public FormatItem
{
public:
OtherFormatItem(const std::string &str = "") : other_message(str) {}
virtual void format(std::stringstream &out, const LogMessage &message) override { out << other_message; }
private:
std::string other_message;
};
Formatter
格式化器,
储存:
- 格式化字符串
- 格式化子项指针数组
- 格式化子项创建者
前两个不必多说,而第三个:
static std::unordered_map<char, std::function<FormatItem::ptr(const std::string &)>> _creaters;
嘿嘿嘿,虽然类型很抽象,但特别好用。
只要看看初始化就明白了:
std::unordered_map<char, std::function<FormatItem::ptr(const std::string &)>> Formatter::_creaters{
{'v', [](const std::string &) -> FormatItem::ptr
{ return std::make_shared<MessageFormatItem>(); }},
{'s', [](const std::string &) -> FormatItem::ptr
{ return std::make_shared<FileNameFormatItem>(); }},
{'#', [](const std::string &) -> FormatItem::ptr
{ return std::make_shared<LineFormatItem>(); }},
{'O', [](const std::string &s) -> FormatItem::ptr
{ return std::make_shared<OtherFormatItem>(s); }},
};
之后添加其他格式化子项时,
只要改改这里就行。
下面是格式化器的实现:
// 格式化器
class Formatter
{
public:
using ptr = std::shared_ptr<Formatter>;
public:
Formatter(const std::string &pattern = "[%s:%#] %v") { analysis(pattern); }
// 消息对象 -> 格式化后的消息字符串
std::string format(const LogMessage &message)
{
std::stringstream ss;
for (auto &e : _formatter_items)
e->format(ss, message);
return ss.str();
}
private:
void analysis(const std::string &pattern)
{
std::string other_message;
for(int i = 0, size = pattern.size(); i < size; ++i)
{
char c = pattern[i];
if(c != '%' || i + 1 == size || _creaters.count(pattern[i + 1]) == 0)
other_message.append(1, c);
else
{
c = pattern[++i]; // 将 i 更新至格式化选项处, 将 c 更新为格式化选项
if(other_message.size()) _formatter_items.push_back(_creaters['O'](other_message));
other_message.clear();
_formatter_items.push_back(_creaters[c](""));
}
}
}
private:
std::string _pattern; // 格式化字符串
std::vector<FormatItem::ptr> _formatter_items; // 解析_pattern得到_formatter_items
static std::unordered_map<char, std::function<FormatItem::ptr(const std::string &)>> _creaters;
};
日志器
Logger
由于目前不涉及多线程,
因此相关的成员暂时不添加。
目前我们留三个最主要的成员:
- 日志名 std::string _name;
- 格式化器 Formatter::ptr _formatter;
- 输出方式 std::shared_ptr<std::vector<Sink::ptr>> _sinks;
class Logger
{
public:
using ptr = std::shared_ptr<Logger>;
enum class Type
{
sync,
async
};
public:
Logger(const std::string &name, Formatter::ptr formater, std::shared_ptr<std::vector<Sink::ptr>> sinks)
: _name(name), _formatter(formater), _sinks(sinks) {}
std::shared_ptr<std::vector<Sink::ptr>> sinks() const { return _sinks; }
const std::string &name() const { return _name; }
void debug(const char *file, size_t line, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
log(LogLevel::Level::debug, file, line, fmt, ap);
va_end(ap);
}
virtual ~Logger() = default;
protected:
virtual void log(LogLevel::Level level, const char *file, size_t line, const char *fmt, va_list ap) = 0;
protected:
std::string _name;
Formatter::ptr _formatter;
std::shared_ptr<std::vector<Sink::ptr>> _sinks;
};
核心功能就是 log 函数,
由 debug, info, warning, error, fatel函数调用,
但同步日志器和异步日志器的输出方式不同,
因此这里声明为纯虚函数。
SyncLogger
class SyncLogger : public Logger
{
public:
SyncLogger(const std::string &name, Formatter::ptr formater, std::shared_ptr<std::vector<Sink::ptr>> sink) : Logger(name, formater, sink) {}
protected:
virtual void log(LogLevel::Level level, const char *file, size_t line, const char *fmt, va_list ap) override
{
char* str;
if(-1 == vasprintf(&str, fmt, ap))
{
free(str);
return;
}
LogMessage log_message(level, line, file, str, _name);
std::string message = _formatter->format(log_message);
for (auto &e : *_sinks)
e->log(message.c_str(), message.size());
}
};
这个项目的核心逻辑就是这一段了:
logger 拿到用户的输入
-> 通过 vasprintf 转换为字符串
-> 构建 LogMessage 对象
-> 用格式化器格式化 LogMessage 对象,返回格式化后的字符串
-> 用日志落地器将消息落地
这就是为什么我画的结构图长这样:
日志器建造者
目前只是基础框架搭建阶段,
Logger 的构造就有三个参数了,
之后随着项目的完善还会增多。
因此建造者是必须的。
Builder
为了能够链式调用,
这里使用奇异递归模板模式。
template <typename T>
class Builder
{
public:
Builder &buildName(const std::string &name)
{
_name = name;
return static_cast<T &>(*this);
}
Builder &buildType(Logger::Type type)
{
_type = type;
return static_cast<T &>(*this);
}
template<typename SinkType, typename ...Args>
Builder &buildSink(Args &&...args) {
auto sink = std::make_shared<SinkType>(std::forward<Args>(args)...);
_sinks.push_back(sink);
return static_cast<T &>(*this);
}
Builder &buildFormatPattern(const std::string& format_pattern)
{
_format_pattern = format_pattern;
return static_cast<T &>(*this);
}
virtual Logger::ptr build() = 0;
virtual ~Builder() = default;
protected:
std::string _name;
std::string _format_pattern;
Logger::Type _type = Logger::Type::sync;
std::vector<Sink::ptr> _sinks;
};
LocalBuilder
class LocalBuilder : public Builder<LocalBuilder>
{
public:
virtual Logger::ptr build()
{
assert(_name.size());
Formatter::ptr formater;
if(_format_pattern.size()) formater = std::make_shared<Formatter>(_format_pattern);
else formater = std::make_shared<Formatter>();
std::shared_ptr<std::vector<Sink::ptr>> sinks = std::make_shared<std::vector<Sink::ptr>>(_sinks);
if (Logger::Type::sync == _type)
return std::make_shared<SyncLogger>(_name, formater, sinks);
else
return std::make_shared<AsyncLogger>(_name, formater, sinks);
}
virtual ~LocalBuilder() override {}
};
功能测试
为了让我们不用手动传 __FILE__ 和 __LINE__,
我们还得再加一句:
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
然后就能开始测试了:
#include "builder.hpp"
int main()
{
auto logger = ly::LocalBuilder()
.buildName("test_logger")
.buildFormatPattern("[filename : %s] [line : %#] message : %v")
.buildSink<ly::StdoutSink>()
.build();
logger->debug("%s\n", "基础框架功能测试");
return 0;
}
希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!