介绍
今天接到 ld 的任务,为 pyhton 脚本增加 grpc 服务的访问日志,要求实现的效果同 WEB 的 API 访问日志相似,需要打印客户端的ip、请求内容、响应时间等记录,还需要根据访问不同的服务进行日志切割。下面是我实现的代码,在此做一个记录。
思路
首先,我想到的是看看 python grpc 有没有提供类似拦截器的接口出来,这样只要继承它的接口,就能专注于业务代码开发。果不其然,点进创建用于为 RPC 提供服务的服务器的方法里 grpc.server 可以看到方法参数提供了一个 interceptors 拦截器列表选项。
下面是拦截器的接口定义。
class ServerInterceptor(six.with_metaclass(abc.ABCMeta)):
"""Affords intercepting incoming RPCs on the service-side."""
@abc.abstractmethod
def intercept_service(self, continuation, handler_call_details):
"""Intercepts incoming RPCs before handing them over to a handler.
Args:
continuation: A function that takes a HandlerCallDetails and
proceeds to invoke the next interceptor in the chain, if any,
or the RPC handler lookup logic, with the call details passed
as an argument, and returns an RpcMethodHandler instance if
the RPC is considered serviced, or None otherwise.
handler_call_details: A HandlerCallDetails describing the RPC.
Returns:
An RpcMethodHandler with which the RPC may be serviced if the
interceptor chooses to service this RPC, or None otherwise.
"""
raise NotImplementedError()
在Python gRPC中,拦截器是一种可用于在gRPC方法调用的不同阶段中执行代码的机制。拦截器允许您在请求被发送和响应被接收之前和之后执行代码,从而使您能够修改请求和响应,记录信息,执行身份验证等等。
当创建一个gRPC拦截器时,您必须定义一个函数,该函数将接收两个参数:continuation、handler_call_details。
其中,continuation是一个可调用对象,它接受一个参数,并将控制权传递给下一个拦截器或gRPC方法处理程序。handler_call_details 是一个对象,其中包含有关要调用的gRPC方法的信息,例如方法名称、超时值和元数据。
但是,当我实际使用这个接口时,发现不能从 handler_call_details 获取到请求的 IP 信息。于是发现了第三方的 Python gRPC 拦截器 grpc_interceptor,下面是它的接口定义。
class ServerInterceptor(grpc.ServerInterceptor, metaclass=abc.ABCMeta):
"""Base class for server-side interceptors.
To implement an interceptor, subclass this class and override the intercept method.
"""
@abc.abstractmethod
def intercept(
self,
method: Callable,
request_or_iterator: Any,
context: grpc.ServicerContext,
method_name: str,
) -> Any: # pragma: no cover
"""Override this method to implement a custom interceptor.
You should call method(request, context) to invoke the next handler (either the
RPC method implementation, or the next interceptor in the list).
Args:
method: Either the RPC method implementation, or the next interceptor in
the chain.
request_or_iterator: The RPC request, as a protobuf message if it is a
unary request, or an iterator of protobuf messages if it is a streaming
request.
context: The ServicerContext pass by gRPC to the service.
method_name: A string of the form "/protobuf.package.Service/Method"
Returns:
This should generally return the result of method(request, context), which
is typically the RPC method response, as a protobuf message, or an
iterator of protobuf messages for streaming responses. The interceptor is
free to modify this in some way, however.
"""
return method(request_or_iterator, context)
在grpc_interceptor中,拦截器需要实现 ServerInterceptor 接口的 intercept 方法,该方法包含以下四个参数:
- method:表示被拦截的gRPC方法。在拦截器中调用该参数可以触发被拦截的gRPC方法。
- request:表示请求参数。在拦截器中调用该参数可以获取请求参数。
- context:表示上下文对象。在拦截器中调用该参数可以访问元数据、取消请求等。
- method_name:表示gRPC方法的名称。在拦截器中调用该参数可以获取gRPC方法的名称。
下面是具体的实现代码。
实现代码
安装依赖
首先该 demo 是在 widows 下 python3.7.6 的环境下跑的,根据python 版本下载对应的依赖。
下面是我用到的依赖:
protobuf==3.19.6
grpc_interceptor==0.15.0
grpcio==1.48.2
grpcio-tools==1.48.2
1、安装所需库
首先,需要安装 gRPC 和 protobuf 库。可以使用以下命令安装:
pip install grpcio grpcio-tools protobuf grpc_interceptor
2、定义服务和消息
在开始编写代码之前,需要定义服务和消息。
syntax = "proto3";
package hello;
// 定义问候服务
service Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}
// 定义请求消息
message HelloRequest {
string name = 1;
}
// 定义响应消息
message HelloResponse {
string message = 1;
}
3、生成 gRPC 代码
接下来,需要使用 protobuf 工具生成 gRPC 代码。可以使用以下命令生成 Python 代码:
python -m grpc_tools.protoc -I . --python_out=.
--grpc_python_out=. .\hello.proto
这将生成名为 hello_pb2.py 和 hello_pb2_grpc.py 的文件,其中包含自动生成的消息和 gRPC 代码。
4、编写拦截器及日志代码
class AccessLogInterceptor(ServerInterceptor, RPCLog):
"""gRPC 访问日志拦截器"""
def intercept(self, method, request, context, method_name):
module_name, cls_name = method_name.split("/")[1].split(".")
# 获取日志器
logger = self.get_logger(module_name, cls_name)
# 获取请求的 IP 地址
request_ip = context.peer().split(':')[1]
# 请求参数 json 化
request_param_str = self.request_param_serialization(request)
start_time = time.time()
try:
response = method(request, context)
except Exception as e:
elapsed_time = time.time() - start_time
logger.error(f'{elapsed_time:.6f}s | {request_ip} | {method_name} | {request_param_str} | {e}')
raise e
elapsed_time = time.time() - start_time
logger.info(f'{elapsed_time:.6f}s | {request_ip} | {method_name} | {request_param_str}')
return response
@staticmethod
def request_param_serialization(request) -> str:
"""
请求参数序列化成
:param request: gRPC 请求参数
:return: json 化的请求参数
"""
request_param_list = []
for obj, value in request.ListFields():
field_name = obj.name
format_str = '"%s":"%s"'
# 整形不加双引号
if isinstance(value, int):
format_str = '"%s":%s'
value = str(value)
# 限制只输出 15 位字符
value = value if len(value) < 15 else value[:15] + '...'
request_param_list.append(format_str % (field_name, value))
return '{%s}' % ",".join(request_param_list)
class RPCLog(object):
# 日志 logger Map, key为 服务名+.+类名 value 为对应的 logger
_logger_map = {}
# rpc日志基本目录
_rpc_log_base_dir = "%s/log/rpc/" % ROOT_PATH
_formatter = logging.Formatter('[dev-tools-v2] %(asctime)s | %(levelname)s | %(message)s')
def get_logger(self, module_name: str, cls_name: str):
"""
获取日志器
:param module_name: 模块名称
:param cls_name: 类名称
"""
logger_str = '%s.%s' % (module_name, cls_name)
# 返回已存在的日志器
if logger_str in self._logger_map:
return self._logger_map[logger_str]
# 新增日志器
logger = self.add_logger(module_name, cls_name)
# 放入缓存
self._logger_map[logger_str] = logger
return logger
def add_logger(self, module_name: str, cls_name: str) -> logging.Logger:
"""
新增日志器
:param module_name: 模块名称
:param cls_name: 类名称
:return: 日志器
"""
logger_str = '%s.%s' % (module_name, cls_name)
logger = logging.getLogger(logger_str)
logger.setLevel(logging.INFO)
# 新增文件处理器
self.add_file_handler(logger, module_name, cls_name)
# 新增控制台处理器
self.add_console_handler(logger)
return logger
def add_file_handler(self, logger: logging.Logger, module_name: str, cls_name: str):
"""
往 logger 新增文件处理器
:param logger: 日志器
:param module_name: 模块名称
:param cls_name: 类名称
:return:
"""
# 增加文件处理器
file_dir = self._rpc_log_base_dir + module_name
if not os.path.isdir(file_dir):
os.makedirs(file_dir, exist_ok=True)
file_path = "%s/%s.log" % (file_dir, cls_name)
file_handle = logging.FileHandler(file_path, encoding='utf8')
file_handle.setFormatter(self._formatter)
logger.addHandler(file_handle)
def add_console_handler(self, logger: logging.Logger):
"""
往 logger 新增控制台处理器
:param logger: 日志器
"""
console_handler = logging.StreamHandler()
console_handler.setFormatter(self._formatter)
logger.addHandler(console_handler)
5、编写服务器代码
现在可以编写服务器代码。服务器将实现 GreeterServicer
,并在接收到 Greeter 消息时将其回显给客户端。下面是服务器代码:
# 实现 Greeter 服务
class Greeter(hello_pb2_grpc.GreeterServicer):
def SayHello(self, request, context):
message = f'Hello, {request.name}!'
return hello_pb2.HelloResponse(message=message)
# 启动 gRPC 服务
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)
, interceptors=[AccessLogInterceptor()]) # 注册定义的日志拦截器
hello_pb2_grpc.add_GreeterServicer_to_server(Greeter(), server)
server.add_insecure_port('[::]:8787')
server.start()
server.wait_for_termination()
if __name__ == '__main__':
serve()
6、测试
为了方便,直接使用 IDEA 自带的 http 插件发送 RPC 请求。
我们看看日志打印了上面,有没有按模块要求写进相应的日志文件中。
可以看到已成功打印日志并输出到日志文件中。