spdlog源码阅读(二):Logger类的实现

日志对象创建

全局默认logger

spdlog库通过logger类提供日志接口,而logger的创建有多种方式。第一种方式是全局默认logger,例如,在官方example日志使用样例中提供的默认的日志调用:

1
2
3
4
5
6
7
8
spdlog::info("Welcome to spdlog version {}.{}.{}  !", SPDLOG_VER_MAJOR, SPDLOG_VER_MINOR,
SPDLOG_VER_PATCH);

spdlog::warn("Easy padding in numbers like {:08d}", 12);
spdlog::critical("Support for int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}", 42);
spdlog::info("Support for floats {:03.2f}", 1.23456);
spdlog::info("Positional args are {1} {0}..", "too", "supported");
spdlog::info("{:>8} aligned, {:<8} aligned", "right", "left");

该方法调用的是默认创建的logger,由registry类创建和管理。registry是一个被设计成单例的类,在构造时自动创建一个输出到终端的logger,并设置成默认logger,以下是相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SPDLOG_INLINE registry::registry()
: formatter_(new pattern_formatter()) {
#ifndef SPDLOG_DISABLE_DEFAULT_LOGGER
// create default logger (ansicolor_stdout_sink_mt or wincolor_stdout_sink_mt in windows).
#ifdef _WIN32
auto color_sink = std::make_shared<sinks::wincolor_stdout_sink_mt>();
#else
auto color_sink = std::make_shared<sinks::ansicolor_stdout_sink_mt>();
#endif

const char *default_logger_name = "";
default_logger_ = std::make_shared<spdlog::logger>(default_logger_name, std::move(color_sink));
loggers_[default_logger_name] = default_logger_;

#endif // SPDLOG_DISABLE_DEFAULT_LOGGER
}

在使用全局函数接口时,其实就是先获取单例registry,获取单例registry的默认logger,再调用默认logger的相关方法,通过模板参数和完美转发将日志进行传递。

1
2
3
4
5
6
7
8
SPDLOG_INLINE spdlog::logger *default_logger_raw() {
return details::registry::instance().get_default_raw();
}

template <typename... Args>
inline void info(format_string_t<Args...> fmt, Args &&...args) {
default_logger_raw()->info(fmt, std::forward<Args>(args)...);
}

工厂方法logger

有时为了灵活性考虑,需要自己管理logger对象,此时可以使用工厂方法创建logger。在每个Sink实现的头文件中,都定义了与Sink有关的工厂方法。例如basic_file_sink就定义了相关的工厂方法。

1
2
3
4
5
6
7
8
9
10
11
//
// factory functions
//
template <typename Factory = spdlog::synchronous_factory>
inline std::shared_ptr<logger> basic_logger_mt(const std::string &logger_name,
const filename_t &filename,
bool truncate = false,
const file_event_handlers &event_handlers = {}) {
return Factory::template create<sinks::basic_file_sink_mt>(logger_name, filename, truncate,
event_handlers);
}

通过工厂方法可以便捷的创建logger对象,通过传入不同的工厂模板可以实现同步或异步logger对象的创建。该方法是一个嵌套模板, 第一层模板参数传入同步/异步工厂方法模板,第二层模板参数传入Sink。

使用方法如下:

1
2
3
4
5
// 同步logger
auto sync_file = spdlog::basic_logger_mt("file_logger", "logs/basic-log.txt", true);
// 异步logger
auto async_file =
spdlog::basic_logger_mt<spdlog::async_factory>("async_file_logger", "logs/async_log.txt");

工厂模板的create方法里调用了registry::initialize_logger。因此,通过工厂方法创建的每个同步logger都会被registry管理。

1
2
3
4
5
6
7
template <typename Sink, typename... SinkArgs>
static std::shared_ptr<spdlog::logger> create(std::string logger_name, SinkArgs &&...args) {
auto sink = std::make_shared<Sink>(std::forward<SinkArgs>(args)...);
auto new_logger = std::make_shared<spdlog::logger>(std::move(logger_name), std::move(sink));
details::registry::instance().initialize_logger(new_logger);
return new_logger;
}

被registry管理的logger可以通过日志名获取logger对象,日志名为logger对象的唯一标识符。

1
auto logger = spdlog::get("console");

自定义logger

也可以不使用工厂方法,自己手动定义一个logger对象。例如有时需要将logger指定多个输出Sink,可以通过以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void multi_sink_example() {
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::warn);
console_sink->set_pattern("[multi_sink_example] [%^%l%$] %v");

auto file_sink =
std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/multisink.txt", true);
file_sink->set_level(spdlog::level::trace);

spdlog::logger logger("multi_sink", {console_sink, file_sink});
logger.set_level(spdlog::level::debug);
logger.warn("this should appear in both console and file");
logger.info("this message should not appear in the console, only in the file");
}

