logger日志系统设计与实现

目录

1.为什么要写日志 

2.实现思想

3.logger日志系统的具体实现

3.1 在src下的include增加头文件:logger.h

3.2 在src下的include增加头文件:lockqueue.h

3.3 在src下创建logger.cc

3.4 完善src的CMakeLists.txt

4. 把日志集成到系统中

4.1 完善prpcprovider.cc


1.为什么要写日志 

图中画圆圈的是我们实现的mprpc框架,这个框架是给别人使用的,把本地的服务发布成远程的RPC服务,框架里最重要的两个成员就是RpcProvider和RpcChannel,他们在运行的过程中会有很多正常的输出信息和错误的信息,我们不可能都cout它们到屏幕上,因为运行时间长了,屏幕上输出的信息特别多,如果万一有什么问题,我们也不好定位,真正用起来的话不方便。所以,一般出问题,我们最直接的方式就是看日志!!!
日志可以记录正常软件运行过程中出现的信息和错误的信息,当我们定位问题,就打开相应的日志去查看,查找。

2.实现思想

假如我们在框架后边输出一个日志模块,我们想把框架运行中的正常的信息和错误信息都记录在日志文件,该怎么做?
左边的两个箭头表示RPC的请求和RPC的响应。
RPC请求过来的时候,我们的框架在执行的时候会产生很多日志文件,我们要写日志,写日志信息的过程是磁盘I/O,磁盘I/O速度很慢,我们不能把磁盘I/O的花销算在RPC请求的业务执行部门里面(否则造成RPC请求处理的效率慢),我们不能把日志花费的时间算在框架业务的执行时间里面,所以,一般在我们的服务器中,增加一个kafka,就可以用在日志系统中做一个消息队列中间件,我们把日志写在一个缓存队列里(这个队列相当于异步的日志写入机制),我们的框架做的就是写日志到内存的队列里面,不做磁盘I/O操作。然后我们在后面有一个专门写日志的线程,就是做磁盘I/O操作,从队头取出日志信息,然后把日志信息写到日志文件中,这样它的磁盘I/O的消耗就不会算在我们的RPC请求的业务当中。

我们的mprpc框架的RpcProvier端是用muduo库实现的,采用的是epoll加多线程,很有可能RPC的处理过程是在多个线程中都会去做这个,多个线程都会去写日志,也就是多个线程都会在这个缓存队列里面添加数据,所以我们的这个缓存队列必须保证线程安全。
我们的C++中有queue这个队列,也就是C++的容器,但是C++容器只考虑使用功能,没有考虑线程安全,所以我们用的是线程互斥机制来维护入队出队的线程安全,写日志的线程也是一个独立的线程,用唯一的互斥锁实现,大家去争这把锁,多线程往队列里写日志,争到锁就写;写线程日志,争到锁,从队列里拿信息,写道文件中,如果队列是空的话,也就是之前的日志都写到日志文件了,写日志的线程这时就不用抢这把互斥锁了,因为没有东西可写。导致写入队列的线程无法及时获取锁,信息无法及时写到队列,破坏了RPC请求的业务的效率。

所以,单单使用一把锁可以解决问题,但是效率低,我们还要处理线程间的通信的问题。用到了线程的互斥+线程的通信
队列如果是空,写日志的线程就一直等待,队列不空的时候,写日志的线程才有必要去抢锁,把日志信息写到日志文件。
我们的日志文件放在当前目录下log。
每一天都生成新的日志文件,有助于在发生问题的时候快速定位。
而且如果当天我们的日志文件如果容量太大,比如说超过20M,就会产生新的!

kafka是开源的,非常著名,分布式消息队列,其中功能:在分布式环境中提供异步的日志写入服务器中间件,日志写入的系统,和我们的queue本质相同,但是它设计高级,高效,高可用性,可容灾,稳定。

3.logger日志系统的具体实现

3.1 在src下的include增加头文件:logger.h

#pragma once
#include "lockqueue.h"
#include <string>


