1

We use a series to explain the complete practice of microservices from requirements to online, from code to k8s deployment, from logging to monitoring, etc.

The whole project uses microservices developed by go-zero, which basically includes go-zero and some middleware developed by related go-zero authors. The technology stack used is basically the self-developed components of the go-zero project team, basically go -zero the whole family bucket.

Actual project address: https://github.com/Mikaelemmmm/go-zero-looklook

1 Overview

When we usually develop, when the program fails, we hope that the error log can be used to quickly locate the problem (then the passed parameters, including the stack information, must be printed to the log), but at the same time, it is more friendly to return to the front-end user. , An understandable error message, then if you only return an error message through one fmt.Error, errors.new, etc., it is definitely impossible, unless the log is recorded at the place where the front-end error message is returned, so that If the log is flying all over the sky, the code is ugly, and the log will be ugly at that time.

Then let's think about it, if there is a unified place to record logs, and only one return err is needed in the business code, the error message and log record information returned to the front end can be separately prompted and recorded. If it is implemented according to this idea, That's not too cool. Yes, go-zero-looklook handles it like this. Let's take a look.

2. RPC error handling

Under normal circumstances, go-zero's rpc service is based on grpc, and the default error returned is grpc's status.Error, which cannot give us a custom error merge, and is not suitable for our custom error, its error code , the error types are all defined in the grpc package, ok, if we can use a custom error return in rpc, and then convert it to grpc's status.Error when the interceptor returns uniformly, then our rpc's err is the same as the api's Can err manage our own mistakes in a unified way?

Let's take a look at what is in the code of grpc's status.Error

 package codes // import "google.golang.org/grpc/codes"

import (
    "fmt"
    "strconv"
)

// A Code is an unsigned 32-bit error code as defined in the gRPC spec.
type Code uint32
.......

The error code corresponding to grpc's err is actually a uint32. We define the error and use uint32 and then convert it to grpc's err when the rpc global interceptor returns.

So we define our own global error code in app/common/xerr

errCode.go

 package xerr

// 成功返回
const OK uint32 = 200

// 前3位代表业务,后三位代表具体功能

// 全局错误码
const SERVER_COMMON_ERROR uint32 = 100001
const REUQES_PARAM_ERROR uint32 = 100002
const TOKEN_EXPIRE_ERROR uint32 = 100003
const TOKEN_GENERATE_ERROR uint32 = 100004
const DB_ERROR uint32 = 100005

// 用户模块

errMsg.go

 package xerr

var message map[uint32]string

func init() {
   message = make(map[uint32]string)
   message[OK] = "SUCCESS"
   message[SERVER_COMMON_ERROR] = "服务器开小差啦,稍后再来试一试"
   message[REUQES_PARAM_ERROR] = "参数错误"
   message[TOKEN_EXPIRE_ERROR] = "token失效,请重新登陆"
   message[TOKEN_GENERATE_ERROR] = "生成token失败"
   message[DB_ERROR] = "数据库繁忙,请稍后再试"
}

func MapErrMsg(errcode uint32) string {
   if msg, ok := message[errcode]; ok {
      return msg
   } else {
      return "服务器开小差啦,稍后再来试一试"
   }
}

func IsCodeErr(errcode uint32) bool {
   if _, ok := message[errcode]; ok {
      return true
   } else {
      return false
   }
}

errors.go

 package xerr

import "fmt"

// 常用通用固定错误
type CodeError struct {
   errCode uint32
   errMsg  string
}

// 返回给前端的错误码
func (e *CodeError) GetErrCode() uint32 {
   return e.errCode
}

// 返回给前端显示端错误信息
func (e *CodeError) GetErrMsg() string {
   return e.errMsg
}

func (e *CodeError) Error() string {
   return fmt.Sprintf("ErrCode:%d,ErrMsg:%s", e.errCode, e.errMsg)
}

