同步异步日志系统
1. 项目介绍
本项目是插件式的同步异步日志系统,不仅能让用户简便的输出日志,也支持多种功能。具体如下:
- 支持多级别日志消息
- 支持同步日志和异步日志
- 支持日志多落地方向:到控制台、文件以及滚动文件中(落地方向支持拓展)
- 支持多线程并发写入日志
开发环境:centos7、vim、g++/gdb、Makefile,不依赖其他库。
异步日志
同步日志指业务线程也负责日志输出,但会影响业务性能。
异步日志的具体做法是业务线程将日志放入缓冲区内,日志线程从中提取日志进行输出,故不会耽误业务线程的运行。
技术重点
- 类层次设计
- 多线程、智能指针、右值引用
- 双缓冲区
- 生产消费模型
- 多设计模式:单例、工厂、建造者
2. 框架设计
日志系统的作用就是将一条消息格式化成指定格式的字符串并写入到指定位置。所以共设计如下几个模块:
- 日志等级限制模块:支持限制日志等级式的输出。
- 日志消息模块:封装一条日志所需的各种要素,如:时间、线程ID、文件名、行号、日志等级、消息主体等
- 日志格式化模块:将日志的各种要素以及消息主体格式化成一个字符串
- 日志落地模块:支持多种落地位置,并支持拓展。
- 日志器模块:对上面多个模块进行对象组合。分为同步日志器模块和异步日志器模块。
- 异步输出线程模块:负责异步日志的实际落地输出。
- 日志器管理模块:以日志器器为单位,支持多日志器输出。对日志器进行全局的管理,以便于能在项目中任何位置获取指定日志器进行输出。

