简介

SPDLog 是一个开源的、快速的、仅有头文件的C++11 日志库,它提供了向流、标准输出、文件、系统日志、调试器等目标输出日志的能力。它支持的平台包括Windows、Linux、Mac、Android;有一下特性:

  1. 非常快,性能是它的主要目标;
  2. 仅包括头文件;
  3. 日志的格式化处理使用开源的fmt库
  4. 可选的printf语法支持;
  5. 非常快的异步模式(可选),支持异步写日志;
  6. 自定义格式;
  7. 条件日志;
  8. 多线程/单线程日志;
  9. 各种日志目标:可对日志文件进行循环输出;可每日生成日志文件;支持控制台日志输出(支持颜色);系统日志;Windows debugger;较容易扩展自定义日志目标;
  10. 支持日志输出级别:阈值级别既可以在运行时也可以在编译时修改。

如何使用

  1. 将代码下载下来的压缩包解压后会得到以下文件,其中include文件夹里是所需的头文件和源码:
  1. 新建一个C++控制台应用程序项目,然后在项目属性页C/C++中常规的附加包含目录中加上include的路径,然后在.cpp中就可以开始测试了:
  1. 代码应用:
  • 简单的使用场景 直接在控制台输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "spdlog/spdlog.h"
int main()
{
//输出不同级别的日志
spdlog::info("Hello, {}!", "World");
spdlog::info("Welcome to spdlog!");
spdlog::error("Some error message with arg: {}", 1);

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("{:<30}", "left aligned");
}

warn,critical,info为不同等级的log,输出在控制台会以不同颜色表示

  • 单一日志文件的使用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include "spdlog/spdlog.h"
#include "spdlog/sinks/basic_file_sink.h" // support for basic file logging

int main()
{
try
{
//在logs/basic.txt中写下Hello world
auto my_logger = spdlog::basic_logger_mt("sbasic_logger", "logs/basic.txt");
my_logger->info("Hello {}", "world");
}
catch (const spdlog::spdlog_ex& ex)
{
std::cout << "Log initialization failed: " << ex.what() << std::endl;
}
}

auto my_logger = spdlog::basic_logger_mt(“basic_logger”, “logs/basic.txt”);中”my_logger“为logger名称,可以随意命名

注意,logger使用完,程序关闭之前需要调用drop函数释放logger对象,如果程序没有关闭,就无法建立同样名称的logger

这种basic log不带滚动,日志文件会一直被写入,不断变大

函数名带后缀_mt的意思是multi thread(速度稍微慢一点点,考虑了多线程并发),_st的意思是single thread(速度较快)

  • 循环日志文件的使用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include "spdlog/spdlog.h"
#include "spdlog/sinks/rotating_file_sink.h" // support for rotating file logging

int main()
{
try
{
auto file_logger = spdlog::rotating_logger_mt("file_logger", "myfilename",
1024 * 1024 * 5, 10);
file_logger->set_level(spdlog::level::debug);
int i = 0;
while (i < 1000000)
{
file_logger->debug("Async message #{}", i);
i++;
}
}
catch (const spdlog::spdlog_ex& ex)
{
std::cout << "Log initialization failed: " << ex.what() << std::endl;
}
}

区别于单一文件,循环日志的生产者类是rotating_logger_mt。rotating_logger_mt初始化的时候需要4个参数。

生产者的名字,自定义即可;
日志文件路径,相对和绝对均可;
单一文件的大小,超过了设置大小就生成一个新的文件。上面代码中设置为5MB;
保留文件数量,超过数量的文件会直接删掉以节省空间。正常使用的时候此数字大一些较好。
代码中生成100万条日志,数据大约是65MB。那么在根目录下就会出现10个日志文件,后缀名由1~9。

rotating log 滚动日志,当日志文件超出规定大小时,会删除当前日志文件中所有内容,重新开始写入

  • daily log的使用
    每天会新建一个日志文件,新建日志文件的时间可以自己设定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include "spdlog/spdlog.h"
