咱们用一个系列来解说从需要到上线、从代码到k8s部署、从日志到监控等各个方面的微服务残缺实际。
整个我的项目应用了go-zero开发的微服务,根本蕴含了go-zero以及相干go-zero作者开发的一些中间件,所用到的技术栈根本是go-zero项目组的自研组件,根本是go-zero全家桶了。
实战我的项目地址:https://github.com/Mikaelemmm...
1、概述
咱们在平时开发时候,程序在出错时,心愿能够通过谬误日志能疾速定位问题(那么传递进来的参数、包含堆栈信息必定就要都要打印到日志),但同时又想返回给前端用户比拟友善、能看得懂的谬误提醒,那这两点如果只通过一个fmt.Error、errors.new等返回一个错误信息必定是无奈做到的,除非在返回前端谬误提醒的中央同时在记录log,这样的话日志满天飞,代码难看不说,日志到时候也会很难看。
那么咱们想一下,如果有一个对立的中央记录日志,同时在业务代码中只须要一个return err 就能将返回给前端的谬误提示信息、日志记录置信信息离开提醒跟记录,如果依照这个思路实现,那几乎不要太爽,是的 go-zero-looklook就是这么解决的,接下来咱们看下。
2、rpc错误处理
依照失常状况下,go-zero的rpc服务是基于grpc的,默认返回的谬误是grpc的status.Error 没法给咱们自定义的谬误合并,并且也不适宜咱们自定义的谬误,它的错误码、谬误类型都是定义死在grpc包中的,ok ,如果咱们在rpc中能用自定义谬误返回,而后在拦截器对立返回时候转成grpc的status.Error , 那么咱们rpc的err跟api的err是不是能够对立治理咱们本人的谬误了呢?
咱们看一下grpc的status.Error的code外面是什么
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.......
grpc的err对应的错误码其实就是一个uint32 , 咱们本人定义谬误用uint32而后在rpc的全局拦截器返回时候转成grpc的err,就能够了
所以咱们本人定义全局错误码在app/common/xerr
errCode.go
package xerr// 胜利返回const OK uint32 = 200// 前3位代表业务,后三位代表具体性能// 全局错误码const SERVER_COMMON_ERROR uint32 = 100001const REUQES_PARAM_ERROR uint32 = 100002const TOKEN_EXPIRE_ERROR uint32 = 100003const TOKEN_GENERATE_ERROR uint32 = 100004const DB_ERROR uint32 = 100005// 用户模块
errMsg.go
package xerrvar message map[uint32]stringfunc 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 xerrimport "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}}
比方咱们在用户注册时候的rpc代码
package logicimport ( "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)
这里咱们应用go默认的errors的包的errors.Wrapf ( 如果这里不明确就去查一下go的errors包下的Wrap、 Wrapf等)
第一个参数, ErrUserAlreadyRegisterError 定义在上方 就是应用xerr.NewErrMsg("该用户已被注册") , 返回给前端敌对的提醒,要记住这里用的是咱们xerr包下的办法
第二个参数,就是记录在服务器日志,能够写具体一点都没关系只会记录在服务器不会被返回给前端
那咱们来看看为什么第一个参数就能是返回给前端的,第二个参数就是记录日志的
⚠️【注】咱们在rpc的启动文件main办法中,加了grpc的全局拦截器,这个很重要 ,如果不加这个没方法实现
package main......func main() { ........ //rpc log,grpc的全局拦截器 s.AddUnaryInterceptors(rpcserver.LoggerInterceptor) .......}
咱们看看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}
当有申请进入到rpc服务时候,先进入拦截器而后就是执行handler办法,如果你想在进入之前解决某些事件就能够写在handler办法之前,那咱们想解决的是返回后果如果有谬误的状况,所以咱们在handler下方应用了github.com/pkg/errors这个包,这个包处理错误是go中常常用到的这不是官网的errors包,然而设计的很好,go官网的Wrap、Wrapf等就是借鉴了这个包的思路。
因为咱们grpc外部业务在返回谬误时候
1)如果是咱们本人业务谬误,咱们会对立用xerr生成谬误,这样就能够拿到咱们定义的错误信息,因为后面咱们本人谬误也是用的uint32,所以在这里对立转成 grpc谬误err = status.Error(codes.Code(e.GetErrCode()), e.GetErrMsg()),那这里获取到的,e.GetErrCode()就是咱们定义的code,e.GetErrMsg() 就是咱们之前定义返回的谬误第二个参数
2)然而还有一种状况是rpc服务异样了底部抛出来的谬误,自身就是grpc谬误了,那这种的咱们间接就记录异样就好了
3、api谬误
当咱们api在logic中调用rpc的Register时候,rpc返回了下面第2步的错误信息 代码如下
......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}
这里同样是应用规范包的errors.Wrapf , 也就是说所有咱们业务中返回谬误都实用规范包的errors,然而外部参数要应用咱们xerr定义的谬误
这里有2个留神点
1)api服务想把rpc返回给前端敌对的谬误提示信息,咱们想间接返回给前端不做任何解决(比方rpc曾经返回了“用户已存在”,api不想做什么解决,就想把这个错误信息间接返回给前端)
针对这种状况,间接就像上图这种写就能够了,将rpc调用处的err间接作为errors.Wrapf 第一个参数扔出去,然而第二个参数最好记录一下本人须要的具体日志不便后续在api log里查看
2)api服务不论rpc返回的是什么错误信息,我就想本人再从新定义给前端返回错误信息(比方rpc曾经返回了“用户已存在”,api想调用rpc时只有有谬误我就返回给前端“用户注册失败”)
针对这种状况,如下这样写即可(当然你能够将xerr.NewErrMsg("用户注册失败") 放到代码上方应用一个变量,这里放变量也能够)
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) } .....}
接下来咱们看最终返回给前端怎么解决的,咱们接着看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) }}
这里能够看到,go-zero-looklook生成的handler代码 有2个中央跟默认官网的goctl生成的代码不一样,就是在处理错误解决的时候,这里替换成咱们本人的错误处理了,在common/result/httpResult.go
【注】有人会说,每次应用goctl都要过去手动改,那不是要麻烦死了,这里咱们应用go-zero给咱们提供的template模版性能(还不晓得这个的就要去官网文档学习一下了),批改一下handler生成模版即可,整个我的项目的模版文件放在deploy/goctl下,这里hanlder批改的模版在deploy/goctl/1.2.3-cli/api/handler.tpl
ParamErrorResult很简略,专门解决参数谬误的
// 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))}
咱们次要来看HttpResult , 业务返回的错误处理的
// 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 : 要记录的日志谬误
errcode : 返回给前端的错误码
errmsg :返回给前端的敌对的谬误提示信息
胜利间接返回,如果遇到谬误了,也是应用github.com/pkg/errors这个包来判断谬误,是不是咱们本人定义的谬误(api中定义的谬误间接应用咱们本人定义的xerr),还是grpc谬误(rpc业务抛出来的),如果是grpc谬误在通过uint32转成咱们本人错误码,依据错误码再去咱们本人定义错误信息中找到定义的错误信息返回给前端,如果是api谬误间接返回给前端咱们本人定义的错误信息,都找不到那就返回默认谬误“服务器开小差了” ,
4、结尾
到这里错误处理曾经音讯形容分明了,接下来咱们要看打印了服务端的谬误日志,咱们该如何收集查看,就波及到日志收集零碎。
我的项目地址
https://github.com/zeromicro/go-zero
欢送应用 go-zero
并 star 反对咱们!
微信交换群
关注『微服务实际』公众号并点击 交换群 获取社区群二维码。