3. 代码设计
3.1 日志等级类
- 定义所有日志等级
- 定义接口,将对应等级的枚举变量转化为字符串
日志共分7个等级,项目会定义一个默认的输出等级,只有当日志的等级大于等于默认的等级时才进行输出。
名称 | 含义 |
---|---|
ON | 开启所有日志 |
DEBUG | 调试级别日志 |
INFO | 用户级提示信息 |
WARN | 警告信息 |
ERROR | 程序错误信息 |
FATAL | 程序致命错误信息 |
OFF | 关闭所有日志 |
struct log_level {
enum value {
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
OFF,
};
const char* to_string(value level) {
switch (level) {
case value::DEBUG: return "DEBUG";
//...
}
}
};
3.2 日志消息类
日志消息类负责,在中间时刻,存储一条日志消息所需要的各项要素:时间、日志等级、源文件名、源代码行号、线程ID、信息主体和日志器名称。
struct log_msg
{
size_t _generate_time; // 日志的产生时间
log_level::value _level; // 日志等级
std::thread::id _tid; // 线程ID
size_t _line; // 行号
std::string _file; // 文件名
std::string _logger; // 日志器名称
std::string _payload; // 消息主体
log_msg(log_level::value level, size_t line,
const std::string&& file, const std::string&& logger, const std::string&& msg);
};
3.3 日志格式化类
功能
该类负责对日志消息进行格式化组织,组织成指定格式的字符串,然后将其返回。
格式控制字符 | 含义 |
---|---|
%d{%H:%M:%S} | 表示日期时间,大括号中内容表示日期时间的格式 |
%T | 表示制表符缩进 |
%t | 表示线程ID |
%p | 表示日志等级 |
%c | 表示日志器名称,不同开发组可以使用自己的日志器进行输出,互不影响 |
%f | 表示源码文件名 |
%l | 表示日志输出时的源码行号 |
%m | 表示日志消息主体 |
%n | 表示换行 |
传入包含控制字符的格式控制串,按其要求输出格式化日志。
[%d{%H:%M:%S}][%p][%t][%c][%f:%l]%T%m%n
[12:09:33][INFO][12649422][root][main.cc:38] socket created\n
设计
抽象出一个格式化子项的基类,再派生出不同格式控制项的子类,如主体消息子项、时间子项、文件名子项等等,以及其他元素子项。
// 抽象格式化子项基类
struct format_item {
using ptr = std::shared_ptr<format_item>;
virtual void format(std::ostream& out, log::log_msg& msg) = 0;
};
// 格式化子项子类 -- 消息、等级、时间、文件名、行号、线程ID、日志器名、制表符、换行、其他
struct payload_format_item : public format_item {
void format(std::ostream& out, log::log_msg& msg) override;
};
struct level_format_item : public format_item {
void format(std::ostream& out, log::log_msg& msg) override;
};
struct time_format_item : public format_item {
time_format_item(const std::string& fmt = "%H:%M:%S");
void format(std::ostream& out, log::log_msg& msg) override;
std::string _time_fmt_str; // %H:%M:%S
};
struct file_format_item : public format_item {
void format(std::ostream& out, log::log_msg& msg) override;
};
struct line_format_item : public format_item {
void format(std::ostream& out, log::log_msg& msg) override;
};
struct thread_format_item : public format_item {
void format(std::ostream& out, log::log_msg& msg) override;
};
struct logger_format_item : public format_item {
void format(std::ostream& out, log::log_msg& msg) override;
};
struct tab_format_item : public format_item {
void format(std::ostream& out, log::log_msg& msg) override;
};
struct nline_format_item : public format_item {
void format(std::ostream& out, log::log_msg& msg) override;
};
struct other_format_item : public format_item {
other_format_item(const std::string& s);
void format(std::ostream& out, log::log_msg& msg) override;
std::string _s;
};
格式化器类中有两个成员,一是格式化规则字符串,控制格式化的要求。二是格式化子项父类的指针数组,保存各个格式化子项的子类对象。
在格式化输出的时候,依次获取数组元素即子类对象。统一调用format方法将内容导入流中,再转换成字符串输出。
class formatter
{
public:
formatter(const std::string& pattern = "[%d{%H:%M:%S}][%p][%t][%c][%f:%l]%T%m%n");
// 格式化msg
void format(std::ostream& out, log_msg& msg);
std::string format(log_msg& msg);
// 解析格式化字符串 - 不是%就向后遍历,直到遇到%,前面是一个原始字符串子项,后面是一个控制字符
bool parsePattern();
private:
// 根据格式控制字符创建格式化子项
void add_item(const std::string& key, const std::string& val);
private:
std::string _pattern; // 格式化规则字符串
std::vector<format_item::ptr> _items;
};
3.4 日志落地类
功能
日志落地类是负责将已格式化好的日志输出到指定位置,并支持扩展将日志落地到不同位置。
一般位置有标准输出、指定文件、滚动文件(文件按照时间或大小进行滚动切换)和自定义落地方向。
设计
抽象出落地模块的基类,不同的落地方向从基类派生。使用简单工厂模式创建不同的落地器。
class log_sink {
public:
using ptr = std::shared_ptr<log_sink>;
log_sink() {}
virtual ~log_sink() {}
virtual void log(std::string& data, size_t len) = 0;
};
// 落地方向:标准输出
class stdout_sink : public log_sink {
public:
void log(std::string& data) override;
};
// 落地方向:指定文件
class file_sink : public log_sink {
public:
file_sink(const std::string& path);
void log(std::string& data) override;
private:
std::string _file;
std::ofstream _ofs;
};
// 落地方向:滚动文件(按大小滚动)
class rolling_sink : public log_sink
{
public:
rolling_sink(const std::string& base_name, size_t max_size);
void log(std::string& data) override;
private:
void rolling();
private:
std::string _base_name; // 基础文件名 ./logs/<base>-
std::ofstream _ofs;
size_t _max_size;
size_t _cur_size;
};
// 日志落地器工厂
class sink_factory
{
public:
template<typename Sinker, typename... Args>
static log_sink::ptr create(Args&&... args);
};
可拓展
拓展实际上就是编写log_sink
的子类,重写void log(std::string& data)
函数。
拓展一个以时间为滚动条件的日志落地模块:
/* 拓展一个以时间为滚动条件的日志落地模块 */
enum time_gap {
SEC_GAP = 1,
MIN_GAP = 60,
HOU_GAP = 60 * 60,
DAY_GAP = 24 * 60 * 60,
};
class roll_by_time : public log::log_sink {
public:
roll_by_time(const std::string& base_name, time_gap gap);
void log(std::string& data) override;
private:
void rolling();
private:
std::string _base_name;
std::ofstream _ofs;
size_t _create_time;
time_gap _gap; // 时间间隔
};
测试
log_sink::ptr sinker1 = sink_factory::create<log::stdout_sink>();
log_sink::ptr sinker2 = sink_factory::create<log::file_sink>
("./log/test_file_sink.log");
log_sink::ptr sinker3 = sink_factory::create<log::rolling_sink>
("./log/test_rolling_sink", 1024 * 1024);
while (true)
{
log_msg msg = {log_level::DEBUG, __LINE__, __FILE__, "root", "Can i make it? "};
log::formatter fmtr;
std::string log = fmtr.format(msg);
sinker1->log(log);
sinker3->log(log);
sinker2->log(log);
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
3.5 日志器模块
设计
对日志等级模块、消息模块、格式化模块和落地模块进行整合,向外提供所有等级的日志输出接口,只有高于等于该等级的日志才能输出。
日志器类的成员有:格式化器、落地器数组(支持多落地输出)、日志等级限制器、日志器名称以及线程互斥锁。
基类和同步日志器
先设计出日志器基类,再派生出同步和异步日志器。
本质上,同步日志器就是直接落地,异步日志器就是落地到内存,二者只有落地方向的不同,故我们将落地方式抽象出来,二者调用各自的落地方法。
class logger
{
public:
using ptr = std::shared_ptr<logger>;
public:
// 多落地
logger(const string& name, log_level level, formatter::ptr& fmtter,
vector<log_sink::ptr>& sinkers);
void debug(const string& file, size_t line, const string& fmt, ...);
void info(const string& file, size_t line, const string& fmt, ...);
void warn(const string& file, size_t line, const string& fmt, ...);
void error(const string& file, size_t line, const string& fmt, ...);
void fatal(const string& file, size_t line, const string& fmt, ...);
protected:
virtual void log(const string& data) = 0;
std::string get_payload(const std::string& fmt, va_list& ap);
protected:
std::string _logger_name;
std::atomic<log_level::value> _limit_level;
formatter::ptr _formatter;
std::vector<log_sink::ptr> _sinkers;
std::mutex _mutex;
};
class sync_logger : public logger
{
public:
sync_logger(const std::string& name, log_level::value level, formatter::ptr& fmtter,
std::vector<log_sink::ptr>& sinkers);
protected:
virtual void log(const std::string& data);
};
日志器建造者
log::formatter::ptr fmtter(new log::formatter);
log::log_sink::ptr sinker1 = log::sink_factory::create<log::stdout_sink>();
log::log_sink::ptr sinker2 = log::sink_factory::create<log::file_sink>
("./log/test_file_sink.log");
log::log_sink::ptr sinker3 = log::sink_factory::create<log::rolling_sink>
("./log/test_rolling_sink", log::memory_size::m1M);
std::vector<log::log_sink::ptr> sinkers = {sinker1, sinker2, sinker3};
log::sync_logger slogger("sync_logger", log::log_level::DEBUG, fmtter, sinkers);
slogger.debug(__FILE__, __LINE__, "%d-%s", 33, "can i make it ?");
slogger.info(__FILE__, __LINE__, "%d-%s", 33, "can i make it ?");
slogger.warn(__FILE__, __LINE__, "%d-%s", 33, "can i make it ?");
slogger.error(__FILE__, __LINE__, "%d-%s", 33, "can i make it ?");
slogger.fatal(__FILE__, __LINE__, "%d-%s", 33, "can i make it ?");
创建一个日志器模块需要提前创建格式化模块和落地模块数组,太过麻烦。所以使用建造者模式来建造日志器,简化使用复杂度。
- 首先我们抽象一个日志器建造者类
- 然后我们派生出具体的建造者类
- 再使用指挥者类构建所需要的所有组件
enum logger_type
{
SYNC_LOGGER = 0,
ASYNC_LOGGER,
};
class logger_builder
{
public:
using ptr = std::shared_ptr<logger_builder>;
public:
logger_builder();
void build_logger_type(const logger_type type);
void build_logger_name(const std::string name);
void build_logger_level(log_level::value level);
void build_formatter(const std::string& pattern);
template<typename SinkType, typename... Args>
void build_sinker(Args&&... args);
virtual logger::ptr build() = 0;
protected:
logger_type _logger_type;
std::string _logger_name;
log_level::value _base_level;
formatter::ptr _formatter;
std::vector<log_sink::ptr> _sinkers;
};
class local_logger_builder : public logger_builder {
public:
logger::ptr build() override;
};
class logger_director {
public:
using ptr = std::shared_ptr<logger_director>;
public:
logger_director(logger_builder::ptr builder);
void construct(logger_type type,
const std::string& name,
log_level::value level,
const std::string& pattern =
"[%d{%F %H:%M:%S}][%p][%t][%c][%f:%l]%T%m%n");
template<typename SinkType, typename... Args>
void construct_sinker(Args&&... args);
private:
logger_builder::ptr _builder;
};
测试
log::logger_builder::ptr lbuiler(new log::local_logger_builder);
log::logger_director::ptr ldirector(new log::logger_director(lbuiler));
ldirector->construct(log::logger_type::SYNC_LOGGER, "root_logger", DEBUG);
ldirector->construct_sinker<log::stdout_sink>();
ldirector->construct_sinker<log::file_sink>("./log/test_file_sink.log");
ldirector->construct_sinker<log::rolling_sink>("./log/test_rolling_sink", m1M);
log::logger::ptr slogger = lbuiler->build();
slogger->debug(__FILE__, __LINE__, "%d-%s", 33, "can i make it ?");
slogger->info(__FILE__, __LINE__, "%d-%s", 33, "can i make it ?");
slogger->warn(__FILE__, __LINE__, "%d-%s", 33, "can i make it ?");
slogger->error(__FILE__, __LINE__, "%d-%s", 33, "can i make it ?");
slogger->fatal(__FILE__, __LINE__, "%d-%s", 33, "can i make it ?");
3.6 异步日志模块
设计
异步日志器,目的是防止日志写入发生阻塞而耽误业务线程的运行。所以业务线程只需将日志内容放入缓冲区中,有异步线程实现落地。

为避免锁降低效率,我们使用双缓冲区策略。
- 当消费缓冲区为空时,就可以进行交换。这样可以避免生产者和消费者之间的锁冲突。
- 在生产端可以使用信号量来尽量的降低生产者之间的锁冲突。

缓冲区类
- 管理字符串数据的缓冲区,本质就是数组
- 当前写入位置的指针
- 当前读取位置的指针
const size_t DEFAULT_BUFFER_SIZE = util::memory_size::m1M;
const size_t DEFAULT_THRESHOLD = util::memory_size::m8M;
const size_t DEFAULT_INCREASE = util::memory_size::m1M;
class buffer
{
public:
buffer(size_t size = DEFAULT_BUFFER_SIZE);
void push(const char* data, size_t len);
void push(const std::string& s);
const char* pop(size_t len);
size_t readable_size();
size_t writable_size();
void move_writer(size_t len);
void move_reader(size_t len);
void reset();
void swap(buffer& b);
bool empty();
private:
void ensure_size(size_t len);
private:
std::vector<char> _buffer;
size_t _reader_idx;
size_t _writer_idx;
};
异步工作器类
异步工作使用双缓冲区设计,外界将数据放入生产缓冲区,异步线程对处理缓冲区的数据进行处理。若处理缓冲区为空,则两者进行交换。
异步工作器类的成员:
- 双缓冲区,生产和消费
- 互斥锁,保证线程安全
- 条件变量,确定交换缓冲区的时机
- 回调函数,提示异步工作器具体如何处理缓冲区的数据
异步工作器类的接口:
- 停止异步工作器
- 添加数据到缓冲区
- 创建线程
class async_looper
{
public:
using ptr = std::shared_ptr<async_looper>;
using handler = std::function<void(buffer&)>;
public:
async_looper(handler& hd);
~async_looper() { stop(); }
void stop();
void push(const char* data, size_t len);
private:
void routine();
handler _callback; // 缓冲区的处理函数
private:
std::atomic<bool> _stop;
buffer _pdr_buf; // producer buffer
buffer _csr_buf; // consumer buffer
std::mutex _mtx;
std::condition_variable _pdr_cv;
std::condition_variable _csr_cv;
std::thread _th;
};
异步日志器类
异步日志器继承自logger日志器类。log函数中,将格式化数据传给异步工作器,异步工作器再将消息放入缓冲区中。
class async_logger : public logger
{
public:
async_logger(const std::string& name,
log_level::value level,
formatter::ptr& fmtter,
std::vector<log_sink::ptr>& sinkers,
looper_type looper_type = looper_type::SAFE_MODE);
protected:
virtual void log(const std::string& data);
public:
void looper_handler(buffer& bf);
private:
async_looper::ptr _looper; // 异步工作器
};
3.7 日志器管理器
设计
日志器管理类,是一个单例类管理所有创建的日志器,以达到在项目任意位置都可以获取单例对象的目的。
单例管理器对象创建时,默认先创建一个日志器类,仅让日志落地标准输出,便于用户的使用。
类的成员:
- 默认日志器
- 管理的日志器的数组
- 互斥锁
提供的接口:
- 添加并管理一个日志器
- 判断是否存在指定名称的日志器
- 获取指定名称的日志器
- 获取默认日志器
class logger_manager
{
public:
static logger_manager& get_instance();
void add_logger(const logger::ptr& logger);
bool has_logger(const std::string& name);
logger::ptr get_logger(const std::string& name);
logger::ptr default_logger();
private:
logger_manager();
private:
std::mutex _mtx;
logger::ptr _default_logger;
std::unordered_map<std::string, logger::ptr> _loggers;
};
3.8 全局接口设计
logly::logger::ptr lg = logly::logger_manager::get_instance().default_logger();
lg->debug(__FILE__, __LINE__, "%s-%d", "test logger manager succ", 666);
上述的最简单的默认日志器的使用方式还是复杂,所以我们提供全局函数,提升日志系统的便捷性。
- 提供获取指定日志器的全局接口(避免操作单例对象)
- 使用宏函数对日志接口进行代理(使用代理模式)
- 提供宏函数,直接进行日志的标准输出打印(不需要考虑日志器)
#define debug(fmt, ...) debug(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define info(fmt, ...) info(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define warn(fmt, ...) warn(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define error(fmt, ...) error(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define fatal(fmt, ...) fatal(__FILE__, __LINE__, fmt, ##__VA_ARGS__)
#define DEBUG(fmt, ...) logly::default_logger()->debug(fmt, ##__VA_ARGS__)
#define INFO(fmt, ...) logly::default_logger()-> info(fmt, ##__VA_ARGS__)
#define WARN(fmt, ...) logly::default_logger()-> warn(fmt, ##__VA_ARGS__)
#define ERROR(fmt, ...) logly::default_logger()->error(fmt, ##__VA_ARGS__)
#define FATAL(fmt, ...) logly::default_logger()->fatal(fmt, ##__VA_ARGS__)
DEBUG("%s-%d", "test global interface succ", 666);
INFO ("%s-%d", "test global interface succ", 666);
WARN ("%s-%d", "test global interface succ", 666);
ERROR("%s-%d", "test global interface succ", 666);
FATAL("%s-%d", "test global interface succ", 666);
logger::ptr logger = logger_manager::get_instance().get_logger("async_logger");
logger->debug("%s-%d", "test global interface succ", 666);
logger->info ("%s-%d", "test global interface succ", 666);
logger->warn ("%s-%d", "test global interface succ", 666);
logger->error("%s-%d", "test global interface succ", 666);
logger->fatal("%s-%d", "test global interface succ", 666);
- 使用
DEBUG
等“大写”的宏函数,就是直接调用默认的日志器进行日志的标准输出。 logger->debug
等是调用“小写”的宏函数,将参数列表替换成带有文件名和行号的。因为宏是全局的,所以优先于类成员函数。
4. 性能测试
4.1 测试设计
- 评判标准:平均每秒能输出多少条(多大空间)日志到文件。
- 控制变量:同步异步日志器、线程数量
- 测试方法:计算指定条数指定长度的日志的输出耗时,得出每秒日志的输出量。
4.2 代码设计
测试工具支持:
- 支持控制写日志的线程数量
- 支持控制写日志的总数量
- 分别对同步异步日志器进行各自的性能测试
实现方式:
封装一个测试接口,参数指定日志器,线程数量、日志数量、单条日志大小。(日志大小指有效载荷大小)
输出之始开始计时,输出完毕计时结束,二者之差就是所耗时间。
每秒输出量 = 日志数量 / 总耗时
每秒输出大小 = 日志数量 * 单条日志大小 / 总耗时
注意:异步日志器启动非安全模式。
const size_t THREAD_NUM = 3;
const size_t MSG_NUM = 100 * 10000;
const size_t MSG_LEN = 50;
void bench(const std::string& logger_name, size_t thread_num, size_t msg_num, size_t msg_len)
{
logly::logger::ptr logger = logly::get_logger(logger_name);
if (logger.get() == nullptr)
return;
printf("测试开始,日志共 %ld 条,单条大小 %ld Bytes,总大小 %ld KB\n\n",
msg_num, msg_len, msg_num * msg_len / 1024);
std::string msg(msg_len - 1, 'a');
size_t msg_per_th = msg_num / thread_num; // 单个线程的输出日志条数
std::vector<std::thread> threads;
std::vector<double> cost_array(thread_num);
for (int i = 0; i < thread_num; i++)
{
threads.emplace_back([&, i]()
{
auto start = std::chrono::high_resolution_clock::now();
for (int j = 0; j < msg_per_th; j++) logger->fatal("%s", msg.c_str());
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> cost = end - start;
cost_array[i] = cost.count();
printf("线程%d:\t输出日志 %ld 条,耗时 %lf s\n", i, msg_per_th, cost_array[i]);
}
);
}
for (auto& th : threads)
th.join();
double final_cost = cost_array[0];
for (auto e : cost_array)
if (final_cost < e) final_cost = e;
size_t count_per_sec = msg_num / final_cost; // 每秒输出日志数量
size_t size_per_sec = msg_num * msg_len / final_cost / 1024; // 每秒输出日志大小 KB
printf("\n\t总耗时:%lf s\n", final_cost);
printf("\t每秒输出日志数量 %ld 条\n", count_per_sec);
printf("\t每秒输出日志大小 %ld KB\n", size_per_sec);
}
void sync_bench()
{
logly::logger_builder::ptr lbuiler(new logly::local_logger_builder);
logly::logger_director::ptr ldirector(new logly::logger_director(lbuiler));
ldirector->construct(logly::logger_type::SYNC_LOGGER, "sync_logger", logly::log_level::DEBUG, "%m%n");
ldirector->construct_sinker<logly::file_sink>("./log/sync.log");
logly::logger_manager::get_instance().add_logger(lbuiler->build());
bench("sync_logger", 1, MSG_NUM, MSG_LEN);
}
void async_bench()
{
logly::logger_builder::ptr lbuiler(new logly::local_logger_builder);
logly::logger_director::ptr ldirector(new logly::logger_director(lbuiler));
ldirector->construct(logger_type::ASYNC_LOGGER,"async_logger",log_level::DEBUG, "%m%n");
ldirector->construct_sinker<logly::file_sink>("./log/async.log");
ldirector->enable_unsafe_mode();
logly::logger_manager::get_instance().add_logger(lbuiler->build());
bench("async_logger", THREAD_NUM, MSG_NUM, MSG_LEN);
}
4.3 测试结果
测试环境:2核2G ubuntu系统云服务器
-------------- 同步日志器测试 --------------
测试开始,日志共 1000000条,单条大小 50 Bytes,总大小 48828 KB
线程0: 输出日志 333333 条,耗时 0.698113 s
线程2: 输出日志 333333 条,耗时 0.700333 s
线程1: 输出日志 333333 条,耗时 0.701003 s
总耗时:0.701003 s
每秒输出日志数量 1426527 条
每秒输出日志大小 69654 KB
-------------- 异步日志器测试 --------------
测试开始,日志共 1000000条,单条大小 50 Bytes,总大小 48828 KB
线程0: 输出日志 333333 条,耗时 0.664660 s
线程1: 输出日志 333333 条,耗时 0.665603 s
线程2: 输出日志 333333 条,耗时 0.665621 s
总耗时:0.665621 s
每秒输出日志数量 1502355 条
每秒输出日志大小 73357 KB