目录
1.引言
2.自定义sink的步骤
3.示例代码
4.总结
1.引言
在spdlog中,自定义sink是一个高级功能,它允许用户根据自己的需求来定义日志的输出方式。
sink是spdlog中的一个核心概念,它代表日志数据的输出目的地。一个日志库可以对应多个sink,即日志数据可以被发送到多个输出流。
sink负责接收日志库生成的日志数据,并将其输出到指定的目的地,如文件、控制台、远程服务器等。
在实际的项目开发中,spdlog系统自带的sink不能满足项目要求,就需要自定义sink来满足项目的要求。
2.自定义sink的步骤
首先看一下基类base_sink定义:
namespace spdlog {
namespace sinks {
template<typename Mutex>
class SPDLOG_API base_sink : public sink
{
public:base_sink();explicit base_sink(std::unique_ptr<spdlog::formatter> formatter);~base_sink() override = default;base_sink(const base_sink &) = delete;base_sink(base_sink &&) = delete;base_sink &operator=(const base_sink &) = delete;base_sink &operator=(base_sink &&) = delete;void log(const details::log_msg &msg) final;void flush() final;void set_pattern(const std::string &pattern) final;void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) final;protected:// sink formatterstd::unique_ptr<spdlog::formatter> formatter_;Mutex mutex_;virtual void sink_it_(const details::log_msg &msg) = 0;virtual void flush_() = 0;virtual void set_pattern_(const std::string &pattern);virtual void set_formatter_(std::unique_ptr<spdlog::formatter> sink_formatter);
};
} // namespace sinks
} // namespace spdlog
1.继承base_sink类
- 自定义sink需要继承spdlog中的base_sink类,并实现其中的虚函数。
- 可以参考spdlog中已有的sink实现,如daily_file_sink、rotating_file_sink等。
2.实现关键函数
sink_it_
:这是最重要的一个接口,用于处理日志数据的输出。在该函数中,你需要将日志数据格式化为指定的格式,并输出到目标位置。- 其他可能需要的函数包括:
start
(用于初始化sink)、flush
(用于刷新缓冲区)、stop
(用于停止sink)等。
3.添加自定义逻辑
- 根据需求,在
sink_it_
函数中添加自定义的逻辑,如根据时间或文件大小来滚动日志文件、将日志数据发送到远程服务器等。
4.编译和测试
- 编写完自定义sink后,需要将其编译并链接到spdlog库中。
- 使用自定义sink进行日志记录,并测试其输出是否符合预期。
3.示例代码
以下是一个简单的自定义sink示例,它结合了daily_file_sink和rotating_file_sink的特点,根据时间和文件大小来滚动日志文件:
spdlog 有 daily_file_sink 每日创建一个 和 rotating_file_sink 根据大小翻滚,但是每个 sink 是独立处理的,没法根据时间和文件大小来同时作为生成条件,只好继承 base_sink 自定义。
首先是文件名的拼接,参照 daily_file_sink 源码:
static filename_t calc_filename(const filename_t &filename, const tm &now_tm)
{filename_t basename, ext;std::tie(basename, ext) = details::file_helper::split_by_extension(filename);return fmt::format(SPDLOG_FILENAME_T("{}_{:04d}-{:02d}-{:02d}{}"), basename, now_tm.tm_year + 1900, now_tm.tm_mon + 1, now_tm.tm_mday, ext);
}
使用自定义的函数拆分了文件路径和后缀 ,然后将日期格式化拼接到一起。
然后是文件的创建和日志的写入,这部分可以放到 sink_it_ 虚函数,参照 daily_file_sink 源码:
void sink_it_(const details::log_msg &msg) override
{auto time = msg.time;bool should_rotate = time >= rotation_tp_;if (should_rotate){auto filename = FileNameCalc::calc_filename(base_filename_, now_tm(time));file_helper_.open(filename, truncate_);rotation_tp_ = next_rotation_tp_();}memory_buf_t formatted;base_sink<Mutex>::formatter_->format(msg, formatted);file_helper_.write(formatted);// Do the cleaning only at the end because it might throw on failure.if (should_rotate && max_files_ > 0){delete_old_();}
}
源码中先计算了下一个时间点 rotation_tp_,当满足条件就新建一个文件并重新计算时间点。显然,文件大小限制也可以塞到这个函数里。
自定义 sink 完整代码:
#pragma once#include <spdlog/common.h>
#include <spdlog/details/file_helper.h>
#include <spdlog/details/null_mutex.h>
#include <spdlog/fmt/fmt.h>
#include <spdlog/sinks/base_sink.h>
#include <spdlog/details/os.h>
#include <spdlog/details/circular_q.h>
#include <spdlog/details/synchronous_factory.h>#include <chrono>
#include <cstdio>
#include <ctime>
#include <mutex>
#include <string>
#include <tuple>namespace spdlog {
namespace sinks {//根据大小和时间生成日志文件
//龚建波 2021-1-18
//参照rotating_file_sink和daily_file_sink自定义
//(生成的时间也可以修改为任意间隔)
//(还有个需求没完成,就是清理旧文件,按照个数或者时间,即max_files参数目前相关逻辑是没用的)
template<typename Mutex>
class easy_file_sink final : public base_sink<Mutex>
{
public:easy_file_sink(filename_t base_filename, size_t max_size, size_t max_files = 0): base_filename_(std::move(base_filename)), max_size_(max_size), max_files_(max_files), filenames_q_(){auto now = log_clock::now();auto filename = daily_filename_(base_filename_, now_tm(now));file_helper_.open(filename, false);//记录大小current_size_ = file_helper_.size();//记录新建时间,24hrotation_tp_ = next_rotation_tp_();if (max_files_ > 0){init_filenames_q_();}}filename_t filename(){std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);return file_helper_.filename();}protected:void sink_it_(const details::log_msg &msg) override{memory_buf_t formatted;base_sink<Mutex>::formatter_->format(msg, formatted);current_size_ += formatted.size();auto time = msg.time;//超过大小原文件改后缀,超过日期新文件改日期名bool should_rotate = false;if(time >= rotation_tp_){should_rotate = true;file_helper_.close();auto filename = daily_filename_(base_filename_, now_tm(time));file_helper_.open(filename, false);current_size_ = file_helper_.size();rotation_tp_ = next_rotation_tp_();}else if(current_size_ >= max_size_){should_rotate = true;file_helper_.close();auto src_name = daily_filename_(base_filename_, now_tm(time));auto target_name = calc_filename_(base_filename_, now_tm(time));//如果ranme失败就用targetname作为当前文件名if (!rename_file_(src_name, target_name)){details::os::sleep_for_millis(200);if (!rename_file_(src_name, target_name)){src_name = target_name;}}file_helper_.open(src_name, false);current_size_ = file_helper_.size();rotation_tp_ = next_rotation_tp_();}file_helper_.write(formatted);// Do the cleaning only at the end because it might throw on failure.if (should_rotate && max_files_ > 0){delete_old_();}}void flush_() override{file_helper_.flush();}private:void init_filenames_q_(){using details::os::path_exists;filenames_q_ = details::circular_q<filename_t>(static_cast<size_t>(max_files_));std::vector<filename_t> filenames;auto now = log_clock::now();while (filenames.size() < max_files_){auto filename = calc_filename_(base_filename_, now_tm(now));if (!path_exists(filename)){break;}filenames.emplace_back(filename);now -= std::chrono::hours(24);}for (auto iter = filenames.rbegin(); iter != filenames.rend(); ++iter){filenames_q_.push_back(std::move(*iter));}}tm now_tm(log_clock::time_point tp){time_t tnow = log_clock::to_time_t(tp);return spdlog::details::os::localtime(tnow);}log_clock::time_point next_rotation_tp_(){auto now = log_clock::now();tm date = now_tm(now);date.tm_hour = 0;date.tm_min = 0;date.tm_sec = 0;auto rotation_time = log_clock::from_time_t(std::mktime(&date));if (rotation_time > now){return rotation_time;}return {rotation_time + std::chrono::hours(24)};}// Delete the file N rotations ago.// Throw spdlog_ex on failure to delete the old file.void delete_old_(){using details::os::filename_to_str;using details::os::remove_if_exists;filename_t current_file = file_helper_.filename();if (filenames_q_.full()){auto old_filename = std::move(filenames_q_.front());filenames_q_.pop_front();bool ok = remove_if_exists(old_filename) == 0;if (!ok){filenames_q_.push_back(std::move(current_file));throw_spdlog_ex("Failed removing daily file " + filename_to_str(old_filename), errno);}}filenames_q_.push_back(std::move(current_file));}//每日名称static filename_t daily_filename_(const filename_t &filename, const tm &now_tm){filename_t basename, ext;//split_by_extension拆分文件后缀std::tie(basename, ext) = details::file_helper::split_by_extension(filename);return fmt::format(SPDLOG_FILENAME_T("{}_{:04d}_{:02d}_{:02d}{}"),basename,now_tm.tm_year + 1900,now_tm.tm_mon + 1,now_tm.tm_mday,ext);}//超过大小后重命名,后面加上时分秒static filename_t calc_filename_(const filename_t &filename, const tm &now_tm){filename_t basename, ext;std::tie(basename, ext) = details::file_helper::split_by_extension(filename);return fmt::format(SPDLOG_FILENAME_T("{}_{:04d}_{:02d}_{:02d}_{:02d}{:02d}{:02d}{}"),basename,now_tm.tm_year + 1900,now_tm.tm_mon + 1,now_tm.tm_mday,now_tm.tm_hour,now_tm.tm_min,now_tm.tm_sec,ext);}static bool rename_file_(const filename_t &src_filename, const filename_t &target_filename){(void)details::os::remove(target_filename);return details::os::rename(src_filename, target_filename) == 0;}filename_t base_filename_;log_clock::time_point rotation_tp_;details::file_helper file_helper_;std::size_t max_size_;std::size_t max_files_;std::size_t current_size_;details::circular_q<filename_t> filenames_q_;
};using easy_file_sink_mt = easy_file_sink<std::mutex>;
using easy_file_sink_st = easy_file_sink<details::null_mutex>;
}template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> easy_logger_mt(const std::string &logger_name, const filename_t &filename, size_t max_size, size_t max_files = 0)
{return Factory::template create<sinks::easy_file_sink_mt>(logger_name, filename, max_size, max_files);
}template<typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> easy_logger_st(const std::string &logger_name, const filename_t &filename, size_t max_size, size_t max_files = 0)
{return Factory::template create<sinks::easy_file_sink_st>(logger_name, filename, max_size, max_files);
}
}
注意:上述代码仅为示例,可能需要根据实际需求进行调整和完善。特别是滚动文件的逻辑部分,需要根据具体的存储要求和文件命名规则来实现。
4.总结
自定义spdlog sink是一个灵活且强大的功能,它允许用户根据自己的需求来定义日志的输出方式。通过继承base_sink类并实现其中的虚函数,用户可以轻松地添加自定义的逻辑来满足特定的日志记录需求。