#include "spdlog/sinks/daily_file_sink.h"

int main() {
// Create a daily logger - a new file is created every day on 2:30am
auto daily_logger = spdlog::daily_logger_mt("daily_logger", "logs/daily.txt", 2, 30);
// trigger flush if the log severity is error or higher
daily_logger->flush_on(spdlog::level::err);
daily_logger->info(123.44);

return 0;
}

如果程序不退出,每天2:30会创建新的文件

  • 异步打印日志文件
    大型项目中经常有很多场景是对时间有着严苛要求的,此时异步调用打印日志功能就显得十分重要了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include "spdlog/spdlog.h"
#include "spdlog/async.h"
#include "spdlog/sinks/rotating_file_sink.h"

int main(){
spdlog::init_thread_pool(10000, 1);
auto file_logger = spdlog::rotating_logger_mt<spdlog::async_factory>("file_logger", "mylogs", 1024 * 1024 * 5, 100);
int i = 0;
file_logger->set_level(spdlog::level::debug);
while (i < 1000000)
{
file_logger->debug("Async message #{}", i);
i++;
}
spdlog::drop_all();
return 0;
}

在初始化的时候使用异步工厂spdlog::async_factory进行初始化即可。

技术分析

  1. 模板
    源码中使用了模板技术

  2. c++11
    源码中使用了c++11 新规范,比如:mutex using 等

  3. 头文件与源码分离,使用include进行包含:

    1
    2
    3
    4
    5

    #ifdef SPDLOG_HEADER_ONLY
    # include "async_logger-inl.h"
    #endif

    源码实现在 async_logger-inl.h中,而头文件async_logger.h 做类声明;

封装

对 PSDLog 进行单件模式封装是有意义的:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//SimLog.h
#pragma once


#ifndef _SIM_LOG_H_
#define _SIM_LOG_H_

#include "spdlog/spdlog.h"
#include "spdlog/sinks/rotating_file_sink.h"
#include "spdlog/sinks/stdout_color_sinks.h"


#ifdef _WIN32
//strrchr:查找字符在指定字符串从右面开始的第一次出现的位置,若是成功,返回该字符以及后面的字符,若是失败,返回NULL
//strcgr:查找字符在指定字符串首次出现的位置
#define __FILENAME__ (strrchr(__FILE__,'\\')?(strrchr(__FILE__,'\\')+1):__FILE__)
#else
#define __FILENAME__ (strrchr(__FILE__,'/')?(strrchr(__FILE__,'/')+1):__FILE__)
#endif //_WIN32

#ifndef SUFFIX
//在错误级别的日志后面追加文件名,函数名,行号
#define SUFFIX(msg) std::string(msg).append(" <")\
.append(__FILENAME__).append("> <").append(__FUNCTION__)\
.append("> <").append(std::to_string(__LINE__))\
.append(">").c_str()
#endif //suffix

/*
日志等级:trace,debug,info,warn,err ,critical
使用方法:包含simlog.h头文件,调用初始化函数,使用LDebug等打印日志信息
例:
SimLog::Instance().InitSimLog("scenario_edit", "scenario_edit_log.txt");
int i = 10;
double d_number = 10.01;
LDebug("SimLog::Async message");
LDebug("SimLog::Async message #{0},d_number:{1}", i,d_number);
注:使用{}格式化字符串,里面的数字为占位符
*/

//#define LTrace(msg,...) SimLog::Instance().GetLogger()->trace(SUFFIX(msg),__VA_ARGS__)
//#define LDebug(...) SimLog::Instance().GetLogger()->debug(__VA_ARGS__)
//#define LInfo(...) SimLog::Instance().GetLogger()->info(__VA_ARGS__)
//#define LWarn(...) SimLog::Instance().GetLogger()->warn(__VA_ARGS__)
//#define LError(msg,...) SimLog::Instance().GetLogger()->error(SUFFIX(msg),__VA_ARGS__)
//#define LCritical(...) SimLog::Instance().GetLogger()->critical(__VA_ARGS__)

