最近項(xiàng)目中為了將具體出錯信息向前端暴露出來叽唱,所以需要定義具體的錯誤碼格式煞茫,主要有如下幾個問題需要解決。
- 錯誤碼的定義地消。
- 因?yàn)殄e誤碼是分布在代碼的各個模塊中,因此最好使用自動化代碼生成工具將錯誤碼收集起來畏妖,類似與
K8S
中的根據(jù)相應(yīng)的tag來生成代碼脉执。 - 由于底層很多命令是通過
gRPC
調(diào)用來完成的,如何將錯誤碼和錯誤信息返回給客戶端也是需要解決的問題瓜客。默認(rèn)情況下client
端返回給server
端的錯誤碼是經(jīng)過封裝處理的适瓦。
錯誤碼的定義
-
錯誤碼是按照各組件來劃分的,其格式為:
AA-BB-CCCC
- A: 項(xiàng)目或模塊名稱; 比如rbd, ceph, docker, disk等谱仪。
- B: 具體子模塊玻熙;比如rbd中的volume,snapshot, volume migration等等疯攒。
- C: 具體錯誤編號嗦随;自增且唯一表示具體某一種錯誤。
-
注意以下規(guī)范:
- 錯誤碼的格式為
16進(jìn)制大寫
敬尺,例如1A-F3-001C -
AA-BB-CCCC
中子模塊BB
如果為01則表示common
的部分枚尼。例如02-01-XXXX表示一些通用的rbd錯誤碼。 -
0000
保留不使用砂吞,0001
統(tǒng)一表示unspecified error
署恍。每個子模塊有自己的0001
編碼盯质,代表屬于這個子模塊的unspecified error
概而。
- 錯誤碼的格式為
錯誤碼
錯誤碼分散于各模塊中呼巷,錯誤碼定義在xxx_error.go文件中赎瑰,其中xxx代表對應(yīng)的模塊。其格式如下:
// ErrCodeRbd defines the id of rbd module
// +ErrCode
const ErrCodeRbd = 0x02
// the sub module of rbd
// +ErrCode=Rbd
const (
ErrCodeRbdCommon = iota + 1
ErrCodeRbdVolume
ErrCodeRbdSnapshot
ErrCodeRbdVolumeMigration
ErrCodeRbdReplication
ErrCodeRbdTrash
)
// list of rbd common error codes
// +ErrCode=Rbd,Common
const (
ErrCodeRbdCommonUnspecifiedError = iota + 1
)
// ErrCodeRbdCommonToMessage is map of common error code to their messages
var ErrCodeRbdCommonToMessage = map[int]string{
ErrCodeRbdCommonUnspecifiedError: "the %s operation failed due to unspecified error",
}
......
錯誤碼文件中的內(nèi)容大致如下:
- 首先定義的是
模塊ID
餐曼,其為常量類型鲜漩,命名時以ErrCode
開頭宇整,后面跟著模塊名鳞青,例如Rbd为朋。命名格式為:ErrCodeAA
习寸。 value部分為16進(jìn)制
值霞溪。 - 接著是
子模塊
的定義鸯匹,01代表common殴蓬,然后根據(jù)各子模塊進(jìn)行擴(kuò)展即可染厅。命名格式為:ErrCodeAABB
- 接著是各子模塊對應(yīng)的
具體錯誤ID
肖粮。命名格式為:ErrCodeAABBCC
- 接著是錯誤ID對應(yīng)的
message
信息涩馆。命名格式為:ErrCodeAABBToMessage
錯誤碼中Tag的設(shè)置
由于錯誤碼分散在代碼的各個模塊中凌净,為了更好的收集所有的錯誤碼并生成對應(yīng)的json文件供前端使用冰寻,所以采用的是k8s方案中的gengo
自動化代碼生成斩芭。代碼分支見microyahoo/gengo
Tag的定義
如上面的代碼片段所示划乖,tag緊挨著const定義琴庵,以+ErrCode
開頭迷殿。
例如上面定義了三個tag
// +ErrCode 加在模塊的上面庆寺,代表這是具體的一個模塊懦尝。
// +ErrCode=Rbd 加在具體的子模塊上面陵霉,代表這是模塊下的子模塊信息撩匕,可能有多個止毕。
// +ErrCode=Rbd,Common 加在具體子模塊對應(yīng)的錯誤信息上面扁凛,代表子模塊有很多具體的錯誤信息
其中第二個tag以+ErrCode
開頭谨朝,后面跟著具體的模塊字币,是以鍵值對的形式展示的洗出。第三個tag也是以+ErrCode
開頭阱洪,后面跟著具體的模塊以及子模塊,其中模塊和子模塊之間以逗號分隔,中間沒有空格盔粹。
自動化生成的json文件如下:
{
"01-01-0001": {
"desc": "CommonUnspecifiedError"
},
"01-01-0002": {
"desc": "CommonJSONUnmarshalError"
},
"01-01-0003": {
"desc": "CommonJSONMarshalError"
},
"02-01-0001": {
"desc": "RbdCommonUnspecifiedError"
},
"02-02-0001": {
"desc": "RbdVolumeUnknownParameter"
},
"02-02-0002": {
"desc": "RbdVolumeNoEntry"
}
}
gRPC error處理
由于代碼中運(yùn)用了很多gRPC
調(diào)用去其他節(jié)點(diǎn)執(zhí)行相應(yīng)的命令玻佩,而gRPC server會將我們執(zhí)行命令返回結(jié)果的錯誤信息封裝成statusError
咬崔,這樣客戶端拿到的error是處理之后的垮斯,不是我們上述自定義的error,因此也就無法獲取定義的錯誤碼和其他自定義的錯誤信息熊杨。具體可以參見google.golang.org/grpc/status/status.go
文件晶府。
41 // statusError is an alias of a status proto. It implements error and Status,
42 // and a nil statusError should never be returned by this package.
43 type statusError spb.Status
44
45 func (se *statusError) Error() string {
46 p := (*spb.Status)(se)
47 return fmt.Sprintf("rpc error: code = %s desc = %s", codes.Code(p.GetCode()), p.GetMessage())
48 }
49
50 func (se *statusError) GRPCStatus() *Status {
51 return &Status{s: (*spb.Status)(se)}
52 }
此問題可以通過分別在server和client端添加自定義的一元攔截器
進(jìn)行處理川陆。過程大致如下:
- client端發(fā)起gRPC調(diào)用较沪,server接收請求后執(zhí)行相應(yīng)的命令,如果執(zhí)行失敗將錯誤信息中的錯誤碼和錯誤信息進(jìn)行封裝成可序列化的控轿。
error.proto
文件定義如下所示解幽,這樣生成的error.pb.go
中的Error實(shí)現(xiàn)了proto.Message
接口躲株,可被序列化之后被client接收并解析。
1 syntax = "proto3";
2
3 package pb;
4
5 message Error {
6 string code = 1;
7 string message = 2;
8 string details = 3;
9 }
server端一元攔截器如下所示:
// ServerErrorInterceptor transfer a error to status error
func ServerErrorInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
resp, err := handler(ctx, req)
return resp, toStatusError(err)
}
func toStatusError(err error) error {
if err == nil {
return nil
}
cause := errors.Cause(err)
pbErr := &pb.Error{
Details: cause.Error(),
}
if coder, ok := cause.(errors.Coder); ok {
pbErr.Code = coder.Code()
pbErr.Message = coder.Message()
pbErr.Details = coder.Details()
}
st := status.New(codes.Internal, cause.Error())
st, e := st.WithDetails(pbErr)
if e != nil {
// make sure pbErr implements proto.Message interface
return errors.NewCommonError(errors.ErrCodeCommonJSONMarshalError, e, pbErr.String())
}
return st.Err()
}
Server端的攔截器主要是將我們定義的帶錯誤碼的error轉(zhuǎn)化為可被序列化的rpc pb.Error
,然后調(diào)用Status.WithDetails()
進(jìn)行序列化磨德,這樣client端攔截器拿到序列化后的pb.Error典挑,返回我們一個實(shí)現(xiàn)了errors.Coder接口的error您觉。這樣client就能獲取定義的錯誤碼琳水,錯誤信息等等在孝。
- client一元攔截器收到server端返回的錯誤信息后進(jìn)行解析。
func ClientErrorInterceptor(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
err := invoker(ctx, method, req, reply, cc, opts...)
if err == nil {
return nil
}
cause := errors.Cause(err)
st, ok := status.FromError(cause)
if ok {
details := st.Details()
if details != nil && len(details) > 0 {
if pbErr, ok := details[0].(*pb.Error); ok {
return newRPCClientError(pbErr.Code, pbErr.Message, pbErr.Details)
}
}
}
return err
}
一元攔截器在執(zhí)行完調(diào)用后對錯誤信息進(jìn)行處理顾彰,其中status.FromError
從錯誤信息中獲取Status涨享,而Status.Details()
方法會將錯誤信息反序列化成我們前面定義的pb.Error
,這樣我們就能拿到定義的錯誤碼吁讨,錯誤信息建丧,以及details了翎朱。