本文已参与「新人创作礼」活动,一起开启掘金创作之路。
C++程序员喜欢造轮子。这里造一个线程安全的日志库。自己造轮子,安全可控,100%国产化。
如何使用
支持stream, printf, format三种日志输出方式。看如下样例。
LOG(INFO) << "hello" << " " << "world" << " " << (INT_MIN%10);
LOG(DEBUG)("%s %d ", "abc", 333) << 345;
LOG(WARNING).Fmt("I am {}, age {}, go go go", "adson", 30);
LOG(ERROR).Fmt("Hello, Fmt");
LOG(FATAL)("this is fatal");
输出效果
I 2022-9-2 14:57:51.755068 trans_acc.cpp:41 TestLog|hello world -8
D 2022-9-2 14:57:51.755085 trans_acc.cpp:42 TestLog|abc 333 345
W 2022-9-2 14:57:51.755090 trans_acc.cpp:43 TestLog|I am adson, age 30, go go go
E 2022-9-2 14:57:51.755095 trans_acc.cpp:44 TestLog|Hello, Fmt
F 2022-9-2 14:57:51.755103 trans_acc.cpp:45 TestLog|this is fatal
依赖头文件
最小依赖的几个头文件。iostream用于向标准输出打日志。sstream用于格式化日志。time用于在日志中打印日期。
#pragma once
#include <iostream>
#include <sstream>
#include <sys/time.h>
日志级别
6种日志级别,分级在日志中很重要。避免日志太多。这里可以在编译的时候指定日志级别。 大于DEFAULT级别的日志不会走格式,也不会输出。不消耗性能。
打印日志头
日志头包括日志级别,日期时间,文件名,函数名,行号。日志级别用FEWIDT代表6种日志级别。文件名用__FILE__取得,函数名用__FUNCTION__取得,行号用__LINE__取得。因为要用这些宏,这也就是为什么C++打日志必须用宏的原因。日期和时间能精确到毫秒。使用gettimeofday函数获取,并使用线程安全的localtime_r函数来转为本地时区的日期和时间。
Log &Print(LogLevel lvl, const char *file_name, const char *func_name, int line_no){
cur_level = lvl;
if(cur_level > level) return *this; #判断级别
const char *p = "FEWIDT";
stream << p[lvl] << ' ';
log_time();
stream << file_name << ':' << line_no << ' ' << func_name << "|";
return *this;
}
printf格式输出日志
C/C++中大家熟悉的printf,用起来很方便。用格式化字符串"%d %s %.2f"来格式化日志很方便。这里通过重载operator()来实现printf的支持。LOG(INFO)("%s %d", "hello", 123);
template<typename ...Args>
Log &operator()(const char *fmt, Args... args){
if(cur_level > level) return *this;
snprintf(log_data, BUF_SIZE, fmt, args...); //先打印到buf中,再输出到stream.性能略差。
stream << log_data;
return *this;
}
C++ stream 方式输出日志
printf的方便输出日志有个缺陷,不同的类型要用不同的占位符,如果不小把%s对应到整数上,会报错。让使用者不胜其烦。而C++的iostream里可以用<<来输出任意类型。这里也支持了。重载了operator<<就能达到这种效果。LOG(INFO) << "abc" << 123;
template<typename T>
Log &operator<<(const T &a) {
if(cur_level > level) return *this;
stream << a;
return *this;
}
format 形式输出字符串
c++20开始在标准库支持std::format了。这里自己实现了std::format的部分功能。它也解决了Printf中%d,%s容易弄错的问题。只要LOG(INFO).Fmt("hello {}, {}", "world", 123);即可实现printf的功能。使用C++11的不定数量模板参数实现。加上模板函数与重载与递归。
template<typename ...Args, typename First>
Log &Fmt(std::string fmt, First first, Args... args){
if(cur_level > level) return *this;
for(size_t i = 0; i < fmt.size() - 1; i++){
if(fmt[i] == '{' && fmt[i+1] == '}'){
stream << fmt.substr(0, i) << first;
return Fmt(fmt.substr(i+2, fmt.size()), args...);
}
}
return *this;
}
Log &Fmt(std::string fmt){
if(cur_level > level) return *this;
stream << fmt;
return *this;
}
LogManager
对Log对象进行管理。Log对象比较重,有stream, buf_data等重对象。不能频繁创建和销毁。每次打日志创建一个轻量的LogManager对象,用户可以持有此对象在循环中进行打日志。在LogManager销毁时会把已经缓存的日志Flush到stdout。
class LogManager{
public:
LogManager(){
}
Log* GetLog() {
static thread_local Log log;
return &log;
}
~LogManager(){
GetLog()->Flush();
}
};
日志输出宏
主要是为了打印行号等宏才能实现的功能。Log类在实现时如Flush, 等使用了虚函数,可以把gflags等现有的日志库封装到子类中使用。
#define LOG(lvl)\
LogManager().GetLog()->Print(lvl, __FILE__, __FUNCTION__, __LINE__)
全量代码,复制粘贴用去吧
#pragma once
#include <iostream>
#include <sstream>
#include <sys/time.h>
namespace AdsonLib
{
enum LogLevel{
FATAL = 0,
ERROR = 1,
WARNING = 2,
INFO = 3,
DEBUG = 4,
TRACE = 5,
};
#ifndef DEFAULT_LOG_LEVEL
#define DEFAULT_LOG_LEVEL TRACE
#endif
class Log {
public:
Log(LogLevel lvl = (LogLevel)DEFAULT_LOG_LEVEL):level(lvl){}
int ResetLevel(LogLevel lvl){
std::swap(lvl, level);
level = lvl;
return lvl;
}
int GetLevel() const {
return level;
}
Log &Print(LogLevel lvl, const char *file_name, const char *func_name, int line_no){
cur_level = lvl;
if(cur_level > level) return *this;
const char *p = "FEWIDT";
stream << p[lvl] << ' ';
log_time();
stream << file_name << ':' << line_no << ' ' << func_name << "|";
return *this;
}
template<typename ...Args>
Log &operator()(const char *fmt, Args... args){
if(cur_level > level) return *this;
snprintf(log_data, BUF_SIZE, fmt, args...);
stream << log_data;
return *this;
}
template<typename ...Args, typename First>
Log &Fmt(std::string fmt, First first, Args... args){
if(cur_level > level) return *this;
for(size_t i = 0; i < fmt.size() - 1; i++){
if(fmt[i] == '{' && fmt[i+1] == '}'){
stream << fmt.substr(0, i) << first;
return Fmt(fmt.substr(i+2, fmt.size()), args...);
}
}
return *this;
}
Log &Fmt(std::string fmt){
if(cur_level > level) return *this;
stream << fmt;
return *this;
}
virtual void Flush() {
if(cur_level <= level){
if(cur_level >= WARNING){
std::cerr << stream.str() << std::endl;
}else{
std::cout << stream.str() << std::endl;
}
}
stream.str("");
stream.flush();
}
virtual ~Log(){
delete log_data;
}
template<typename T>
Log &operator<<(const T &a) {
if(cur_level > level) return *this;
stream << a;
return *this;
}
void log_time()
{
struct timeval tv;
struct tm tm;
gettimeofday(&tv, NULL);
localtime_r(&tv.tv_sec, &tm);
stream << tm.tm_year + 1900 << '-' << tm.tm_mon + 1 << '-' << tm.tm_mday << ' '
<< tm.tm_hour << ':' << tm.tm_min << ":" << tm.tm_sec << '.' << tv.tv_usec << ' ';
}
private:
const int BUF_SIZE = 4096;
std::ostringstream stream;
LogLevel level;
LogLevel cur_level;
char *log_data = new char[BUF_SIZE];
};
class LogManager{
public:
LogManager(){
}
Log* GetLog() {
static thread_local Log log;
return &log;
}
~LogManager(){
GetLog()->Flush();
}
};
#define LOG(lvl)\
LogManager().GetLog()->Print(lvl, __FILE__, __FUNCTION__, __LINE__)
} //namespace AdsonLib