文章目录
P7P8两节视频新增内容不多,主要看下优化日志输出使用的宏函数。本次记录的内容比较简单,没有一步一步详细写出来,如果对P7之前的代码不存在问题,那么写到这里应该不会存在疑惑。同时建议每一次看代码时都去捋一下日志几大组件之间的关系,看多了真的有点绕。
p7
1.TabFormatItem
新建一个字符标记,优化日志输出格式
class TabFormatItem : public LogFormatter::FormatItem { // 输出tab
public:
TabFormatItem(const std::string& fmt = "") {}
void format(std::ostream& os, std::shared_ptr<Logger> logger,LogLevel::Level level,LogEvent::ptr event) override {
os << "\t";
}
};
别忘了去解析字符模板map中进行注册
static std::map<std::string, std::function<FormatItem::ptr(const std::string& str)>> s_format_items = {
// {"m",[](const std::string& fmt) { return FormatItem::ptr(new MessageFormatItem(fmt)); } }
#define XX(str,C) \
{#str, [] (const std::string& fmt) { return FormatItem::ptr(new C(fmt)); }}
XX(m, MessageFormatItem), //m:消息
XX(p, LevelFormatItem), //p:日志级别
XX(r, EplaseFormatItem), //r:累计毫秒数
XX(c, LoggerNameFormatItem), //c:日志名称
XX(t, ThreadIdFormatItem), //t:线程id
XX(n, LineFormatItem), //n:换行
XX(d, DateTimeFormatItem), //d:时间
XX(l, LineFormatItem), //l:行号
XX(F, FiberIdFormatItem), //F:协程id
XX(T, TabFormatItem), //T:table
XX(f, FileNameFormatItem) //f:文件名
#undef XX
更改日志默认的格式器模板
%d{%Y-%m-%d %H:%M:%S}%T%t%T%F%T[%p]%T[%c]%T%f:%l%T%m%n
查看效果
2.init函数,对于{}内容的解析
对格式化模板的解析函数有一点小改动,
-
if(!fmt_status && !isalpha(m_pattern[n]) && m_pattern[n] != '{' && m_pattern[n] != '}')
条件判断加入对当前解析状态fmt_status的判断,防止在{}未解析完跳出。同时把字符标记str的截取放在这里
str = m_pattern.substr(i + 1, n - i - 1);
,在跳出while后的fmt_status判断条件里,就可以不用再提取。简单改变了一下代码逻辑,自己不更改也没问题 -
if(m_pattern[n] == '}') { fmt = m_pattern.substr(fmt_begin + 1, n - fmt_begin - 1); // std::cout << fmt << std::endl; fmt_status = 0; ++ n; break; }
判断}时,结束后fmt_status不再为2,更改为0,也是再跳出while后的fmt_status判断条件里,少写一个判断语句。
-
if (n == m_pattern.size()) { //最后一个字符, 每次获得str都是走到下一个字符然后进行截取,所以只有最后一个字符需要特殊处理 if (str.empty()) { str = m_pattern.substr(i + 1); } } }
while循环中,每次可以看作使用双指针的形式,当遍历到最后一个元素时,就会产生越界,从而漏掉最后一个字符的提取。
-
if(fmt_status == 0) { if(!nstr.empty()) { vec.push_back(std::make_tuple(nstr, std::string(), 0)); nstr.clear(); } vec.push_back(std::make_tuple(str, fmt, 1)); 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("<parse_error>", fmt, 0)); }
这里的判断再经过上面的几个改动后就可以精简一些。
3.Util.h
目前该文件的里的函数有获取线程ID和协程ID。
只实现了一个使用SYS_gettid获取线程ID的函数,不会的可以百度一下这个函数。
// util.h
#ifndef __SYKAR_UTIL_H__
#define __SYKAR_UTIL_H__
#include<pthread.h>
#include<sys/types.h>
#include<sys/syscall.h>
#include <unistd.h>
#include<stdint.h>
namespace sylar {
// 获取线程ID
pid_t GetTreadId();
// 获取协程ID
uint32_t GetFiberId();
}
#endif
// util.cc
#include "util.h"
namespace sylar {
pid_t GetTreadId() { return syscall(SYS_gettid); }
uint32_t GetFiberId() {
return 0; // TODO
}
}
4.CmakeLists
把util.cc加入cmake配置文件中
set(LIB_SRC
sylar/log.cc
sylar/util.cc
)
5.优化日志输出-流式输出
每次我们在定义日志器的时候比较麻烦,需要去传入一堆参数,而很多参数都是代码会自动获取到的,唯一需要改动的就是传入的日志级别,sylar这里使用了宏函数来使日志的定义更加的方便简洁。
-
新建一个LogEventWrap类,用来专门存放event事件
// log.h class LogEventWrap { public: LogEventWrap(LogEvent::ptr e); ~LogEventWrap(); std::stringstream& getSS(); LogEvent::ptr getEvent() const { return m_event;} private: LogEvent::ptr m_event; }; // log.cc LogEventWrap::LogEventWrap(LogEvent::ptr e) : m_event(e) { } LogEventWrap::~LogEventWrap() { m_event->getLogger()->log(m_event->getLevel(), m_event); // 把自己写入日志 } std::stringstream& LogEventWrap::getSS() { return m_event->getSS(); }
有一个指向event的智能指针成员,一个获得日志打印输出的函数getSS(),和获得event的getEvent函数。
-
#define SYLAR_LOG_LEVEL(logger, level) \ if(logger->getLevel() <= level) \ sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger,level, \ __FILE__,__LINE__,0,sylar::GetTreadId(), \ sylar::GetFiberId(), time(0)))).getSS() #define SYLAR_LOG_DEBUG(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::DEBUG) #define SYLAR_LOG_INFO(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::INFO) #define SYLAR_LOG_WARN(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::WARN) #define SYLAR_LOG_ERROR(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::ERROR) #define SYLAR_LOG_FATAL(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::FATAL)
宏函数里面的代码就是之前我们在test.cc初始化一个日志器使用的方法。
p8
1.优化日志输出-格式化输出
我们在定义日志时,每次传入的格式模板可能不同,在把日志存入文件时,过滤的条件也可能不同,所以sylar继续使用宏函数允许我们自定义传入格式模板
// 格式化输出
#define SYLAR_LOG_FMT_LEVEL(logger, level, fmt, ...) \
if(logger->getLevel() <= level) \
sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, \
__FILE__, __LINE__, 0, sylar::GetTreadId(),\
sylar::GetFiberId(), time(0)))).getEvent()->format(fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_DEBUG(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::DEBUG, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_INFO(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::INFO, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_WARN(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::WARN, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_ERROR(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::ERROR, fmt, __VA_ARGS__)
#define SYLAR_LOG_FMT_FATAL(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::FATAL, fmt, __VA_ARGS__)
下面详细解析:
-
void format(const char* fmt, ...); // [?] void format(const char* fmt, va_list al); void LogEvent::format(const char* fmt, ...) { va_list al; va_start(al, fmt); //引入stdarg.h format(fmt, al); va_end(al); } void LogEvent::format(const char* fmt, va_list al) { char* buf = nullptr; int len = vasprintf(&buf, fmt, al); if(len != -1) { m_ss << std::string(buf, len); free(buf); } }
在LogEvent中新增两个函数,用于自定义模板
第一个函数用了可变参数的方法,…的内容可以在输出时用宏定义__VA_ARGS替代。函数体中还使用了va_list,首先定义了一个变量指向va_list,然后使用方法va_start初始化al,使其指向第一个可变参数的地址,接着传入第二个format函数,处理完毕后使用va_end结束可变参数的获取
第二个函数传入格式模板fmt,指向可变参数的va_list对象al,创建一个缓冲区buf,使用va_list对像里面的vasprintf方法将格式化数据从可变参数列表写入缓冲区,如果写入的值不为空,则保存到LogEvent的成员m_ss中
2.日志管理器
新增LoggerManager,对日志进行管理
//log.h
// 日志管理器
class LoggerManager {
public:
LoggerManager();
Logger::ptr getLogger(const std::string& name);
private:
std::map<std::string, Logger::ptr> m_loggers; // 日志器容器
Logger::ptr m_root; // 主日志器
};
// 日志器管理类单例模型
typedef sylar::Singleton<LoggerManager> LoggerMgr;
//log.cc
LoggerManager::LoggerManager() {
m_root.reset(new Logger);
m_root->addAppender(LogAppender::ptr(new StdoutAppender));
}
Logger::ptr LoggerManager::getLogger(const std::string& name) {
auto it = m_loggers.find(name);
return it == m_loggers.end() ? m_root : it->second;
}
构造函数会初始化一个默认日志器,getLogger会在日志管理器中寻找目标日志
3.单例模型设计
这一块第一次遇到,目前不咋懂,先占个位,后面再记录
#ifndef __SINGLETON_H__
#define __SINGLETON_H__
namespace sylar {
template<class T, class X = void, int N = 0> // T 类型 X 为了创造多个实例对应的Tag N 同一个Tag创造多个实例索引
class Singleton {
public:
static T* GetInstance() {
static T v;
return &v;
}
};
template<class T, class X = void, int N = 0>
class SingletonPtr {
public:
static std::shared_ptr<T> GetInstance() {
static std::shared_ptr<T> v(new T);
return v;
}
};
}
#endif
测试(无调试步骤)
贴一下当前代码的测试案例
test.cc
#include <iostream>
#include "../sylar/log.h"
#include "../sylar/util.h"
int main(int argc, char** argv) {
sylar::Logger::ptr logger(new sylar::Logger);
logger->addAppender(sylar::LogAppender::ptr(new sylar::StdoutAppender));
sylar::FileLogAppender::ptr file_appender(new sylar::FileLogAppender("./log.txt"));
sylar::LogFormatter::ptr fmt(new sylar::LogFormatter("%d%T%m%n"));
file_appender->setFormatter(fmt);
file_appender->setLevel(sylar::LogLevel::FATAL); // 过滤特定级别level
logger->addAppender(file_appender);
SYLAR_LOG_DEBUG(logger) << "asdasda";
SYLAR_LOG_FMT_FATAL(logger, "fmt eeror %s", "hy");
auto l = sylar::LoggerMgr::GetInstance()->getLogger("xx");
SYLAR_LOG_INFO(l) << "XX";
return 0;
}
结果
生成的日志文件:
el::FATAL); // 过滤特定级别level
logger->addAppender(file_appender);
SYLAR_LOG_DEBUG(logger) << "asdasda";
SYLAR_LOG_FMT_FATAL(logger, "fmt eeror %s", "hy");
auto l = sylar::LoggerMgr::GetInstance()->getLogger("xx");
SYLAR_LOG_INFO(l) << "XX";
return 0;
}
**结果**
[外链图片转存中...(img-B5hsKY6Q-1697164453590)]
生成的日志文件:
![image-20231013102459920](https://img-blog.csdnimg.cn/img_convert/739a84f3f35b4305e9905fd195a1a68f.png)