func NewErrCodeMsg(errCode uint32, errMsg string) *CodeError {
   return &CodeError{errCode: errCode, errMsg: errMsg}
}
func NewErrCode(errCode uint32) *CodeError {
   return &CodeError{errCode: errCode, errMsg: MapErrMsg(errCode)}
}

func NewErrMsg(errMsg string) *CodeError {
   return &CodeError{errCode: SERVER_COMMON_ERROR, errMsg: errMsg}
}

For example, our rpc code when users register

 package logic

import (
    "context"

    "looklook/app/identity/cmd/rpc/identity"
    "looklook/app/usercenter/cmd/rpc/internal/svc"
    "looklook/app/usercenter/cmd/rpc/usercenter"
    "looklook/app/usercenter/model"
    "looklook/common/xerr"

    "github.com/pkg/errors"
    "github.com/tal-tech/go-zero/core/logx"
    "github.com/tal-tech/go-zero/core/stores/sqlx"
)

var ErrUserAlreadyRegisterError = xerr.NewErrMsg("该用户已被注册")

type RegisterLogic struct {
    ctx    context.Context
    svcCtx *svc.ServiceContext
    logx.Logger
}

func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
    return &RegisterLogic{
        ctx:    ctx,
        svcCtx: svcCtx,
        Logger: logx.WithContext(ctx),
    }
}

func (l *RegisterLogic) Register(in *usercenter.RegisterReq) (*usercenter.RegisterResp, error) {

    user, err := l.svcCtx.UserModel.FindOneByMobile(in.Mobile)
    if err != nil && err != model.ErrNotFound {
        return nil, errors.Wrapf(xerr.ErrDBError, "mobile:%s,err:%v", in.Mobile, err)
    }

    if user != nil {
        return nil, errors.Wrapf(ErrUserAlreadyRegisterError, "用户已经存在 mobile:%s,err:%v", in.Mobile, err)
    }

    var userId int64

    if err := l.svcCtx.UserModel.Trans(func(session sqlx.Session) error {

        user := new(model.User)
        user.Mobile = in.Mobile
        user.Nickname = in.Nickname
        insertResult, err := l.svcCtx.UserModel.Insert(session, user)
        if err != nil {
            return errors.Wrapf(xerr.ErrDBError, "err:%v,user:%+v", err, user)
        }
        lastId, err := insertResult.LastInsertId()
        if err != nil {
            return errors.Wrapf(xerr.ErrDBError, "insertResult.LastInsertId err:%v,user:%+v", err, user)
        }
        userId = lastId

        userAuth := new(model.UserAuth)
        userAuth.UserId = lastId
        userAuth.AuthKey = in.AuthKey
        userAuth.AuthType = in.AuthType
        if _, err := l.svcCtx.UserAuthModel.Insert(session, userAuth); err != nil {
            return errors.Wrapf(xerr.ErrDBError, "err:%v,userAuth:%v", err, userAuth)
        }
        return nil
    }); err != nil {
        return nil, err
    }

    // 2、生成token.
    resp, err := l.svcCtx.IdentityRpc.GenerateToken(l.ctx, &identity.GenerateTokenReq{
        UserId: userId,
    })
    if err != nil {
        return nil, errors.Wrapf(ErrGenerateTokenError, "IdentityRpc.GenerateToken userId : %d , err:%+v", userId, err)
    }

    return &usercenter.RegisterResp{
        AccessToken:  resp.AccessToken,
        AccessExpire: resp.AccessExpire,
        RefreshAfter: resp.RefreshAfter,
    }, nil
}
 errors.Wrapf(ErrUserAlreadyRegisterError, "用户已经存在 mobile:%s,err:%v", in.Mobile, err)

Here we use the errors.Wrapf of go's default errors package (if you don't understand here, check the Wrap, Wrapf, etc. under the errors package of go)

The first parameter, ErrUserAlreadyRegisterError defined above is to use xerr.NewErrMsg("The user has been registered") to return a friendly prompt to the front end, remember to use the method under our xerr package

