早在19年5月就在某站上看到sylar的视频了,一直认为这是一个非常不错的视频,还有幸加了sylar本人的wx,由于本人一直是自学编程,基础不扎实,也没有任何人的督促,没能坚持下去,每每想起倍感惋惜。恰逢互联网寒冬,在家无事,遂提笔再续前缘。
为了能更好的看懂sylar,本套笔记会分两步走,每个系统都会分为两篇博客。
分别是【知识储备篇】和【代码分析篇】
(ps:纯粹做笔记的形式给自己记录下,欢迎大家评论,不足之处请多多赐教)
本篇内容很细,与原有代码有偏差,带有自己理解的情况下编写的代码。
本文重在 “思考如何思考”,这样才能把知识消化在自己的 “胃” 里
当然这里时刻提醒自己要做 “黄昏中起飞的猫头鹰”
日志管理-代码分析
1.类图概括
首先列出日志系统中的类,这里只是简单列出大致的关系并不是最正确的"类图"(或者算不上"类图")
列出这张图意在有一个大致的了解,便于后续的理解。
2.明确目的
想必大家都感觉某站上的视频难啃吧。除了视频声音较小之外更重要的是UP主在讲解时并没有先说明要做什么,要做成什么样,也就是大家不明白目的是什么。导致视频开始时处于一脸懵的状态,没熬到最后就不知道在说什么。最近看到一个视频是关于雷军的,大致意思是:“我读书时也有不懂得地方,直接跳过,通篇读完会恍然大悟,然后再读一边。” 这句话多少给了我点动力,所以打算开始写这一系列的博客。
言归正传,日志系统无非就是:
需要一个方法输出指定的信息方便我们观察程序的运行状态
最简单的方法:
//c++ 风格日志输出
std::cout << "Good good study, day day up!" << std::endl;
//c 风格日志输出
printf("My name is XYZ");
首先我们看看这个方式缺什么,我们一步一步完善。
首先我们需要对日志进行管理(虽然暂时不知道具体要管理什么)但是起码要有日志类 Logger 不要多想先来个空类,一个空类需要考虑 :
0.是否需要私有字段 (我想日志管器肯定是要多个的,用于区别,起码要有名称 所以来个名称字段)
1.是否需要智能指针 (明显这个类是需要被实例化的,这里推荐使用智能指针)
2.是否需要有参构造 (应该是需要的,我们在创建日志器的时候怎么说也要给个名字的)
3.是否需要析构函数 (目前没有想到要析构的内容所以先空着)
4.是否需要get/set方法 (应该是需要的,起码我们之后是希望能拿到日志器名称的)
5.是否需要什么方法 (应该是需要的,起码要一个日志输出的方法)
综上所述我们可以这么定义:
#include <string> //日志名称用到了字符串
#include <memory> //智能指针所需的头文件
class Logger{
public:
//定义智能指针
typedef std::shared_ptr<Logger> ptr;
//日志名称有且仅有构造时可以指定,如果未指定则给个默认名称 root
//这里传引用类型是为了避免不必要的拷贝操作
//使用const是为了规定这个名称在内部不再会被改变
Logger(const std::string& name = "root");
//希望名称能被获取 但不应该被改变所以用const 为避免无用的拷贝所以返回引用类型
const std::string& getName() const { return m_name; }
//定义了一个输出日志的方法 但不清楚参数和具体实现
void log();
private:
//这里使用 m_ 开头是一个私有变量的规范
std::string m_name;
};
//头文件中已经有默认值定义了 这里就不需要了(也就是不需要 name="root"),否则编译器会报错
Logger::Logger(const std::string& name)
:m_name(name){
}
void Logger::log(){}
好然后我们分析要管理什么,第一个想到的就是日志级别,目前还不清楚怎么管理级别,但是能确定级别肯定是需要定义的,那么先把级别定义出来再说,作为程序员,日志级别有哪些还是能猜出来的,没吃过猪肉总见过猪跑吧:
//一般来说就是用枚举的方式来定义
class LogLevel{
public:
enum Level{
UNKNOW = 0, //起手先来个未知级别兜底
DEBUG = 1, //调试级别
INFO = 2, //普通信息级别
WARN = 3, //警告信息
ERROR = 4, //错误信息
FATAL = 5 //灾难级信息
};
};
再有就是,我们想要输出什么呢?仅仅是简单的文本吗?NO! 至少我们想要的是这种格式:
// 时间 线程id 线程名称 协程id [日志级别] [日志名称] 文件名:行号: 消息 换行符号
2023-11-07 10:06:00 2048 thread_name 1024 [INFO] [logger] /apps/sylar/tests/test_log.cc:40 消息内容
我们把这里的信息分成两类:、
1.一类是辅助信息:时间 、线程id、线程名称、协程id、[日志级别]、[日志名称]、文件名:行号、换行符号
2.一类是业务信息:消息
我们何不定义一个LogEvent来承载这些信息呢?之后我们就可以以这个对象来传递要输出的信息了。
所以我们封装一下就有了LogEvent类:
#include <stdint.h>
class LogEvent {
public:
typedef std::shared_ptr<LogEvent> ptr;
LogEvent(LogLevel::Level level
,const char* file, int32_t m_line, uint32_t elapse
, uint32_t thread_id, uint32_t fiber_id, uint64_t time);
const char* getFile() const { return m_file;}
int32_t getLine() const { return m_line;}
uint32_t getElapse() const { return m_elapse;}
uint32_t getThreadId() const { return m_threadId;}
uint32_t getFiberId() const { return m_fiberId;}
uint64_t getTime() const { return m_time;}
LogLevel::Level getLevel() const { return m_level;}
private:
const char* m_file = nullptr; //文件名
int32_t m_line = 0; //行号
uint32_t m_elapse = 0; //程序启动开始到现在的毫秒数
uint32_t m_threadId = 0; //线程id
uint32_t m_fiberId = 0; //协程id
uint64_t m_time = 0; //时间戳
LogLevel::Level m_level; //日志级别
};
现在我们日志器有了,级别也定义了,日志事件也有了,那肯定要把三者联系起来。
我们需要给日志器本身一个级别,标记这个日志器能输出的最大的日志级别。
我们还需要给日志器的输出方法传递一个当前要查看的日志级别,用来限制查看的内容。例:void log(LogLevel::Level level);
由于我们在日志事件中已经定义了级别,所以我们只需要这么写:void log(LogEvent event);
以上描述我们简单解释以下:
1.比如我们定义日志器的最大可输出级别为 INFO 那么证明日志器有能力输出 INFO及其之下的级别的日志(也就是该日志器有能力输出 UNKNOW 、DEBUG 、INFO级别的日志)
2.如果我们传入想要查看的最大级别是 DEBUG 那么最终我们能看到(UNKNOW 、 DEBUG)这两个级别的日志虽然这个日志器有能力输出三个级别 但我们指定了最大级别 那么就要以我们指定的为主。
3.如果我们传入的最大日志级别是ERROR,那么由于这个日志器最大日志输出级别只有到 INFO级的所以我们仍旧只能看到(UNKNOW 、 DEBUG 、INFO)这三级。
好,经过以上分析可以确定,日志器需要一个级别字段作为自身输出的最大能力,输出方法也要一个级别作为参数作为想要指定查看的最大级别。
那么改造后如下:
#include <iostream>
#include <string> //日志名称用到了字符串
#include <memory> //智能指针所需的头文件
#include <stdint.h>
class LogLevel{
public:
enum Level{
UNKNOW = 0, //起手先来个未知级别兜底
DEBUG = 1, //调试级别
INFO = 2, //普通信息级别
WARN = 3, //警告信息
ERROR = 4, //错误信息
FATAL = 5 //灾难级信息
};
};
class LogEvent {
public:
typedef std::shared_ptr<LogEvent> ptr;
LogEvent(LogLevel::Level level
,const char* file, int32_t m_line, uint32_t elapse
, uint32_t thread_id, uint32_t fiber_id, uint64_t time);
const char* getFile() const { return m_file;}
int32_t getLine() const { return m_line;}
uint32_t getElapse() const { return m_elapse;}
uint32_t getThreadId() const { return m_threadId;}
uint32_t getFiberId() const { return m_fiberId;}
uint64_t getTime() const { return m_time;}
LogLevel::Level getLevel() const { return m_level;}
private:
LogLevel::Level m_level; //日志级别
const char* m_file = nullptr; //文件名
int32_t m_line = 0; //行号
uint32_t m_elapse = 0; //程序启动开始到现在的毫秒数
uint32_t m_threadId = 0; //线程id
uint32_t m_fiberId = 0; //协程id
uint64_t m_time = 0; //时间戳
};
//这里要注意,最好参数列表顺序和私有字段顺序对应,有些版本的编译器需要这样规定
LogEvent::LogEvent(LogLevel::Level level
,const char* file, int32_t line, uint32_t elapse
, uint32_t thread_id, uint32_t fiber_id, uint64_t time)
:m_level(level)
,m_file(file)
,m_line(line)
,m_elapse(elapse)
,m_threadId(thread_id)
,m_fiberId(fiber_id)
,m_time(time){
}
class Logger{
public:
//定义智能指针
typedef std::shared_ptr<Logger> ptr;
//日志名称有且仅有构造时可以指定,如果未指定则给个默认名称 root
//这里传引用类型是为了避免不必要的拷贝操作
//使用const是为了规定这个名称在内部不再会被改变
Logger(const std::string& name = "root");
//希望名称能被获取 但不应该被改变所以用const 为避免无用的拷贝所以返回引用类型
const std::string& getName() const { return m_name; }
LogLevel::Level getLevel() const { return m_level; }
void setLevel(LogLevel::Level val) {m_level = val;}
//定义了一个输出日志的方法 传入想要查看的最大日志级别
void log(LogEvent::ptr event);
private:
//这里使用 m_ 开头是一个私有变量的规范
std::string m_name;
//日志器能输出的最大日志级别
LogLevel::Level m_level;
};
//头文件中已经有默认值定义了 这里就不需要了(也就是不需要 nmae="root"),否则编译器会报错
Logger::Logger(const std::string& name)
:m_name(name)
//这里指定日志器一个自身默认级别是DEBUG
,m_level(LogLevel::DEBUG){
}
void Logger::log(LogEvent::ptr event){
//如果想要查看的级别大于等于当前日志器能查看的级别,那么才进行输出
if(event->getLevel() >= m_level){
std::cout << "日志输出模拟" << std::endl;
}
}
//此时我们可以写一个测试类试试
int main(int argc,char** argv){
//创建一个日志事件(这里的内容随便定义,因为我们没有真正用到它)
LogEvent::ptr event(new LogEvent(LogLevel::WARN,0, 1, 2, 3,4, time(0)));
Logger::ptr lg(new Logger("XYZ"));
//由于默认是DEBUG级别 WARN>DEBUG 所以这里会输出
lg->log(event); //日志输出模拟
//将日志器级别改为ERROR
lg->setLevel(LogLevel::ERROR);
//此时 WARN<ERROR 所以不会输出任何信息
lg->log(event);
return 0;
}
接下来我们再来思考,以上代码关于级别的已经初步处理了,我们还缺少什么?
是的 没错 线上环境不仅仅是将日志打印在控制台,还需要能打印到本地磁盘,或者远程日志服务器等等。
所以我们需要在Logger类中进行设计,起码需要有两种类型的“适配器”来承接各自的业务。
1.一个适配器负责打印日志到控制台 StdoutLogAppender
2.另一个负责打印到本地磁盘文件 FileLogAppender
3.为了未来的扩展性,所以这里要抽象一个基类来实现多态 LogAppender
编写适配器基类:LogAppender
所以接下来思考,LogAppender类需要什么。
0.是否需要私有字段 (目前好像不需要)
1.是否需要智能指针 (明显这个类是需要使用该类指针指向子类对象的所以肯定需要)
2.是否需要有参构造 (目前好像不需要)
3.是否需要析构函数 (应该是需要的,因为这个是基类需要被继承,不仅仅要析构函数,还要虚析构!!!)
4.是否需要get/set方法 (目前好像不需要,连私有字段都没更不用get/set了)
5.是否需要什么方法 (应该是需要的,起码要一个日志输出的方法而且没有实现体,应该定义为纯虚函数)
好!老办法开始写:
class LogAppender{
public:
//定义智能指针
typedef std::shared_ptr<LogAppender> ptr;
//虚析构 空函数没有复杂逻辑所以 直接定义掉
virtual ~LogAppender(){}
//输出函数为纯虚函数,因为具体实现各个子类不一样,由各个子类自己决定
virtual void log(LogEvent::ptr event) = 0;
};
编写输出到控制台适配器类:StdoutLogAppender
//输出到控制台的Appender
class StdoutLogAppender : public LogAppender {
public:
typedef std::shared_ptr<StdoutLogAppender> ptr;
//这里的override用于表示是重写父类方法的
void log(LogEvent::ptr event) override;
};
void StdoutLogAppender::log(LogEvent::ptr event){
std::cout << "输出到控制台" << std::endl;
}
编写输出到文件适配器类:FileLogAppender
暂时不需要做文件操作,只要先写这个类占位就行,也就是和上面的StdoutLogAppender一样先输出到控制台就行,具体文件操作留给后面完成(先占位,后补全)
//定义输出到文件的Appender
class FileLogAppender : public LogAppender {
public:
typedef std::shared_ptr<FileLogAppender> ptr;
FileLogAppender(const std::string& filename);
void log(LogEvent::ptr event) override;
private:
std::string m_filename;
};
FileLogAppender::FileLogAppender(const std::string& filename)
:m_filename(filename) {
}
void FileLogAppender::log(LogEvent::ptr event) {
std::cout << "输出到文件:" << m_filename << std::endl;
}
那么问题来了 适配器类在哪调用呢?
必然是Logger类的log方法内部咯。
因为一个Logger类可以输出日志到多个地方,所以可以有多个LogAppender。
那么我们就需要有个列表容器来保存多个LogAppender,并且提供新增和删除的方法。
如下改造 直接贴上全部代码:
#include <iostream>
#include <string> //日志名称用到了字符串
#include <memory> //智能指针所需的头文件
#include <stdint.h>
#include <list>
class LogLevel{
public:
enum Level{
UNKNOW = 0, //起手先来个未知级别兜底
DEBUG = 1, //调试级别
INFO = 2, //普通信息级别
WARN = 3, //警告信息
ERROR = 4, //错误信息
FATAL = 5 //灾难级信息
};
};
class LogEvent {
public:
typedef std::shared_ptr<LogEvent> ptr;
LogEvent(LogLevel::Level level
,const char* file, int32_t m_line, uint32_t elapse
, uint32_t thread_id, uint32_t fiber_id, uint64_t time);
const char* getFile() const { return m_file;}
int32_t getLine() const { return m_line;}
uint32_t getElapse() const { return m_elapse;}
uint32_t getThreadId() const { return m_threadId;}
uint32_t getFiberId() const { return m_fiberId;}
uint64_t getTime() const { return m_time;}
LogLevel::Level getLevel() const { return m_level;}
private:
LogLevel::Level m_level; //日志级别
const char* m_file = nullptr; //文件名
int32_t m_line = 0; //行号
uint32_t m_elapse = 0; //程序启动开始到现在的毫秒数
uint32_t m_threadId = 0; //线程id
uint32_t m_fiberId = 0; //协程id
uint64_t m_time = 0; //时间戳
};
LogEvent::LogEvent(LogLevel::Level level
,const char* file, int32_t line, uint32_t elapse
, uint32_t thread_id, uint32_t fiber_id, uint64_t time)
:m_level(level)
,m_file(file)
,m_line(line)
,m_elapse(elapse)
,m_threadId(thread_id)
,m_fiberId(fiber_id)
,m_time(time){
}
class LogAppender{
public:
//定义智能指针
typedef std::shared_ptr<LogAppender> ptr;
//虚析构 空函数没有复杂逻辑所以 直接定义掉
virtual ~LogAppender(){}
//输出函数为纯虚函数,因为具体实现各个子类不一样,由各个子类自己决定
virtual void log(LogEvent::ptr event) = 0;
};
//输出到控制台的Appender
class StdoutLogAppender : public LogAppender {
public:
typedef std::shared_ptr<StdoutLogAppender> ptr;
//这里的override用于表示是重写父类方法的
void log(LogEvent::ptr event) override;
};
void StdoutLogAppender::log(LogEvent::ptr event){
std::cout
<< event->getTime() << " "
<< event->getThreadId() << " "
<< event->getFiberId() << " "
<< "["
<< event->getLevel()
<< "] "
<< event->getFile() << ":" << event->getLine() << " "
<< "输出到控制台的信息"
<< std::endl;
}
class FileLogAppender : public LogAppender {
public:
typedef std::shared_ptr<FileLogAppender> ptr;
FileLogAppender(const std::string& filename);
void log(LogEvent::ptr event) override;
private:
std::string m_filename;
};
FileLogAppender::FileLogAppender(const std::string& filename)
:m_filename(filename) {
}
void FileLogAppender::log(LogEvent::ptr event) {
std::cout << "输出到文件:" << m_filename << std::endl;
}
class Logger{
public:
//定义智能指针
typedef std::shared_ptr<Logger> ptr;
//日志名称有且仅有构造时可以指定,如果未指定则给个默认名称 root
//这里传引用类型是为了避免不必要的拷贝操作
//使用const是为了规定这个名称在内部不再会被改变
Logger(const std::string& name = "root");
//希望名称能被获取 但不应该被改变所以用const 为避免无用的拷贝所以返回引用类型
const std::string& getName() const { return m_name; }
LogLevel::Level getLevel() const { return m_level; }
void setLevel(LogLevel::Level val) {m_level = val;}
//定义了一个输出日志的方法 传入想要查看的最大日志级别
void log(LogEvent::ptr event);
//新增一个适配器
void addAppender(LogAppender::ptr appender);
//删除一个适配器
void delAppender(LogAppender::ptr appender);
private:
//这里使用 m_ 开头是一个私有变量的规范
std::string m_name;
//日志器能输出的最大日志级别
LogLevel::Level m_level;
//Appender集合
std::list<LogAppender::ptr> m_appenders;
};
//头文件中已经有默认值定义了 这里就不需要了(也就是不需要 nmae="root"),否则编译器会报错
Logger::Logger(const std::string& name)
:m_name(name)
//这里指定日志器一个自身默认级别是DEBUG
,m_level(LogLevel::DEBUG){
}
void Logger::addAppender(LogAppender::ptr appender) {
m_appenders.push_back(appender);
}
void Logger::delAppender(LogAppender::ptr appender) {
for(auto it = m_appenders.begin();
it != m_appenders.end(); ++it) {
if(*it == appender) {
m_appenders.erase(it);
break;
}
}
}
void Logger::log(LogEvent::ptr event){
//如果想要查看的级别大于等于当前日志器能查看的级别,那么才进行输出
if(event->getLevel() >= m_level){
for(auto& i : m_appenders) {
i->log(event);
}
}
}
//此时我们可以写一个测试类试试
int main(int argc,char** argv){
LogEvent::ptr event(new LogEvent(
LogLevel::INFO, //日志级别
"log.txt", //文件名称
1, //行号
1234567, //程序运行时间
2, //线程ID
3, //协程ID
time(0) //当前时间
));
Logger::ptr lg(new Logger("XYZ"));
//添加控制台输出适配器
StdoutLogAppender::ptr stdApd(new StdoutLogAppender());
lg->addAppender(stdApd);
//添加控制台输出适配器
FileLogAppender::ptr fileApd(new FileLogAppender("log.txt"));
lg->addAppender(fileApd);
//输出
lg->log(event);
return 0;
}
可以看到有以下输出:
1713584707 2 3 [2] log.txt:1 输出到控制台的信息
输出到文件:log.txt
其实到这为止,不知不觉中主体思路已经出来了。接下来我们继续分析继续完善。
这里可以看到 我们输出内容是不完善的
1.日志级别也不够人性化 我们需要显示 具体的名称而不是值
2.时间没有人性化的输出
3.文件名称和行号是我们自己输入的,这里需要自动获取才行
4.线程ID和协程ID也是需要自动获取(协程我们之后再处理)
5.一行输出的内容格式和顺序是我们写死的,这里希望能自己指定
综上所述,我们一步步来进行改造:
1.处理日志级别的人性化展示
如果看过【日志管理-知识储备篇】的话应该能立马想到LogLevel中我们可以实现一个ToString转换函数。
如下改造:
class LogLevel{
public:
enum Level{
UNKNOW = 0, //起手先来个未知级别兜底
DEBUG = 1, //调试级别
INFO = 2, //普通信息级别
WARN = 3, //警告信息
ERROR = 4, //错误信息
FATAL = 5 //灾难级信息
};
static const char* ToString(LogLevel::Level level);
};
const char* LogLevel::ToString(LogLevel::Level level){
switch(level) {
#define XX(name) \
case LogLevel::name: \
return #name; \
break;
XX(DEBUG);
XX(INFO);
XX(WARN);
XX(ERROR);
XX(FATAL);
#undef XX
default:
return "UNKNOW";
}
return "UNKNOW";
}
同样的在输出方法里调用 ToString 方法
void StdoutLogAppender::log(LogEvent::ptr event){
std::cout
<< event->getTime() << " "
<< event->getThreadId() << " "
<< event->getFiberId() << " "
<< "["
<< LogLevel::ToString(event->getLevel())
<< "] "
<< event->getFile() << ":" << event->getLine() << " "
<< "输出到控制台的信息"
<< std::endl;
}
最终可以得到以下输出信息,可以看到日志级别已经比较人性化了,能一眼看出是INFO级别:
1713586930 2 3 [INFO] log.txt:1 输出到控制台的信息
2.处理日志时间的人性化展示
看过我的 【日志管理-知识储备篇】的知识点05 应该知道怎么做:
void StdoutLogAppender::log(LogEvent::ptr event){
//格式化时间
const std::string format = "%Y-%m-%d %H:%M:%S";
struct tm tm;
time_t t = event->getTime();
localtime_r(&t, &tm);
char tm_buf[64];
strftime(tm_buf, sizeof(tm_buf), format.c_str(), &tm);
std::cout
//<< event->getTime() << " "
<< tm_buf << " "
<< event->getThreadId() << " "
<< event->getFiberId() << " "
<< "["
<< LogLevel::ToString(event->getLevel())
<< "] "
<< event->getFile() << ":" << event->getLine() << " "
<< "输出到控制台的信息"
<< std::endl;
}
最终可以看到时间也能一眼看懂了:
2024-04-20 04:47:54 2 3 [INFO] log.txt:1 输出到控制台的信息
3.处理日志输出文件:行号 自动获取
//此时我们可以写一个测试类试试
int main(int argc,char** argv){
//创建一个日志事件(这里的内容随便定义,因为我们没有真正用到它)
LogEvent::ptr event(new LogEvent(
LogLevel::INFO, //日志级别
__FILE__, //文件名称
__LINE__, //行号
1234567, //运行时间
0, //线程ID
0, //协程ID
time(0) //当前时间
));
Logger::ptr lg(new Logger("XYZ"));
//添加控制台输出适配器
StdoutLogAppender::ptr stdApd(new StdoutLogAppender());
lg->addAppender(stdApd);
//输出
lg->log(event);
return 0;
}
可以看到以下输出:
2024-04-20 04:53:35 2 3 [INFO] /apps/sylar/tests/test.cc:115 输出到控制台的信息
4.处理日志中 线程ID 的显示
看过【日志管理-知识储备篇】的应该也马上能知道怎么做
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
int main(int argc,char** argv){
//创建一个日志事件(这里的内容随便定义,因为我们没有真正用到它)
LogEvent::ptr event(new LogEvent(
LogLevel::INFO, //日志级别
__FILE__, //文件名称
__LINE__, //行号
1234567, //运行时间
syscall(SYS_gettid),//线程ID
0, //协程ID
time(0) //当前时间
));
Logger::ptr lg(new Logger("XYZ"));
//添加控制台输出适配器
StdoutLogAppender::ptr stdApd(new StdoutLogAppender());
lg->addAppender(stdApd);
//输出
lg->log(event);
return 0;
}
可以看到以下输出中 线程ID 也能自动获取了
2024-04-20 05:05:37 4294 3 [INFO] /apps/sylar/tests/test.cc:119 输出到控制台的信息
5.处理日志的输出格式(实现自定义格式和格式解析)
我们参造Log4j的方式,定义一个想要的格式。比如:
格式:"%d{%Y-%m-%d %H:%M:%S}%T%t%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"
* %m 消息
* %p 日志级别
* %r 累计毫秒数
* %c 日志名称
* %t 线程id
* %n 换行
* %d 时间
* %f 文件名
* %l 行号
* %T 制表符
* %F 协程id
我们可以定义一个 LogFormatter 来专门处理这个事情
老样子,我们设计一个 LogFormatter 类需要什么?
0.是否需要私有字段 (我想是需要一个接收模板字符串的字段的 m_pattern)
1.是否需要智能指针 (我想这是必要的)
2.是否需要有参构造 (应该是需要的,我们在构造的时候就可以指定模板字符串)
3.是否需要析构函数 (暂时没有需要)
4.是否需要get/set方法 (目前看来不需要)
5.是否需要什么方法 (应该是需要的)
---- 要一个初始化的方法,私有模板字符串解析出来
---- 要一个格式化的方法,传入日志事件,返回格式化后的字符串)
综上所述可以得出以下的类:
class LogFormatter{
public:
typedef std::shared_ptr<LogFormatter> ptr;
LogFormatter(const std::string& pattern);
void init();
std::string format(LogEvent::ptr event);
private:
std::string m_pattern;
};
LogFormatter::LogFormatter(const std::string& pattern)
:m_pattern(pattern){
//在初始化时就将pattern解析好
init();
}
//我们需要将模板字符串解析成 符号:子串:解析方式 的结构
//例如这个模板 "%d{%Y-%m-%d %H:%M:%S}%T%t%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"
//可以解析成
//符号 子串 解析方式 注释
//"d" "%Y-%m-%d %H:%M:%S" 1 #当前时间
//"T" "" 1 #制表(4空格)
//"t" "" 1 #线程ID
//"T" "" 1 #制表(4空格)
//"F" "" 1 #协程ID
//"T" "" 1 #制表(4空格)
//"[" "" 0 #普通字符
//"p" "" 1 #日志级别
//"]" "" 0 #普通字符
//"T" "" 1 #制表(4空格)
//"[" "" 0 #普通字符
//"c" "" 1 #日志器名称
//"]" "" 0 #普通字符
//"T" "" 1 #制表(4空格)
//"f" "" 1 #文件名称
//":" "" 0 #普通字符
//"l" "" 1 #行号
//"T" "" 1 #制表(4空格)
//"m" "" 1 #消息
//"n" "" 1 #换行
LogFormatter::init(){
//我们粗略的把上面的解析对象分成两类 一类是普通字符串 另一类是可被解析的
//可以用 tuple来定义 需要的格式 std::tuple<std::string,std::string,int>
//<符号,子串,类型> 类型0-普通字符串 类型1-可被解析的字符串
//可以用一个 vector来存储 std::vector<std::tuple<std::string,std::string,int> > vec;
std::vector<std::tuple<std::string,std::string,int> > vec;
//解析后的字符串
std::string nstr;
//循环中解析
for(size_t i = 0; i < m_pattern.size(); ++i) {
// 如果不是%号
// nstr字符串后添加1个字符m_pattern[i]
if(m_pattern[i] != '%') {
nstr.append(1, m_pattern[i]);
continue;
}
// m_pattern[i]是% && m_pattern[i + 1] == '%' ==> 两个%,第二个%当作普通字符
if((i + 1) < m_pattern.size()) {
if(m_pattern[i + 1] == '%') {
nstr.append(1, '%');
continue;
}
}
// m_pattern[i]是% && m_pattern[i + 1] != '%', 需要进行解析
size_t n = i + 1; // 跳过'%',从'%'的下一个字符开始解析
int fmt_status = 0; // 是否解析大括号内的内容: 已经遇到'{',但是还没有遇到'}' 值为1
size_t fmt_begin = 0; // 大括号开始的位置
std::string str;
std::string fmt; // 存放'{}'中间截取的字符
// 从m_pattern[i+1]开始遍历
while(n < m_pattern.size()) {
// m_pattern[n]不是字母 & m_pattern[n]不是'{' & m_pattern[n]不是'}'
if(!fmt_status && (!isalpha(m_pattern[n]) && m_pattern[n] != '{'
&& m_pattern[n] != '}')) {
str = m_pattern.substr(i + 1, n - i - 1);
break;
}
if(fmt_status == 0) {
if(m_pattern[n] == '{') {
// 遇到'{',将前面的字符截取
str = m_pattern.substr(i + 1, n - i - 1);
//std::cout << "*" << str << std::endl;
fmt_status = 1; // 标志进入'{'
fmt_begin = n; // 标志进入'{'的位置
++n;
continue;
}
} else if(fmt_status == 1) {
if(m_pattern[n] == '}') {
// 遇到'}',将和'{'之间的字符截存入fmt
fmt = m_pattern.substr(fmt_begin + 1, n - fmt_begin - 1);
//std::cout << "#" << fmt << std::endl;
fmt_status = 0;
++n;
// 找完一组大括号就退出循环
break;
}
}
++n;
// 判断是否遍历结束
if(n == m_pattern.size()) {
if(str.empty()) {
str = m_pattern.substr(i + 1);
}
}
}
if(fmt_status == 0) {
if(!nstr.empty()) {
// 保存其他字符 '[' ']' ':'
vec.push_back(std::make_tuple(nstr, std::string(), 0));
nstr.clear();
}
// fmt:寻找到的格式
vec.push_back(std::make_tuple(str, fmt, 1));
// 调整i的位置继续向后遍历
i = n - 1;
} else if(fmt_status == 1) {
// 没有找到与'{'相对应的'}' 所以解析报错,格式错误
std::cout << "pattern parse error: " << m_pattern << " - " << m_pattern.substr(i) << std::endl;
vec.push_back(std::make_tuple("<<pattern_error>>", fmt, 0));
}
}
if(!nstr.empty()) {
vec.push_back(std::make_tuple(nstr, "", 0));
}
//输出看下
for(auto& it : vec) {
std::cout
<< std::get<0>(it)
<< " : " << std::get<1>(it)
<< " : " << std::get<2>(it)
<< std::endl;
}
}
LogFormatter::format(LogEvent::ptr event){
return "";
}
int main(int argc,char** argv){
//创建一个日志事件(这里的内容随便定义,因为我们没有真正用到它)
LogEvent::ptr event(new LogEvent(
LogLevel::INFO,
"log.txt",
1,
1234567,
2,
3,
time(0)
));
LogFormatter::ptr formatter(new LogFormatter("%d{%Y-%m-%d %H:%M:%S}%T%t%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"));
formatter->format(event);
return 0;
}
以下为输出
d : %Y-%m-%d %H:%M:%S : 1
T : : 1
t : : 1
T : : 1
N : : 1
T : : 1
F : : 1
T : : 1
[ : : 0
p : : 1
] : : 0
T : : 1
[ : : 0
c : : 1
] : : 0
T : : 1
f : : 1
: : : 0
l : : 1
T : : 1
m : : 1
n : : 1
可以看出我们已经能解析出对应的元组了,这时我们需要有对应的解析方法把各自的符号来进行具体的解析。
我们可以定义内部类来完成各自的解析操作。如下:
class LogFormatter{
public:
...
class FormatItem {
public:
typedef std::shared_ptr<FormatItem> ptr;
//有子类 需要虚析构
virtual ~FormatItem() {}
virtual void format(std::ostream& os, LogEvent::ptr event) = 0;
};
private:
...
std::vector<FormatItem::ptr> m_items;
};
class MessageFormatItem : public LogFormatter::FormatItem {
public:
MessageFormatItem(const std::string& str = "") {}
void format(std::ostream& os, LogEvent::ptr event) override {
os << "Message";
}
};
class LevelFormatItem : public LogFormatter::FormatItem {
public:
LevelFormatItem(const std::string& str = "") {}
void format(std::ostream& os, LogEvent::ptr event) override {
os << LogLevel::ToString(level);
}
};
class ElapseFormatItem : public LogFormatter::FormatItem {
public:
ElapseFormatItem(const std::string& str = "") {}
void format(std::ostream& os, LogEvent::ptr event) override {
os << event->getElapse();
}
};
class NameFormatItem : public LogFormatter::FormatItem {
public:
NameFormatItem(const std::string& str = "") {}
void format(std::ostream& os, LogEvent::ptr event) override {
os << "Name";
}
};
class ThreadIdFormatItem : public LogFormatter::FormatItem {
public:
ThreadIdFormatItem(const std::string& str = "") {}
void format(std::ostream& os, LogEvent::ptr event) override {
os << event->getThreadId();
}
};
class FiberIdFormatItem : public LogFormatter::FormatItem {
public:
FiberIdFormatItem(const std::string& str = "") {}
void format(std::ostream& os, LogEvent::ptr event) override {
os << event->getFiberId();
}
};
class DateTimeFormatItem : public LogFormatter::FormatItem {
public:
DateTimeFormatItem(const std::string& format = "%Y-%m-%d %H:%M:%S")
:m_format(format) {
if(m_format.empty()) {
m_format = "%Y-%m-%d %H:%M:%S";
}
}
void format(std::ostream& os, LogEvent::ptr event) override {
struct tm tm;
time_t time = event->getTime();
localtime_r(&time, &tm);
char buf[64];
strftime(buf, sizeof(buf), m_format.c_str(), &tm);
os << buf;
}
private:
std::string m_format;
};
class FilenameFormatItem : public LogFormatter::FormatItem {
public:
FilenameFormatItem(const std::string& str = "") {}
void format(std::ostream& os, LogEvent::ptr event) override {
os << event->getFile();
}
};
class LineFormatItem : public LogFormatter::FormatItem {
public:
LineFormatItem(const std::string& str = "") {}
void format(std::ostream& os, LogEvent::ptr event) override {
os << event->getLine();
}
};
class NewLineFormatItem : public LogFormatter::FormatItem {
public:
NewLineFormatItem(const std::string& str = "") {}
void format(std::ostream& os, LogEvent::ptr event) override {
os << std::endl;
}
};
class StringFormatItem : public LogFormatter::FormatItem {
public:
StringFormatItem(const std::string& str)
:m_string(str) {}
void format(std::ostream& os, LogEvent::ptr event) override {
os << m_string;
}
private:
std::string m_string;
};
class TabFormatItem : public LogFormatter::FormatItem {
public:
TabFormatItem(const std::string& str = "") {}
void format(std::ostream& os, LogEvent::ptr event) override {
os << "\t";
}
private:
std::string m_string;
};
这样一来的话我们就可以将各个类型的解析器和对应的字符绑定了,将字符和解析类用Map容器存放起来,在调用格式化方法时就可以根据符号来调用对应的解析器 获得解析后的字符串
我们继续改造:
void LogFormatter::init(){
...
//以下的编写方式绝对堪称经典!!!
static std::map<std::string, std::function<FormatItem::ptr(const std::string& str)> > s_format_items = {
#define XX(str, C) \
{#str, [](const std::string& fmt) { return FormatItem::ptr(new C(fmt));}}
XX(m, MessageFormatItem),
XX(p, LevelFormatItem),
XX(r, ElapseFormatItem),
XX(c, NameFormatItem),
XX(t, ThreadIdFormatItem),
XX(n, NewLineFormatItem),
XX(d, DateTimeFormatItem),
XX(f, FilenameFormatItem),
XX(l, LineFormatItem),
XX(T, TabFormatItem),
XX(F, FiberIdFormatItem),
#undef XX
};
for(auto& i : vec) {
if(std::get<2>(i) == 0) {
m_items.push_back(FormatItem::ptr(new StringFormatItem(std::get<0>(i))));
} else {
auto it = s_format_items.find(std::get<0>(i));
if(it == s_format_items.end()) {
m_items.push_back(FormatItem::ptr(new StringFormatItem("<<error_format %" + std::get<0>(i) + ">>")));
} else {
m_items.push_back(it->second(std::get<1>(i)));
}
}
}
}
至此我们可以开始编写format类了
std::string LogFormatter::format(LogEvent::ptr event){
std::stringstream ss;
for(auto& i : m_items) {
i->format(ss,event);
}
return ss.str();
}
这个时候我们可以写个测试方法试用一下:
#include <iostream>
#include <string> //日志名称用到了字符串
#include <memory> //智能指针所需的头文件
#include <stdint.h>
#include <list>
#include <vector>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <tuple>
#include <map>
#include <sstream>
int main(int argc,char** argv){
//创建一个日志事件(这里的内容随便定义,因为我们没有真正用到它)
LogEvent::ptr event(new LogEvent(
LogLevel::INFO, //日志级别
__FILE__, //文件名称
__LINE__, //行号
1234567, //运行时间
syscall(SYS_gettid),//线程ID
0, //协程ID
time(0) //当前时间
));
LogFormatter::ptr formatter(new LogFormatter("%d{%Y-%m-%d %H:%M:%S}%T%t%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"));
std::cout << formatter->format(event);
return 0;
}
可以看到以下输出(这里除了 日志器名称和 输出内容 其他都已经完成了)
2024-04-20 10:23:15 5065 0 [INFO] [Name] /apps/sylar/tests/test.cc:527 Message
除了日志器名称和输出内容 我们先将格式化的调用时机给确认好:
//在适配器中添加私有字段 格式器
class LogAppender {
public:
...
void setFormatter(LogFormatter::ptr val) { m_formatter = val;}
LogFormatter::ptr getFormatter() const { return m_formatter;}
protected:
...
LogFormatter::ptr m_formatter;
};
//控制台输出器的方法
void StdoutLogAppender::log(LogEvent::ptr event) {
std::cout << m_formatter->format(event);
}
//文件输出器暂时不写(我们最后再做扩展)
void FileLogAppender::log(LogEvent::ptr event) {
...
}
以下是测试代码
#include <iostream>
#include <string>
#include <memory>
#include <stdint.h>
#include <list>
#include <vector>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <tuple>
#include <map>
#include <sstream>
//此时我们可以写一个测试类试试
int main(int argc,char** argv){
//创建一个日志事件(这里的内容随便定义,因为我们没有真正用到它)
LogEvent::ptr event(new LogEvent(
LogLevel::INFO, //日志级别
__FILE__, //文件名称
__LINE__, //行号
1234567, //运行时间
syscall(SYS_gettid),//线程ID
0, //协程ID
time(0) //当前时间
));
Logger::ptr lg(new Logger("XYZ"));
LogFormatter::ptr formatter(new LogFormatter("%d{%Y-%m-%d %H:%M:%S}%T%t%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"));
//添加控制台输出适配器
StdoutLogAppender::ptr stdApd(new StdoutLogAppender());
stdApd->setFormatter(formatter);
lg->addAppender(stdApd);
lg->log(event);
return 0;
}
可以完美输出(除了日志器名称和内容)
2024-04-20 10:44:59 5205 0 [INFO] [Name] /apps/sylar/tests/test.cc:511 Message
我们先来解决日志器名称的问题
我们可以将日志器名称通过构造存放到LogEvent的私有字段中去且提供get方法
class LogEvent {
public:
...
LogEvent(const std::string& logName, LogLevel::Level level
,const char* file, int32_t m_line, uint32_t elapse
, uint32_t thread_id, uint32_t fiber_id, uint64_t time);
const std::string& getLogName() const { return m_logName;}
private:
...
std::string m_logName;
};
同时修改 NameFormatItem 类
class NameFormatItem : public LogFormatter::FormatItem {
public:
NameFormatItem(const std::string &str = "") {}
void format(std::ostream &os, LogEvent::ptr event) override {
os << event->getLogName();
}
};
此时调用
int main(int argc, char **argv) {
Logger::ptr lg(new Logger("XYZ"));
LogEvent::ptr event(new LogEvent(lg->getName(),
LogLevel::INFO, //日志级别
__FILE__, //文件名称
__LINE__, //行号
1234567, //运行时间
syscall(SYS_gettid), //线程ID
0, //协程ID
time(0) //当前时间
));
LogFormatter::ptr formatter(new LogFormatter(
"%d{%Y-%m-%d %H:%M:%S}%T%t%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"));
//添加控制台输出适配器
StdoutLogAppender::ptr stdApd(new StdoutLogAppender());
stdApd->setFormatter(formatter);
lg->addAppender(stdApd);
lg->log(event);
return 0;
}
输出内容(可以看到日志器名称XYZ被输出了)
2024-04-20 13:29:07 6285 0 [INFO] [XYZ] /apps/sylar/tests/test.cc:17 Message
现在就剩下 Message 信息的传入没有搞定了。
不要着急我们继续,我们要改造LogEvent 需要增加 字符流对象.
class LogEvent {
public:
typedef std::shared_ptr<LogEvent> ptr;
LogEvent(const std::string logName, LogLevel::Level level, const char *file,
int32_t m_line, uint32_t elapse, uint32_t thread_id,
uint32_t fiber_id, uint64_t time);
const std::string& getLogName() const { return m_logName; }
const char *getFile() const { return m_file; }
int32_t getLine() const { return m_line; }
uint32_t getElapse() const { return m_elapse; }
uint32_t getThreadId() const { return m_threadId; }
uint32_t getFiberId() const { return m_fiberId; }
uint64_t getTime() const { return m_time; }
LogLevel::Level getLevel() const { return m_level; }
std::string getContent() const { return m_ss.str(); } //【此处增加流对象转字符串!!!】
std::stringstream& getSS() { return m_ss;} //【此处增加流对象get方法提供流式调用!!!】
private:
std::string m_logName; //日志名称
LogLevel::Level m_level; //日志级别
const char *m_file = nullptr; //文件名
int32_t m_line = 0; //行号
uint32_t m_elapse = 0; //程序启动开始到现在的毫秒数
uint32_t m_threadId = 0; //线程id
uint32_t m_fiberId = 0; //协程id
uint64_t m_time = 0; //时间戳
std::stringstream m_ss; //字符流【此处增加流对象!!!】
};
测试方法
int main(int argc, char **argv) {
Logger::ptr lg(new Logger("XYZ"));
LogEvent::ptr event(new LogEvent(lg->getName(),
LogLevel::INFO, //日志级别
__FILE__, //文件名称
__LINE__, //行号
1234567, //运行时间
syscall(SYS_gettid), //线程ID
0, //协程ID
time(0) //当前时间
));
LogFormatter::ptr formatter(new LogFormatter(
"%d{%Y-%m-%d %H:%M:%S}%T%t%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"));
//添加控制台输出适配器
StdoutLogAppender::ptr stdApd(new StdoutLogAppender());
stdApd->setFormatter(formatter);
lg->addAppender(stdApd);
//流式调用 设置要输出的内容
event->getSS() << "hello sylar";
lg->log(event);
return 0;
}
可以看到 要输出的主要信息完美展现!
2024-04-20 13:44:28 6369 0 [INFO] [XYZ] /apps/sylar/tests/test.cc:17 hello sylar
至此 日志输出的整体流程已经完毕!
还没完!!!
接下来我们开始优化调用方式和管理类
首先考虑到调用时需要调用 log(event)的当时进行最终输出,这样就无法进行完整的流式输出了。
如果看过 【日志管理-知识储备篇】知识点03 那么应该知道怎么做了
此时我们可以这么办:
定义一个 LogEventWarp类 进行 RAII的方式 调用输出
class LogEventWrap {
public:
LogEventWrap(Logger::ptr logger, LogEvent::ptr e);
~LogEventWrap();
LogEvent::ptr getEvent() const { return m_event; }
std::stringstream &getSS();
private:
Logger::ptr m_logger;
LogEvent::ptr m_event;
};
LogEventWrap::LogEventWrap(Logger::ptr logger, LogEvent::ptr e)
: m_logger(logger), m_event(e) {
}
LogEventWrap::~LogEventWrap() {
m_logger->log( m_event);
}
std::stringstream &LogEventWrap::getSS() { return m_event->getSS(); }
老规矩测试一下:
int main(int argc, char **argv) {
std::cout << "======START======" << std::endl;
Logger::ptr lg(new Logger("XYZ"));
LogEvent::ptr event(new LogEvent(lg->getName(),
LogLevel::INFO, //日志级别
__FILE__, //文件名称
__LINE__, //行号
1234567, //运行时间
syscall(SYS_gettid), //线程ID
0, //协程ID
time(0) //当前时间
));
LogFormatter::ptr formatter(new LogFormatter(
"%d{%Y-%m-%d %H:%M:%S}%T%t%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"));
//添加控制台输出适配器
StdoutLogAppender::ptr stdApd(new StdoutLogAppender());
stdApd->setFormatter(formatter);
lg->addAppender(stdApd);
event->getSS() << "hello sylar";
// lg->log(event);
LogEventWrap(lg, event).getSS() << " 追加内容";
std::cout << "=======END=======" << std::endl;
return 0;
}
可以看到输出信息:
======START======
2024-04-20 14:34:18 6777 0 [INFO] [XYZ] /apps/sylar/tests/test.cc:16 hello sylar 追加内容
=======END=======
日志很完美 能流式调用了 而且精准输出,但是调用过程较复杂,所以用 宏 简化一下:
#define LOG_LEVEL(logger, level) \
if (logger->getLevel() <= level) \
LogEventWrap(logger, LogEvent::ptr(new LogEvent( \
logger->getName(), level, __FILE__, __LINE__, 0, \
syscall(SYS_gettid), 1, time(0)))) \
.getSS()
int main(int argc, char **argv) {
std::cout << "======START======" << std::endl;
Logger::ptr lg(new Logger("XYZ"));
LogEvent::ptr event(new LogEvent(lg->getName(),
LogLevel::INFO, //日志级别
__FILE__, //文件名称
__LINE__, //行号
1234567, //运行时间
syscall(SYS_gettid), //线程ID
0, //协程ID
time(0) //当前时间
));
LogFormatter::ptr formatter(new LogFormatter(
"%d{%Y-%m-%d %H:%M:%S}%T%t%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"));
//添加控制台输出适配器
StdoutLogAppender::ptr stdApd(new StdoutLogAppender());
stdApd->setFormatter(formatter);
lg->addAppender(stdApd);
LOG_LEVEL(lg,LogLevel::INFO) << "Hello XYZ !";
std::cout << "=======END=======" << std::endl;
return 0;
}
以下是输出
======START======
2024-04-20 14:44:56 6946 1 [INFO] [XYZ] /apps/sylar/tests/test.cc:29 Hello XYZ !
=======END=======
看着已经ok了,但是还是需要手动设置appender和formatter信息,
其实我们可以定义一些默认appender和formatter,这样我们就能更简化。
这里实在是不想赘述了,本文也是为了自己做笔记才写的,自己懂了就不写的这么细了。
要是实在有问题,可以私聊我。
最后附上管理类实现(管理类是个单例,具体单例可以看我的【日志管理-知识储备篇】知识点04):
class LoggerManager {
public:
LoggerManager();
Logger::ptr getLogger(const std::string& name);
void init();
Logger::ptr getRoot() const { return m_root;}
private:
std::map<std::string, Logger::ptr> m_loggers;
Logger::ptr m_root;
};
typedef sylar::Singleton<LoggerManager> LoggerMgr;
}
LoggerManager::LoggerManager() {
m_root.reset(new Logger);
m_root->addAppender(LogAppender::ptr(new StdoutLogAppender));
}
Logger::ptr LoggerManager::getLogger(const std::string& name) {
auto it = m_loggers.find(name);
return it == m_loggers.end() ? m_root : it->second;
}
实在写不动了,写的很细,大家如果真的认真阅读了,一定能有收获。
起码对我自己来说,Sylar的一期日志管理部分已经很熟悉了。
接下来我要开始编写 配置管理相关内容了。加油!!!
永远记得要做 “黄昏中起飞的猫头鹰”