//定义宏 LOG_INFO("xxx %d %s", 20, "xxxx")
//可变参,提供给用户更轻松的使用logger 
//snprintf, 缓冲区,缓冲区的长度,写的格式化字符串, ##__VA_ARGS__。
//代表了可变参的参数列表,填到缓冲区当中,然后 logger.Log(c)
#define LOG_INFO(logmsgformat, ...)\
    do \
    {  \
        Logger &logger=Logger::GetInstance();\
        logger.SetLogLevel(INFO);\
        char c[1024]={0};\
        snprintf(c,1024,logmsgformat,##__VA_ARGS__);\
        logger.Log(c);\
    }while(0)\

#define LOG_ERR(logmsgformat, ...)\
    do \
    {  \
        Logger &logger=Logger::GetInstance();\
        logger.SetLogLevel(ERROR);\
        char c[1024]={0};\
        snprintf(c,1024,logmsgformat,##__VA_ARGS__);\
        logger.Log(c);\
    }while(0)\

enum LOgLevel
{
    INFO,  //普通信息
    ERROR, //错误信息
};

//Mprpc框架提供的日志系统
class Logger
{
public:
    //获取日志的单例
    static Logger& GetInstance();
    //设置日志级别
    void SetLogLevel(LOgLevel level);
    //写日志
    void Log(std::string msg);
private:
    int m_loglevel; //记录日志级别
    LockQueue<std::string> m_lckQue;//日志缓冲队列

    Logger();
    Logger(const Logger&)=delete;
    Logger(Logger&&)=delete;
};

3.2 在src下的include增加头文件:lockqueue.h

异步缓冲队列的实现

#pragma once 
#include <queue>
#include <thread>
#include <mutex> //pthread_mutex_t
#include <condition_variable> //pthread_condition_t

//异步写日志的日志队列
template<typename T>//模板代码,成员函数实现部分只能写在这个文件中
class LockQueue
{
public:
    //多个worker线程都会写日志queue
    void Push(const T &data)
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        m_queue.push(data);
        m_condvariable.notify_one();
    }

    //一个线程读日志queue,写日志文件
    T Pop()
    {
        std::unique_lock<std::mutex> lock(m_mutex);
        while(m_queue.empty())
        {
            //日志队列为空,线程进入wait状态
            m_condvariable.wait(lock);
        }

        T data=m_queue.front();
        m_queue.pop();
        return data;
    }
private:
    std::queue<T> m_queue;
    std::mutex m_mutex;
    std::condition_variable m_condvariable;
};

3.3 在src下创建logger.cc

#include "logger.h"

#include <time.h>
#include <iostream>

// 获取日志的单例
Logger& Logger::GetInstance()
{
    static Logger logger;
    return logger;
}

Logger::Logger()
{
    //启动专门的写日志线程
    std::thread writeLogTask([&](){
        for(;;)
        {
            //获取当前的日期,然后取日志信息,写入相应的日志文件当中 a+
            time_t now=time(nullptr);
            tm *nowtm=localtime(&now);

            char file_name[128];
            sprintf(file_name,"%d-%d-%d-log.txt",nowtm->tm_year+1900,nowtm->tm_mon+1,nowtm->tm_mday);

            FILE *pf=fopen(file_name,"a+");
            if(pf==nullptr)
            {
                std::cout<<"logger file:"<<file_name<<" open error!"<<std::endl;
                exit(EXIT_FAILURE);
            }

            std::string msg=m_lckQue.Pop();

            char time_buf[128]={0};
            sprintf(time_buf,"%d:%d:%d =>[%s] ",
                    nowtm->tm_hour,
                    nowtm->tm_min,
                    nowtm->tm_sec,
                    (m_loglevel==INFO?"INFO":"ERROR"));
            msg.insert(0,time_buf);
            msg.append("\n");

            fputs(msg.c_str(),pf);
            fclose(pf);
        }
    });
    //设置分离线程,守护线程
    writeLogTask.detach();
}
// 设置日志级别
void Logger::SetLogLevel(LOgLevel level)
{
    m_loglevel=level;
}

// 写日志
void Logger::Log(std::string msg)
{
    m_lckQue.Push(msg);
}

3.4 完善src的CMakeLists.txt

4. 把日志集成到系统中

4.1 完善prpcprovider.cc

#include "rpcprovider.h"
#include "mprpcapplication.h"
#include "rpcheader.pb.h"
#include "logger.h"

/*
service_name=> service描述(一个服务由一个服务名字对应)
                    =》 service*  记录服务对象
                    method_name => method方法对象
json:存储键值对,基于文本存储;数据有对应的键值
protobuf:基于二进制存储,存储效率更高;紧密存储,不携带除数据外的任何信息,整体来说protobuf存储效率更高,占用的带宽更少,同样带宽传输的数据量更大
            不仅可以提供类型的序列化和反序列化,还提供了service rpc方法的描述
*/
//这里是框架提供给外部使用的,可以发布rpc方法的函数接口
void RpcProvider::NotifyService(google::protobuf::Service *service) 
{
    ServiceInfo service_info;

    //获取了服务对象的描述信息
    const google::protobuf::ServiceDescriptor* pserviceDesc=service->GetDescriptor();
    //获取服务的名字
    std::string service_name=pserviceDesc->name();
    //获取服务对象service的方法的数量
    int methodCnt=pserviceDesc->method_count();

    //std::cout<<"service_name:"<<service_name<<std::endl;
    LOG_INFO("service_name:%s",service_name.c_str());

    for(int i=0;i<methodCnt;++i)
    {
        //获取了服务对象指定下标的服务方法的描述(抽象描述) UserService Login
        const google::protobuf::MethodDescriptor* pmethodDesc=pserviceDesc->method(i);
        std::string method_name=pmethodDesc->name();
        service_info.m_methodMap.insert({method_name,pmethodDesc});
        //std::cout<<"method_name:"<<method_name<<std::endl;
        LOG_INFO("method_name:%s",method_name.c_str());
    }
    service_info.m_service=service;
    m_serviceMap.insert({service_name,service_info});
    
}

// 启动rpc服务节点,开始提供rpc远程网络调用服务
void RpcProvider::Run()
{
    std::string ip=MprpcApplication::GetInstance().GetConfig().Load("rpcserverip");
    uint16_t port=atoi(MprpcApplication::GetInstance().GetConfig().Load("rpcserverport").c_str());
    muduo::net::InetAddress address(ip,port);

    //创建TcpServer对象
    muduo::net::TcpServer server(&m_eventLoop,address,"RpcProvider");

    //绑定连接回调和消息读写回调方法 ,muduo库的好处是:分离了网络代码和业务代码
    server.setConnectionCallback(std::bind(&RpcProvider::OnConnection, this, std::placeholders::_1));//预留1个参数std::placeholders::_1
    server.setMessageCallback(std::bind(&RpcProvider::OnMessage, this, std::placeholders::_1, 
            std::placeholders::_2, std::placeholders::_3));//预留3个参数std::placeholders::_1,2,3

    //设置muduo库的线程数量
    server.setThreadNum(4);

    std::cout<<"RpcProvider start service at ip:"<<ip<<"port:"<<port<<std::endl;

    //启动网络服务
    server.start();
    m_eventLoop.loop();
}

//新的socket连接回调
void RpcProvider::OnConnection(const muduo::net::TcpConnectionPtr& conn)
{
    if(!conn->connected())
    {
        //和rpc client的连接断开了
        conn->shutdown();
    }
}
/*
在框架内部,RpcProvider和RpcConsumer协商好之间通信用的protobuf数据类型
service_name  method_name  args   定义proto的message类型,进行数据头的序列化和反序列化
                                  service_name  method_name  args_size
16UserServiceLoginzhang san123456

header_size(4个字节)+header_str+args_str
10 "10"
10000 "10000"
std::string insert和copy方法
*/
// 已建立连接用户的读写事件回调  如果远程有一个rpc服务的调用请求,那么OnMessage方法就会响应
void RpcProvider::OnMessage(const muduo::net::TcpConnectionPtr& conn,
                            muduo::net::Buffer* buffer,
                            muduo::Timestamp)
{
    //网络上接收的远程rpc调用请求的字符流    Login  args
    std::string recv_buf=buffer->retrieveAllAsString();

    //从字符流中读取前4个字节的内容
    uint32_t header_size = 0;
    recv_buf.copy((char*)&header_size,4,0);

    //根据header_size读取数据头的原始字符流,反序列化数据,得到rpc请求的详细消息
    std::string rpc_header_str=recv_buf.substr(4,header_size);
    mprpc::RpcHeader rpcHeader;
    std::string service_name;
    std::string method_name;
    uint32_t args_size;
    if(rpcHeader.ParseFromString(rpc_header_str))
    {
        //数据头反序列化成功
        service_name=rpcHeader.service_name();
        method_name=rpcHeader.method_name();
        args_size=rpcHeader.args_size();
    }
    else
    {
        //数据头反序列化失败
        std::cout<<"rpc_header_str:"<<rpc_header_str<<" parse error!"<<std::endl;
        return;
    }

    //获取rpc方法参数的字符流数据
    std::string args_str=recv_buf.substr(4+header_size,args_size);

    //打印调试信息
    std::cout<<"===================================================="<<std::endl;
    std::cout<<"header_size:"<<header_size<<std::endl;
    std::cout<<"rpc_header_str:"<<rpc_header_str<<std::endl;
    std::cout<<"service_name:"<<service_name<<std::endl;
    std::cout<<"method_name:"<<method_name<<std::endl;
    std::cout<<"args_str:"<<args_str<<std::endl;
    std::cout<<"===================================================="<<std::endl;

    //获取service对象和method对象
    auto it=m_serviceMap.find(service_name);
    if(it==m_serviceMap.end())
    {
        std::cout<<service_name<<" is not exist!"<<std::endl;
        return;
    }

    auto mit=it->second.m_methodMap.find(method_name);
    if(mit==it->second.m_methodMap.end())
    {
        std::cout<<service_name<<":"<<method_name<<"is not exist!"<<std::endl;
        return;
    }

    google::protobuf::Service* service=it->second.m_service;//获取service对象  new UserService
    const google::protobuf::MethodDescriptor* method=mit->second;//获取method对象  Login

    //生成rpc方法调用的请求request和响应response参数
    google::protobuf::Message* request=service->GetRequestPrototype(method).New();
    if(!request->ParseFromString(args_str))
    {
        std::cout<<"request parse error,content:"<<args_str<<std::endl;
        return;
    }
    google::protobuf::Message* response=service->GetResponsePrototype(method).New();

    //给下面的method方法的调用,绑定一个Closure的回调函数
    google::protobuf::Closure* done=google::protobuf::NewCallback<RpcProvider,
                                                                  const muduo::net::TcpConnectionPtr&,
                                                                  google::protobuf::Message*>
                                                                  (this,
                                                                  &RpcProvider::SendRpcResponse,
                                                                  conn,response);

    //在框架上根据远端rpc请求,调用当前rpc节点上发布的方法
    //new UserService().Login(controller,request,response,done)
    service->CallMethod(method,nullptr,request,response,done);
}

// Closure的回调操作,用于序列化rpc的响应和网络发送
void RpcProvider::SendRpcResponse(const muduo::net::TcpConnectionPtr &conn, google::protobuf::Message *response)
{
    std::string response_str;
    if(response->SerializeToString(&response_str))//response进行序列化
    {
        //序列化成功后,通过网络把rpc方法执行的结果发送给rpc的调用方
        conn->send(response_str);
    }
    else
    {
        std::cout<<"serialize response_str error!"<<std::endl;
    }
    conn->shutdown();//模拟http的短链接服务,由rpcprovider主动断开连接
}

测试时,辅助使用的脚本语言,清空文件内容 

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值