#define LTrace(msg,...) { if(!SimLog::Instance().Init()) SimLog::Instance().InitSimLog(); SimLog::Instance().GetLogger()->trace(SUFFIX(msg),__VA_ARGS__);};
#define LDebug(...) { if(!SimLog::Instance().Init()) SimLog::Instance().InitSimLog(); SimLog::Instance().GetLogger()->debug(__VA_ARGS__);};
#define LInfo(...) { if(!SimLog::Instance().Init()) SimLog::Instance().InitSimLog(); SimLog::Instance().GetLogger()->info(__VA_ARGS__);};
#define LWarn(...) { if(!SimLog::Instance().Init()) SimLog::Instance().InitSimLog(); SimLog::Instance().GetLogger()->warn(__VA_ARGS__);};
#define LError(msg,...) { if(!SimLog::Instance().Init()) SimLog::Instance().InitSimLog(); SimLog::Instance().GetLogger()->error(SUFFIX(msg),__VA_ARGS__);};
#define LCritical(...) { if(!SimLog::Instance().Init()) SimLog::Instance().InitSimLog(); SimLog::Instance().GetLogger()->critical(__VA_ARGS__);};

class SimLog final
{
public:
static SimLog& Instance()
{
static SimLog log;
return log;
};

std::atomic_bool& Init() { return m_init; }

void InitSimLog()
{
::AllocConsole();
if (m_stdout)
fclose(m_stdout);
freopen_s(&m_stdout, "CONOUT$", "w+t", stdout);
my_logger_ = spdlog::stdout_color_mt("console");
SetLevel();
m_init = true;
}

void InitSimLog(const std::string &file_name)
{
DWORD id = GetCurrentProcessId();
InitSimLog(file_name, std::to_string(id));
}

void InitSimLog(const std::string& file_name, const std::string& logger_name)
{
InitSimLog(file_name, logger_name, 1048576 * 3, 3);
}

void InitSimLog(const std::string &file_name, const std::string &logger_name, size_t max_size, size_t max_files/*, int log_level = spdlog::level::trace*/)
{
//spdlog::set_level(static_cast<spdlog::level::level_enum>(log_level));
my_logger_ = spdlog::rotating_logger_mt(logger_name, file_name, max_size, max_files);
my_logger_->flush_on(spdlog::level::err);
//spdlog::flush_every(std::chrono::seconds(3));
m_init = true;
}

void EndLog()
{
// Release all spdlog resources, and drop all loggers in the registry.
// spdlog::shutdown This is optional (only mandatory if using windows + async log).
if (my_logger_ && m_async)
spdlog::shutdown();
};

void SetLevel(int level = spdlog::level::trace)
{
spdlog::set_level(static_cast<spdlog::level::level_enum>(level));
};

auto GetLogger()
{
return my_logger_;
}

private:
SimLog()= default;

~SimLog()
{
EndLog();
if (m_stdout) {
spdlog::drop("console");
FreeConsole();
fclose(m_stdout);
}
};

SimLog(const SimLog& other) = delete;

SimLog& operator=(const SimLog& other) = delete;

private:
std::shared_ptr<spdlog::logger> my_logger_;
std::atomic_bool m_init{false};
std::atomic_bool m_async{false};
FILE* m_stdout{ nullptr };
};

#endif //_SIMLOG_H_

其他封装参见:

  1. spdlog_wrapper
  2. 基于spdlog开源日志库的封装日志库
  3. 基于C++ spdlog日志库的完善封装

不足

目前这个库有一大缺陷就是不支持日志压缩,要知道项目中如果打开了Debug级别的日志,日志量可能是非常恐怖的,如果分割文件的时候不能压缩文件将是对硬盘空间的极大浪费(日志压缩率一般在95%左右)。

参考

spdlog 基本结构分析