日志模块
需求分析
随着C++的普及,人手WebServer的现象越来越严重,于是,笔者出此一文。缓解该现象的恶化。
为什么需要日志?
这里我们不扯大的方面,就拿我们将要写的服务器来讲,日志模块就是服务器的基础模块,在服务器长期稳定运行的过程中,都会追踪有哪些客户对该台服务器发起了请求,对于概率性error事件,可以在重复测试时通过日志来查询错误复现时候的情况。记录error或者crash时的信息(时间、关键变量的值、出错的文件及行号、线程等)简言之,日志是跟踪和回忆某个时刻或者时间段内的程序行为进而定位问题的一种重要手段。
日志系统的设计
日志的实现有同步和异步两种方式,两种方式优缺点如下:
优点 | 缺点 | |
---|---|---|
异步日志 | 执行效率更高,一般应用程序只用将日志输出到一块缓存中,由另起的线程将缓存中的日志输出到磁盘上,减少了系统的IO负担 | 当系统崩溃时,容易丢失内存中来不及写入的日志。日志本身的代码实现、调试更复杂 |
同步日志 | 事件发生就输出,系统崩溃不会出现丢日志的情况,日志输出顺序可控,代码实现简单 | 效率更低,增加系统IO负担 |
笔者只实现了同步日志,要实现异步日志,只要继承LogAppender接口即可
日志级别
class LogLevel{
public:
enum Level{
UNKNOW = 0, //未设置
DEBUG = 1, //调试信息
INFO = 2, //一般信息
WARONG = 3, //警告信息
ERROR = 4, //错误信息
FATAL = 5 //致命信息,一般配合ASSRT使用,触发该信息,系统自接退出
};
public:
static const std::string ToString(LogLevel::Level level);
};
作用
- 以便Logger、Appender对象输出日志级别高于自身设定值的日志,达到忽视掉级别低于设定值的日志的效果(日志开关)。
- 方便维护人员按分类过滤查看日志。
日志事件
class LogEvent{
public:
typedef std::shared_ptr<LogEvent> ptr;
public:
LogEvent(const char* file, uint32_t line, .../*省略*/);
const char* getFile()const { return m_file; }
uint32_t getLine()const { return m_line; }
/*
其他私有成员的get方法省略
。。。。。。。。。。
*/
private:
//文件名
const char* m_file;
//行号
uint32_t m_line;
//程序启动到现在的秒数
uint32_t m_elapse;
//线程号
uint32_t m_threadId;
//协程号
uint32_t m_fiberId;
//时间戳
uint64_t m_timeStamp;
//线程名
std::string m_threadName;
//日志消息
std::stringstream m_msg;
//日志级别
LogLevel::Level m_level;
//输出该条日志的logger对象
std::shared_ptr<Logger> m_logger;
};
解析
记录了文件名、行号、线程号、协程号、时间戳等信息,利用m_logger对象输出该条日志。
日志包装器
示例一
class LogEventWrap{
public:
typedef std::shared_ptr<LogEventWrap> ptr;
public:
LogEventWrap(LogEvent::ptr event);
virtual ~LogEventWrap();
std::stringstream& getMsg();
private:
LogEvent::ptr m_event;
};
示例二
#define LUNAR_LOG_LEVEL(logger, level) \
if((logger)->getLevel() <= level) \
(lunar::LogEventWrap(lunar::LogEvent::ptr(new lunar::LogEvent(__FILE__, __LINE__,\
lunar::GetElapse(), lunar::GetThreadId(),\
lunar::GetFiberId(), time(nullptr),\
lunar::Thread::GetName(), level, (logger))))).getMsg()
#define LUNAR_LOG_DEBUG(logger) LUNAR_LOG_LEVEL(logger, lunar::LogLevel::Level::DEBUG)
#define LUNAR_LOG_INFO(logger) LUNAR_LOG_LEVEL(logger, lunar::LogLevel::Level::INFO)
#define LUNAR_LOG_WARONG(logger) LUNAR_LOG_LEVEL(logger, lunar::LogLevel::Level::WARONG)
#define LUNAR_LOG_ERROR(logger) LUNAR_LOG_LEVEL(logger, lunar::LogLevel::Level::ERROR)
#define LUNAR_LOG_FATAL(logger) LUNAR_LOG_LEVEL(logger, lunar::LogLevel::Level::FATAL)
#define LUNAR_LOG_ROOT() lunar::LoggerMgr::GetInstance()->getRoot()
#define LUNAR_LOG_NAME(name) lunar::LoggerMgr::GetInstance()->getLoggerByName(name)
使用方式
void test_log(){
LUNAR_LOG_DEBUG(LUNAR_LOG_ROOT()) << "hello";
LUNAR_LOG_DEBUG(LUNAR_LOG_NAME("system")) << "system hello";
}
作用
- 配合LogEvent对象,产生一个临时匿名LogEventWrap对象,因为匿名对象生命周期是一行,故可以在析构函数调用m_event.getLogger()获得LogEvent对象的m_logger成员,从而利用m_logger输出LogEvent对象,(从而简化了日志的使用)。具体实现参考示例二。
日志格式化器
参考代码
class LogFormatter{
public:
typedef std::shared_ptr<LogFormatter>ptr;
public:
class FormatItem{
public:
typedef std::shared_ptr<FormatItem> ptr;
public:
virtual void format(LogEvent::ptr event, std::ostream& os) = 0;
};
public:
LogFormatter(const std::string &pattern = "%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%f%T[%p]%T[%c]%T%F:%l%T%m%n");
void format(LogEvent::ptr event, std::ostream& os);
private:
void init();
private:
std::vector<FormatItem::ptr> m_items;
std::string m_pattern;
};
解析
LogFormatter定义了一个内部类FormatItem,像LogEvent里面的每一项成员的输出,都对应一个继承实现FormatItem的类,这种做法更加精细了各个类的分工,LogFormatter有一个类型是std::vectorFormatItem::ptr的成员,该成员由用户输入的pattern字符串决定,pattern字符串每个格式化字符子串亦对应LogEvent里面的成员。
class FileFormatItem : public LogFormatter::FormatItem{
public:
FileFormatItem(const std::string& str) { };
virtual void format(LogEvent::ptr event, std::ostream& os) override{
os << event->getFile();
}
};
class LineFormatItem : public LogFormatter::FormatItem{
public:
LineFormatItem(const std::string& str) { };
virtual void format(LogEvent::ptr event, std::ostream& os) override{
os << event->getLine();
}
};
//其他格式类省略...
格式化子串 | 作用 |
---|---|
%s | 用户在pattern中加的修饰字符,该字符原样原位输出 |
%d | 日期时间,后面可跟一对括号指定时间格式,比如%d{%Y-%m-%d %H:%M:%S},这里的格式字符与C语言strftime一致 |
%t | 线程id |
%N | 线程名称 |
%f | 协程id |
%p | 日志级别 |
%c | 日志器名称 |
%F | 文件名 |
%l | 行号 |
%m | 消息 |
%T | 制表符 |
%n | 换行 |
%r | 该日志器创建后的累计运行毫秒数 |
格式化子串的解析,以及到FormatItem派生类的映射由init成员函数完成,解析的过程采用的是简单的状态机,解析完成后,每个格式化子串经hash表映射的回调函数产生对应FormatItem派生类的,填充m_items成员,格式化子串类可以通过花括号传参,如时间的解析具体格式可以用花括号指定
"%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%f%T[%p]%T[%c]%T%F:%l%T%m%n"
例如,以上pattern串效果如下:
日志输出器
class LogAppender{
public:
typedef std::shared_ptr<LogAppender> ptr;
typedef Mutex MutexType;
public:
virtual ~LogAppender() { };
virtual void log(LogEvent::ptr event) = 0;
public:
//返回值使用期间,m_formatter可能被更改
LogFormatter::ptr getFormatter();
void setFormatter(LogFormatter::ptr fmt);
protected:
LogLevel::Level m_level = LogLevel::Level::DEBUG;
LogFormatter::ptr m_formatter;
MutexType m_mutex;
};
class FileLogAppender : public LogAppender{
public:
FileLogAppender(const char* name = "log.txt");
virtual ~FileLogAppender() override;
virtual void log(LogEvent::ptr event) override;
private:
void reopen();
private:
std::ofstream m_of;
std::string m_prefixName;
std::string m_fileName;
uint32_t m_logCount;
time_t m_lastFlush;
};
class StdoutLogAppender : public LogAppender{
public:
virtual void log(LogEvent::ptr event) override;
virtual ~StdoutLogAppender() override{ };
};
解析
故名思意,就是将日志最终输出到哪里(是某个文件、控制台终端、或者数据库等),这里要实现自己的日志输出地只需继承LogAppender,实现log虚基类即可,格式化输出前,会判断事件级别,是否高于或等于自身级别,只有高于或等于自身级别的日志才输出。时间原因,笔者只实现了同步日志的终端输出、文件输出,感兴趣的伙伴可以参考muduo的异步日志,继承LogAppender实现异步的日志输出。
日志器
class Logger{
public:
typedef std::shared_ptr<Logger> ptr;
typedef RWMutex MutexType;
public:
void log(LogEvent::ptr event);
public:
Logger(const std::string& name = "system");
LogLevel::Level getLevel()const { return m_level; }
void setLevel(LogLevel::Level level) { m_level = level; }
void addAppender(LogAppender::ptr apd);
void delAppender(LogAppender::ptr apd);
LogFormatter::ptr getFormatter();
void setFormatter(LogFormatter::ptr fmt);
std::string getName();
void setName(const std::string& name);
private:
std::list<LogAppender::ptr> m_Appenders;
LogLevel::Level m_level = LogLevel::Level::DEBUG;
LogFormatter::ptr m_formatter;
std::string m_name;
MutexType m_mutex;
};
解析
考虑到一条日志可能有多种去出(终端、文件、数据库等)Logger类才是提供给用户输出日志的接口,Logger::log也会对事件的级别和自身的级别左判断,只有高于或等于自身级别的日志,才会依次调用m_Appenders中每个日志输出器来输出日志。
日志各个类关系图
测试程序
void test_log(){
lunar::Logger::ptr logger(new lunar::Logger());
lunar::LogFormatter::ptr formatter(new lunar::LogFormatter("[%p]%T[%N:%T%t]%T[Fiber ID :%T%f]%T%F:%l%n"));
logger->setFormatter(formatter);
logger->addAppender(lunar::LogAppender::ptr(new lunar::StdoutLogAppender()));
LUNAR_LOG_DEBUG(logger) << "hello logger";
}
结果如下
一些奇怪编程方式的解释
1. C++类的成员对象定义成指针有什么好处?
-
动态内存管理:使用指针可以在程序运行时动态地分配和释放内存,这对于需要灵活管理对象的大小和生命周期的情况非常有用。
-
延迟初始化:通过将成员对象定义为指针,可以将对象的实际创建推迟到需要的时候。这可以提高程序的性能,避免不必要的对象创建和销毁。
-
对象共享:多个类实例可以共享同一个对象,通过指向同一个对象的指针来实现。这在某些情况下可以节省内存,并且可以确保多个对象之间的状态一致性。
-
多态性支持:指针可以用于实现多态性,允许在运行时根据对象的实际类型来调用相应的方法。这对于实现面向对象编程中的继承和多态性非常有用。
需要注意的是,使用指针也带来了一些额外的复杂性和风险,如空指针引用和内存泄漏等。因此,在使用指针时需要小心处理,并确保正确地管理内存和处理指针的生命周期。
总结
- 为保证日志模块的安全性,已对日志模块必要的地方加锁。
- 学习日志模块可以帮助学习实践很多设计模式。