The second parameter is recorded in the server log. It doesn't matter if you can write in detail, it will only be recorded in the server and will not be returned to the front end.

Then let's see why the first parameter can be returned to the front end, and the second parameter is the log

⚠️[Note] We have added the global interceptor of grpc in the main method of the rpc startup file. This is very important. If we don't add this, we can't achieve it.

 package main

......

func main() {
    ........

    //rpc log,grpc的全局拦截器
    s.AddUnaryInterceptors(rpcserver.LoggerInterceptor)

    .......
}

Let's look at the specific implementation of rpcserver.LoggerInterceptor

 import (
    ...
    "github.com/pkg/errors"
)

func LoggerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
   resp, err = handler(ctx, req)
   if err != nil {
      causeErr := errors.Cause(err)                // err类型
      if e, ok := causeErr.(*xerr.CodeError); ok { //自定义错误类型
         logx.WithContext(ctx).Errorf("【RPC-SRV-ERR】 %+v", err)

         //转成grpc err
         err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg())
      } else {
         logx.WithContext(ctx).Errorf("【RPC-SRV-ERR】 %+v", err)
      }
   }

   return resp, err
}

When a request enters the rpc service, it first enters the interceptor and then executes the handler method. If you want to process something before entering, you can write it before the handler method. What we want to do is to return the result if there is an error. , so we use the github.com/pkg/errors package under the handler. This package is often used in go to handle errors. This is not the official errors package, but it is well designed. Go's official Wrap, Wrapf, etc. are Inspired by this package.

Because our grpc internal business returns an error when

​ 1) If it is our own business error, we will use xerr to generate errors in a unified way, so that we can get the error information we defined, because we also used uint32 for our own errors, so we will uniformly convert them into grpc errors err = status .Error(codes.Code(e.GetErrCode()), e.GetErrMsg()), what we get here, e.GetErrCode() is the code we defined, e.GetErrMsg() is the error we defined and returned before second parameter

2) But there is also a situation where the rpc service is abnormal and the error thrown at the bottom is itself a grpc error, then we just record the exception directly.

3. API error

When our api calls rpc's Register in logic, rpc returns the error message of step 2 above. The code is as follows

 ......
func (l *RegisterLogic) Register(req types.RegisterReq) (*types.RegisterResp, error) {
    registerResp, err := l.svcCtx.UsercenterRpc.Register(l.ctx, &usercenter.RegisterReq{
        Mobile:   req.Mobile,
        Nickname: req.Nickname,
        AuthKey:  req.Mobile,
        AuthType: model.UserAuthTypeSystem,
    })
    if err != nil {
        return nil, errors.Wrapf(err, "req: %+v", req)
    }

    var resp types.RegisterResp
    _ = copier.Copy(&resp, registerResp)

    return &resp, nil
}

Here is also the errors.Wrapf of the standard package, that is to say, all the errors returned in our business are applicable to the errors of the standard package, but the internal parameters should use the errors defined by our xerr

There are 2 points to note here

1) The api service wants to return rpc to the front-end friendly error message, we want to return it directly to the front-end without doing any processing (for example, rpc has returned "user already exists", the api does not want to do anything, just want to put this error message return directly to the front end)

In response to this situation, just write it like the above picture, throw the err at the rpc call directly as the first parameter of errors.Wrapf, but the second parameter is best to record the detailed log you need for convenience Check it out in the api log later

2) No matter what error message is returned by rpc for api service, I want to redefine it myself to return the error message to the front end (for example, rpc has returned "user already exists", and when api wants to call rpc, as long as there is an error, I will return it to the front end "User registration failed")

In response to this situation, you can write it as follows (of course, you can put xerr.NewErrMsg("user registration failed") above the code to use a variable, and you can also put a variable here)

 func (l *RegisterLogic) Register(req types.RegisterReq) (*types.RegisterResp, error) {
    .......
    if err != nil {
        return nil, errors.Wrapf(xerr.NewErrMsg("用户注册失败"), "req: %+v,rpc err:%+v", req,err)
    }
    .....
}

