日志系统扩展二:日志服务器的实现
- 一、设计
- 1.为何要这么扩展?
- 2.应用层协议的选择
- 1.HTTP?
- 2.自定义应用层协议
- 二、自定义应用层协议的实现
- 1.声明日志器
- 1.服务器和客户端这里日志器的关联
- 2.枚举类型的定义
- 3.sinks数组的定义
- 2.打印日志
- 1.logMessage结构体的定义
- 2.sinklogRequest
- 3.commResponse
- 4.打开连接的请求
- 1.局部日志器问题
- 2.客户端与服务器的资源隔离问题
- 1.单客户端与服务器的资源隔离问题
- 2.多客户端与服务器的资源隔离问题
- 3.openConnectionRequest
- 4.画图
- 三、服务器实现
- 1.连接模块的实现
- 1.成员变量
- 2.成员函数
- 3.通信类型转换接口的实现
- 4.declareLogger
- 5.sinklog
- 1.log_pure实现
- 2.sinklog
- 6.basicCommonResponse
- 7.连接管理模块实现
- 2.服务器模块实现
- 1.成员变量
- 2.成员函数
- 3.构造函数和start函数
- 4.事件注册响应函数
- 四、客户端实现
- 1.成员变量
- 2.成员函数
- 1.构造和start函数
- 2.业务模块和网络模块通信类型的转换接口
- 3.事件注册响应回调函数
- 4.connect
- 5.declareLogger
- 6.web_log
- 1.注意
- 五、网络落地类的实现
- 1.RemoteSinkManager
- 1.单例的设计
- 2.配置文件的设计
- 2.RemoteSink的实现
- 六、异步日志工作器的实现
- 1.异步缓冲区的实现
- 1.设计
- 2.代码
- 2.异步工作器的实现
- 七、Logger部分的修改
- 1.Logger类的修改
- 1.SinkMode字段的添加
- 2.log_web成员函数的增加
- 2.LoggerBuilder类的修改
- 1.成员变量
- 2.成员函数
- 1.buildSinkStr
- 1.if和if constexpr的区别
- 2.代码
- 2.buildWebLogSink
- 3.getpattern
- 八、定时器刷新文件缓冲区的实现
- 1.为何要有这个类
- 2.FileElem
- 3.FileTimerHelper
- 4.FileSink的修改
- 5.RollSinkBySize的修改
- 九、测试
- 1.同步
- 2.异步安全
- 3.异步不安全
一、设计
1.为何要这么扩展?
在大数据时代,现在的大型项目基本都是分布式,所以日志打印集中到一台或者几台服务器上挺正常的
因此日志打印需要跨主机进行打印,也就是跨主机的进程间通信,也就是网络通信,因此我们要支持网络服务
2.应用层协议的选择
1.HTTP?
HTTP协议主要用于需要将服务发布到互联网当中,允许浏览器作为客户端访问时
我们的日志服务器不需要发布到互联网当中,无需浏览器作为客户端进行访问
而且HTTP协议当中很多字段(状态码、响应码、Cookie等等)我们也不需要
因此我们不用HTTP协议
2.自定义应用层协议
我们就用protobuf来进行req和resp的序列化、反序列化了,毕竟快嘛
而网络服务我们就用muduo库了,毕竟是基于reactor模式的高并发网络库,使用方便,功能强大,且效率高
二、自定义应用层协议的实现
1.声明日志器
用户想要在服务器这里打印日志,首先就需要在服务器声明一个日志器,然后才能通过该日志器进行日志打印
1.服务器和客户端这里日志器的关联
那么怎么设计这个日志器的创建呢?
我们的这个远程网络落地方向也是LogSink的一种,因此,向服务器打印日志也需要在客户端创建日志器
而用户想要创建的日志器,大概率就是它在客户端这里创建的日志器
因此,服务器当中的日志器的所有属性都来自于创建它的客户端的日志器
只不过,有一个地方需要进行改动:
服务器当中的LoggerType统一用异步的日志打印方式,目的是业务线程,让业务线程能够去处理更多的请求
但是,为了程序的可维护性和可读性,我们采用条件编译的方式来处理这种情况
2.枚举类型的定义
因为日志器当中有LogLevel、LoggerType、AsyncType,这三个字段需要进行跨网络传输
因此我们也需要在proto文件当中定义这三个类型,同时,为了让网络模块跟业务模块进行充分解耦,我们稍微改一下名字(尽管有命名空间)
enum logLevel
{
UNKNOWN = 0;
DEBUG = 1;
INFO = 2;
WARNING = 3;
ERROR = 4;
FATAL = 5;
OFF = 6;
}
enum loggerType
{
SYNC = 0;
ASYNC = 1;
}
enum asyncType
{
ASYNC_SAFE = 0;
ASYNC_UNSAFE = 1;
}
至此,我们就能够写出declareLoggerRequest的大部分内容
这个rid是用来保证客户端操作的同步,同时也具备ACK的功能
message declareLoggerRequest
{
string rid = 1;
string logger_name = 2;
loggerType logger_type = 3;
logLevel limit_level = 4;
string format_pattern = 5;
asyncType async_type = 6;
// 还差具体的sinks数组
}
3.sinks数组的定义
因为LogSink模块一共分为StdoutSink,FileSink,RollSinkBySize,SqliteSink,MySQLSink
而且,其中,创建StdoutSink无需任何参数,而FileSink、SqliteSink、MySQLSink都需要一个string
RollSinkBySize需要一个string和一个size_t
我们之前是采用C++的可变模板参数包来实现不定参的,而想要在网络传输这里所谓的不定参就白搭了
因此,我们需要额外用一个数组来存args参数,所以就需要嵌套使用repeated字段
而这个SinkType我们也想用枚举来表示一下,有了SinkType之后,文件Sink和数据库Sink就可以放到一起了
enum sink_type
{
StdoutSink = 0;
FileSink = 1;
RollSinkBySize = 2;
SqliteSink = 3;
MySQLSink = 4;
}
message sink_args
{
sink_type type = 1;
repeated string args = 2;
}
message declareLoggerRequest
{
string rid = 1;
string logger_name = 2;
loggerType logger_type = 3;
logLevel limit_level = 4;
string format_pattern = 5;
asyncType async_type = 6;
repeated sink_args sinks = 7;
}
2.打印日志
1.logMessage结构体的定义
因为日志有不同的落地方向,因此需要客户端向服务器发送一个LogMessage,因此我们需要在proto文件当中定义一个logMessage字段
注意:因为std::thread::id无法转为任何一个整形,因此thread_id这个字段我们存string
message logMessage
{
logLevel level = 1;
int64 time = 2;
string file = 3;
int32 line = 4;
//注意:因为std::thread::id无法转为任何一个整形,因此thread_id这个字段我们存string
string thread_id = 5;
string logger_name = 6;
string body = 7;
}
实际分布式组件之间进行通信时,如果采用网络的方式进行结构化数据之间的交换
如果采用protobuf的话,为了降低业务模块和网络模块之间的耦合度
一般会进行业务模块当中结构化字段和protobuf当中结构化字段的相互转化,且这一操作通常在网络模块进行
因为网络模块肯定是要依赖于业务模块对外提供服务的,而业务模块不应该依赖于网络模块
2.sinklogRequest
为了更好地支持客户端的异步网络日志打印服务,提供网络吞吐量,降低网络IO次数,提高效率
我们把message用repeated修饰
// 有些时候日志落地要求有序,因此我们也给日志打印一个rid,保证操作的顺序性
// 即使是异步打印, 因为异步缓冲区的打印是有序的, 因此只要业务线程将数据放到异步缓冲区当中, 那么实际落地就是有序的
// 而且也可以当作打印日志req的一种ack
message sinklogRequest
{
string rid = 1;
string logger_name = 2;
repeated logMessage message = 3;
}
3.commResponse
服务器要对客户端的每个req都进行一次resp,向客户端报告是否成功,以及错误信息
message commonResponse
{
string rid = 1;
bool ok = 2;
string err_msg = 3;
}
4.打开连接的请求
1.局部日志器问题
2.客户端与服务器的资源隔离问题
1.单客户端与服务器的资源隔离问题
因为服务器和客户端的日志器是一一对应的,且客户端这里已经实现了日志器的资源隔离
因此服务器那里的资源隔离也随之自动实现了
无需我们进行别的任何操作
2.多客户端与服务器的资源隔离问题
因为单客户端与服务器之间是资源隔离的,所以我们只需要在服务器这里能够区分不同客户端即可
因为全局日志器的管理对象是单例的,所以我们只能这么玩:
在客户端创建之后立刻跟服务器发起一个openConnectionRequest,里面携带connection_id字段
然后该客户端所操作的所有日志器都加上connection_id这个前缀,这样的话,就可以通过日志器的前缀名来区分不同客户端了
因为实际应用当中,日志器的名称一般都要规范命名,所以不用担心出于巧合导致的重名问题
3.openConnectionRequest
message openConnectionRequest
{
string rid = 1;
string connection_id = 2;
}
4.画图
三、服务器实现
为了进行多个客户端之间的资源隔离,我们将服务细分为连接,由连接模块对用户提供具体的业务服务
我之前在这篇博客【项目第二弹:第三方工具选择与介绍、使用muduo库和protobuf搭建简易服务器和客户端、异步工作线程池实现】当中介绍过muduo库的使用,以及muduo库配合protobuf的使用(重点介绍了ProtobufCodec和ProtobufDispatcher)
1.连接模块的实现
事先using一下类型,给上构造函数
using declareLoggerRequestPtr = std::shared_ptr<declareLoggerRequest>;
using sinklogRequestPtr = std::shared_ptr<sinklogRequest>;
using ProtobufCodecPtr = std::shared_ptr<ProtobufCodec>;
Connection(const std::string &connection_id, const muduo::net::TcpConnectionPtr &conn, const ProtobufCodecPtr &codec)
: _connection_id(connection_id), _conn(conn), _codec(codec){}
1.成员变量
因为一个客户端对应于一个TCP连接,因此我们的Connection对象天然就要跟一个TCP连接相绑定
所以Connection所需成员变量如下:
其中:_connection_id是日志器前缀,用来区分不同客户端的
_conn是该连接对应的底层TCP连接
_codec是muduo库提供的支持protobuf的用于进行跟底层TCP缓冲区交互的一个协议处理类
std::string _connection_id;
muduo::net::TcpConnectionPtr _conn;
ProtobufCodecPtr _codec;
业务模块所需成员就是那个单例对象和一个GlobalLoggerBuilder
因为存在多线程同时访问同一个Connection对象的情况,因此我们就不把GlobalLoggerBuilder设置为成员变量了
否则还需要加锁,这算是一种以空间换时间的方式
2.成员函数
连接模块所需成员函数如下:
- 声明日志器的declareLogger
- 打印日志的sinklog
他们分别只需要接受对应的RequestPtr作为参数即可
需要提供网络模块和业务模块之间通信类型转换的接口:
- getMessage
- getLoggerType
- getLogLevel
- getAsyncType
- convertToLoggerName
需要提供向对应客户端进行ack的Response接口
basicCommonResponse
下面我们来实现即可
3.通信类型转换接口的实现
这些接口没啥难的,不赘述了
LogMessage::ptr getMessage(const logMessage& message)
{
return std::make_shared<LogMessage>(getLogLevel(message.level()),message.file(),message.line(),convertToLoggerName(message.logger_name()),message.body());
}
LoggerType getLoggerType(loggerType type)
{
return (type == loggerType::SYNC) ? LoggerType::SYNC : LoggerType::ASYNC;
}
LogLevel::value getLogLevel(logLevel level)
{
switch (level)
{
case logLevel::UNKNOWN:
return LogLevel::value::UNKNOWN;
case logLevel::DEBUG:
return LogLevel::value::DEBUG;
case logLevel::INFO:
return LogLevel::value::INFO;
case logLevel::WARNING:
return LogLevel::value::WARNING;
case logLevel::ERROR:
return LogLevel::value::ERROR;
case logLevel::FATAL:
return LogLevel::value::FATAL;
case logLevel::OFF:
return LogLevel::value::OFF;
default:
default_fatal("未知logLevel:%s", logLevel_Name(level));
return LogLevel::value::UNKNOWN;
}
}
AsyncType getAsyncType(asyncType type)
{
return (type == asyncType::ASYNC_SAFE) ? AsyncType::ASYNC_SAFE : AsyncType::ASYNC_UNSAFE;
}
std::string convertToLoggerName(const std::string &old_logger_name)
{
return _connection_id + " " + old_logger_name;
}
4.declareLogger
- 转换日志器名称
- 查找是否有该日志器
- 构造日志器
- 调用basicCommonResponse进行ack
在构造日志器阶段按需调用上面的类型转换接口即可,不要忘了异步打印的条件编译
void declareLogger(const declareLoggerRequestPtr &req)
{
// 0. 转换日志器名称
std::string my_logger_name = convertToLoggerName(req->logger_name());
// 1.查找是否有该日志器
if (getLogger(my_logger_name).get() != nullptr)
{
basicCommonResponse(req->rid(), true, "该日志器已经存在,但不影响后续操作");
return;
}
// 2. 构造日志器
GlobalLoggerBuilder::ptr _global_builder = std::make_shared<GlobalLoggerBuilder>();
_global_builder->buildLoggerName(my_logger_name);
#ifdef SERVER_ASYNC
// 统一用异步的日志打印方式,目的是业务线程,让业务线程能够去处理更多的请求
_global_builder->buildLoggerType(LoggerType::ASYNC);
#else
_global_builder->buildLoggerType(getLoggerType(req->logger_type()));
#endif
_global_builder->buildLimitLevel(getLogLevel(req->limit_level()));
_global_builder->buildFormatter(req->format_pattern());
_global_builder->buildAsyncType(getAsyncType(req->async_type()));
_global_builder->buildSinkMode(SinkMode::LOCAL);
for (int i = 0; i < req->sinks_size(); i++)
{
sink_type fs_type = req->sinks(i).type();
switch (fs_type)
{
case sink_type::StdoutSink:
_global_builder->buildFSLogSink<ns_log::StdoutSink>();
break;
case sink_type::FileSink:
_global_builder->buildFSLogSink<ns_log::FileSink>(req->sinks(i).args(0));
break;
case sink_type::RollSinkBySize:
_global_builder->buildFSLogSink<ns_log::RollSinkBySize>(req->sinks(i).args(0), std::stoi(req->sinks(i).args(1)));
break;
case sink_type::SqliteSink:
_global_builder->buildDBLogSink<ns_log::SqliteSink>(req->sinks(i).args(0));
break;
case sink_type::MySQLSink:
_global_builder->buildDBLogSink<ns_log::MySQLSink>(req->sinks(i).args(0));
break;
default:
default_fatal("声明日志器时,遇到未知sink_type: %s", sink_type_Name(fs_type));
break;
}
}
_global_builder->build();
// 3. commonResponse
basicCommonResponse(req->rid(), true, "一切顺利");
}
5.sinklog
1.log_pure实现
在sinklog这里,我们需要直接打印用户传入的LogMessage,因此我们让Logger提供一个接口:log_pure,专门用来打印一个已经构建好了的LogMessage
关于这个_sink_mode:
enum class SinkMode
{
LOCAL,
ROMOTE,
BOTH
};
ROMOTE 表示该日志器只进行网络打印
LOCAL 表示该日志器只进行本地打印
BOTH 表示该日志器既进行网络打印,又进行本地打印
void log_pure(const LogMessage::ptr& message)
{
if (_sink_mode == SinkMode::ROMOTE || _sink_mode == SinkMode::BOTH)
{
log_web(message);
}
if (_sink_mode == SinkMode::LOCAL || _sink_mode == SinkMode::BOTH)
{
log_db(message);
if (!_fs_sinks.empty())
{
// 2. 将LogMessage进行格式化
std::string real_message = _formatter->format(*message);
// 3. 复用log进行实际的日志落地
log_fs(real_message.c_str(), real_message.size());
}
}
}
有了log_pure之后,我们就可以让construct来复用这个函数了
void construct(LogLevel::value level, const std::string &file, size_t line, char *body)
{
// 1. 构造LogMessage::ptr
LogMessage::ptr message = std::make_shared<LogMessage>(level, file, line, _logger_name, body);
// 2. 复用log_pure进行日志打印
log_pure(message);
// 3. free掉body,否则会内存泄漏
free(body);
body = nullptr;
}
2.sinklog
- 转换日志器名称
- 拿到日志器
- 调用log_pure进行日志打印
- ack
void sinklog(const sinklogRequestPtr &req)
{
// 0. 转换日志器名称
std::string my_logger_name = convertToLoggerName(req->logger_name());
// 1. 拿到日志器
Logger::ptr logger = getLogger(my_logger_name);
if (logger.get() == nullptr)
{
basicCommonResponse(req->rid(), false, "日志落地失败,因为当前日志器并不存在:" + my_logger_name);
return;
}
for (int i = 0; i < req->message_size(); i++)
{
const logMessage &message = req->message(i);
logger->log_pure(getMessage(message));
}
// basicCommonResponse
basicCommonResponse(req->rid(), true, "一切顺利");
}
6.basicCommonResponse
void basicCommonResponse(const std::string &rid, bool ok, const std::string &err_msg)
{
// 构建resp并发出
commonResponse resp;
resp.set_rid(rid);
resp.set_ok(ok);
resp.set_err_msg(err_msg);
_codec->send(_conn, resp);
}
7.连接管理模块实现
连接管理模块主要负责Connection对象的增、删、查
既然Connection对象需要跟TcpConnection::ptr进行一一对应,因此我们用哈希表来组织管理
因为存在多线程同时访问的情况,因此我们给一把互斥锁保证哈希表的线程安全
class ConnectionManager
{
public:
using ptr = std::shared_ptr<ConnectionManager>;
void OpenConnection(const std::string &connection_id, const muduo::net::TcpConnectionPtr &conn, const ProtobufCodecPtr &codec)
{
std::unique_lock<std::mutex> ulock(_mutex);
if (_myconn_map.count(conn))
return;
_myconn_map.insert(std::make_pair(conn, std::make_shared<Connection>(connection_id, conn, codec)));
}
void CloseConnection(const muduo::net::TcpConnectionPtr &conn)
{
std::unique_lock<std::mutex> ulock(_mutex);
_myconn_map.erase(conn);
}
Connection::ptr getConnection(const muduo::net::TcpConnectionPtr &conn)
{
std::unique_lock<std::mutex> ulock(_mutex);
auto iter = _myconn_map.find(conn);
if (iter == _myconn_map.end())
return Connection::ptr();
return iter->second;
}
private:
std::mutex _mutex;
std::unordered_map<muduo::net::TcpConnectionPtr, Connection::ptr> _myconn_map;
};
2.服务器模块实现
1.成员变量
- muduo库通信所需:EventLoop 、TcpServer 、ProtobufDispatcher 、ProtobufCodecPtr
- 连接管理模块句柄
// 必须先构造loop,在构造server
muduo::net::EventLoop _loop;
muduo::net::TcpServer _server;
// 必须先构造dispatcher,在构造codec
ProtobufDispatcher _dispatcher;
ProtobufCodecPtr _codec;
ConnectionManager::ptr _conn_manager;
2.成员函数
- 构造函数
- 启动服务的start函数
- 事件注册响应回调函数
OnUnknownCallback
OnDeclareLoggerCallback
OnSinkLogCallback
OnOpenConnectionCallback
OnConnection - 基础ack函数
basicCommonResponse
3.构造函数和start函数
using ptr = std::shared_ptr<LogServer>;
LogServer(uint16_t port)
: _server(&_loop, muduo::net::InetAddress("0.0.0.0", port), "LogServer", muduo::net::TcpServer::Option::kReusePort), _dispatcher(std::bind(&LogServer::OnUnknownCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)), _codec(std::make_shared<ProtobufCodec>(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))), _conn_manager(std::make_shared<ConnectionManager>())
{
_dispatcher.registerMessageCallback<declareLoggerRequest>(std::bind(&LogServer::OnDeclareLoggerCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
_dispatcher.registerMessageCallback<sinklogRequest>(std::bind(&LogServer::OnSinkLogCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
_dispatcher.registerMessageCallback<openConnectionRequest>(std::bind(&LogServer::OnOpenConnectionCallback,this,std::placeholders::_1,std::placeholders::_2,std::placeholders::_3));
_server.setConnectionCallback(std::bind(&LogServer::OnConnection, this, std::placeholders::_1));
_server.setMessageCallback(std::bind(&ProtobufCodec::onMessage, _codec.get(), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
}
void start()
{
// 设置监听套接字等等
_server.start();
// 设置事件循环
_loop.loop();
}
4.事件注册响应函数
void OnUnknownCallback(const muduo::net::TcpConnectionPtr &conn, const MessagePtr &message, muduo::Timestamp)
{
default_fatal("未知请求, 我们将断开连接");
if (conn->connected())
conn->shutdown();
}
void OnDeclareLoggerCallback(const muduo::net::TcpConnectionPtr &conn, const declareLoggerRequestPtr &req, muduo::Timestamp)
{
Connection::ptr my_conn = _conn_manager->getConnection(conn);
if (my_conn.get() == nullptr)
{
basicCommonResponse(conn, req->rid(), false, "声明日志器失败,因为业务连接尚未建立");
return;
}
my_conn->declareLogger(req);
}
void OnSinkLogCallback(const muduo::net::TcpConnectionPtr &conn, const sinklogRequestPtr &req, muduo::Timestamp)
{
Connection::ptr my_conn = _conn_manager->getConnection(conn);
if (my_conn.get() == nullptr)
{
basicCommonResponse(conn, req->rid(), false, "打印日志失败,因为业务连接尚未建立");
return;
}
my_conn->sinklog(req);
}
void OnOpenConnectionCallback(const muduo::net::TcpConnectionPtr &conn, const openConnectionRequestPtr &req, muduo::Timestamp)
{
_conn_manager->OpenConnection(req->connection_id(), conn, _codec);
// 构建resp并发出
basicCommonResponse(conn, req->rid(), true, "一切顺利");
}
void OnConnection(const muduo::net::TcpConnectionPtr &conn)
{
if (conn->connected())
{
default_info("连接建立成功");
}
else
{
// 删除该连接
_conn_manager->CloseConnection(conn);
conn->shutdown();
default_info("连接断开成功");
}
}
void basicCommonResponse(const muduo::net::TcpConnectionPtr &conn, const std::string &rid, bool ok, const std::string &err_msg)
{
// 构建resp并发出
commonResponse resp;
resp.set_rid(rid);
resp.set_ok(ok);
resp.set_err_msg(err_msg);
_codec->send(conn, resp);
}
四、客户端实现
1.成员变量
- muduo库网络通信成员:
EventLoopThread 、TcpClient 、TcpConnectionPtr 、CountDownLatch 、ProtobufDispatcher 、ProtobufCodec - 用来协调保持任务顺序性,并保存ack的response的哈希表以及互斥锁和条件变量
// 先初始化EventLoopThread, 后初始化TcpClient
muduo::net::EventLoopThread _loop_thread;
muduo::net::TcpClient _client;
muduo::net::TcpConnectionPtr _conn;
muduo::CountDownLatch _latch;
ProtobufDispatcher _dispatcher;
ProtobufCodec _codec;
// 用来协调保持任务顺序性的哈希表
std::mutex _mutex;
std::condition_variable _cv;
std::unordered_map<std::string, commonResponsePtr> _resp_map;
2.成员函数
- 构造函数和start函数
- 业务模块和网络模块通信类型的转换接口
- 事件注册回调处理函数
- 对外提供的服务接口
1.构造和start函数
LogClient(const std::string &server_ip, uint16_t server_port)
: _client(_loop_thread.startLoop(), muduo::net::InetAddress(server_ip, server_port), "LogClient"),
_dispatcher(std::bind(&LogClient::OnUnknownCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)),
_codec(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)),
_latch(1)
{
_dispatcher.registerMessageCallback<commonResponse>(std::bind(&LogClient::OnCommonResponse, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
_client.setConnectionCallback(std::bind(&LogClient::OnConnection, this, std::placeholders::_1));
_client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, &_codec, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
start();
}
void start()
{
_client.connect();
_latch.wait();
}
2.业务模块和网络模块通信类型的转换接口
这也没啥可说的
loggerType getloggerType(LoggerType type)
{
return (type == LoggerType::SYNC) ? loggerType::SYNC : loggerType::ASYNC;
}
logLevel get_logLevel(LogLevel::value level)
{
switch (level)
{
case LogLevel::value::UNKNOWN:
return logLevel::UNKNOWN;
case LogLevel::value::DEBUG:
return logLevel::DEBUG;
case LogLevel::value::INFO:
return logLevel::INFO;
case LogLevel::value::WARNING:
return logLevel::WARNING;
case LogLevel::value::ERROR:
return logLevel::ERROR;
case LogLevel::value::FATAL:
return logLevel::FATAL;
case LogLevel::value::OFF:
return logLevel::OFF;
default:
return logLevel::UNKNOWN;
}
}
asyncType get_asyncType(AsyncType type)
{
return (type == AsyncType::ASYNC_SAFE) ? asyncType::ASYNC_SAFE : asyncType::ASYNC_UNSAFE;
}
3.事件注册响应回调函数
收到commonResponse之后,把这个resp放到哈希表当中,然后唤醒用户(工作线程)即可
void OnConnection(const muduo::net::TcpConnectionPtr &conn)
{
if (conn->connected())
{
_conn = conn;
std::cout << "连接建立成功\n";
_latch.countDown();
}
else
{
if (_conn.get() && _conn->connected())
_conn->shutdown();
_conn.reset();
std::cout << "连接断开成功\n";
}
}
void OnUnknownCallback(const muduo::net::TcpConnectionPtr &conn, const MessagePtr &message, muduo::Timestamp)
{
std::cout << "未知请求, 即将断开连接\n";
if (conn->connected())
conn->shutdown();
}
void OnCommonResponse(const muduo::net::TcpConnectionPtr &conn, const commonResponsePtr &resp, muduo::Timestamp)
{
std::unique_lock<std::mutex> ulock(_mutex);
_resp_map.insert(std::make_pair(resp->rid(), resp));
_cv.notify_all();
}
用户每调用完一个服务接口之后,都要调用这个waitResponse去等待并获取ack
commonResponsePtr waitResponse(const std::string &rid)
{
std::unique_lock<std::mutex> ulock(_mutex);
_cv.wait(ulock, [this, &rid]() -> bool
{ return _resp_map.count(rid); });
return _resp_map[rid];
}
4.connect
客户端对用户提供的服务接口,都是按照一下步骤来进行的:
- 根据用户传入的参数构造request发送给服务器
- 调用waitResponse等待服务器的ACK
bool connect(const std::string &connection_id)
{
openConnectionRequest req;
req.set_rid(UuidHelper::uuid());
req.set_connection_id(connection_id);
if (!_conn->connected())
{
std::cout << "连接已断开, 无法创建Connection\n";
return false;
}
// 发送req
_codec.send(_conn, req);
commonResponsePtr resp = waitResponse(req.rid());
std::cout << "创建连接情况: " << resp->err_msg() << "\n";
return resp->ok();
}
5.declareLogger
需要用户传入logger_name、logger_type、level、pattern、async_type、sink_vec
注意:这个sink_vec是一个二维数组,我们对其进行遍历和解析,存到declareLoggerRequest的repeated sinks字段当中
我们是这么规定这个二维数组的:
这个max_size是一个string,传递构造时,不要忘了stoi
注意:loggername无需在客户端进行转换,因为服务器会进行转换的
bool declareLogger(const std::string &logger_name, LoggerType logger_type, LogLevel::value level, const std::string &pattern, AsyncType async_type, const std::vector<std::vector<std::string>> &sink_vec)
{
// 1. 构建req
declareLoggerRequest req;
req.set_rid(UuidHelper::uuid());
req.set_logger_name(logger_name);
req.set_logger_type(getloggerType(logger_type));
req.set_limit_level(get_logLevel(level));
req.set_format_pattern(pattern);
req.set_async_type(get_asyncType(async_type));
for (int i = 0; i < sink_vec.size(); i++)
{
auto sinks = sink_vec[i];
sink_args *sink_arg = req.mutable_sinks()->Add();
if (sinks[0] == "StdoutSink")
{
sink_arg->set_type(sink_type::StdoutSink);
}
else if (sinks[0] == "FileSink")
{
sink_arg->set_type(sink_type::FileSink);
}
else if (sinks[0] == "RollSinkBySize")
{
sink_arg->set_type(sink_type::RollSinkBySize);
}
else if (sinks[0] == "SqliteSink")
{
sink_arg->set_type(sink_type::SqliteSink);
}
else if (sinks[0] == "MySQLSink")
{
sink_arg->set_type(sink_type::MySQLSink);
}
for (int j = 1; j < sinks.size(); j++)
{
sink_arg->mutable_args()->Add()->operator=(sinks[j]);
}
}
if (!_conn->connected())
{
std::cout << "连接已断开, 无法声明日志器\n";
return false;
}
// 2. 发送请求
_codec.send(_conn, req);
// 3. 等待响应
commonResponsePtr resp = waitResponse(req.rid());
std::cout << "声明日志器情况: " << resp->err_msg() << "\n";
return resp->ok();
}
6.web_log
1.注意
thread::id也不支持to_string,只能通过ostringstream和流插入的方式来构造出string来
只有通过传递LogMessage,才能保证日志服务器上面打印的日志的确切信息是客户端的
小心这个ostraingstream!!,他的clear只是清空格式状态,不会清空里面缓冲区当中的内容!!
int main()
{
std::ostringstream oss;
for(int i=0;i<10;i++)
{
oss<<i;
std::cout<<oss.str()<<"\n";
oss.clear();
}
return 0;
}
结果:
要想清空的话:使用oss.str("");
int main()
{
std::ostringstream oss;
for(int i=0;i<10;i++)
{
oss<<i;
std::cout<<oss.str()<<"\n";
oss.clear();
oss.str("");
}
return 0;
}
bool web_log(const std::vector<LogMessage::ptr> &message)
{
if (message.empty())
return true;
sinklogRequest req;
req.set_rid(UuidHelper::uuid());
req.set_logger_name(message[0]->_logger_name);
std::ostringstream oss;
for (auto &elem : message)
{
logMessage *message_ptr = req.mutable_message()->Add();
oss << elem->_thread_id;
message_ptr->set_thread_id(oss.str());
oss.clear();
oss.str("");
message_ptr->set_logger_name(elem->_logger_name);
message_ptr->set_time(elem->_time);
message_ptr->set_level(get_logLevel(elem->_level));
message_ptr->set_file(elem->_file);
message_ptr->set_line(elem->_line);
message_ptr->set_body(elem->_body);
}
if (!_conn->connected())
{
std::cout << "连接已断开, 无法发送日志器打印请求\n";
return false;
}
_codec.send(_conn, req);
commonResponsePtr resp = waitResponse(req.rid());
std::cout << "打印日志情况: " << resp->err_msg() << "\n";
return resp->ok();
}
五、网络落地类的实现
1.RemoteSinkManager
1.单例的设计
因为用户可以创建多个RemoteSink,一个RemoteSink跟一个日志器相关联
而客户端的TCP连接只需要保存一份即可,因此我们将对应的LogClient对象的智能指针用一个RemoteSinkManager的全局单例对象来管理这份资源即可
采用懒汉的方式,用户需要时才建立TCP连接
2.配置文件的设计
因为实际应用当中,一个客户端当中的日志基本都只会落地到一个日志服务器上,不会存在落地到多个日志服务器上的情况
所以服务器的server_ip和server_port该客户端只有一份
且该客户端应该只有一个connection_id才对
因此我们要求用户在我们指定目录下的指定配置文件当中按照我们指定的格式填好server_ip、server_port和connection_id
且这种配置文件一般比较重要,不能允许用户随意传入,必须通过修改我们指定的配置文件进行相应属性的配置
(方便进行权限管理,比如只有root或者sudoer才能修改)
static const std::string remote_conf_file="/home/wzs/weblog/remote/client_conf/remote.conf";
class RemoteSinkManager
{
public:
static RemoteSinkManager &getInstance()
{
static RemoteSinkManager remote_sink_manager;
return remote_sink_manager;
}
LogClient::ptr getClient()
{
return _client;
}
private:
RemoteSinkManager()
{
init();
}
~RemoteSinkManager() {}
RemoteSinkManager(const RemoteSinkManager &) = delete;
const RemoteSinkManager &operator=(const RemoteSinkManager &) = delete;
void init()
{
std::ifstream ifs(remote_conf_file);
if (!ifs.is_open())
{
std::cout << "ifs.is_open())\n";
abort();
}
std::string server_ip;
int server_port = -1;
std::string connection_id;
std::string line;
while (getline(ifs, line))
{
size_t pos = line.find(":");
if (pos != std::string::npos)
{
std::string prev = line.substr(0, pos);
if (prev == "server_ip")
server_ip = line.substr(pos + 1);
else if (prev == "server_port")
server_port = std::stoi(line.substr(pos + 1));
else if (prev == "connection_id")
connection_id = line.substr(pos + 1);
}
}
ifs.close();
std::cout << server_ip << " " << server_port << " " << connection_id << "\n";
_client = std::make_shared<LogClient>(server_ip, server_port);
std::cout << "_client建立成功\n";
if (!_client->connect(connection_id))
{
std::cout << "服务器创建connection对象失败\n";
abort();
}
}
LogClient::ptr _client;
};
2.RemoteSink的实现
RemoteSink需要提供两种log_web函数:一种支持同步,一种支持异步
在构造时就进行declareLogger
class RemoteSink
{
public:
using ptr = std::shared_ptr<RemoteSink>;
RemoteSink(const std::string &logger_name, LoggerType logger_type,
LogLevel::value limit_level, const std::string &pattern, AsyncType async_type, const std::vector<std::vector<std::string>> &sink_vec)
: _client(RemoteSinkManager::getInstance().getClient())
{
if (_client.get() == nullptr)
{
std::cout << "_client.get()==nullptr\n";
abort();
}
_client->declareLogger(logger_name, logger_type, limit_level, pattern, async_type, sink_vec);
}
void log_web(LogMessage::ptr message)
{
_client->web_log({message});
}
void log_web(const std::vector<LogMessage::ptr> &message)
{
_client->web_log(message);
}
private:
LogClient::ptr _client;
};
六、异步日志工作器的实现
1.异步缓冲区的实现
1.设计
因为log_web需要vector<LogMessage::ptr>
,跟log_db需要的vector<LogMessage>
雷同,因此我们可以抽象出一个基类LogMessageBuffer,让DBBuffer和RemoteBuffer都继承于LogMessageBuffer
那么vector<LogMessage::ptr>
和vector<LogMessage>
我们该留谁呢?
因为缓冲区当中的数据牵扯到大量的拷贝,且LogMessage::ptr的拷贝代价远远小于LogMessage,因此我们果断选择LogMessage::ptr
2.代码
跟之前的DBBuffer的设计与实现方式一模一样,我们就不赘述了
class LogMessageBuffer
{
public:
LogMessageBuffer(size_t default_size = default_buff_size)
{
_buffer.reserve(default_size);
}
void push(const LogMessage::ptr &message)
{
if (_buffer.size() == _buffer.capacity())
{
_buffer.reserve(2 * _buffer.capacity());
}
_buffer.push_back(message);
}
bool empty()
{
return _buffer.empty();
}
bool full()
{
return _buffer.size() == _buffer.capacity();
}
size_t readableSize()
{
return _buffer.size();
}
void swap(LogMessageBuffer &buffer)
{
_buffer.swap(buffer._buffer);
}
void clear()
{
_buffer.clear();
}
std::vector<LogMessage::ptr> getBuffer()
{
return _buffer;
}
private:
std::vector<LogMessage::ptr> _buffer;
};
class DBBuffer : public LogMessageBuffer
{
public:
DBBuffer(size_t default_size = default_buff_size)
: LogMessageBuffer(default_size)
{
}
};
class RemoteBuffer : public LogMessageBuffer
{
public:
RemoteBuffer(size_t default_size = default_buff_size)
: LogMessageBuffer(default_size)
{
}
};
既然把DBBuffer当中的日志缓冲区修改了,那么相应调用的地方也需要随之修改,没啥难的,这里就不赘述了
2.异步工作器的实现
跟当初添加DBBuffer之后的修改是一样的,这里也不赘述了,直接给代码:
增加一份成员变量:
Resource _remote_resource;
AsyncRemoteCallback _remote_callback;
RemoteBuffer _remote_buffer_produce;
RemoteBuffer _remote_buffer_consume;
std::thread _remote_worker;
增加两个成员函数:
void push_web(const LogMessage::ptr &message)
{
{
std::unique_lock<std::mutex> ulock(_remote_resource._mutex);
if (_async_type == AsyncType::ASYNC_SAFE && _remote_buffer_produce.full())
{
_remote_resource._cond_produce.wait(ulock, [this]() -> bool
{ return !_isrunning || !_remote_buffer_produce.full(); });
}
_remote_buffer_produce.push(message);
}
_remote_resource._cond_consume.notify_all();
}
void remote_thread_routine()
{
while (true)
{
{
std::unique_lock<std::mutex> ulock(_remote_resource._mutex);
// 当停止运行,且生产缓冲区没有数据了,才能退出
if (!_isrunning && _remote_buffer_produce.empty())
break;
_remote_resource._cond_consume.wait(ulock, [this]() -> bool
{ return !_isrunning || !_remote_buffer_produce.empty(); });
// 醒了之后即使发现当停止运行,且生产缓冲区没有数据了,也不能退, 因为停止时业务线程有可能还需要放数据
// 因为二者想要醒来需要竞争锁, 因此不敢说当前生产缓冲区无数据我就退出,不行,需要等到下次循环时的判断
// 交换生产和消费缓冲区
_remote_buffer_consume.swap(_remote_buffer_produce);
}
if (_async_type == AsyncType::ASYNC_SAFE)
{
// 唤醒生产者
_remote_resource._cond_produce.notify_all();
}
// 调用日志落地回调函数
_remote_callback(_remote_buffer_consume);
// 把_buffer_consume重置
_remote_buffer_consume.clear();
}
}
七、Logger部分的修改
1.Logger类的修改
1.SinkMode字段的添加
给日志器增加SinkMode字段,这个之前介绍过了
enum class SinkMode
{
LOCAL,
ROMOTE,
BOTH
};
2.log_web成员函数的增加
virtual void log_web(LogMessage::ptr message) = 0;
在SyncLogger当中:
virtual void log_web(LogMessage::ptr message)
{
// 这里必须要加锁,因为存在多线程同时调用同一个日志器对象的log函数的情况
std::unique_lock<std::mutex> ulock(_mutex);
for (auto &sinks : _remote_sinks)
{
sinks->log_web(message);
}
}
在AsyncLogger当中:
virtual void log_web(LogMessage::ptr message)
{
_looper->push_web(message);
}
virtual void realWebLog(RemoteBuffer &buffer)
{
for (auto &elem : _remote_sinks)
{
elem->log_web(buffer.getBuffer());
}
}
2.LoggerBuilder类的修改
1.成员变量
一共需要增加以下三个成员:
SinkMode _sink_mode = SinkMode::LOCAL;
std::vector<RemoteSink::ptr> _remote_sinks;
std::vector<std::vector<std::string>> _sinks_vec;
2.成员函数
1.buildSinkStr
因为用户在buildLogSink的时候是采用可变模板参数包的方式来进行参数传递的,只能通过递归的方式来解析参数包,可是我们这里是复用了LogFactory当中create函数
因此我们只能要求用户调用我们提供的这个函数,来构建SinkStr
1.if和if constexpr的区别
if 是在运行时进行条件判断,而if constexpr是在编译时进行条件判断的
if constexpr主要用于模板元编程,例如:根据模板参数的类型进行不同的代码生成,或者在编译时选择不同的算法实现
因此我们采用C++17的if constexpr
C++17提供了is_same_v<T,V>用来判断T和V的类型是否相同
2.代码
template <class SinkType>
void buildSinkStr(std::vector<std::string> sink_str)
{
std::string type_str;
if constexpr (std::is_same_v<SinkType, ns_log::StdoutSink>)
type_str = "StdoutSink";
else if constexpr (std::is_same_v<SinkType, ns_log::FileSink>)
type_str = "FileSink";
else if constexpr (std::is_same_v<SinkType, ns_log::RollSinkBySize>)
type_str = "RollSinkBySize";
else if constexpr (std::is_same_v<SinkType, ns_log::SqliteSink>)
type_str = "SqliteSink";
else if constexpr (std::is_same_v<SinkType, ns_log::MySQLSink>)
type_str = "MySQLSink";
else
{
std::cout << "buildSinkStr失败,因为遇到了未知的SinkType类型\n";
return;
}
sink_str.insert(sink_str.begin(), type_str);
_sinks_vec.push_back(std::move(sink_str));
}
2.buildWebLogSink
要求子类在重写build时,检查_sink_mode, 如果需要进行网络落地,则调用该函数
virtual void buildWebLogSink()
{
_remote_sinks.push_back(std::make_shared<RemoteSink>(_logger_name, _logger_type, _limit_level, get_pattern(), _async_type, _sinks_vec));
}
不要忘了在build这里调用一下buildWebLogSink
if (_sink_mode == SinkMode::ROMOTE || _sink_mode == SinkMode::BOTH)
{
this->buildWebLogSink();
}
3.getpattern
在Formatter当中提供一个getPattern
std::string getPattern()
{
return _pattern;
}
在LoggerBuilder这里提供一个getpattern
std::string get_pattern()
{
return _formatter.get() == nullptr ? "" : _formatter->getPattern();
}
八、定时器刷新文件缓冲区的实现
1.为何要有这个类
因为文件是全缓冲的,只有在语言级缓冲区满了/close/进程正常退出之后才会进行刷新
而我们知道,服务器是24小时永远不停机的,因此如果长时间语言级缓冲区都没满的,那么就会导致有些日志被耽搁了好久
因此日志文件一般都是要定时刷新的,所以我们可以采用muduo的定时器来编写一个专门用于刷新文件缓冲区的一个类
2.FileElem
因为我们执行定时任务的线程跟具体进行文件读写的线程不是同一个线程,而对于ofstream对象来说
当一个线程正在对该对象进行write时,另一个线程对其进行flush,是存在线程安全问题的
因此我们进行加锁(用原子类白搭,因为这里写操作涉及到两个原子操作,而两个原子操作放一起之后就不原子了)
【当然,大家也可以用CAS】
因此我们对ofstream进行了一层封装:
// 单例全局文件定时刷新日志器
namespace ns_log
{
struct FileElem
{
using ptr = std::shared_ptr<FileElem>;
std::ofstream _ofs;
std::mutex _mutex;
};
}
3.FileTimerHelper
这个定时器一次性监控管理无数个FileElem::ptr,因此我们把他搞成全局的单例对象(懒汉)
注意:定时任务就是固定的:
检查该文件是否被打开,是否处于写入状态,如果被打开且没有处于写状态,才执行写入操作
namespace ns_helper
{
const double default_interval = 10.0;
class FileTimerHelper
{
public:
static FileTimerHelper &getInstance()
{
static FileTimerHelper timer;
return timer;
}
void set_timer(const ns_log::FileElem::ptr &sp)
{
std::unique_lock<std::mutex> ulock(_mutex);
auto iter = timer_map.find(sp);
if (iter == timer_map.end())
{
timer_map[sp] = loop->runEvery(_interval, std::bind(&FileTimerHelper::timerCallback, this, sp));
}
else
{
std::cout << "该数据已经被设置过定时了\n";
}
}
void cancel_timer(const ns_log::FileElem::ptr &sp)
{
std::unique_lock<std::mutex> ulock(_mutex);
auto iter = timer_map.find(sp);
if (iter != timer_map.end())
{
loop->cancel(iter->second);
timer_map.erase(iter);
}
}
void shutdown()
{
std::unique_lock<std::mutex> ulock(_mutex);
if (_running)
{
_running = false;
for (auto &kv : timer_map)
{
loop->cancel(kv.second);
}
loop->quit();
}
}
private:
FileTimerHelper(double interval = default_interval)
: _interval(interval), loop(loop_thread.startLoop()) {}
~FileTimerHelper()
{
if (_running)
{
shutdown();
}
}
FileTimerHelper(const FileTimerHelper &) = delete;
const FileTimerHelper &operator=(const FileTimerHelper &) = delete;
void timerCallback(const ns_log::FileElem::ptr &sp)
{
std::unique_lock<std::mutex> ulock(sp->_mutex);
if (sp->_ofs.is_open())
{
sp->_ofs.flush();
}
}
double _interval;
bool _running = true;
// 存在多线程同时访问的需求,因此要加互斥锁
std::mutex _mutex;
std::unordered_map<ns_log::FileElem::ptr, muduo::net::TimerId> timer_map;
muduo::net::EventLoopThread loop_thread;
muduo::net::EventLoop *loop;
};
}
4.FileSink的修改
构造时设置定时任务
析构时取消定时任务
class FileSink : public FSLogSink
{
public:
FileSink(const std::string &filename)
: _filename(filename), _file_elem(std::make_shared<FileElem>())
{
// 1. 创建filename所在目录
FileHelper::createDir(FileHelper::getPath(_filename));
// 2. 打开文件(二进制 + 追加写)
_file_elem->_ofs.open(_filename, std::ios::binary | std::ios::app);
if (!_file_elem->_ofs.is_open())
{
std::cout << "FileSink 打开文件失败, 文件名: " << _filename << "\n";
abort();
}
// 3. 设置定时任务
FileTimerHelper::getInstance().set_timer(_file_elem);
}
~FileSink()
{
// 1. 删除定时任务
FileTimerHelper::getInstance().cancel_timer(_file_elem);
// 2. 防止fd泄露
if (_file_elem->_ofs.is_open())
_file_elem->_ofs.close();
}
virtual void log_fs(const char *data, size_t len)
{
std::unique_lock<std::mutex> ulock(_file_elem->_mutex);
_file_elem->_ofs.write(data, len);
if (!_file_elem->_ofs.good())
{
std::cout << "FileSink 写入文件有问题, 文件名: " << _filename << "\n";
abort();
}
}
private:
std::string _filename;
FileElem::ptr _file_elem;
};
5.RollSinkBySize的修改
打开文件时设置定时任务,关闭文件时取消定时任务,析构时取消定时任务
class RollSinkBySize : public FSLogSink
{
public:
RollSinkBySize(const std::string &basename, size_t max_size)
: _basename(basename), _file_elem(std::make_shared<FileElem>()), _max_size(max_size), _cur_size(0), _name_count(0)
{
check();
}
~RollSinkBySize()
{
// 取消定时任务
FileTimerHelper::getInstance().cancel_timer(_file_elem);
// 防止fd泄露
if (_file_elem->_ofs.is_open())
_file_elem->_ofs.close();
}
virtual void log_fs(const char *data, size_t len)
{
check();
std::unique_lock<std::mutex> ulock(_file_elem->_mutex);
_file_elem->_ofs.write(data, len);
if (!_file_elem->_ofs.good())
{
std::cout << "RollSinkBySize 写入文件有问题, 基础文件名: " << _basename << "\n";
abort();
}
_cur_size += len;
}
private:
void check()
{
if (!_file_elem->_ofs.is_open() || _cur_size >= _max_size)
{
// 0. 关闭旧文件
if (_file_elem->_ofs.is_open())
{
// 取消定时任务
FileTimerHelper::getInstance().cancel_timer(_file_elem);
_file_elem->_ofs.close();// 因为已经取消了定时任务,所以这里的close操作线程安全
}
// 1. 拿到新的文件名
std::string newFile = createNewFile();
// 2. 创建该文件
FileHelper::createDir(FileHelper::getPath(newFile));
// 3. 打开该文件
_file_elem->_ofs.open(newFile, std::ios::app | std::ios::binary);
if (!_file_elem->_ofs.is_open())
{
std::cout << "RollSinkBySize 打开文件有问题, 基础文件名: " << _basename << ", 该文件名: " << newFile << "\n";
abort();
}
// 4. 清空cur_size
_cur_size = 0;
// 6. 添加定时任务
FileTimerHelper::getInstance().set_timer(_file_elem);
}
}
std::string createNewFile()
{
size_t count = _name_count.fetch_add(1); // 原子性进行++并返回++之前的值
std::ostringstream oss;
// 基础文件名 + 时间戳(年月日时分秒)+ 序号 + ".log"
time_t now = DateHelper::now();
struct tm *tm = localtime(&now);
oss << _basename << tm->tm_year + 1900 << std::setw(2) << std::setfill('0') << tm->tm_mon + 1 << std::setw(2) << std::setfill('0') << tm->tm_mday
<< std::setw(2) << std::setfill('0') << tm->tm_hour
<< std::setw(2) << std::setfill('0') << tm->tm_min << std::setw(2) << std::setfill('0')
<< tm->tm_sec << "-" << count << ".log";
return oss.str();
}
std::string _basename;
FileElem::ptr _file_elem;
std::atomic<size_t> _name_count;
size_t _max_size;
size_t _cur_size;
};
九、测试
const std::string web_logger_name = "remote_logger";
void bench_web(int thread_num, size_t log_num, size_t per_size)
{
// 1. 获取全局日志器
Logger::ptr logger = getLogger(web_logger_name);
assert(logger.get() != nullptr);
// 2. 构造日志body
std::string body = std::string(per_size - 1, 'A'); // 为'\n'流出位置
// 3. 创建线程,并为其分配要打印的日志数量
size_t per_count = log_num / thread_num;
std::vector<std::thread> thread_vec(thread_num);
// 4. 维护每个线程运行的时间
std::vector<double> time_vec(thread_num);
// 5. 启动线程,打印日志
for (int i = 0; i < thread_num; i++)
{
// i必须要传值捕捉,因为主线程会修改i, 所以i线程不安全
thread_vec[i] = std::thread(
[&, i, per_count]() mutable
{
// 1. 补充日志数量
if (i == thread_num - 1)
per_count += log_num % thread_num;
// 2. 计时
auto start = std::chrono::high_resolution_clock::now();
for (int j = 0; j < per_count; j++)
{
logger->fatal("%s", body.c_str());
}
// 3. 取消计时
auto end = std::chrono::high_resolution_clock::now();
// 4. 记录时间
std::chrono::duration<double> tm = end - start;
time_vec[i] = tm.count();
// 5. 打印运行状况
std::cout << "\t线程-" << i << "\t打印日志数:" << per_count << "\t总耗时:" << time_vec[i] << "秒\n";
});
}
// 6. join线程
for (auto &worker : thread_vec)
{
worker.join();
}
// 7. 遍历求出最长时间
// 每个线程之间都是并发运行的,因此耗时最长的线程就是日志打印总耗时
double max_time = 0.0;
for (auto &elem : time_vec)
{
max_time = std::max(max_time, elem);
}
// 8. 打印总结果
// 总耗时、每秒输出日志数量、每秒输出日志大小
double per_second_num = (1.0 * log_num) / max_time;
double per_second_size = (log_num * per_size * 1.0) / (max_time * 1024); // 以KB为单位
std::cout << "\t总耗时:" << max_time << "\n";
std::cout << "\t每秒输出日志数量:" << per_second_num << "\n";
std::cout << "\t每秒输出日志大小:" << per_second_size << "KB\n";
}
void init_web(LoggerType logger_type, AsyncType async_type = AsyncType::ASYNC_SAFE)
{
LoggerBuilder::ptr builder = std::make_shared<GlobalLoggerBuilder>();
builder->buildLoggerName(web_logger_name);
builder->buildSinkMode(SinkMode::ROMOTE);
builder->buildLoggerType(logger_type);
builder->buildLimitLevel(LogLevel::value::WARNING);
builder->buildSinkStr<ns_log::FileSink>({"./logfile/file.log"});
builder->buildSinkStr<ns_log::RollSinkBySize>({"./logfile/roll-", "10485760"});
builder->buildSinkStr<ns_log::SqliteSink>({"./database/test.db"});
builder->buildSinkStr<ns_log::MySQLSink>({"./mysql.conf"});
builder->build();
}
const int log_num = 1024 * 1024;
const int per_size = 100;
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "Usage: " << "./bench_web sync:1 async_safe:2 async_unsafe:3 " << " thread_num\n";
return 1;
}
int thread_num = std::stoi(argv[2]);
switch (std::stoi(argv[1]))
{
case (1):
{
init_web(LoggerType::SYNC);
break;
}
case (2):
{
init_web(LoggerType::ASYNC, AsyncType::ASYNC_SAFE);
break;
}
case (3):
{
init_web(LoggerType::ASYNC, AsyncType::ASYNC_UNSAFE);
break;
}
}
bench_web(thread_num, log_num, per_size);
system("rm -rf ./logfile");
return 0;
}
1.同步
服务器我们就统一用异步方式了
为了方便测试,我们把日志消息的ACK的cout去掉
同步肯定很慢,因为每次只请求打印一条日志,要进行一百万次网络请求,就算服务器开异步也挽救不了多少性能
而且我们的客户端和服务器在同一台云服务器上面,所以肯定没有之前本地落地方式快
2.异步安全
因为咱们异步太快了,所以很快就超出了protobufCodec当中应用层协议规定的长度:
六千多万字节,可是咱们:
所以不能打印这么多日志,那么我们就打印个40万条数据得了
3.异步不安全
以上就是日志系统扩展二:日志服务器的实现哦~