日志系统扩展二:日志服务器的实现

news2024/11/17 2:53:46

日志系统扩展二:日志服务器的实现

  • 一、设计
    • 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.成员函数

连接模块所需成员函数如下:

  1. 声明日志器的declareLogger
  2. 打印日志的sinklog

他们分别只需要接受对应的RequestPtr作为参数即可

需要提供网络模块和业务模块之间通信类型转换的接口:

  1. getMessage
  2. getLoggerType
  3. getLogLevel
  4. getAsyncType
  5. 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

  1. 转换日志器名称
  2. 查找是否有该日志器
  3. 构造日志器
  4. 调用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
  1. 转换日志器名称
  2. 拿到日志器
  3. 调用log_pure进行日志打印
  4. 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.成员变量

  1. muduo库通信所需:EventLoop 、TcpServer 、ProtobufDispatcher 、ProtobufCodecPtr
  2. 连接管理模块句柄
// 必须先构造loop,在构造server
muduo::net::EventLoop _loop;
muduo::net::TcpServer _server;

// 必须先构造dispatcher,在构造codec
ProtobufDispatcher _dispatcher;
ProtobufCodecPtr _codec;

ConnectionManager::ptr _conn_manager;

2.成员函数

  1. 构造函数
  2. 启动服务的start函数
  3. 事件注册响应回调函数
    OnUnknownCallback
    OnDeclareLoggerCallback
    OnSinkLogCallback
    OnOpenConnectionCallback
    OnConnection
  4. 基础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.成员变量

  1. muduo库网络通信成员:
    EventLoopThread 、TcpClient 、TcpConnectionPtr 、CountDownLatch 、ProtobufDispatcher 、ProtobufCodec
  2. 用来协调保持任务顺序性,并保存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.成员函数

  1. 构造函数和start函数
  2. 业务模块和网络模块通信类型的转换接口
  3. 事件注册回调处理函数
  4. 对外提供的服务接口

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

客户端对用户提供的服务接口,都是按照一下步骤来进行的:

  1. 根据用户传入的参数构造request发送给服务器
  2. 调用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.异步不安全

在这里插入图片描述

以上就是日志系统扩展二:日志服务器的实现哦~

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

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

相关文章

MySQL record 06 part

事务、存储过程 事务&#xff1a; MySQL的同步&#xff0c;同步是指 together done&#xff0c;要么一起前进&#xff0c;要么一起后退的意思。 注意&#xff0c;回滚 rollback 对已经提交 commit 的数据是无效的&#xff0c;也就是说&#xff0c;只能对没有被提交 commit …

CSS 布局三大样式简单学习

目录 1. css 浮动 1.1 效果1 1.2 效果2 1.3 效果3 1.4 效果4 2. css 定位 2.1 absolute 2.2 relative 2.3 fixed 3. css 盒子模型 3.1 效果1 3.2 效果2 3.3 效果3 3.4 效果4 1. css 浮动 1.1 效果1 1.2 效果2 1.3 效果3 1.4 效果4 2. css 定位 2.1 absolute 2.2 …

thinkphp 做分布式服务+读写分离+分库分表(分区)(后续接着写)

thinkphp 做分布式服务读写分离分库分表&#xff08;分区&#xff09; 引言 thinkphp* 大道至简一、分库分表分表php 分库分表hash算法0、分表的方法&#xff08;thinkphp&#xff09;1、ThinkPHP6 业务分表之一&#xff1a;UID 发号器2、ThinkPHP6 业务分表之二&#xff1a;用…

希尔排序(C语言实现)

目录 1.希尔排序( 缩小增量排序 ) 2.动图 ​编辑 3.代码实现 预排序实现 子序列排列实现 单趟排序实现 对整组数进行子排序 希尔排序代码 代码测试 时间复杂度分析 希尔排序的特性总结&#xff1a; 1.希尔排序( 缩小增量排序 ) 基本思想&#xff1a; 1.先选定一个…

QTCreator 调试:unknown debugger type “No engine“

QTCreator 调试&#xff1a;unknown debugger type "No engine" - kaizenly - 博客园 (cnblogs.com) 一开始Debuggers---Auto-detected这里第一row第一个项是标红的&#xff0c;然后没改东西&#xff0c;点完应用Apply以后&#xff0c;就可以调试了...&#xff08;不…

在python爬虫中xpath方式提取lxml.etree._ElementUnicodeResult转化为字符串str类型

简单提取网页中的数据时发现的 当通过xpath方式提取出需要的数据的text文本后想要转为字符串&#xff0c;但出现lxml.etree._ElementUnicodeResult的数据类型不能序列化&#xff0c;在网上查找到很多说是编码问题Unicode编码然后解码什么的&#xff1b;有些是(导入的xml库而不…

深度学习之概率论预备知识点(3)

在深度学习中&#xff0c;概率论和数理统计是理解许多算法背后的理论基础。这些知识在处理不确定性、估计模型参数、理解数据分布等方面非常关键 1、概率 一种用来描述随机事件发生的可能性的数字度量&#xff0c;表示某一事件发生的可能性。 概率并不客观存在&#xff0c;是…

Android Choreographer 监控应用 FPS