Next, let's see how it is finally returned to the front end, and then we'll look at app/usercenter/cmd/api/internal/handler/user/registerHandler.go

 func RegisterHandler(ctx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var req types.RegisterReq
        if err := httpx.Parse(r, &req); err != nil {
            httpx.Error(w, err)
            return
        }

        l := user.NewRegisterLogic(r.Context(), ctx)
        resp, err := l.Register(req)
        result.HttpResult(r, w, resp, err)
    }
}

It can be seen here that the handler code generated by go-zero-looklook is different from the code generated by the default official goctl in two places, that is, when dealing with error handling, it is replaced with our own error handling, in common/ result/httpResult.go

[Note] Some people will say that every time you use goctl, you have to come and change it manually. That's not to be troublesome. Here we use the template template function provided by go-zero (if you don't know this, you need to go to the official documentation to learn it). ), just modify the handler to generate the template. The template file of the entire project is placed under deploy/goctl. Here, the template modified by hanlder is in deploy/goctl/1.2.3-cli/api/handler.tpl

ParamErrorResult is very simple, specially dealing with parameter errors

 // http 参数错误返回
func ParamErrorResult(r *http.Request, w http.ResponseWriter, err error) {
   errMsg := fmt.Sprintf("%s ,%s", xerr.MapErrMsg(xerr.REUQES_PARAM_ERROR), err.Error())
   httpx.WriteJson(w, http.StatusBadRequest, Error(xerr.REUQES_PARAM_ERROR, errMsg))
}

We mainly look at HttpResult, the error handling returned by the business

 // http返回
func HttpResult(r *http.Request, w http.ResponseWriter, resp interface{}, err error) {
    if err == nil {
        // 成功返回
        r := Success(resp)
        httpx.WriteJson(w, http.StatusOK, r)
    } else {
        // 错误返回
        errcode := xerr.SERVER_COMMON_ERROR
        errmsg := "服务器开小差啦,稍后再来试一试"

        causeErr := errors.Cause(err) // err类型
        if e, ok := causeErr.(*xerr.CodeError); ok {
            // 自定义错误类型
            // 自定义CodeError
            errcode = e.GetErrCode()
            errmsg = e.GetErrMsg()
        } else {
            if gstatus, ok := status.FromError(causeErr); ok {
                // grpc err错误
                grpcCode := uint32(gstatus.Code())
                if xerr.IsCodeErr(grpcCode) {
                    // 区分自定义错误跟系统底层、db等错误,底层、db错误不能返回给前端
                    errcode = grpcCode
                    errmsg = gstatus.Message()
                }
            }
        }

        logx.WithContext(r.Context()).Errorf("【API-ERR】 : %+v ", err)
        httpx.WriteJson(w, http.StatusBadRequest, Error(errcode, errmsg))
    }
}

err : the log error to log

errcode : The error code returned to the front end

errmsg : friendly error message returned to the front end

Returns directly after success. If an error is encountered, the package github.com/pkg/errors is used to judge the error, whether it is an error defined by ourselves (the error defined in the api directly uses the xerr defined by ourselves), or a grpc error (thrown by the rpc business), if the grpc error is converted into our own error code through uint32, according to the error code, go to our own defined error message to find the defined error message and return it to the front end, if it is an api error, return it directly to the front end If we can't find the error message defined by ourselves, then return the default error "The server has deserted",

4. End

The error handling has been described clearly here. Next, we need to see the error log printed on the server side. How to collect and view it involves the log collection system.

project address

https://github.com/zeromicro/go-zero

Welcome go-zero and star support us!

WeChat exchange group

Follow the official account of " Microservice Practice " and click on the exchange group to get the QR code of the community group.


kevinwan
931 声望3.5k 粉丝

go-zero作者