如何高效的写log, 目前GitHub有很多log的轮子,简单的测试了一下,发现nanolog是最快的(当然这个基本满足需求,所有就没有再找其他的)
nanolog有两个版本.
- C++17的版本, 地址如下: https://github.com/PlatformLab/NanoLog
- C++11的版本, 地址如下: https://github.com/Iyengar111/NanoLog
1. nanolog的基本思想
格式化是一个很耗时的操作,因此nanolog把格式化的内容放到线程里头去做。待写入的日志全部放到一个buffer里头,标志写入每个日志的类型。目前支持int/uint/double/string/char/char*这些类型。
2. 例子
LOG_INFO << "Logging " << benchmark << i << 0 << 'K' << -42.42;
LOG_INFO返回的是一个NanoLogLine的对象,NanoLogLine里支持各个类型形参的opertor<<, 如下:
NanoLogLine& operator<<(char arg);
NanoLogLine& operator<<(int32_t arg);
NanoLogLine& operator<<(uint32_t arg);
NanoLogLine& operator<<(int64_t arg);
NanoLogLine& operator<<(uint64_t arg);
NanoLogLine& operator<<(double arg);
NanoLogLine& operator<<(std::string const & arg);
3.调用过程
3.1 待输出的日志放到NanoLogLine对象里
#define NANO_LOG(LEVEL) nanolog::NanoLog() == nanolog::NanoLogLine(LEVEL, __FILE__, __func__, __LINE__)
#define LOG_INFO nanolog::is_logged(nanolog::LogLevel::INFO) && NANO_LOG(nanolog::LogLevel::INFO)
#define LOG_WARN nanolog::is_logged(nanolog::LogLevel::WARN) && NANO_LOG(nanolog::LogLevel::WARN)
#define LOG_CRIT nanolog::is_logged(nanolog::LogLevel::CRIT) && NANO_LOG(nanolog::LogLevel::CRIT)
3.2 把NanoLogLine对象作为参数调用NanoLog的operator ==, 把NanoLogLine 的内容add到atomic_nanologger(NanoLogger)里
struct NanoLog
{
bool operator==(NanoLogLine &);
};
std::atomic < NanoLogger * > atomic_nanologger;
bool NanoLog::operator==(NanoLogLine & logline)
{
atomic_nanologger.load(std::memory_order_acquire)->add(std::move(logline));
return true;
}
3.3 NanoLogger对象在构造的时候,会有开启一个线程( m_thread(&NanoLogger::pop, this), 线程的钩子函数就是pop,pop函数负责把日志写入文件
NanoLogger的线程进行写日志
class NanoLogger
{
public:
NanoLogger(NonGuaranteedLogger ngl, std::string const & log_directory, std::string const & log_file_name, uint32_t log_file_roll_size_mb)
: m_state(State::INIT)
, m_buffer_base(new RingBuffer(std::max(1u, ngl.ring_buffer_size_mb) * 1024 * 4))
, m_file_writer(log_directory, log_file_name, std::max(1u, log_file_roll_size_mb))
, m_thread(&NanoLogger::pop, this)
{
m_state.store(State::READY, std::memory_order_release);
}
NanoLogger(GuaranteedLogger gl, std::string const & log_directory, std::string const & log_file_name, uint32_t log_file_roll_size_mb)
: m_state(State::INIT)
, m_buffer_base(new QueueBuffer())
, m_file_writer(log_directory, log_file_name, std::max(1u, log_file_roll_size_mb))
, m_thread(&NanoLogger::pop, this)
{
m_state.store(State::READY, std::memory_order_release);
}
~NanoLogger()
{
//等待写日志线程结束
m_state.store(State::SHUTDOWN);
m_thread.join();
}
void add(NanoLogLine && logline)
{
m_buffer_base->push(std::move(logline));
}
void pop()
{
// Wait for constructor to complete and pull all stores done there to this thread / core.
while (m_state.load(std::memory_order_acquire) == State::INIT)
std::this_thread::sleep_for(std::chrono::microseconds(50));
NanoLogLine logline(LogLevel::INFO, nullptr, nullptr, 0);
//m_state在构造函数完成时,就是ready状态
while (m_state.load() == State::READY)
{
//有log就写,否则就是睡眠50微妙。如果我们不需要实时,可以让其睡眠更久一点
if (m_buffer_base->try_pop(logline))
m_file_writer.write(logline);
else
std::this_thread::sleep_for(std::chrono::microseconds(50));
}
// Pop and log all remaining entries
// 最后调用析构函数时,m_state为shutdown,最后一次写入log
while (m_buffer_base->try_pop(logline))
{
m_file_writer.write(logline);
}
}
private:
enum class State
{
INIT,
READY,
SHUTDOWN
};
std::atomic < State > m_state;
std::unique_ptr < BufferBase > m_buffer_base;
FileWriter m_file_writer;
std::thread m_thread;
};
4. log的类型
void initialize(GuaranteedLogger gl, std::string const & log_directory, std::string const & log_file_name, uint32_t log_file_roll_size_mb);
void initialize(NonGuaranteedLogger ngl, std::string const & log_directory, std::string const & log_file_name, uint32_t log_file_roll_size_mb);
在初始化的时候,需要指定logger的类型,GuaranteedLogger 保证日志不丢失,使用的是一个queue_buffer. NonGuaranteedLogger则不保证日志完整(当写入很快很快的时候),使用的是一个ring_buffer. 。一般来说,日志都不会丢失啦。
5. 其他
nanolog在每一行日志里头,都会加上时间戳、日志级别、线程id、文件名、函数名、行数行号。 例子如下:
[2019-04-15 00:09:15.505866][INFO][140067778569984][non_guaranteed_nanolog_benchmark.cpp:nanolog_benchmark:25] Logging benchmark6580K-42.42
写那么多信息,一方面需要更多内存,另一个方面频繁调用获取时间戳和线程id的API也会消耗一点时间,因此可以根据需要把不需要的信息去掉,减少消耗。