Choreographer 是 Android 提供的一个强大的工具类&#xff0c;用于协调动画、绘制和视图更新的时间。它的主要作用是协调应用的绘制过程&#xff0c;以确保流畅的用户体验。Choreographer 也可以帮助我们获取帧时间信息&#xff0c;从而为性能监测和优化提供重要的数据支持。 …

IDEA中Quarkus框架(3.13版本)开发、调试、部署、打包等

code-with-quarkus code-with-quarkus 是使用官网生成的demo项目 这个项目使用Quarkus&#xff08;使用3.13.0版本&#xff0c;该版本支持JDK21&#xff09;&#xff0c;超音速亚原子Java框架。官网地址: https://quarkus.io/. 环境要求 OS: Windows 10.0 jdk 11 maven 3.9…

淘宝扭蛋机小程序,扭蛋机文化下的新体验

在数字化时代中&#xff0c;扭蛋机逐渐从传统的线下机器转移到了线上互联网中&#xff0c;市场得到了创新发展。扭蛋机小程序具有便捷、多样化、个性化的特点&#xff0c;迎合了当下消费者的线上消费习惯&#xff0c;又能够让扭蛋机玩家体验到新鲜有趣的扭蛋。 扭蛋机是一种热…

python简单的小项目-关于央行储蓄占比情况的数据可视化

该数据来源于锐思数据库&#xff0c;如果数据有偏差&#xff0c;可能是本人搜索的问题&#xff0c;希望大家谅解。 数据大纲&#xff1a; 其中我们制作折现统计图需要用到的是截止日期&#xff0c;表达数据最后获取的日期&#xff0c;而更新时间则是数据时效性的表示&#xff…

django项目添加测试数据的三种方式

文章目录 自定义终端命令Faker添加模拟数据基于终端脚本来完成数据的添加编写python脚本编写shell脚本执行脚本需要权限使用shell命令来完成测试数据的添加 添加测试数据在工作中一共有三种方式&#xff1a; 可以根据django的manage.py指令进行[自定义终端命令]可以采用第三方…

pthread_cond_signal 和pthread_cond_wait

0、pthread_join()函数作用&#xff1a; pthread_join() 函数会一直阻塞调用它的线程&#xff0c;直至目标线程执行结束&#xff08;接收到目标线程的返回值&#xff09;&#xff0c;阻塞状态才会解除。如果 pthread_join() 函数成功等到了目标线程执行结束&#xff08;成功获取…

【C++】list详解及模拟实现

目录 1. list介绍 2. list使用 2.1 修改相关 2.2 遍历 2.3 构造 2.4 迭代器 2.5 容量相关 2.6 元素访问 2.7 操作相关 3. 模拟实现 3.1 节点类 3.1.1 初始结构 3.1.2 节点的构造函数 3.2 迭代器类 3.2.1 初始结构 3.2.2 迭代器 3.2.3 迭代器-- 3.2.4 解引…

1.随机事件与概率

第一章 随机时间与概率 1. 随机事件及其运算 1.1 随机现象 ​ 确定性现象&#xff1a;只有一个结果的现象 ​ 确定性现象&#xff1a;结果不止一个&#xff0c;且哪一个结果出现&#xff0c;人们事先并不知道 1.2 样本空间 ​ 样本空间&#xff1a;随机现象的一切可能基本…

ML 系列:机器学习和深度学习的深层次总结(05)非线性回归

图 1.不同类型的回归 一、说明 非线性回归是指因变量和自变量之间存在非线性关系的模型。该模型比线性模型更准确、更灵活&#xff0c;可以获取两个或多个变量之间复杂关系的各种曲线。 二、关于 当数据之间的关系无法用直线预测并且呈曲线形式时&#xff0c;我们应该使用非线性…

MySQL篇(索引)(持续更新迭代)

目录 一、简介 二、有无索引情况 1. 无索引情况 2. 有索引情况 3. 优劣势 三、索引结构 1. 简介 2. 存储引擎对于索引结构的支持情况 3. 为什么InnoDB默认的索引结构是Btree而不是其它树 3.1. 二叉树&#xff08;BinaryTree&#xff09; 3.2. 红黑树&#xff08;RB&a…

6、等级保护政策内容

数据来源&#xff1a;6.等级保护政策内容_哔哩哔哩_bilibili 信息安全产品管理与响应 等级管理 对信息系统中使用的信息安全产品实行按等级管理&#xff0c;信息安全事件应分等级响应与处置。 预测评服务由测评公司和咨询公司提供预测评服务&#xff0c;根据技术要求和测评要…

高校心理辅导系统:Spring Boot技术实现指南

目 录 摘 要 I ABSTRACT II 1绪 论 1 1.1研究背景 1 1.2设计原则 1 1.3论文的组织结构 2 2 相关技术简介 3 2.1Java技术 3 2.2B/S结构 3 2.3MYSQL数据库 4 2.4Springboot框架 4 3 系统分析 6 3.1可行性分析 6 3.1.1技术可行性 6 3.1.2操作可行性 6 3.1.3经济可行性 6 3.1.4法律…

[OpenGL]使用OpenGL绘制带纹理三角形

一、简介 本文介绍了如何使用使用OpenGL绘制带纹理三角形。 在绘制带纹理的三角形时&#xff0c; 首先使用.h读取准备好的.png格式的图片作为纹理&#xff0c;然后在fragment shader中使用 ... in vec2 textureCoord; uniform sampler2D aTexture1; void main() {FragColor …