kratos是bilibili的一个微服务开源框架,里面封装了很多功能,可以很方便的进行grpc和http接口的开发。
一.各种中间件
1.1参数校验中间件
kratos 生成 proto文件名.pb.validate.go 文件的时候会同时生成参数验证的相关方法,
package validate
import (
"context"
"fmt"
"mykratos-example/config/global"
"mykratos-example/errcode/errtools"
"strconv"
"github.com/go-kratos/kratos/v2/middleware"
)
type Error struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (err Error) Error() string {
return err.Message
}
type validator interface {
Validate() error
}
// Validator is a validator middleware.
func Validator() middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (reply interface{}, err error) {
if v, ok := req.(validator); ok {
if err := v.Validate(); err != nil {
//参数异常统一处理,xxxxx
return nil, 返回自定义错误
}
}
return handler(ctx, req)
}
}
}
1.2服务端打印请求日志中间件
// Server is an server logging middleware.
package logging
import (
"context"
"fmt"
"github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/middleware"
"github.com/go-kratos/kratos/v2/transport"
"github.com/go-kratos/kratos/v2/transport/grpc"
"github.com/go-kratos/kratos/v2/transport/http"
"mykratos-example/tools/utiltools"
)
func Server() middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (interface{}, error) {
// 获取请求参数
var params, message string
if stringer, ok := req.(fmt.Stringer); ok {
params = stringer.String()
} else {
params = fmt.Sprintf("%+v", req)
}
if transPort, ok := transport.FromServerContext(ctx); ok {
//http调用还是grpc调用
if transPort.Kind() == transport.KindHTTP {
if info, ok := transPort.(*http.Transport); ok {
// http打印信息
request := info.Request()
message = fmt.Sprintf("kind:Server, component:HTTP, clientIp:%v, path:%v, method:%v, request:{%v}, header:%v",
request.Host, request.URL.String(), request.Method, params, request.Header)
}
} else {
// grpc打印信息
if info, ok := transPort.(*grpc.Transport); ok {
message = fmt.Sprintf("kind:Server, component:GRPC, method:POST, request:{%v}, path:%v", params, info.Endpoint())
}
}
}
reply, err := handler(ctx, req)
if err == nil {
//处理正常就打印info日志
utiltools.LogInfo(message + fmt.Sprintf(" result:{%v} \n", reply))
} else {
//处理异常就打印error日志
utiltools.LogError(message + fmt.Sprintf(" errorCode:%v, error:%v \n", uint32(errors.Code(err)), err.Error()))
}
return reply, err
}
}
}
1.3链路追踪中间件
func tracerProvider() (*tracesdk.TracerProvider, error) {
// Create the Jaeger exporter
url := global.GetConfigValAsStr(constants.SERVICE_NAME)
fmt.Println("tp url:", url, global.GetConfigValAsStr(constants.SERVICE_NAME))
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
if err != nil {
return nil, err
}
tp := tracesdk.NewTracerProvider(
// Always be sure to batch in production.
tracesdk.WithBatcher(exp),
// Record information about this application in an Resource.
tracesdk.WithResource(resource.NewSchemaless(
semconv.ServiceNameKey.String(global.GetConfigValAsStr(constants.SERVICE_NAME)),
attribute.String("environment", "development"),
attribute.Int64("ID", 1),
)),
)
return tp, nil
}
放入中间件
1.4权限校验中间件
验证是否登录,身份信息等,同时向上下文传递用户身份信息。
// Token is an token login middleware.
package token
import (
"context"
"fmt"
"regexp"
"strings"
"github.com/go-kratos/kratos/v2/metadata"
"mykratos-example/config/global"
"mykratos-example/errcode"
"mykratos-example/errcode/errtools"
"github.com/go-kratos/kratos/v2/middleware"
"github.com/go-kratos/kratos/v2/transport"
"github.com/go-kratos/kratos/v2/transport/grpc"
"github.com/go-kratos/kratos/v2/transport/http"
)
func Token() middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (interface{}, error) {
//全局是否开启验证开关
authOn := configclient.GetConfigValAsBool(global.AUTH_ON)
fmt.Println("sso auth status:", authOn)
if !authOn { //不验证直接返回
return handler(ctx, req)
}
var token string
if transPort, okContext := transport.FromServerContext(ctx); okContext {
if transPort.Kind() == transport.KindHTTP {
//http请求处理
if info, okHttp := transPort.(*http.Transport); okHttp {
request := info.Request()
reqHeader := request.Header
url := request.URL.Path
fmt.Println("url:", url)
// 白名单url不处理
if okHttp = IsWhitePath(whitePathStr, url); okHttp {
return handler(ctx, req)
}
//从请求头里面获取身份信息,或者cookie session等
token = reqHeader.Get("请求头名称")
if token == "" {
return nil, errors.New("返回自定义响应错误提示")
}
}
} else {
// grpc处理
if info, ok := transPort.(*grpc.Transport); ok {
reqHeader := info.RequestHeader()
//从请求头里面获取身份信息
token = reqHeader.Get("请求头名称")
if token == "" {
return nil,errors.New("返回自定义响应错误提示")
}
}
}
}
//根据请求头信息做自己的权限验证逻辑
//自定义一些上下文对象
ctx = context.WithValue(ctx, key1, value1)
ctx = context.WithValue(ctx, key2, value2)
//自定义grpc元数据上下文对象
ctx = metadata.AppendToClientContext(ctx, key3,value3)
ctx = metadata.AppendToClientContext(ctx, key4,value4)
return handler(ctx, req)
}
}
}
1.5响应错误拦截处理中间件
package errors
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"mykratos-example/config/global"
"mykratos-example/errcode"
)
type errorMessage struct {
Code int `json:"code"`
Message string `json:"message"`
}
func ErrorHandle(rsp http.ResponseWriter, req *http.Request, err error) {
if err == nil {
return
}
switch v := err.(type) {
case *errcode.ClientErr:
ResponseCode(rsp, v.TypeCode, v.HttpCode, v.Message)
case *errcode.ServerErr:
ResponseCode(rsp, v.TypeCode, v.HttpCode, v.Message)
default:
message := fmt.Sprintf("unKnow error: %v", err)
clientErr := errcode.NewUnKnowErr(errcode.InternalDefaultError, 200, message)
ResponseCode(rsp, clientErr.TypeCode, clientErr.HttpCode, clientErr.Message)
}
}
//默认响应
func ResponseCode(rsp http.ResponseWriter, typeCode, httpCode int, message string) {
body, _ := json.Marshal(errorMessage{Code: resCode, Message: message})
rsp.Header().Add("Content-Type", "application/json")
if httpCode == 0 {
httpCode = 200
}
rsp.WriteHeader(httpCode)
rsp.Write(body)
}
1.6.自定义序列化规则中间件
//自定义序列化规则
package marshal
import (
"encoding/json"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
var MarshalOptions = protojson.MarshalOptions{
EmitUnpopulated: true,
UseProtoNames: true,
}
type JsonEncoder struct {
}
func (j JsonEncoder) Marshal(v interface{}) ([]byte, error) {
if m, ok := v.(proto.Message); ok {
return MarshalOptions.Marshal(m)
}
return json.Marshal(v)
}
func (j JsonEncoder) Unmarshal(data []byte, v interface{}) error {
return json.Unmarshal(data, v)
}
func (j JsonEncoder) Name() string {
return "json"
}
// MyResponseEncoder encodes the object to the HTTP response. 按照正常下划线格式解析
// 也可以直接在主方法main里面设置 json.MarshalOptions.UseProtoNames = true
func MyResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
if r.Header.Get("args_type") != "" {
myCode := JsonEncoder{}
data, err := myCode.Marshal(v)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/"+myCode.Name()+";charset=UTF-8")
w.Write(data)
return nil
}
return kratosHttp.DefaultResponseEncoder(w, r, v)
}
1.7grpc元数据传递
根据kratos官方文档操作:
https://go-kratos.dev/docs/component/metadata
坑:metadata默认名字问题
1.服务端元数据的名字必须是x-md-开头
2.客户端元数据的名字必须是x-md-global-开头
如果不想使用kratos官方的元数据获取方式,也可以自己实现这两个方法
1.8链路追踪
链路追踪可以看到服务的整个调用链路,可以很好的排查问题,当然有一定的性能损耗。
准备工作:本地装好jaeger
1.先安装好docker
2.运行下面的命令获取jaeger镜像并启动一个本地的容器
docker run -d -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p 5775:5775/udp -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 14268:14268 -p 14269:14269 -p 9411:9411 jaegertracing/all-in-one:latest
3.客户端上报地址
http://127.0.0.1:14268/api/traces
4.正常启动容器后浏览器访问
http://localhost:16686/
可以看到一个前端界面
import (
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/trace/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.opentelemetry.io/otel/trace"
)
// 初始化一个tracerProvider
func tracerProvider() (*tracesdk.TracerProvider, error) {
// Create the Jaeger exporter
url := "http://127.0.0.1:14268/api/traces"
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
if err != nil {
return nil, err
}
tp := tracesdk.NewTracerProvider(
// Always be sure to batch in production.
tracesdk.WithBatcher(exp),
// Record information about this application in an Resource.
tracesdk.WithResource(resource.NewSchemaless(
semconv.ServiceNameKey.String(global.GetConfigValAsStr("mykratos-example")),
attribute.String("environment", "development"),
attribute.Int64("ID", 1),
)),
)
return tp, nil
}
引入中间件
向服务发送http请求,链路就会被采集,打开刚刚的前端界面,可以看到:
二.辅助工具:生成Swagger文档
方法1
引入插件:
import "github.com/go-kratos/swagger-api/openapiv2"
方法2:
安装protoc插件
go get -u github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2
然后执行命令 protoc -I . --openapiv2_out ./ --openapiv2_opt logtostderr=true --openapiv2_opt json_names_for_fields=false xxxx文件名.proto
会在当前目录生成文档