这个作业属于哪个课程 | 广工2023软件工程课程 |
---|---|
这个作业要求在哪里 | 作业要求 |
文章内容 | 第一篇敏捷冲刺 |
目录
一、任务分配与预期任务量
整个系统的实现可分为几个任务模块,每个模板大约需要一天时间来完成,每个模块及模块的主要负责人如下表所示
模块 | 主要负责人 |
---|---|
日志模块 | 钟海超 |
配置模块 | 李昊旃 |
线程模块 | 江周勉 |
协程模块 | 宫旭 |
协程调度模块 | 赵光明 |
I/O协程调度模块 | 李伟东 |
Hook模块 | 邱棋浩(组长) |
二、近日任务安排
时间 | 任务 |
---|---|
今天 | 日志模块实现 |
明天 | 配置模块实现 |
三、今日实现
今天的任务安排是实现日志模块,关于日志模块的学习与实现如下所示。
1. 日志系统
1.1 为什么需要日志系统
- 对于体量小的程序,出现bug时多数通过调试器打断点、分步调试的方式来点位错误,这是可行的;对于体量大的程序,在本地开发环境测试代码没有问题,但是在上线后缺出现bug,再通过调试器的方式来调试就不再可行。
- 对于多线程的程序,某个线程出现问题时,仅仅靠调试器来复现、定位就会有更棘手的问题:别的线程能否继续执行,会不会影响到复现,其他进程的数据难以追踪,从而增大了发现问题、解决问题的难度.
1.2 概念
- 日志是记录系统运行过程中各种重要信息的文件,在系统运行过程中由各进程创建并记录。
- 日志模块用于格式化输出程序日志,方便从日志中定位程序运行过程中出现的问题。
- 这里的日志除了日志内容本身之外,还应该包括:
a. 文件名/行号,
b. 时间戳,
c. 线程/协程号,
d. 模块名称,
e. 日志级别等额外信息,
f. 甚至在打印致命的日志时,还应该附加程序的栈回溯信息,以便于分析和排查问题。
1.3 作用
记录系统的运行过程及异常信息,为快速定位系统运行中出现的问题及开发过程中的程序调试问题提供详细信息。
从设计上看,一个完整的日志模块应该具备以下功能:
- 区分不同的级别,比如常见的DEBUG/INFO/WARN/ERROR等级别。
a. 日志模块可以通过指定级别实现只输出某个级别以上的日志,这样可以灵活开关一些不重要的日志输出
b. 比如程序在调试阶段可以设置一个较低的级别,以便看到更多的调度日志信息,程序发布之后可以设置一个较高的级别,以减少日志输出,提高性能。- 区分不同的输出地。
a. 不同的日志可以输出到不同的位置,比如可以输出到标准输出,输出到文件,输出到syslog,输出到网络上的日志服务器等,甚至同一条日志可以同时输出到多个输出地。- 区分不同的类别。
a. 日志可以分类并命名,一个程序的各个模块可以使用不同的名称来输出日志,这样可以很方便地判断出当前日志是哪个程序模块输出的。- 日志格式可灵活配置。
a. 可以按需指定每条日志是否包含文件名/行号、时间戳、线程/协程号、日志级别、启动时间等内容。
b. 可通过配置文件的方式配置以上功能。 就需要一个配置模块
好程序的日志可以帮助我们大大减轻后期维护压力
2. 日志模块实现
2.1 UML类图
2.2 设计思想
仿照log4j的模式
将日志抽象成Logger(日志器),LogAppender(输出落地点),LogFormat(日志格式器)三大模块。
- Logger, 对外使用的类,输入的日志级别大于等于Logger的日志,才会被真正写入。可以有多个Logger,不同的Logger,记录不同类型的日志,比如将系统框架日志和业务逻辑日志分离。
- LogAppender, 定义日志的输出落地点,目前实现了控制台日志(StdoutLogAppender),文件日志(FileLogAppender).两种类型。拥有自己的日志级别和日志格式,可以灵活定义不同的输出。主要用于区分日志级别,将error日志,单独输出到一个文件,以防被其他类型的日志淹没。
- LogFormat,日志格式,通过字符串自定义日志的格式,仿printf格式。可以灵活定义日志格式。
日志模块的工作流程
- 初始化LogFormatter,LogAppender,Logger。
- 通过宏定义提供流式风格和格式化风格的日志接口。
每次写日志时,通过宏自动生成对应的日志事件LogEvent,并且将日志事件和日志器Logger包装到一起,生成一个LogEventWrap对象。- 日志接口执行结束后,LogEventWrap对象析构,在析构函数里调用Logger的log方法将日志事件进行输出。
2.3 核心类说明
a. LogFormatter:日志格式器
b. LogAppender:日志输出器
c. Logger:日志器
d. LoggerManager:日志器管理类
e. LogEvent:日志事件器
f. LogEventWrap:日志事件包装器
g. LogLevel:日志级别类
a. LogFormatter(日志格式器)
- 作用:执行日志格式化,负责日志格式的初始化;解析日志格式,将用户自定义的日志格式解析为对应的FormatItem
- 日志格式举例:
%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n- 格式解析
%d{%Y-%m-%d %H:%M:%S} : %d 标识输出的是时间 {%Y-%m-%d %H:%M:%S}为时间格式,可选 DateTimeFormatItem
%T : Tab[\t] TabFormatItem
%t : 线程id ThreadIdFormatItem
%N : 线程名称 ThreadNameFormatItem
%F : 协程id FiberIdFormatItem
%p : 日志级别 LevelFormatItem
%c : 日志名称 NameFormatItem
%f : 文件名 FilenameFormatItem
%l : 行号 LineFormatItem
%m : 日志内容 MessageFormatItem
%n : 换行符[\r\n] NewLineFormatItem
class LogFormatter {
public:
typedef std::shared_ptr<LogFormatter> ptr;
LogFormatter(const std::string& pattern);
//将LogEvent格式化成字符串
std::string format(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event);
public:
// 具体日志格式项
class FormatItem {
public:
typedef std::shared_ptr<FormatItem> ptr;
virtual ~FormatItem() {}
// 将对于的日志格式内容写入到os
virtual void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;
};
void init();
bool isError() const { return m_error;}
const std::string getPattern() const { return m_pattern;}
private:
//日志格式
std::string m_pattern;
//通过日志格式解析出来的FormatItem,支持扩展
std::vector<FormatItem::ptr> m_items;
bool m_error = false;
};
b. LogAppender(日志输出器)
日志落地点抽象。目前只要实现了输出到控制台(StdoutLogAppender)和输出到文件(FileLogAppender),LogAppender可以拥有自己的LogFormat。
一个日志器,可以对应多个LogAppender。也就是说写一条日志,可以落到多个输出,并且每个输出的格式都可以不一样。
Appender有单独的日志级别,可以自定义不同级别的日志,输出到不同的Appender,常用于将错误日志统一输出到一个地方。
以后可以通过扩展LogAppender,实现向日志服务器写日志(利用socket)
class LogAppender {
friend class Logger;
public:
typedef std::shared_ptr<LogAppender> ptr;
typedef Spinlock MutexType;
virtual ~LogAppender() {}
//将日志输出到对应的落地点
virtual void log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;
//将日志落地点输出成Yaml格式的配置
virtual std::string toYamlString() = 0;
void setFormatter(LogFormatter::ptr val);
LogFormatter::ptr getFormatter();
LogLevel::Level getLevel() const { return m_level;}
void setLevel(LogLevel::Level val) { m_level = val;}
protected:
LogLevel::Level m_level = LogLevel::DEBUG;
bool m_hasFormatter = false;
MutexType m_mutex;
//日志格式器
LogFormatter::ptr m_formatter;
};
//输出到控制台
class StdoutLogAppender : public LogAppender {
public:
typedef std::shared_ptr<StdoutLogAppender> ptr;
void log(Logger::ptr logger, LogLevel::Level level, LogEvent::ptr event) override;
std::string toYamlString() override;
};
//输出到日志文件
class FileLogAppender : public LogAppender {
public:
typedef std::shared_ptr<FileLogAppender> ptr;
FileLogAppender(const std::string& filename);
void log(Logger::ptr logger, LogLevel::Level level, LogEvent::ptr event) override;
std::string toYamlString() override;
bool reopen();
private:
//文件名
std::string m_filename;
std::ofstream m_filestream;
uint64_t m_lastTime = 0;
};
c. 日志器
日志器,包含一个日志格式器,一个root Logger,N个LogAppender。
提供日志写入方法。根据日志器的配置格式和内容。将日志写到对应的地方。
class Logger : public std::enable_shared_from_this<Logger> {
friend class LoggerManager;
public:
typedef std::shared_ptr<Logger> ptr;
typedef Spinlock MutexType;
Logger(const std::string& name = "root");
// 写入日志,指定日志级别
void log(LogLevel::Level level, LogEvent::ptr event);
// 写debug日志
void debug(LogEvent::ptr event);
// 写info日志
void info(LogEvent::ptr event);
// 写warn日志
void warn(LogEvent::ptr event);
// 写error日志
void error(LogEvent::ptr event);
// 写fatal日志
void fatal(LogEvent::ptr event);
// 添加appender
void addAppender(LogAppender::ptr appender);
// 删除appender
void delAppender(LogAppender::ptr appender);
// 清空appender
void clearAppenders();
LogLevel::Level getLevel() const { return m_level;}
// 设置日志级别
void setLevel(LogLevel::Level val) { m_level = val;}
// 获取日志名称
const std::string& getName() const { return m_name;}
// 设置日志格式
void setFormatter(LogFormatter::ptr val);
// 设置文本日志格式
void setFormatter(const std::string& val);
LogFormatter::ptr getFormatter();
// 转成Yaml格式的配置文本
std::string toYamlString();
private:
std::string m_name;
//日志级别,低于该级别不会输出
LogLevel::Level m_level;
MutexType m_mutex;
//appender集合
std::list<LogAppender::ptr> m_appenders;
//日志格式
LogFormatter::ptr m_formatter;
//主日志器,如果当前日志未定义,使用主日志器输出
Logger::ptr m_root;
};
d. LoggerManager(日志器管理类)
管理所有的日志器,并且可以通过解析Yaml配置,动态创建或修改日志器相关的内容(日志级别,日志格式,输出落地点等等)
class LoggerManager {
public:
typedef Spinlock MutexType;
LoggerManager();
// 获取名称为name的日志器
// 如果name不存在,则创建一个,并使用root配置
Logger::ptr getLogger(const std::string& name);
void init();
Logger::ptr getRoot() const { return m_root;}
// 转成yaml格式的配置
std::string toYamlString();
private:
MutexType m_mutex;
// 所有日志器
std::map<std::string, Logger::ptr> m_loggers;
// 主日志器(默认日志器)
Logger::ptr m_root;
};
typedef sylar::Singleton<LoggerManager> LoggerMgr;
};
e. LogEvent(日志事件器)
日志事件的封装,将要写的日志,填充到LogEvent中。填充完毕之后,写入到对应的logger中。
class LogEvent {
public:
typedef std::shared_ptr<LogEvent> ptr;
LogEvent(std::shared_ptr<Logger> logger, LogLevel::Level level
,const char* file, int32_t line, uint32_t elapse
,uint32_t thread_id, uint32_t fiber_id, uint64_t time
,const std::string& thread_name);
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;}
const std::string& getThreadName() const { return m_threadName;}
std::string getContent() const { return m_ss.str();}
std::shared_ptr<Logger> getLogger() const { return m_logger;}
LogLevel::Level getLevel() const { return m_level;}
std::stringstream& getSS() { return m_ss;}
void format(const char* fmt, ...);
void format(const char* fmt, va_list al);
private:
// 文件名
const char* m_file = nullptr;
// 行号
int32_t m_line = 0;
// 程序启动累计耗时
uint32_t m_elapse = 0;
// 线程id
uint32_t m_threadId = 0;
// 协程id
uint32_t m_fiberId = 0;
// 日志事件
uint64_t m_time = 0;
// 线程名称
std::string m_threadName;
// 线程消息体流
std::stringstream m_ss;
// 目标日志器
std::shared_ptr<Logger> m_logger;
// 日志级别
LogLevel::Level m_level;
};
f. LogEventWrap(日志事件包装器)
日志事件包装类,其实就是将日志事件和日志器包装到一起,因为一条日志只会在一个日志器上进行输出。
将日志事件和日志器包装到一起后,方便通过宏定义来简化日志模块的使用。
另外,LogEventWrap还负责在构建时指定日志事件和日志器,在析构时调用日志器的log方法将日志事件进行输出。
// 日志事件包装类型(利用析构函数,触发日志写入)
class LogEventWrap {
public:
LogEventWrap(LogEvent::ptr e);
~LogEventWrap();
//获取日志事件
LogEvent::ptr getEvent() const { return m_event;}
//获取日志事件流
std::stringstream& getSS();
private:
//日志事件
LogEvent::ptr m_event;
};
g. LogLevel(日志级别类)
给日志系统设计了六个级别:
- DEBUG:详细的信息,通常只出现在诊断问题上
- INFO:确认一切按预期运行
- WARN:一个迹象表明,一些意想不到的事情发生了,或表明一些问题在不久的将来(例如。磁盘空间低”)。这个软件还能按预期工作。
- ERROR:更严重的问题,软件没能执行一些功能
- FARAL:一个严重的错误,这表明程序本身可能无法继续运行
- UNKNOW:未知级别
class LogLevel {
public:
//日志级别枚举
enum Level {
/// 未知级别
UNKNOW = 0,
/// DEBUG 级别
DEBUG = 1,
/// INFO 级别
INFO = 2,
/// WARN 级别
WARN = 3,
/// ERROR 级别
ERROR = 4,
/// FATAL 级别
FATAL = 5
};
//将日志级别转成文本输出
static const char* ToString(LogLevel::Level level);
//将文本转换成日志级别
static LogLevel::Level FromString(const std::string& str);
};
2.4 常用宏
为了写代码中方便,快捷的使用日志,提供的简便宏
a. 宏定义
//使用logger写入日志级别为level的日志(流式日志)
#define SYLAR_LOG_LEVEL(logger, level) \
if(logger->getLevel() <= level) \
sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, \
__FILE__, __LINE__, 0, sylar::GetThreadId(),\
sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getSS()
//使用logger写入日志界别为debug的日志(流式日志)
#define SYLAR_LOG_DEBUG(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::DEBUG)
//使用logger写入日志界别为info的日志(流式日志)
#define SYLAR_LOG_INFO(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::INFO)
//使用logger写入日志界别为warn的日志(流式日志)
#define SYLAR_LOG_WARN(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::WARN)
//使用logger写入日志界别为error的日志(流式日志)
#define SYLAR_LOG_ERROR(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::ERROR)
//使用logger写入日志界别为fatal的日志(流式日志)
#define SYLAR_LOG_FATAL(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::FATAL)
//使用logger写入日志级别为level的日志(格式化,printf)
#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::GetThreadId(),\
sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getEvent()->format(fmt, __VA_ARGS__)
//使用logger写入日志界别为debug的日志(格式化,printf)
#define SYLAR_LOG_FMT_DEBUG(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::DEBUG, fmt, __VA_ARGS__)
//使用logger写入日志界别为info的日志(格式化,printf)
#define SYLAR_LOG_FMT_INFO(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::INFO, fmt, __VA_ARGS__)
//使用logger写入日志界别为warn的日志(格式化,printf)
#define SYLAR_LOG_FMT_WARN(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::WARN, fmt, __VA_ARGS__)
//使用logger写入日志界别为error的日志(格式化,printf)
#define SYLAR_LOG_FMT_ERROR(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::ERROR, fmt, __VA_ARGS__)
//使用logger写入日志界别为fatal的日志(格式化,printf)
#define SYLAR_LOG_FMT_FATAL(logger, fmt, ...) SYLAR_LOG_FMT_LEVEL(logger, sylar::LogLevel::FATAL, fmt, __VA_ARGS__)
//获取主日志器
#define SYLAR_LOG_ROOT() sylar::LoggerMgr::GetInstance()->getRoot()
//获取指定名称的日志器,如果不存在则创建
#define SYLAR_LOG_NAME(name) sylar::LoggerMgr::GetInstance()->getLogger(name)
b. 实例代码
#include "sylar/log.h"
//定义一个日志器(这里使用的是root)
static sylar::Logger::ptr g_logger = SYLAR_LOG_ROOT();
int main(int argc, char** argv) {
// 使用流式风格写日志
SYLAR_LOG_INFO(g_logger) << "hello logger stream";
// 使用格式化写日志
SYLAR_LOG_FMT_INFO(g_logger, "%s", "hello logger format");
return 0;
}
四、感想与期望
今天是敏捷冲刺的第一天,任务是实现并测试日志模块.今天的任务完成的还算成功,虽然中途碰到不少问题,但经过大家的努力,问题都基本解决了,日志功能可以正常运行了.这是一个好的开始,这让我们对接下来的任务充满信心.