diff --git a/content/en/docs/kitex/Tutorials/framework-exten/middleware.md b/content/en/docs/kitex/Tutorials/framework-exten/middleware.md index b51e94b91d..c7d195db66 100644 --- a/content/en/docs/kitex/Tutorials/framework-exten/middleware.md +++ b/content/en/docs/kitex/Tutorials/framework-exten/middleware.md @@ -6,83 +6,259 @@ description: > --- ## Introduction +Kitex, as a lightweight RPC framework, offers powerful extensibility and primarily provides two methods of extension: one is a relatively low-level approach that involves adding middleware directly, and the other is a higher-level approach that involves adding suites. The following mainly introduces the usage of middleware. -Middleware is the major method to extend the Kitex framework. Most of the Kitex-based extensions and secondary development features are based on middleware. +## Middleware +Middleware is a relatively low level of extension. Most of the Kitex-based extension and secondary development functions are based on middleware to achieve. +Kitex's Middleware is defined in `pkg/endpoint/endpoint.go`, the most important of which are two types: -Before extending, it is important to remember two principles: - -1. Middleware and Suit are only allowed to be set before initializing Server and Client, do not allow modified dynamically. -2. Middlewares are executed in the order in which they were added. +1. `Endpoint` is a function that accepts ctx, req, resp and returns err. Please refer to the following "Example" code. +2. Middleware (hereinafter referred to as MW) is also a function that receives and returns an Endpoint. +```golang +type Middleware func(Endpoint) Endpoint +``` -Middleware is defined in `pkg/endpoint/endpoint.go`, the two major types are: +In fact, a middleware is essentially a function that takes an Endpoint as input and returns an Endpoint as output. This ensures transparency to the application, as the application itself is unaware of whether it is being decorated by middleware. Due to this feature, middlewares can be nested and used in combination. -1. `Endpoint` is a function that accepts ctx, req, resp and returns err, see the example below. -2. `Middleware` (aka MW) is also a function that receives and returns an `Endpoint`. 3. +Middlewares are used in a chained manner. By invoking the provided next function, you can obtain the response (if any) and error returned by the subsequent middleware. Based on this, you can perform the necessary processing and return an error to the previous middleware (be sure to check for errors returned by next and avoid swallowing errors) or set the response accordingly. -In fact, a middleware is a function whose input and output are both `Endpoint`, which ensures the transparency to the application, and the application itself does not need to know whether it is decorated by the middleware. Due to this feature, middleware can be nested. +### Client Middleware -Middleware should be used in series, by calling the next, you can get the response (if any) and err returned by the latter middleware, and then process accordingly and return the err to the former middleware (be sure to check the err of next function returned, do not swallow the err) or set the response. +There are two ways to add Client Middleware: -## Client-side Middleware +1. Client.WithMiddleware adds Middleware to the current client, which is executed after service circuit breaker and timeout Middleware ; +2. Client.WithInstanceMW adds middleware to the current client, which is executed after service discovery and load balance. If there is an instance circuit breaker, it will be executed after the instance circuit breaker (if Proxy is used, it will not be called, such as in Mesh mode). + Note that the above functions should all be passed as options when creating the client. -There are two ways to add client-side middleware: +Client Middleware call sequence: -1. `client.WithMiddleware` adds a middleware to the current client, executes after service circuit breaker middleware and timeout middleware. -2. `client.WithInstanceMW` adds a middleware to the current client and executes after service discovery and load balancing. If there has instance circuit breaker, this middleware will execute after instance circuit breaker. (if `Proxy` is used, it will not be called). + 1. XDS routing, service level circuit breaker , timeout; + 2. ContextMiddleware; + 3. Middleware set by Client.WithMiddleware ; + 4. ACLMiddleware; + 5. Service Discovery , Instance circuit breaker , Instance-Level Middleware/Service Discovery, Proxy Middleware + 6. IOErrorHandleMW -Note that the above functions should all be passed as `Option`s when creating the client. +The above can be seen in [https://github.com/cloudwego/kitex/blob/develop/client/client.go](https://github.com/cloudwego/kitex/blob/develop/client/client.go) -The order of client middleware calls: -1. the middleware set by `client.WithMiddleware` -2. ACLMiddleware -3. (ResolveMW + client.WithInstanceMW + PoolMW / DialerMW) / ProxyMW -4. IOErrorHandleMW +### Context Middleware +Context Middleware is essentially Client Middleware, but the difference is that it is controlled by ctx whether and which to inject middleware. +The introduction of Context Middleware is to provide a method that can inject Client Middleware globally or dynamically. Typical usage scenarios include statistics on which downstream interfaces are called. +Middleware can be injected into ctx using `ctx = client.WithContextMiddlewares(ctx, mw)`. +Note: Context Middleware executes before middleware set by `client.WithMiddleware()`. -The order in which the calls are returned is reversed. +### Server Middleware +There are indeed certain differences between server-side middleware and client-side middleware. +You can add server-side middleware through server.WithMiddleware, which is used in the same way as the client and passed in through Option when creating the server. -The order of all middleware calls on the client side can be seen in `client/client.go`. +Server Middleware call sequence: -## Context Middleware + 1. ErrHandleMW + 2. ACLMiddleware + 3. Middleware set by Server.WithMiddleware -Context middleware is also a client-side middleware, but the difference is that it is controlled by ctx whether to inject the middleware or which middleware should be injected. +The above can be seen in https://github.com/cloudwego/kitex/blob/develop/server/server.go +### Example +We can use the following example to see how to use Middleware. +### Request/Reponse +If we need to print out the request content before the request, and then print out the response content after the request, we can write the following middleware: -The introduction of Context Middleware is to provide a way to globally or dynamically inject Client Middleware. Typical usage scenario is to count which downstreams are called in this call-chain. +```golang +/* +type Request struct { + Message string `thrift:"Message,1,required" frugal:"1,required,string" json:"Message"` + Base *base.Base `thrift:"Base,255,optional" frugal:"255,optional,base.Base" json:"Base,omitempty"` +} -Context Middleware only exists in the context call-chain, which can avoid problems caused by third-party libraries injecting uncontrollable middleware. +type Response struct { + Message string `thrift:"Message,1,required" frugal:"1,required,string" json:"Message"` + BaseResp *base.BaseResp `thrift:"BaseResp,255,optional" frugal:"255,optional,base.BaseResp" json:"BaseResp,omitempty"` +} +*/ +import "github.com/cloudwego/kitex/pkg/utils" + +func ExampleMiddleware(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, request, response interface{}) error { + if arg, ok := request.(utils.KitexArgs); ok { + if req := arg.GetFirstArgument().(*echo.Request; req != nil { + klog.Debugf("Request Message: %v", req.Message) + } + } + err := next(ctx, request, response) + if result, ok := response.(utils.KitexResult); ok { + if resp, ok := result.GetResult().(*echo.Response); ok { + klog.Debugf("Response Message: %v", resp.Message) + // resp.SetSuccess(...) could be used to replace customized response + // But notice: the type should be the same with the response of this method + } + } + return err + } +} +``` +The provided example is for illustrative purposes, and it is indeed important to exercise caution when implementing such logging practices in a production environment. Logging every request and response indiscriminately can indeed have performance implications, especially when dealing with large response bodies. -Middleware can be injected into ctx with `ctx = client.WithContextMiddlewares(ctx, mw)` . +### Precautions +If RPCInfo is used in custom middleware, be aware that RPCInfo will be recycled after the rpc ends, so if you use goroutine operation RPCInfo in middleware, there will be issues . Please avoid such operations . -Note: Context Middleware will be executed before Client Middleware. +### gRPC Middleware +As we all know, in addition to Thrift, Kitex also supports the protobuf and gRPC encoding/decoding protocols. In the case of protobuf, it refers to using protobuf exclusively to define the payload format, and the service definition only includes unary methods. However, if streaming methods are introduced, Kitex will use the gRPC protocol for encoding/decoding and communication. -## Server-side Middleware +For services using protobuf (unary only), the development of middleware remains consistent with the previous context, as the design of both is identical. -The server-side middleware is different from the client-side. +However, if streaming methods are used, the development of middleware is completely different. Therefore, the usage of gRPC streaming middleware is explained separately as a standalone unit. -You can add server-side middleware via `server.WithMiddleware`, and passing `Option` when creating the server. +For streaming methods, such as client stream, server stream, bidirectional stream, etc., and considering that the sending and receiving of messages (Recv & Send) have their own business logic control, middleware can not cover the messages themselves. Therefore, if you want to implement request/response logging at the message level during Send/Recv operations, you need to wrap Kitex's streaming.Stream as follows: -The order of server-side middleware calls can be found in `server/server.go`. +```golang +type wrappedStream struct { + streaming.Stream +} -## Example +func (w *wrappedStream) RecvMsg(m interface{}) error { + log.Printf("Receive a message: %T(%v)", m, m) + return w.Stream.RecvMsg(m) +} -You can see how to use the middleware in the following example. +func (w *wrappedStream) SendMsg(m interface{}) error { + log.Printf("Send a message: %T(%v)", m, m) + return w.Stream.SendMsg(m) +} -If you have a requirement to print out the request and the response, we can write the following MW: +func newWrappedStream(s streaming.Stream) streaming.Stream { + return &wrappedStream{s} +} -```go -func PrintRequestResponseMW(next endpoint.Endpoint) endpoint.Endpoint { - return func(ctx context.Context, request, response interface{}) error { - fmt.Printf("request: %v\n", request) - err := next(ctx, request, response) - fmt.Printf("response: %v", response) - return err +``` +Then, within the middleware, insert the wrapped streaming.Stream object at specific invocation points. +```golang +import "github.com/cloudwego/kitex/pkg/streaming" + +// A middleware that can be used for both client-side and server-side in Kitex with gRPC/Thrift/TTheader-protobuf +func DemoGRPCMiddleware(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, req, res interface{}) error { + + var Nil interface{} // can not switch nil directly in go + switch Nil { + case req: // The current middleware is used for the client-side and specifically designed for streaming methods + err := next(ctx, req, res) + // The stream object can only be obtained after the final endpoint returns + if tmp, ok := res.(*streaming.Result); err == nil && ok { + tmp.Stream = newWrappedStream(tmp.Stream) // wrap stream object + } + return err + case res: // The current middleware is used for the server-side and specifically designed for streaming methods + if tmp, ok := req.(*streaming.Args); ok { + tmp.Stream = newWrappedStream(tmp.Stream) // wrap stream object + } + default: // pure unary method, or thrift method + // do something else + } + return next(ctx, req, res) } } ``` +Explanation of the request/response parameter types obtained within the Kitex middleware in different scenarios of gRPC: + +| Scenario | Request Type | Response Type | +|----------------------------------|---------------------------|-----------------------------| +| Kitex-gRPC Server Unary/Streaming | *streaming.Args | nil | +| Kitex-gRPC Client Unary | *xxxservice.XXXMethodArgs | *xxxservice.XXXMethodResult | +| Kitex-gRPC Client Streaming | nil | *streaming.Result | + +## Summary +Middleware is indeed a lower-level implementation of extensions, typically used to inject simple code containing specific functionalities. However, in complex scenarios, single middleware may not be sufficient to meet the business requirements. In such cases, a more comprehensive approach is needed, which involves assembling multiple middlewares or options into a complete middleware layer. Users can develop this requirement based on suites, refer to [Suite Extend](https://www.cloudwego.io/zh/docs/kitex/tutorials/framework-exten/suite/) + +## FAQ +### How to recover handler panic in middleware +Question: +A handler who wanted to recover their own business in middleware threw a panic and found that the panic had already been recovered by the framework. + +Description: +The framework will recover and report the panic in Handler. If you want to capture panic in custom middleware, you can determine the type of error returned in middleware (whether it is `kerrors.ErrPanic`). +```golang +func TestServerMiddleware(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, req, resp interface{}) (err error) { + err = next(ctx, req, resp) + if errors.Is(err, kerrors.ErrPanic) { + fmt.Println("capture panic") + } + return err + } +} +``` +### How to get the real Request/Response in Middleware? + +Due to implementation needs, the req and resp passed in middlewares are not the req and resp passed by the real user, but an object wrapped by Kitex, specifically a structure similar to the following. -Assuming we are at Server side, we can use `server.WithMiddleware(PrintRequestResponseMW)` to use this MW. +#### Thrift -**The above scenario is only for example, not for production, there will be performance issues. ** +```golang +// req +type ${XService}${XMethod}Args struct { + Req *${XRequest} `thrift:"req,1" json:"req"` +} + +func (p *${XService}${XMethod}Args) GetFirstArgument() interface{} { + return p.Req +} + + +// resp +type ${XService}${XMethod}Result struct { + Success *${XResponse} `thrift:"success,0" json:"success,omitempty"` +} -## Attention +func (p *${XService}${XMethod}Result) GetResult() interface{} { + return p.Success +} +``` + +#### Protobuf + +```golang +// req +type ${XMethod}Args struct { + Req *${XRequest} +} + +func (p *${XMethod}Args) GetReq() *${XRequest} { + if !p.IsSetReq() { + return ${XMethod}Args_Req_DEFAULT + } + return p.Req +} + + +// resp +type ${XMethod}Result struct { + Success *${XResponse} +} + +func (p *${XMethod}Result) GetSuccess() *${XResponse} { + if !p.IsSetSuccess() { + return ${XMethod}Result_Success_DEFAULT + } + return p.Success +} +``` + +The above generated code can be seen in kitex_gen directory. +Therefore, there are three solutions for the business side to obtain the real req and resp: +1. If you can determine which method is being called and the type of req used, you can directly obtain the specific Args type through type assertion, and then obtain the real req through the GetReq method. +2. For thrift generated code, by asserting `GetFirstArgument` or `GetResult` , obtain `interface{}`, and then do type assertion to the real req or resp (Note: Since the returned `interface{}` contains a type, judging `interface{}` nil cannot intercept the case where req/resp itself is a null pointer, so we need to judge whether the asserted req/resp is a null pointer again); +3. Obtain the real request/response body through reflection method, refer to the code: + +```golang +var ExampleMW endpoint.Middleware = func(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, request, response interface{}) error { + reqV := reflect.ValueOf(request).MethodByName("GetReq").Call(nil)[0] + log.Infof(ctx, "request: %T", reqV.Interface()) + err := next(ctx, request, response) + respV := reflect.ValueOf(response).MethodByName("GetSuccess").Call(nil)[0] + log.Infof(ctx, "response: %T", respV.Interface()) + return err + } +} +``` -If RPCInfo is used in a custom middleware, please pay attention to that RPCInfo will be recycled after the rpc is finished. If you start a goroutine in the middleware to modify RPCInfo, there will have some problems. diff --git a/content/en/docs/kitex/Tutorials/framework-exten/suite.md b/content/en/docs/kitex/Tutorials/framework-exten/suite.md index e87776c2e6..f716a05184 100644 --- a/content/en/docs/kitex/Tutorials/framework-exten/suite.md +++ b/content/en/docs/kitex/Tutorials/framework-exten/suite.md @@ -5,52 +5,39 @@ weight: 2 description: > --- -## Encapsulating Custom Governance Modules - -Suite is a high-level abstraction of extensions, a combination and encapsulation of Option and Middleware. - -As mentioned in the middleware extensions document, there are two principles should be remembered in extensions: - -1. Middleware and Suit are only allowed to be set before initializing Server and Client, do not allow modified dynamically. -2. Behind override ahead. - -These tow principle is also valid for Suite. - -Suite is defined as follows: - -```go +## Introduction +Suite is a high-level abstraction for extensions, which can be understood as a combination and encapsulation of Option and Middleware. +In the process of expansion, we need to remember two principles: +1. Suite can only be set when initializing Server and Client, and dynamic modification is not allowed. +2. The suite is executed in the order of the settings. For the client, it executes in the order of the settings, while for the server, it is the opposite. + +The definition of Suite is as follows: +```golang type Suite interface { Options() []Option } ``` -// TODO: Add example. - -Both Server side and Client side use the `WithSuite` method to enable new Suite. +Both Server and Client use the `WithSuite` method to enable the new suite. -When initializing Server and Client, Suite is setup in DFS(Deep First Search) way. - -For example, if we have the following code: - -```go -type s1 struct { - timeout time.Duration -} - -func (s s1) Options() []client.Option { - return []client.Option { client.WithRPCTimeout(s.timeout)} +## Example +```golang +type mockSuite struct{ + config *Config } -type s2 struct { -} - -func (s2) Options() []client.Option { - return []client.Option{client.WithSuite(s1{timeout:1*time.Second}), client.WithRPCTimeout(2*time.Second)} +func (m *mockSuite) Options() []Option { + return []Option{ + WithClientBasicInfo(mockEndpointBasicInfo), + WithDiagnosisService(mockDiagnosisService), + WithRetryContainer(mockRetryContainer), + WithMiddleware(mockMW(m.config)), + WithSuite(mockSuite2), + } } ``` -Then if we use `client.WithSuite(s2{}), client.WithRPCTimeout(3*time.Second)`, it will execute `client.WithSuite(s1{})` first, followed by `client. WithRPCTimeout(1*time.Second)`, followed by `client.WithRPCTimeout(2*time.Second)`, and finally `client.WithRPCTimeout(3*time.Second)`. After this initialization, the value of RPCTimeout will be set to 3s (see the principle described at the beginning). +The above code defines a simple client suite implementation, and we can use `client.WithSuite(&mockSuite{})` in the code to use all middleware/options encapsulated by this suite. ## Summary - -Suite is a higher-level combination and encapsulation, and it is recommended that third-party developers provide Kitex extensions based on Suite. Suite allows dynamically injecting values at creation time, or dynamically specifying values in its own middleware at runtime, making it easier for users and third-party developers to use and develop without relying on global variables, and making it possible to use different configurations for each client. +Suite is a higher-level combination and encapsulation. It is highly recommended for third-party developers to provide Kitex extensions to the outside world based on Suite. Suite allows dynamic injection of values during creation or dynamically specifying values in its middleware based on certain runtime values. This makes it more convenient for users and third-party developers, eliminating the need for reliance on global variables and enabling the possibility of using different configurations for each client. diff --git a/content/zh/docs/kitex/Tutorials/framework-exten/middleware.md b/content/zh/docs/kitex/Tutorials/framework-exten/middleware.md index 6035069d8b..55a162d092 100644 --- a/content/zh/docs/kitex/Tutorials/framework-exten/middleware.md +++ b/content/zh/docs/kitex/Tutorials/framework-exten/middleware.md @@ -6,81 +6,257 @@ description: > --- ## 介绍 +Kitex 作为一个轻量级的 RPC 框架,提供了十分强大的扩展性,主要提供了两种扩展的方法:一种比较 low level 的是直接增加 middleware 中间件;还有一种比较 high level 的方法是增加 suite 套件。以下主要介绍 middleware 中间件的使用方式。 -Middleware 是扩展 Kitex 框架的一个主要的方法,大部分基于 Kitex 的扩展和二次开发的功能都是基于 middleware 来实现的。 +## Middleware +middleware 是一种比较 low level 的扩展方式,大部分基于 Kitex 的扩展和二次开发的功能都是基于 middleware 来实现的。 +Kitex 的中间件定义在 `pkg/endpoint/endpoint.go` 中,其中最主要的是两个类型: +1. Endpoint 是一个函数,接受 ctx、req、resp ,返回err,可参考下文「示例」代码; +2. Middleware(下称MW)也是一个函数,接收同时返回一个 Endpoint。 +```golang + type Middleware func(Endpoint) Endpoint +``` -在扩展过程中,要记得两点原则: +实际上一个中间件就是一个输入是 Endpoint,输出也是 Endpoint 的函数,这样保证了对应用的透明性,应用本身并不会知道是否被中间件装饰的。由于这个特性,中间件可以嵌套使用。 -1. 中间件和套件都只允许在初始化 Server、Client 的时候设置,不允许动态修改。 -2. Middleware 是按照添加的先后顺序执行的。 +中间件是串连使用的,通过调用传入的 next,可以得到后一个中间件返回的 response(如果有)和 err,据此作出相应处理后,向前一个中间件返回 err(务必判断 next err 返回,勿吞了 err )或者设置 response。 -Kitex 的中间件定义在 `pkg/endpoint/endpoint.go` 中,其中最主要的是两个类型: +### 客户端中间件 + 有两种方法可以添加客户端中间件: +1. client.WithMiddleware 对当前 client 增加一个中间件,在 Service 熔断和超时中间件之后执行; +2. client.WithInstanceMW 对当前 client 增加一个中间件,在服务发现、负载均衡之后执行,如果有实例熔断器,会在实例熔断器后执行(如果使用了 Proxy 则不会调用到,如 Mesh 模式下)。 + 注意,上述函数都应该在创建 client 时作为传入的 Option。 -1. `Endpoint` 是一个函数,接受 ctx、req、resp,返回 err,可参考下方示例; -2. `Middleware`(下称 MW)也是一个函数,接收同时返回一个 `Endpoint`。 +客户端中间件调用顺序; -实际上一个中间件就是一个输入是 `Endpoint`,输出也是 `Endpoint` 的函数,这样保证了对应用的透明性,应用本身并不会知道是否被中间件装饰的。由于这个特性,中间件可以嵌套使用。 + 1. xDS 路由、服务级别熔断、超时; + 2. ContextMiddleware; + 3. client.WithMiddleware 设置的中间件; + 4. ACLMiddleware; + 5. 服务发现、实例熔断、实例级 Middleware / 服务发现、代理 Middleware + 6. IOErrorHandleMW -中间件是串连使用的,通过调用传入的 next,可以得到后一个中间件返回的 response(如果有)和 err,据此作出相应处理后,向前一个中间件返回 err(务必判断 next err 返回,勿吞了 err)或者设置 response。 +以上可以详见[https://github.com/cloudwego/kitex/blob/develop/client/client.go](https://github.com/cloudwego/kitex/blob/develop/client/client.go) +### Context 中间件 +Context 中间件本质上也是一种客户端中间件,但是区别是,其由 ctx 来控制是否注入以及注入哪些中间件。 +Context 中间件的引入是为了提供一种能够全局或者动态注入 Client 中间件的方法,典型的使用场景比如统计某个接口调用了哪些下游。 +可以通过 `ctx = client.WithContextMiddlewares(ctx, mw)` 来向 ctx 注入中间件。 +注意:Context 中间件会在 `client.WithMiddleware` 设置的中间件之前执行。 -## 客户端中间件 +### 服务端中间件 +服务端的中间件和客户端有一定的区别。 +可以通过 server.WithMiddleware 来增加 server 端的中间件,使用方式和 client 一致,在创建 server 时通过 Option 传入。 -有两种方法可以添加客户端中间件: +服务端中间件调用顺序: -1. `client.WithMiddleware` 对当前 client 增加一个中间件,在 Service 熔断和超时中间件之后执行; -2. `client.WithInstanceMW` 对当前 client 增加一个中间件,在服务发现、负载均衡之后执行,如果有实例熔断器,会在实例熔断器后执行(如果使用了 Proxy 则不会调用到,如 Mesh 模式下)。 + 1. ErrHandleMW + 2. ACLMiddleware + 3. server.WithMiddleware 设置的中间件 -注意,上述函数都应该在创建 client 时作为传入的 `Option`。 +以上可以详见[https://github.com/cloudwego/kitex/blob/develop/server/server.go](https://github.com/cloudwego/kitex/blob/develop/server/server.go) -客户端中间件调用顺序 : -1. `client.WithMiddleware` 设置的中间件 -2. ACLMiddleware -3. (ResolveMW + client.WithInstanceMW + PoolMW / DialerMW) / ProxyMW -4. IOErrorHandleMW +### 示例 +我们可以通过以下这个例子来看一下如何使用中间件。 -调用返回的顺序则相反。 +#### 获取 Request/Reponse +假如我们现在需要在请求前打印出 request 内容,再请求后打印出 response 内容,可以编写如下的 MW: +```golang +/* +type Request struct { + Message string `thrift:"Message,1,required" frugal:"1,required,string" json:"Message"` + Base *base.Base `thrift:"Base,255,optional" frugal:"255,optional,base.Base" json:"Base,omitempty"` +} -客户端所有中间件的调用顺序可以看 `client/client.go`。 +type Response struct { + Message string `thrift:"Message,1,required" frugal:"1,required,string" json:"Message"` + BaseResp *base.BaseResp `thrift:"BaseResp,255,optional" frugal:"255,optional,base.BaseResp" json:"BaseResp,omitempty"` +} +*/ +import "github.com/cloudwego/kitex/pkg/utils" + +func ExampleMiddleware(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, request, response interface{}) error { + if arg, ok := request.(utils.KitexArgs); ok { + if req := arg.GetFirstArgument().(*echo.Request; req != nil { + klog.Debugf("Request Message: %v", req.Message) + } + } + err := next(ctx, request, response) + if result, ok := response.(utils.KitexResult); ok { + if resp, ok := result.GetResult().(*echo.Response); ok { + klog.Debugf("Response Message: %v", resp.Message) + // resp.SetSuccess(...) 可以用于替换自定义的响应结果 + // 但要注意:类型应与该 method 的结果类型相同 + } + } + return err + } +} +``` -## Context 中间件 +以上方案仅为示例,慎用于生产:因为日志输出所有 req/resp 会有性能问题。无视 response 体大小,输出大量日志是一个非常消耗性能的操作,一个特别大的 response 可以是秒级的耗时。 -Context 中间件本质上也是一种客户端中间件,但是区别是,其由 ctx 来控制是否注入以及注入哪些中间件。 +### 注意事项 +如果自定义 middleware 中用到了 RPCInfo,要注意 RPCInfo 在 rpc 结束之后会被回收,所以如果在 middleware 中起了 goroutine 操作 RPCInfo 会出问题,请避免这类操作。 -Context 中间件的引入是为了提供一种能够全局或者动态注入 Client 中间件的方法,典型的使用场景比如统计某个接口调用了哪些下游。但是这种全局性设置只会在 ctx 调用链中存在,可以规避第三方库注入不可控的中间件引起的问题。 +### gRPC 中间件 +众所周知,kitex 除了 thrift,还支持了 protobuf 和 gRPC 的编解码协议,其中 protobuf 是指只用 protobuf 来定义 payload 格式,并且其 service 定义里的方法只有 unary 方法的情况;一旦引入了 streaming 方法,那么 kitex 会使用 gRPC 协议来做编解码和通信。 -可以通过 `ctx = client.WithContextMiddlewares(ctx, mw)` 来向 ctx 注入中间件。 +使用 protobuf(仅 unary)的服务,其中间件的编写与上文一致,因为两者的设计是完全一样的。 -注意:Context 中间件会在 Client 中间件之前执行。 +如果使用了 streaming 方法,那么中间件的编写则是完全不同的,因此,这里单独将gRPC streaming的中间件的用法说明列为一个单元。 -## 服务端中间件 +对于 streaming 方法,由于存在 client stream、server stream、bidirectional stream 等形式,并且 message 的收发(Recv & Send)都是有业务逻辑控制的,所以中间件并不能 cover 到 message 本身。因此,假设要在 Message 收发环节实现请求/响应的日志打印,需要对 Kitex 的 `streaming.Stream` 做如下封装: +```golang +type wrappedStream struct { + streaming.Stream +} -服务端的中间件和客户端有一定的区别。 +func (w *wrappedStream) RecvMsg(m interface{}) error { + log.Printf("Receive a message: %T(%v)", m, m) + return w.Stream.RecvMsg(m) +} -可以通过 `server.WithMiddleware` 来增加 server 端的中间件,使用方式和 client 一致,在创建 server 时通过 `Option` 传入。 +func (w *wrappedStream) SendMsg(m interface{}) error { + log.Printf("Send a message: %T(%v)", m, m) + return w.Stream.SendMsg(m) +} -总的服务端中间件的调用顺序可以看 `server/server.go`。 +func newWrappedStream(s streaming.Stream) streaming.Stream { + return &wrappedStream{s} +} -## 示例 +``` +然后,在中间件内在特定调用时机插入封装后的 `streaming.Stream` 对象。 +```golang +import "github.com/cloudwego/kitex/pkg/streaming" + +// 一个能同时适用于客户端和服务端的 kitex gRPC/thrift/ttheader-protobuf 的中间件 +func DemoGRPCMiddleware(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, req, res interface{}) error { + + var Nil interface{} // go 里不能直接 switch nil + switch Nil { + case req: // 当前中间件用于客户端,并且是 streaming 方法 + err := next(ctx, req, res) + // stream 对象要在最终 endpoint return 后才能获取 + if tmp, ok := res.(*streaming.Result); err == nil && ok { + tmp.Stream = newWrappedStream(tmp.Stream) // 包装 stream 对象 + } + return err + case res: // 当前中间件用于服务端,并且是 streaming 方法 + if tmp, ok := req.(*streaming.Args); ok { + tmp.Stream = newWrappedStream(tmp.Stream) // 包装 stream 对象 + } + default: // 纯 unary 方法,或 thrift 方法 + // do something else + } + return next(ctx, req, res) + } +} +``` -我们可以通过以下这个例子来看一下如何使用中间件。 +在 Kitex middleware 内获取的 request/response 参数类型在 gRPC 不同场景下的说明: + +| 场景 | Request 类型 | Response 类型 | +|-----------------------------------|-------------------------|---------------------------| + | Kitex-gRPC Server Unary/Streaming | *streaming.Args | nil | + | Kitex-gRPC Client Unary | *xxxservice.XXXMethodArgs | *xxxservice.XXXMethodResult | + | Kitex-gRPC Client Streaming | nil | *streaming.Result | + +## 总结 +Middleware 是一种比较低层次的扩展的实现,一般用于注入包含特定功能的简单代码。而在复杂场景下,一个 middleware 封装通常无法满足业务需求,这时候需要更完善的套件组装多个 middleware/options 来实现一个完整的中间层,用户可基于 suite 来进行开发,参考[扩展套件Suite](https://www.cloudwego.io/zh/docs/kitex/tutorials/framework-exten/suite/) + +## FAQ +### 如何在 middleware 里 recover handler 排除的 panic +问题: +想在 middleware 里 recover 自己业务的 handler 抛出的 panic,发现 panic 已经被框架 recover 了。 + +说明: +框架会 recover Handler 内的 panic 并上报。若希望在自定义的 middleware 中捕获 panic,可以在 middleware 内判断返回的 error 的类型(是否为 kerrors.ErrPanic)。 +```golang +func TestServerMiddleware(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, req, resp interface{}) (err error) { + err = next(ctx, req, resp) + if errors.Is(err, kerrors.ErrPanic) { + fmt.Println("capture panic") + } + return err + } +} +``` -假如我们现在有需求,需要在请求前打印出 request 内容,再请求后打印出 response 内容,可以编写如下的 MW: +### 如何在中间件获取到真实的 Request / Response -```go -func PrintRequestResponseMW(next endpoint.Endpoint) endpoint.Endpoint { - return func(ctx context.Context, request, response interface{}) error { - fmt.Printf("request: %v\n", request) - err := next(ctx, request, response) - fmt.Printf("response: %v", response) - return err - } +由于实现需要,`endpoint.Endpoint` 中传递的 req 和 resp 并不是真正用户所传递的 req 和 resp,而是由 Kitex 包装过一层的一个对象,具体为类似如下的一个结构。 + +#### Thrift + +```golang +// req +type ${XService}${XMethod}Args struct { + Req *${XRequest} `thrift:"req,1" json:"req"` +} + +func (p *${XService}${XMethod}Args) GetFirstArgument() interface{} { + return p.Req +} + + +// resp +type ${XService}${XMethod}Result struct { + Success *${XResponse} `thrift:"success,0" json:"success,omitempty"` +} + +func (p *${XService}${XMethod}Result) GetResult() interface{} { + return p.Success } ``` -假设我们是 Server 端,就可以使用 `server.WithMiddleware(PrintRequestResponseMW)` 来使用这个 MW 了。 +#### Protobuf + +```golang +// req +type ${XMethod}Args struct { + Req *${XRequest} +} + +func (p *${XMethod}Args) GetReq() *${XRequest} { + if !p.IsSetReq() { + return ${XMethod}Args_Req_DEFAULT + } + return p.Req +} -**以上方案仅为示例,不可用于生产,会有性能问题。** -## 注意事项 +// resp +type ${XMethod}Result struct { + Success *${XResponse} +} + +func (p *${XMethod}Result) GetSuccess() *${XResponse} { + if !p.IsSetSuccess() { + return ${XMethod}Result_Success_DEFAULT + } + return p.Success +} +``` -如果自定义 middleware 中用到了 RPCInfo,要注意 RPCInfo 在 rpc 结束之后会被回收,所以如果在 middleware 中起了 goroutine 操作 RPCInfo 会出问题,不能这么做。 +以上生成代码可以在 kitex_gen 中看到。 +所以,用户有三种方案获取到真实的 req 和 resp: +1. 如果你能确定调用的具体是哪个方法,用的 req 的类型,可以直接通过类型断言拿到具体的 Args 类型,然后通过 GetReq 方法就能拿到真正的 req; +2. 对于 thrift 生成代码,通过断言 `GetFirstArgument` 或者 `GetResult`,获取到 `interface{}`,然后进行类型断言成真实的 req 或者 resp(注意:由于返回的 `interface{}` 包含类型,`interface{}` 判断 nil 无法拦截 req/resp 本身为空指针的情况,需判断断言后的 req/resp 是否为空指针); +3. 通过反射方法获取真实的请求/响应体,参考代码: + +```golang +var ExampleMW endpoint.Middleware = func(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, request, response interface{}) error { + reqV := reflect.ValueOf(request).MethodByName("GetReq").Call(nil)[0] + log.Infof(ctx, "request: %T", reqV.Interface()) + err := next(ctx, request, response) + respV := reflect.ValueOf(response).MethodByName("GetSuccess").Call(nil)[0] + log.Infof(ctx, "response: %T", respV.Interface()) + return err + } +} +``` diff --git a/content/zh/docs/kitex/Tutorials/framework-exten/suite.md b/content/zh/docs/kitex/Tutorials/framework-exten/suite.md index bb3225b417..8a06d52cfc 100644 --- a/content/zh/docs/kitex/Tutorials/framework-exten/suite.md +++ b/content/zh/docs/kitex/Tutorials/framework-exten/suite.md @@ -5,54 +5,43 @@ weight: 2 description: > --- -## Suite 扩展 - 封装自定义治理模块 +## 介绍 Suite(套件)是一种对于扩展的高级抽象,可以理解为是对于 Option 和 Middleware 的组合和封装。 - -在 middleware 扩展一文中我们有说到,在扩展过程中,要记得两点原则: - -1. 中间件和套件都只允许在初始化 Server、Client 的时候设置,不允许动态修改。 -2. 后设置的会覆盖先设置的。 - -这个原则针对 Suite 也是一样有效的。 +在扩展过程中,我们需要要记得两点原则: +1. Suite 套件只允许在初始化 Server、Client 的时候设置,不允许动态修改。 +2. suite 套件是按设置的顺序来执行的,client 是先设置先执行,而 server 则相反。 Suite 的定义如下: -```go +```golang type Suite interface { Options() []Option } ``` -这也是为什么说,Suite 是对于 Option 和 Middleware(通过 Option 设置)的组合和封装。 - -// TODO: 增加示例。 - -Server 端和 Client 端都是通过 `WithSuite` 这个方法来启用新的套件。 +Server 端和 Client 端都是通过 WithSuite 这个方法来启用新的套件。 -在初始化 Server 和 Client 的时候,Suite 是采用 DFS(Deep First Search) 方式进行设置。 - -举个例子,假如我有以下代码: - -```go -type s1 struct { - timeout time.Duration -} - -func (s s1) Options() []client.Option { - return []client.Option{client.WithRPCTimeout(s.timeout)} -} +## 示例 -type s2 struct { +```golang +type mockSuite struct{ + config *Config } -func (s2) Options() []client.Option { - return []client.Option{client.WithSuite(s1{timeout:1*time.Second}), client.WithRPCTimeout(2*time.Second)} +func (m *mockSuite) Options() []Option { + return []Option{ + WithClientBasicInfo(mockEndpointBasicInfo), + WithDiagnosisService(mockDiagnosisService), + WithRetryContainer(mockRetryContainer), + WithMiddleware(mockMW(m.config)), + WithSuite(mockSuite2), + } } ``` -那么如果我在创建 client 时传入 `client.WithSuite(s2{}), client.WithRPCTimeout(3*time.Second)`,在初始化的时候,会先执行到 `client.WithSuite(s1{})`,然后是 `client.WithRPCTimeout(1*time.Second)`,接着是 `client.WithRPCTimeout(2*time.Second)`,最后是 `client.WithRPCTimeout(3*time.Second)`。这样初始化之后,RPCTimeout 的值会被设定为 3s(参见开头所说的原则)。 +以上代码定义了一个简单的 Client suite 实现,我们可以在代码中使用 `client.WithSuite(&mockSuite{})` 来使用这个 suite 封装的所有 middleware/option。 ## 总结 -Suite 是一种更高层次的组合和封装,更加推荐第三方开发者能够基于 Suite 对外提供 Kitex 的扩展,Suite 可以允许在创建的时候,动态地去注入一些值,或者在运行时动态地根据自身的某些值去指定自己的 middleware 中的值,这使得用户的使用以及第三方开发者的开发都更加地方便,无需再依赖全局变量,也使得每个 client 使用不同的配置成为可能。 \ No newline at end of file +Suite 是一种更高层次的组合和封装,更加推荐第三方开发者基于 Suite 对外提供 Kitex 的扩展,Suite 可以允许在创建的时候,动态地去注入一些值,或者在运行时动态地根据自身的某些值去指定自己的 middleware 中的值,这使得用户的使用以及第三方开发者的开发都更加地方便,无需再依赖全局变量,也使得每个 client 使用不同的配置成为可能。