这种方法创建的logger对象不会被registry管理,如果需要加入管理可以手动执行

1
2
3
4
5
6
7
8
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%^%l%$] %v");

auto my_logger = std::make_shared<spdlog::logger>("my_logger", console_sink);
my_logger->set_level(spdlog::level::debug);

// 将 logger 对象注册到 spdlog 的管理系统中
spdlog::register_logger(my_logger);

日志接口

logger类通过重载实现了丰富的接口,多数的接口都指向一个公共实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// common implementation for after templated public api has been resolved
template <typename... Args>
void log_(source_loc loc, level::level_enum lvl, string_view_t fmt, Args &&...args) {
bool log_enabled = should_log(lvl);
bool traceback_enabled = tracer_.enabled();
if (!log_enabled && !traceback_enabled) {
return;
}
SPDLOG_TRY {
memory_buf_t buf;
#ifdef SPDLOG_USE_STD_FORMAT
fmt_lib::vformat_to(std::back_inserter(buf), fmt, fmt_lib::make_format_args(args...));
#else
fmt::vformat_to(fmt::appender(buf), fmt, fmt::make_format_args(args...));
#endif

details::log_msg log_msg(loc, name_, lvl, string_view_t(buf.data(), buf.size()));
log_it_(log_msg, log_enabled, traceback_enabled);
}
SPDLOG_LOGGER_CATCH(loc)
}

传入的参数的含义分别为:

  • source_loc,用来记录文件名、函数名和行号的结构体,用来保存__FILE__、__func__和__line__等宏信息;
  • level::level_enum 日记的等级
  • string_view_t和args,日志内容及其参数,可以选择使用fmt的实现,若使用C++17及以上,可以选择std中的实现。

将传入的参数保存至log_msg结构体中,传递给log_it_方法。日志等级通过should_log函数进行过滤。除了日记记录level,还有flush_level_,当日志等级高于flush_level_时立即对日志内容刷新。

这两个变量都是用原子变量,保证多线程下的线程安全。

logger类也支持传递string类型的参数,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
void log(log_clock::time_point log_time,
source_loc loc,
level::level_enum lvl,
string_view_t msg) {
bool log_enabled = should_log(lvl);
bool traceback_enabled = tracer_.enabled();
if (!log_enabled && !traceback_enabled) {
return;
}

details::log_msg log_msg(log_time, loc, name_, lvl, msg);
log_it_(log_msg, log_enabled, traceback_enabled);
}

string_view_t支持隐式转换,调用时可以直接传递std::string类型

日志的输出

logger类不负责日志输出的具体实现,而是在**sink_it_**方法中调用Sink::log方法实现日志的输出

1
2
3
4
5
6
7
8
9
10
11
12
SPDLOG_INLINE void logger::sink_it_(const details::log_msg &msg) {
for (auto &sink : sinks_) {
if (sink->should_log(msg.level)) {
SPDLOG_TRY { sink->log(msg); }
SPDLOG_LOGGER_CATCH(msg.source)
}
}

if (should_flush_(msg)) {
flush_();
}
}

而Sink是一个抽象类,通过派生类的override来实现不同的日志输出功能。

backtracer调试功能

spdlog 的 backtracer 功能是一个非常有用的调试工具,有时候输出的DEBUG信息过多会导致问题排查起来困难,而使用backtracer可以在问题出现时,打印最近的一些日志。这个功能特别适合于调试那些只有在特定条件下才会出现问题的场景

使用方法如下:

1
2
3
4
5
auto logger = spdlog::stdout_color_mt("console");
logger->enable_backtrace(32); // 存储最近的32条消息
...
// 需要查看最近的日志
logger->dump_backtrace();

它的内部使用circular_q环形队列来保存日志,当达到最大容量时,新的消息会覆盖最旧的消息。由于是环形队列,避免了不必要的拷贝和移动开销,具有较高的性能。

性能优化

logger实现了移动构造函数和swap函数,通过移动语义避免了对象的重复构造,具有较高的性能,。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SPDLOG_INLINE void logger::swap(spdlog::logger &other) SPDLOG_NOEXCEPT {
name_.swap(other.name_);
sinks_.swap(other.sinks_);

// swap level_
auto other_level = other.level_.load();
auto my_level = level_.exchange(other_level);
other.level_.store(my_level);

// swap flush level_
other_level = other.flush_level_.load();
my_level = flush_level_.exchange(other_level);
other.flush_level_.store(my_level);

custom_err_handler_.swap(other.custom_err_handler_);
std::swap(tracer_, other.tracer_);
}
作者

echo

发布于

2024-09-28

更新于

2024-10-13

许可协议

评论