Sylar C++高性能服务器学习记录02 【日志管理-代码分析篇】

news2024/11/16 15:44:36

早在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的一期日志管理部分已经很熟悉了。
接下来我要开始编写 配置管理相关内容了。加油!!!
永远记得要做 “黄昏中起飞的猫头鹰”

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1611224.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

部署轻量级Gitea替代GitLab进行版本控制(一)

Gitea 是一款使用 Golang 编写的可自运营的代码管理工具。 Gitea Official Website gitea: Gitea的首要目标是创建一个极易安装&#xff0c;运行非常快速&#xff0c;安装和使用体验良好的自建 Git 服务。我们采用Go作为后端语言&#xff0c;这使我们只要生成一个可执行程序即…

【EtherCAT】FMMU和SM简介

目录 一、简介 1、 FMMU 2、SM (1) 缓冲模式 (2)邮箱模式 3、FMMU将物理存储器映射到逻辑过程数据映射的配置原理 二、FMMU和SM在EtherCAT从站控制器的存储空间分配 三、FMMU和SM部分寄存器描述(LAN9253) 1、FMMU 2、SM 四、FMMU和SM的数据结构&#xff08;soem主站&…

修复vite中使用react提示Fast refresh only works when a file only exports components.

前言 我通过 vite 构建了一个 react 应用并使用 react.lazy 来懒加载组件&#xff0c;但是在使用过程中 一直提示 Fast refresh only works when a file only exports components. Move your component(s) to a separate file.eslint(react-refresh/only-export-components)。…

x-cmd ai | x openai - 用于发送 openai API 请求,以及与 ChatGPT 对话

介绍 Openai 模块是 Openai 大模型 Chatgpt 3 和 ChatGPT 4 命令行实现。x-cmd 提供了多个不同平台间多种 AI 大模型的调用能力。无论是本地模型还是 Web 服务上的模型&#xff0c;用户都可以在不同的 AI 大模型间直接无缝切换&#xff0c;并能把之前的聊天记录发送给新的大模…

第64天:服务攻防-框架安全CVE复现Apache ShiroApache Solr

目录 思维导图 案例一&#xff1a;Apache Shiro-组件框架安全 shiro反序列化 cve_2016_4437 CVE-2020-17523 CVE-2020-1957 案例二&#xff1a;Apache Solr-组件框架安全 远程命令执行 RCE&#xff08;CVE-2017-12629&#xff09; 任意文件读取 AND 命令执行&#xff08…

C++ 速成

C 概述 c 融合了3中不同的编程方式&#xff1a; C语言代表的过程性语言C 在C语言基础上添加的类代表的面向对象语言C 模板支持的泛型编程 C 标准 一种描述C 的一些语法规则的代码准则 C11 C 应用 游戏 C 效率是一个很重要的原因&#xff0c;绝大部分游戏殷勤都是C写的 网…

【刷题】 二分查找进阶

送给大家一句话&#xff1a; 你向神求助是因为相信神&#xff0c;神没有回应你是因为神相信你 ε≡٩(๑>₃<)۶ &#xfeff;ε≡٩(๑>₃<)۶ &#xfeff;ε≡٩(๑>₃<)۶ 一心向学 二分查找进阶 1 前言Leetcode 852. 山脉数组的峰顶索引题目描述算法思…

Unity类银河恶魔城学习记录13-5,6 p146 Delete save file,p147 Encryption of saved data源代码

Alex教程每一P的教程原代码加上我自己的理解初步理解写的注释&#xff0c;可供学习Alex教程的人参考 此代码仅为较上一P有所改变的代码 【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili FileDataHandler.cs using System; using System.IO; using UnityEngine; p…

Git TortoiseGit 安装使用详细教程

前言 Git 是一个免费的开源分布式版本控制系统&#xff0c;是用来保存工程源代码历史状态的命令行工具&#xff0c;旨在处理从小型到非常大型的项目&#xff0c;速度快、效率高。《请查阅Git详细说明》。TortoiseGit 是 Git 的 Windows Shell 界面工具&#xff0c;基于 Tortoi…

pg内核之日志管理器(五)WAL日志

