极好用的C嘎嘎日志库spdlog快速上手指南✨ A Highly Efficient C++ Logging Library: spdlog Quick Start Guide ✨!!! Author:@kkl 本文已经施工完毕🧑🌾🧑🌾!
写在前面 以前在接触一些比较基础的嵌入式MCU项目的时候,写的都是纯C,基本上大家都是直接使用printf来做日志打印与调试,也没有见到哪个项目有将打印输出到log的规范操作,于是也就默认学习大家这么做,任何时候都是printf。
后面上玩起ESP32,发现乐鑫确实让MCU在封装抽象的路上大大地向前迈出重要的一步,借鉴了许多高级语言的思想,提供的SDK里面也带有分级log的接口,可以畅快地在程序里写C++,可是日志打印依然寒碜的可怜,尤其arduino下的ESP32部分SDK未兼容,日志打印还是printf用的最多。
后面接触linux的时候,看见很多标准规范的项目里头都有完备的日志系统,spdlog的字眼时常浮现眼前,当时也不以为然,估计是因为自己做的项目的体量实在是小,所以没有这方面的需求。
工作后,写代码时,我导总是提起日志的重要性,一旦程序出现了什么崩溃错误,几乎所有资深码农的第一反应就是:赶紧搂一眼日志。我与嵌入式作伴,如今也有三个年头啦,spdlog作为一个高性能的日志库在C++的地位还是无人能敌,基本上所能见的开源项目你都能在代码里发现它的身影…
所以,spdlog好啊,得学啊,因为真程序猿必会spdlog(笑!
…
开始 spdlog有啥好
高效且迅速: 就是快,怎么解释,在大量日志记录场景下也能保持较低延迟和较高吞吐量。
编译移植友好: header-only,spdlog是头文件库,所有的代码都是写在头文件(.h),因此想要使用这个库,包含头文件即可,没有额外的编译或者依赖链接其他库文件。
支持异步模式: spdlog提供可选的异步日志记录机制,我们都知道日志打印多少会消耗一些时间,如果日志打多了程序就变得卡卡的,而异步模式,能够将日志打印操作放入后台线程执行,从而避免阻塞主线程,保证主线程丝滑运行。
多目标输出: 支持将日志输出到控制台、文件,还支持日志轮转。
线程安全: 保证在多线程情况下打印日志不会打架,不会像printf一样,多线程下发会变成夹!心!饼!干!
如何移植 下载源代码,然后把整个源码目录拷贝到我们对应的项目工程下,然后在makefile或者cmake里面包含这个源码当中的include即可,然后一键编译,不可能会报错滴,这样就算是移植成功了,够不够友好!
1 2 3 4 5 6 7 git clone https://github.com/gabime/spdlogmv ./spdlog ./myProject/lib
核心概念 这是【官方的wiki点我】 请拿好!!!
logger: 日志对象,每个logger内包含一个sink组成的vector容器,每个sink可以分别设置优先级,logger本身也可以设置优先级。
sink: 直译是水槽,实际上是引流的对象或者可以认为sink就是日志输出目标,可以在控制台输出、也可在文件中输出。
formatter: 格式化对象,spdlog有默认的格式,你也可以个性化自定义格式,如[日期时间]、[代码路径]、[函数名]、[行数]、[线程tid]、[logger名称]、[log级别]、[log内容]…
level: 日志级别,spdlog提供了几个日志级别,分别是trace , debug , info , warn , error , critical 等…
逻辑关系:每个logger包含一个vector,该vector由一个或多个std::shared_ptr组成,logger的每条日志都会调用sink对象,由sink对象按照formatter的格式输出到sink指定的地方(有可能是控制台、文件等),这里呢,不再详细的讲解各个组件怎么使用,ai时代,随处可得;我们马上来看快速使用的方法,ai时代,就是要快(如此单押!
使用方法 请你来戳这里:) 看最最最权威的使用方法介绍说明吧!
快速入门 spdlog提供了最为便捷的默认logger,输出到控制台,多线程的,彩色的。无需创建便可使用。
1 2 spdlog::info ("Hello, {}!" , "World" );
将日志输出到控制台 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> #include "spdlog/spdlog.h" #include "spdlog/sinks/stdout_color_sinks.h" int main (int argc, char *argv[]) { auto consoleLogger = spdlog::stdout_color_mt ("console" ); consoleLogger->info ("hello world" ); spdlog::get ("console" )->info ("hello world" ); return 0 ; }
将日志保存到文件 基本日志 直接把日志写到指定文件当中,注意:文件会随着日志的写入越来越大。
1 2 3 4 5 6 7 #include "spdlog/sinks/basic_file_sink.h" auto basicLogger = spdlog::basic_logger_mt ("basic" , "./log/basic.log" ); basicLogger->debug ("hello world" ); spdlog::get ("basic" )->info ("hello {}" , "hugokkl" );
轮转日志 直接把日志写到指定文件当中,你可以设置文件大小和数量,当一个文件大小满了,就会新建一个文件继续轮转,当轮转数量超过设置的文件数量,就会把最初的日志文件覆盖掉,然后日志继续轮转。
1 2 3 4 5 6 7 8 9 10 #include "spdlog/sinks/rotating_file_sink.h" auto max_size = 1048576 * 5 ;auto max_files = 3 ;auto logger = spdlog::rotating_logger_mt ("rotate" , "./log/rotate.log" , max_size, max_files); logger->debug ("hello world" ); spdlog::get ("rotate" )->info ("hello {}" , "hugokkl" );
定时日志 每天的指定时间会新建一个日志文件继续存储日志,并把原来的日志文件以日期命名归档。
1 2 3 4 5 6 7 8 #include "spdlog/sinks/daily_file_sink.h" auto logger = spdlog::daily_logger_mt ("daily" , "./log/daily.log" , 2 , 30 ); logger->debug ("hello world" ); spdlog::get ("daily" )->info ("hello {}" , "hugokkl" );
更多玩法 大多数时候,我们可能不止在一个地方输出日志,我们可能既要在控制台上打印日志、又要将日志保存入文件当中,此时要怎么办呢,spdlog早已为你想到了解决办法,只要你在一个logger里面创建一个vector,里面既包含控制台sink又包含文件sink,即可一石二鸟🤩
话不多说,我这里编写了一个自用的logger文件对,实现了上述的需求,来一起看看吧!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 #include <iostream> #include "spdlog/async.h" #include "spdlog/sinks/daily_file_sink.h" #include "spdlog/sinks/rotating_file_sink.h" #include "spdlog/sinks/stdout_color_sinks.h" #include "logger.h" #if __cplusplus >= 201703L #include <filesystem> namespace fs = std::filesystem;#else #include <experimental/filesystem> namespace fs = std::experimental::filesystem;#endif std::shared_ptr<spdlog::logger> Logger::g_logger = nullptr ;void Logger::init (const std::string logDir, const std::string logName, bool isLograteDay, size_t maxSize, int logFileNum) { std::string log_path; try { if (!fs::exists (logDir)) { fs::create_directories (logDir); } log_path = logDir + "/" + logName; spdlog::init_thread_pool (8192 , 1 ); std::shared_ptr<spdlog::sinks::sink> file_sink; if (isLograteDay) { file_sink = std::make_shared <spdlog::sinks::daily_file_sink_mt>(log_path, 0 , 0 ); } else { file_sink = std::make_shared <spdlog::sinks::rotating_file_sink_mt>(log_path, maxSize, logFileNum, false ); } auto console_sink = std::make_shared <spdlog::sinks::stdout_color_sink_mt>(); std::vector<std::shared_ptr<spdlog::sinks::sink>> sinks; sinks.push_back (file_sink); sinks.push_back (console_sink); g_logger = std::make_shared <spdlog::async_logger>( logName, sinks.begin (), sinks.end (), spdlog::thread_pool (), spdlog::async_overflow_policy::block); g_logger->set_pattern ("[%Y-%m-%d %H:%M:%S.%e] [tid %t] [%^%l%$] %v" ); g_logger->set_level (spdlog::level::debug); spdlog::set_default_logger (g_logger); g_logger->info ("start log (output to file and console)" ); } catch (const spdlog::spdlog_ex &ex) { std::cerr << "Logger init failed: " << ex.what () << "\n" ; std::cerr << "Fallback to /tmp" << std::endl; log_path = "/tmp/" + logName; auto file_sink = std::make_shared <spdlog::sinks::rotating_file_sink_mt>(log_path, maxSize, logFileNum); auto console_sink = std::make_shared <spdlog::sinks::stdout_color_sink_mt>(); std::vector<std::shared_ptr<spdlog::sinks::sink>> sinks; sinks.push_back (file_sink); sinks.push_back (console_sink); g_logger = std::make_shared <spdlog::async_logger>( logName, sinks.begin (), sinks.end (), spdlog::thread_pool (), spdlog::async_overflow_policy::block); g_logger->set_pattern ("[%Y-%m-%d %H:%M:%S] [%^%l%$] %v" ); g_logger->set_level (spdlog::level::debug); spdlog::set_default_logger (g_logger); } }std::shared_ptr<spdlog::logger> Logger::get () { return g_logger; }void Logger::shutdown () { if (g_logger) { spdlog::drop_all (); g_logger = nullptr ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #pragma once #include <stdio.h> #include <stdarg.h> #include <stdbool.h> #include <time.h> #include <string> #include <memory> #include "spdlog/spdlog.h" class Logger {public : static void init (const std::string logDir, const std::string logName, bool isLograteDay, size_t maxSize, int logFileNum) ; static std::shared_ptr<spdlog::logger> get () ; static void shutdown () ;protected :private : Logger () = default ; static std::shared_ptr<spdlog::logger> g_logger; };
其他函数 日志打印等级level设置 低于设置level的等级将不会被打印.
1 2 3 4 5 6 7 8 spdlog::set_level (spdlog::level::debug); logger->set_level (spdlog::level::debug); sink->set_level (spdlog::level::debug);
更全面的格式设置请你看这儿:D .
1 2 3 4 5 6 7 8 spdlog::set_pattern (" [%H:%M:%S %z] [thread %t] %v " ); logger->set_pattern (" [%H:%M:%S %z] [thread %t] %v " ); sink->set_pattern (" [%H:%M:%S %z] [thread %t] %v " );
…
FAQ st/mt的区别 函数的结尾st/mt,是用于区分对象是单线程使用还是多线程使用:
st:单线程版本,不加锁,效率更高。
mt:多线程版本,用于多线程程序,保证线程安全。
手动创建和自动创建logger的区别 通过手动创建的logger要手动通过函数spdlog::register_logger()手动注册到全局注册表中才能使用spdlog::get()来获取logger对象,而自动创建会自动注册,可以直接使用spdlog::get()获取logger对象。
因此,必须先创建注册才能获取噢。
写在后面 先挖个坑,开个头,不然我怕啥时候又因为忙呀忙呀就忘记了,扯远了,先去干活去,等待填坑…
spdlog一用一个不吱声,恭喜你又习得一项名为spdlog的史诗级技能!
鸣谢
感谢上述开源教程,学习了很多,抄了很多(笑。