概述
spdlog是一个快速、可扩展的 c++ 日志库,它提供了简单易用的接口和灵活的配置选项,spdlog 支持多种日志级别、多线程安全,可以将日志输出到终端、文件或者其他自定义的目标,它具有高性能和低开销的特点,适用于各种规模的应用程序和系统。
获取源码
如果要将源码添加进你的工程里,请从 github 获取
https://github.com/gabime/spdlog
- 如果使用的是 conan 包管理,可以从这里获取
https://conan.io/center/recipes/spdlog
注意
不同版本之间的spdlog不兼容,若调用的库使用低版本spdlog编译,然后链接使用高版本spdlog会导致未知异常奔溃,已知有ROS2,livox驱动,解决方案为使用和库一致的spdlog版本。
ros2另外的解决方案
rclcpp::init(argc, argv, rclcpp::InitOptions().auto_initialize_logging(false)); // 不使用ROS2日志
完整工程
https://github.com/letterso/z_project
包装器
singleton.h
// singleton.h
#ifndef __SINGLETON_H__
#define __SINGLETON_H__
#include <iostream>
#include <string>
template<class T>
class Singleton {
protected:
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
Singleton() = default;
~Singleton() = default;
public:
template<typename... Args>
static T &instance(Args &&...args) {
static T obj(std::forward<Args>(args)...);
return obj;
}
};
#endif // __SINGLETON_H__
spdlog_wraper.h
#ifndef __LOGGER_H__
#define __LOGGER_H__
#include "make_unique.h"
#include "singleton.h"
#include <spdlog/spdlog.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#define ASYNC_MODE
#ifdef ASYNC_MODE
#include <spdlog/async.h>
#endif
#include <memory>
#include <filesystem>
// normal日志
#define LOG_DIR "./log" // 日志根目录
#define LOG_TOPIC "node" // 日志tag
#define LOG_FILE_SIZE 1024 * 1024 * 5 // 5MB
#define LOG_ROTATION 3 // 日志文件满3个时开始滚动日志
#define LOG_FLUSH_F 1 // 1秒flush一次
#define LOG_FLUSH_ON spdlog::level::info // 当打印这个级别日志时flush
#define PATTERN "[%Y-%m-%d %H:%M:%S.%e][%^%L%$] %v" // 日志样式
// evaluate日志
#define LOG_TIME_TOPIC "time"
#define EVALOG_PATTERN "%v" // 日志样式
// 周期
#define LOG_MAX_DAY 7 // 最大保存天数
#define LOGC(fmt, ...) \
Singleton<Logger>::instance().log_critical(fmt, ##__VA_ARGS__)
#define LOGE(fmt, ...) \
Singleton<Logger>::instance().log_error(fmt, ##__VA_ARGS__)
#define LOGW(fmt, ...) \
Singleton<Logger>::instance().log_warn(fmt, ##__VA_ARGS__)
#define LOGI(fmt, ...) \
Singleton<Logger>::instance().log_info(fmt, ##__VA_ARGS__)
#define LOGD(fmt, ...) \
Singleton<Logger>::instance().log_debug(fmt, ##__VA_ARGS__)
#define LOGT(fmt, ...) \
Singleton<Logger>::instance().log_trace(fmt, ##__VA_ARGS__)
#define EVALOGTIME(fmt, ...) \
Singleton<Logger>::instance().log_time(fmt, ##__VA_ARGS__)
class Logger {
public:
Logger(const Logger &) = delete;
Logger &operator=(const Logger &) = delete;
Logger() = default;
~Logger()
{
for (const auto &name : m_logger_name)
{
if (spdlog::get(name))
{
spdlog::get(name)->flush();
spdlog::drop(name);
}
}
spdlog::shutdown(); // 若工程中其他库使用spdlog或者和ROS2一起使用,需要注释
}
bool init() {
auto &&function = [&]() {
#ifdef ASYNC_MODE
// 初始化内存池
spdlog::init_thread_pool(8192, 1);
SPDLOG_INFO("[LOGGER] Use Async Mode");
#endif
// 创建主日志
if(!m_logger){
// 创建sinks
auto stdout_sink = std::make_shared<stdout_sink_t>();
auto rotating_sink = std::make_shared<rotating_sink_t>(LOG_TOPIC + ".log", LOG_FILE_SIZE, LOG_ROTATION);
std::vector<spdlog::sink_ptr> sinks{stdout_sink, rotating_sink};
#ifdef ASYNC_MODE
// 构建日志器
m_logger = std::make_shared<spdlog::async_logger>(LOG_TOPIC, sinks.begin(), sinks.end(), spdlog::thread_pool(), spdlog::async_overflow_policy::block);
#else
m_logger = std::make_shared<spdlog::logger>(LOG_TOPIC, sinks.begin(), sinks.end());
#endif
m_logger->set_pattern(PATTERN);
if (is_debug_mode()) {
m_logger->set_level(spdlog::level::debug);
} else {
m_logger->set_level(spdlog::level::info);
}
m_logger->flush_on(LOG_FLUSH_ON);
m_logger_name.push_back(LOG_TOPIC);
spdlog::register_logger(m_logger);
}
// 创建评估日志
if (is_evaluate_mode()) {
createAndConfigureLogger(LOG_TIME_TOPIC, LOG_TIME_TOPIC + ".log");
}
};
try {
function();
return true;
} catch (const std::exception &e) {
SPDLOG_ERROR("[LOGGER] Construct logger error: {}", e.what());
return false;
}
// 设置全局定时刷新
spdlog::flush_every(std::chrono::seconds(LOG_FLUSH_F));
}
inline void flush()
{
for (const auto &name : m_logger_name)
{
if (spdlog::get(name))
{
spdlog::get(name)->flush();
}
}
}
template <typename... Args>
inline void log_critical(const char *fmt, Args... args)
{
if (m_logger)
{
m_logger->critical(fmt, args...);
}
else
{
SPDLOG_CRITICAL(fmt, args...);
}
}
template <typename... Args>
inline void log_error(const char *fmt, Args... args)
{
if (m_logger)
{
m_logger->error(fmt, args...);
}
else
{
SPDLOG_ERROR(fmt, args...);
}
}
template <typename... Args>
inline void log_warn(const char *fmt, Args... args)
{
if (m_logger)
{
m_logger->warn(fmt, args...);
}
else
{
SPDLOG_WARN(fmt, args...);
}
}
template <typename... Args>
inline void log_info(const char *fmt, Args... args)
{
if (m_logger)
{
m_logger->info(fmt, args...);
}
else
{
SPDLOG_INFO(fmt, args...);
}
}
template <typename... Args>
inline void log_debug(const char *fmt, Args... args)
{
if (m_logger)
{
m_logger->debug(fmt, args...);
}
else
{
SPDLOG_DEBUG(fmt, args...);
}
}
template <typename... Args>
inline void log_trace(const char *fmt, Args... args)
{
if (m_logger)
{
m_logger->trace(fmt, args...);
}
else
{
SPDLOG_TRACE(fmt, args...);
}
}
template <typename... Args>
inline void log_time(const char *fmt, Args... args)
{
if(spdlog::get(LOG_TIME_TOPIC))
spdlog::get(LOG_TIME_TOPIC)->info(fmt, args...);
}
private:
std::shared_ptr<spdlog::logger> m_logger;
std::vector<std::string> m_logger_name;
using stdout_sink_t = spdlog::sinks::stdout_color_sink_mt;
using rotating_sink_t = spdlog::sinks::rotating_file_sink_mt;
using basic_sink_t = spdlog::sinks::basic_file_sink_mt;
static bool is_debug_mode() {
char *var = getenv("debug");
if (nullptr == var) {
return false;
}
if (0 == strcmp(var, "on")) {
SPDLOG_INFO("[LOGGER] debug mode on");
return true;
}
return false;
}
static bool is_evaluate_mode() {
char *var = getenv("evaluate");
if (nullptr == var) {
return false;
}
if (0 == strcmp(var, "on")) {
SPDLOG_INFO("[LOGGER] evaluate mode on");
return true;
}
return false;
}
void createAndConfigureLogger(
const std::string &logger_name,
const std::string &filename)
{
// 创建文件 Sink
auto file_sink = std::make_shared<basic_sink_t>(filename, true);
#ifdef ASYNC_MODE
// 创建异步 Logger
auto logger = std::make_shared<spdlog::async_logger>(
logger_name,
file_sink, // 每个Logger绑定自己的文件Sink
spdlog::thread_pool(),
spdlog::async_overflow_policy::block);
#else
auto logger = std::make_shared<spdlog::logger>(logger_name, file_sink);
#endif
logger->set_pattern(EVALOG_PATTERN);
logger->set_level(spdlog::level::info);
logger->flush_on(spdlog::level::info);
spdlog::register_logger(logger);
m_logger_name.push_back(logger_name);
}
};
#endif // __LOGGER_H__
使用例子
#include "logger.h"
int main() {
// 日志初始化
auto &logger_instance = Singleton<Logger>::instance();
if (!logger_instance.init())
{
LOGC("Failed to create logger");
return -1;
}
// 日志输出
LOGI("hello world");
return 0;
}
基于时间进行日志管理
日志按照7天为一个周期,自动清除日期最旧的目录,当前日期下每次程序启动会根据当前时间创建目录,并将当次日志保存在该目录下。
├── log
│ └── date
│ └── time
│ └── myapp.log
日志目录生成类
class LoggerManager {
private:
std::string _time_file_name;
private:
void create_directories() {
// 获取当前日期
auto current_time = std::chrono::system_clock::now();
std::time_t time_t_format = std::chrono::system_clock::to_time_t(current_time);
std::tm tm_format = *std::localtime(&time_t_format);
// 使用年月日创建一级目录
std::ostringstream date_oss;
date_oss << LOG_DIR << "/" << std::put_time(&tm_format, "%Y%m%d");
std::string date_file_name = date_oss.str();
if (!std::filesystem::is_directory(date_file_name) || !std::filesystem::exists(date_file_name)) {
std::filesystem::create_directories(date_file_name);
}
// 使用时分秒创建二级目录
std::ostringstream time_oss;
time_oss << LOG_DIR << "/" << std::put_time(&tm_format, "%Y%m%d") << "/"
<< std::put_time(&tm_format, "%H%M%S");
_time_file_name = time_oss.str();
if (!std::filesystem::is_directory(_time_file_name) || !std::filesystem::exists(_time_file_name)) {
std::filesystem::create_directories(_time_file_name);
}
}
static void delete_directories() {
for (auto &entry : std::filesystem::directory_iterator(LOG_DIR)) { // 遍历当前文件夹
if (entry.is_directory()) {
// 获取文件夹名称
std::string folder_name = entry.path().filename().string();
// 确保文件夹名符合日期格式
if (folder_name.size() == 8) { // YYYYMMDD
std::tm folder_time = {};
std::istringstream ss(folder_name);
ss >> std::get_time(&folder_time, "%Y%m%d");
if (ss.fail()) {
SPDLOG_ERROR("[LOGGER] date parsing failed: {}", folder_name);
continue;
}
// 将文件夹时间转换为time_t
std::time_t folder_time_t = std::mktime(&folder_time);
std::time_t now = std::time(nullptr);
// 计算日期差
double days_old = std::difftime(now, folder_time_t) / (60 * 60 * 24);
// 删除超过最大保存天数的文件夹
if (days_old >= LOG_MAX_DAY) {
std::filesystem::remove_all(entry.path());
SPDLOG_INFO("[LOGGER] remove old directories: {}", entry.path().string());
}
}
}
}
}
public:
LoggerManager() {
create_directories();
delete_directories();
}
std::string get_log_file_name() const{
return _time_file_name;
}
~LoggerManager() {}
};
使用
std::string log_file_name = Singleton<LoggerManager>::instance().get_log_file_name();
auto rotating_sink = std::make_shared<rotating_sink_t>(log_file_name + "/" + LOG_TOPIC + ".log", LOG_FILE_SIZE, LOG_ROTATION);
自定义formatter
logger_eigen_formatter.h增加对Eigen矩阵输出的支持
#ifndef __LOGGER_EIGEN_FORMATTER_H__
#define __LOGGER_EIGEN_FORMATTER_H__
#include <spdlog/spdlog.h>
#include <Eigen/Core>
#include <Eigen/Geometry>
#include <sstream>
// Custom formatter for Eigen::DenseBase types
template<typename T>
struct fmt::formatter<T, std::enable_if_t<std::is_base_of<Eigen::DenseBase<T>, T>::value, char>> {
constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
return ctx.end();
}
template <typename FormatContext>
auto format(const Eigen::DenseBase<T>& mat, FormatContext& ctx) const -> decltype(ctx.out()) {
std::stringstream ss;
ss << mat;
return fmt::format_to(ctx.out(), "{}", ss.str());
}
};
#endif // __LOGGER_EIGEN_FORMATTER_H__
logger_cv_formatter.h增加对cv::Mat输出的支持
#ifndef __LOGGER_CV_FORMATTER_H__
#define __LOGGER_CV_FORMATTER_H__
#include <spdlog/spdlog.h>
#include <opencv2/core/mat.hpp>
#include <sstream>
// Custom formatter for cv::Mat types
template <>
struct fmt::formatter<cv::Mat> {
// 解析格式字符串,如果不需要自定义格式选项,可以直接返回 ctx.end()
constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
// 例如,你可以支持像 "{:brief}" 或 "{:full}" 这样的格式选项
// 这里为了简化,我们不处理任何格式选项
return ctx.end();
}
// 格式化 cv::Mat 对象
template <typename FormatContext>
auto format(const cv::Mat& mat, FormatContext& ctx) const -> decltype(ctx.out()) {
std::stringstream ss;
ss << "Mat (rows=" << mat.rows << ", cols=" << mat.cols << ", type=" << mat.type() << ", channels=" << mat.channels() << ")\n";
// 决定要打印的行数和列数,避免输出过大的矩阵
int rows_to_print = std::min(mat.rows, 5); // 示例:最多打印5行
int cols_to_print = std::min(mat.cols, 8); // 示例:最多打印8列
for (int i = 0; i < rows_to_print; ++i) {
ss << "[";
for (int j = 0; j < cols_to_print; ++j) {
// 根据Mat的类型来获取元素并打印
// 这是一个简化版本,实际应用中可能需要更完善的类型处理
switch (mat.type() & CV_MAT_DEPTH_MASK) {
case CV_8U: ss << static_cast<int>(mat.at<uchar>(i, j)); break;
case CV_8S: ss << static_cast<int>(mat.at<schar>(i, j)); break;
case CV_16U: ss << mat.at<ushort>(i, j); break;
case CV_16S: ss << mat.at<short>(i, j); break;
case CV_32S: ss << mat.at<int>(i, j); break;
case CV_32F: ss << mat.at<float>(i, j); break;
case CV_64F: ss << mat.at<double>(i, j); break;
default: ss << "?"; break; // 未知类型
}
if (j < cols_to_print - 1) {
ss << ", ";
}
}
ss << "]";
if (i < rows_to_print - 1) {
ss << "\n";
}
}
if (mat.rows > rows_to_print || mat.cols > cols_to_print) {
ss << "\n... (truncated)";
}
return fmt::format_to(ctx.out(), "{}", ss.str());
}
};
#endif // __LOGGER_CV_FORMATTER_H__
参考
https://cseek.github.io/posts/spdlog-wraper/