出现的问题

最近在使用 kratos 开发 api 的时候, 由于通过 proto 生成的 server handler 不符合业务的需求, 需要通过自定义 handlerFunc 来定义接口。 在开发中为了程序的可观测性,我使用了 kratos 提供的 logging 中间件。

在使用的过程中, 我发现自定义的 HandlerFunc 的请求日志没有显示, 而 proto 生成的请求正确显示了。

问题的原因

为了找到出现这种情况的原因, 我在官方的 Github 仓库提了个 issue(#1566), 得到了维护人员的解答。

出现这种情况的原因是 自己定义的 handlerFunc 不走 middleware 中间件, 需要自定义 http filter 才能解决。

解决

经过对 http filter 例子的简单研究, 参考了官方的 logging 中间件, 我自己实现了 logging filter.

代码:

package middleware

import (
	"fmt"
	"net/http"
	"time"

	"github.com/go-kratos/kratos/v2/errors"
	"github.com/go-kratos/kratos/v2/log"
	"github.com/go-kratos/kratos/v2/transport"
)

func Server(logger log.Logger) func(next http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			var (
				code      int32
				reason    string
				kind      string
				operation string
			)
			ctx := r.Context()
			startTime := time.Now()
			if info, ok := transport.FromServerContext(ctx); ok {
				kind = info.Kind().String()
				operation = info.Operation()
			}
			next.ServeHTTP(w, r)

			if se := errors.FromError(nil); se != nil {
				code = se.Code
				reason = se.Reason
			}
			level, stack := extractError(nil)
			_ = log.WithContext(ctx, logger).Log(level,
				"kind", "server",
				"component", kind,
				"operation", operation,
				"args", extractArgs(r),
				"code", code,
				"reason", reason,
				"stack", stack,
				"latency", time.Since(startTime).Seconds(),
			)
		})
	}
}

// extractArgs returns the string of the req
func extractArgs(req interface{}) string {
	if stringer, ok := req.(fmt.Stringer); ok {
		return stringer.String()
	}
	return fmt.Sprintf("%+v", req)
}

// extractError returns the string of the error
func extractError(err error) (log.Level, string) {
	if err != nil {
		return log.LevelError, fmt.Sprintf("%+v", err)
	}
	return log.LevelInfo, ""
}

顺利的显示了请求的日志:

INFO ts=2021-10-22T21:21:23+08:00 caller=logging.go:34 service.id=DESKTOP-DE38CMV service.name= service.version= trace_id= span_id= kind=server component=http op
eration=/test args=&{Method:GET URL:/test Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image
/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9] Accept-Encoding:[gzip, deflate, br] Accept-Language:[zh,zh-CN;q=0.9] Cache-Control:
[max-age=0] Connection:[keep-alive] Cookie:[Pycharm-c725b11f=c83c5d07-f28a-4c41-8b0f-c4c1664b7990; _ga=GA1.1.862586244.1625668431] Sec-Ch-Ua:["Chromium";v="94",
"Google Chrome";v="94", ";Not A Brand";v="99"] Sec-Ch-Ua-Mobile:[?0] Sec-Ch-Ua-Platform:["Windows"] Sec-Fetch-Dest:[document] Sec-Fetch-Mode:[navigate] Sec-Fetch
-Site:[none] Sec-Fetch-User:[?1] Upgrade-Insecure-Requests:[1] User-Agent:[Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrom
e/94.0.4606.81 Safari/537.36]] Body:{} GetBody:<nil> ContentLength:0 TransferEncoding:[] Close:false Host:localhost:8000 Form:map[] PostForm:map[] MultipartForm:
<nil> Trailer:map[] RemoteAddr:[::1]:8787 RequestURI:/test TLS:<nil> Cancel:<nil> Response:<nil> ctx:0xc0000f4c90} code=0 reason= stack= latency=0

参考