概念 WAL日志 数据库运行过程中&#xff0c;数据一般是会保存在内存和磁盘中&#xff0c;为保证数据的安全性&#xff0c;防止数据库崩溃时数据不丢失&#xff0c;一般都是要保证数据实时落盘的&#xff0c;但是又由于磁盘随机IO读写速率与内存相比慢很多&#xff0c;如果每个…

Python爬虫爬取中药材价格数据

&#x1f388; 博主&#xff1a;一只程序猿子 &#x1f388; 博客主页&#xff1a;一只程序猿子 博客主页 &#x1f388; 个人介绍&#xff1a;爱好(bushi)编程&#xff01; &#x1f388; 创作不易&#xff1a;喜欢的话麻烦您点个&#x1f44d;和⭐&#xff01; &#x1f388;…

【Ubuntu20.04+Noetic】UR5e+Gazebo+Moveit

环境准备 创建工作空间 mkdir -p ur5e_ws/src cd ur5e_ws/srcUR机械臂软件包 UR官方没更新最新的noetic的分支,因此安装melodic,并需要改动相关文件。 安装UR的模型配置包,包里面有UR模型文件,moveit配置等: cd ~/ur5e_ws/src git clone -b melodic-devel https://git…

llama-factory SFT 系列教程 (四),lora sft 微调后,使用vllm加速推理

文章目录 文章列表&#xff1a;背景简介llama-factory vllm API 部署融合 lora 模型权重 vllm API 部署HuggingFace API 部署推理API 部署总结 vllm 不使用 API 部署&#xff0c;直接推理数据集 tenplatevllm 代码部署 文章列表&#xff1a; llama-factory SFT系列教程 (一)&a…

【Django】调用django的pbkdf2_sha256加密算法测试

基于django搭建的系统中&#xff0c;用到pbkdf2_sha256&#xff08;&#xff08;Password-Based Key Derivation Function 2&#xff09;&#xff09;加密算法&#xff0c;这里做些代码测试、总结。 PBKDF2简介 PBKDF2是一种基于密码的密钥派生函数&#xff0c;用于从用户提供的…

cv2技术原理-图像旋转原理及手动实现

cv2技术原理-图像旋转原理及手动实现 1、图像旋转opencv实现2、cv2.getRotationMatrix2D函数解释3、数学原理推导旋转矩阵M4、手动计算旋转矩阵M5、旋转矩阵M的使用6、使用旋转矩阵M手动实现旋转功能 1、图像旋转opencv实现 图像旋转在对数据集数据增强&#xff08;主要是随机…

Spring 声明式事务控制

1. 编程式事务控制相关对象 1.1 PlatformTransactionManager PlatformTransactionManager 接口是 spring 的事务管理器&#xff0c;它提供了我们常用的操作事务的方法。 PlatformTransactionManager 是接口类型&#xff0c;不同的 Dao 层技术则有不同的实现类。例如:Dao层技…

Sileo安装插件报错:Depends mobilesubstrate

Sileo安装插件报错 iOS 15.8系统&#xff0c;使用palera1n越狱&#xff0c;然后使用Sileo安装插件的时候&#xff0c;有些插件会报错&#xff1a;Depends mobilesubstrate&#xff08;比如AppStore plus&#xff09; 报错的原因分析 从提示信息很容易可以看出&#xff0c;当…

upload-labs第十一十二关

第十一关 $is_upload false; $msg null; if(isset($_POST[submit])){$ext_arr array(jpg,png,gif);$file_ext substr($_FILES[upload_file][name],strrpos($_FILES[upload_file][name],".")1);if(in_array($file_ext,$ext_arr)){$temp_file $_FILES[upload_fil…

MySQL-进阶篇-一条sql更新语句是如何执行的(redo log和binlog)

上一篇&#xff1a;一条sql查询语句是如何执行的 http://t.csdnimg.cn/nV3EY 摘自&#xff1a;林晓斌MySQL实战45讲——第二篇 更新语句的执行过程与上一篇查询流程相同&#xff0c;本篇简写。 但多了两个重要的日志模块&#xff1a;redo log&#xff08;重做日志&#xff0…

leetcode:滑动窗口----3. 无重复字符的最长子串

给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长 子串 的长度。 示例 1: 输入: s "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc"&#xff0c;所以其长度为 3。示例 2: 输入: s "bbbbb" 输出: 1 解释: 因为…