feat: print error message according to error code

This commit is contained in:
zhuyasen 2025-04-12 11:33:33 +08:00
parent 65e013ee3e
commit ef814cf96a
4 changed files with 198 additions and 89 deletions

View File

@ -24,6 +24,13 @@ var (
"/health": {},
}
// Print error by specified codes
printErrorBySpecifiedCodes = map[int]bool{
http.StatusInternalServerError: true,
http.StatusBadGateway: true,
http.StatusServiceUnavailable: true,
}
emptyBody = []byte("")
contentMark = []byte(" ...... ")
)
@ -81,6 +88,15 @@ func WithIgnoreRoutes(routes ...string) Option {
}
}
// WithPrintErrorByCodes set print error by specified codes
func WithPrintErrorByCodes(code ...int) Option {
return func(o *options) {
for _, c := range code {
printErrorBySpecifiedCodes[c] = true
}
}
}
// WithRequestIDFromContext name is field in context, default value is request_id
func WithRequestIDFromContext() Option {
return func(o *options) {
@ -154,19 +170,14 @@ func Logging(opts ...Option) gin.HandlerFunc {
return
}
// print input information before processing
buf := bytes.Buffer{}
_, _ = buf.ReadFrom(c.Request.Body)
fields := []zap.Field{
zap.String("method", c.Request.Method),
zap.String("url", c.Request.URL.String()),
}
if c.Request.Method == http.MethodPost || c.Request.Method == http.MethodPut || c.Request.Method == http.MethodPatch || c.Request.Method == http.MethodDelete {
fields = append(fields,
zap.Int("size", buf.Len()),
zap.ByteString("body", getRequestBody(&buf, o.maxLength)),
)
sizeField := zap.Skip()
bodyField := zap.Skip()
if c.Request.Method == http.MethodPost || c.Request.Method == http.MethodPut ||
c.Request.Method == http.MethodPatch || c.Request.Method == http.MethodDelete {
sizeField = zap.Int("size", buf.Len())
bodyField = zap.ByteString("body", getRequestBody(&buf, o.maxLength))
}
reqID := ""
@ -174,14 +185,24 @@ func Logging(opts ...Option) gin.HandlerFunc {
if v, isExist := c.Get(ContextRequestIDKey); isExist {
if requestID, ok := v.(string); ok {
reqID = requestID
fields = append(fields, zap.String(ContextRequestIDKey, reqID))
}
}
} else if o.requestIDFrom == 2 {
reqID = c.Request.Header.Get(HeaderXRequestIDKey)
fields = append(fields, zap.String(ContextRequestIDKey, reqID))
}
o.log.Info("<<<<", fields...)
reqIDField := zap.Skip()
if reqID != "" {
reqIDField = zap.String(ContextRequestIDKey, reqID)
}
// print input information before processing
o.log.Info("<<<<",
zap.String("method", c.Request.Method),
zap.String("url", c.Request.URL.String()),
sizeField,
bodyField,
reqIDField,
)
c.Request.Body = io.NopCloser(&buf)
@ -192,19 +213,22 @@ func Logging(opts ...Option) gin.HandlerFunc {
// processing requests
c.Next()
// print return message after processing
fields = []zap.Field{
zap.Int("code", c.Writer.Status()),
// print response message after processing
httpCode := c.Writer.Status()
fields := []zap.Field{
zap.Int("code", httpCode),
zap.String("method", c.Request.Method),
zap.String("url", c.Request.URL.Path),
zap.Int64("time_us", time.Since(start).Microseconds()),
zap.Int("size", newWriter.body.Len()),
zap.ByteString("body", getResponseBody(newWriter.body, o.maxLength)),
reqIDField,
}
if reqID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, reqID))
if printErrorBySpecifiedCodes[httpCode] {
o.log.WithOptions(zap.AddStacktrace(zap.PanicLevel)).Error(">>>>", fields...)
} else {
o.log.Info(">>>>", fields...)
}
o.log.Info(">>>>", fields...)
}
}
@ -232,21 +256,28 @@ func SimpleLog(opts ...Option) gin.HandlerFunc {
} else if o.requestIDFrom == 2 {
reqID = c.Request.Header.Get(HeaderXRequestIDKey)
}
reqIDField := zap.Skip()
if reqID != "" {
reqIDField = zap.String(ContextRequestIDKey, reqID)
}
// processing requests
c.Next()
// print return message after processing
httpCode := c.Writer.Status()
fields := []zap.Field{
zap.Int("code", c.Writer.Status()),
zap.Int("code", httpCode),
zap.String("method", c.Request.Method),
zap.String("url", c.Request.URL.String()),
zap.Int64("time_us", time.Since(start).Microseconds()),
zap.Int("size", c.Writer.Size()),
reqIDField,
}
if reqID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, reqID))
if printErrorBySpecifiedCodes[httpCode] {
o.log.WithOptions(zap.AddStacktrace(zap.PanicLevel)).Error("Gin response", fields...)
} else {
o.log.Info("Gin response", fields...)
}
o.log.Info("Gin msg", fields...)
}
}

View File

@ -1,10 +1,12 @@
package middleware
import (
"strings"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/go-dev-frame/sponge/pkg/gin/response"
"github.com/go-dev-frame/sponge/pkg/httpcli"
@ -20,7 +22,7 @@ func runLogHTTPServer() string {
serverAddr, requestAddr := utils.GetLocalHTTPAddrPairs()
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
r := gin.New()
r.Use(RequestID())
// default Print Log
@ -33,6 +35,7 @@ func runLogHTTPServer() string {
WithRequestIDFromHeader(),
WithRequestIDFromContext(),
WithIgnoreRoutes("/ping"), // ignore path /ping
WithPrintErrorByCodes(409),
))
// custom zap log
@ -45,17 +48,22 @@ func runLogHTTPServer() string {
logger.Info("test request id", GCtxRequestIDField(c))
response.Success(c, "hello world")
}
helloFunErr := func(c *gin.Context) {
logger.Info("test request id", GCtxRequestIDField(c))
response.Output(c, 500, "hello world error")
}
pingFun := func(c *gin.Context) {
response.Success(c, "ping")
}
r.GET("/hello", helloFun)
r.GET("/ping", pingFun)
r.GET("/hello", helloFun)
r.DELETE("/hello", helloFun)
r.POST("/hello", helloFun)
r.PUT("/hello", helloFun)
r.PATCH("/hello", helloFun)
r.POST("/helloErr", helloFunErr)
go func() {
err := r.Run(serverAddr)
@ -149,26 +157,38 @@ func TestRequest(t *testing.T) {
t.Errorf("got: %s, want: %s", got, wantHello)
}
})
t.Run("post hello error", func(t *testing.T) {
err := httpcli.Post(result, requestAddr+"/helloErr", &User{"foobar"})
assert.Equal(t, true, strings.Contains(err.Error(), "500"))
})
}
func runLogHTTPServer2() string {
serverAddr, requestAddr := utils.GetLocalHTTPAddrPairs()
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
r := gin.New()
r.Use(RequestID())
r.Use(SimpleLog(
WithLog(logger.Get()),
WithMaxLen(200),
WithRequestIDFromContext(),
WithRequestIDFromHeader(),
WithPrintErrorByCodes(409),
))
pingFun := func(c *gin.Context) {
response.Success(c, "ping")
helloFun := func(c *gin.Context) {
logger.Info("test request id", GCtxRequestIDField(c))
response.Success(c, "hello world")
}
helloFunErr := func(c *gin.Context) {
logger.Info("test request id", GCtxRequestIDField(c))
response.Output(c, 500, "hello world error")
}
r.GET("/ping", pingFun)
r.GET("/hello", helloFun)
r.GET("/helloErr", helloFunErr)
go func() {
err := r.Run(serverAddr)
@ -184,16 +204,18 @@ func runLogHTTPServer2() string {
func TestRequest2(t *testing.T) {
requestAddr := runLogHTTPServer2()
result := &httpcli.StdResult{}
t.Run("get ping", func(t *testing.T) {
err := httpcli.Get(result, requestAddr+"/ping")
if err != nil {
t.Error(err)
return
}
got := result.Data.(string)
if got != "ping" {
t.Errorf("got: %s, want: ping", got)
}
wantHello := "hello world"
t.Run("get hello", func(t *testing.T) {
result := &httpcli.StdResult{}
err := httpcli.Get(result, requestAddr+"/hello", httpcli.WithParams(map[string]interface{}{"id": "100"}))
assert.NoError(t, err)
assert.Equal(t, wantHello, result.Data.(string))
})
t.Run("get hello error", func(t *testing.T) {
result := &httpcli.StdResult{}
err := httpcli.Get(result, requestAddr+"/helloErr")
assert.Equal(t, true, strings.Contains(err.Error(), "500"))
})
}

View File

@ -6,9 +6,12 @@ import (
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/go-dev-frame/sponge/pkg/errcode"
zapLog "github.com/go-dev-frame/sponge/pkg/logger"
)
@ -28,6 +31,11 @@ func UnaryClientLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryClientInter
}
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
// ignore printing of the specified method
if ignoreLogMethods[method] {
return invoker(ctx, method, req, reply, cc, opts...)
}
startTime := time.Now()
var reqIDField zap.Field
@ -39,16 +47,21 @@ func UnaryClientLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryClientInter
err := invoker(ctx, method, req, reply, cc, opts...)
statusCode := status.Code(err)
fields := []zap.Field{
zap.String("code", status.Code(err).String()),
zap.String("code", statusCode.String()),
zap.Error(err),
zap.String("type", "unary"),
zap.String("method", method),
zap.Int64("time_us", time.Since(startTime).Microseconds()),
reqIDField,
}
if err != nil && printErrorBySpecifiedCodes[statusCode] {
logger.WithOptions(zap.AddStacktrace(zapcore.PanicLevel)).Error("invoker error", fields...)
} else {
logger.Info("invoker result", fields...)
}
logger.Info("invoker result", fields...)
return err
}
}
@ -66,6 +79,11 @@ func StreamClientLog(logger *zap.Logger, opts ...LogOption) grpc.StreamClientInt
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string,
streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
// ignore printing of the specified method
if ignoreLogMethods[method] {
return streamer(ctx, desc, cc, method, opts...)
}
startTime := time.Now()
var reqIDField zap.Field
@ -77,46 +95,55 @@ func StreamClientLog(logger *zap.Logger, opts ...LogOption) grpc.StreamClientInt
clientStream, err := streamer(ctx, desc, cc, method, opts...)
statusCode := status.Code(err)
fields := []zap.Field{
zap.String("code", status.Code(err).String()),
zap.String("code", statusCode.String()),
zap.Error(err),
zap.String("type", "stream"),
zap.String("method", method),
zap.Int64("time_us", time.Since(startTime).Microseconds()),
reqIDField,
}
if err != nil && printErrorBySpecifiedCodes[statusCode] {
logger.WithOptions(zap.AddStacktrace(zapcore.PanicLevel)).Error("streamer invoker error", fields...)
} else {
logger.Info("streamer invoker result", fields...)
}
logger.Info("invoker result", fields...)
return clientStream, err
}
}
// ---------------------------------- server interceptor ----------------------------------
var defaultMaxLength = 300 // max length of response data to print
var ignoreLogMethods = map[string]struct{}{} // ignore printing methods
var defaultMaxLength = 300 // max length of response data to print
var defaultMarshalFn = func(reply interface{}) []byte {
data, _ := json.Marshal(reply)
return data
}
var ignoreLogMethods = map[string]bool{ // ignore printing methods
"/grpc.health.v1.Health/Check": true,
}
var printErrorBySpecifiedCodes = map[codes.Code]bool{
codes.Internal: true,
codes.Unavailable: true,
errcode.StatusInternalServerError.Code(): true,
errcode.StatusServiceUnavailable.Code(): true,
}
// LogOption log settings
type LogOption func(*logOptions)
type logOptions struct {
maxLength int
fields map[string]interface{}
ignoreMethods map[string]struct{}
isReplaceGRPCLogger bool
marshalFn func(reply interface{}) []byte // default json.Marshal
}
func defaultLogOptions() *logOptions {
return &logOptions{
maxLength: defaultMaxLength,
fields: make(map[string]interface{}),
ignoreMethods: make(map[string]struct{}),
marshalFn: defaultMarshalFn,
maxLength: defaultMaxLength,
marshalFn: defaultMarshalFn,
}
}
@ -142,13 +169,12 @@ func WithReplaceGRPCLogger() LogOption {
}
}
// WithLogFields adding a custom print field
func WithLogFields(kvs map[string]interface{}) LogOption {
// WithPrintErrorByCodes set print error by grpc codes
func WithPrintErrorByCodes(code ...codes.Code) LogOption {
return func(o *logOptions) {
if len(kvs) == 0 {
return
for _, c := range code {
printErrorBySpecifiedCodes[c] = true
}
o.fields = kvs
}
}
@ -167,7 +193,7 @@ func WithMarshalFn(fn func(reply interface{}) []byte) LogOption {
func WithLogIgnoreMethods(fullMethodNames ...string) LogOption {
return func(o *logOptions) {
for _, method := range fullMethodNames {
o.ignoreMethods[method] = struct{}{}
ignoreLogMethods[method] = true
}
}
}
@ -176,7 +202,6 @@ func WithLogIgnoreMethods(fullMethodNames ...string) LogOption {
func UnaryServerLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryServerInterceptor {
o := defaultLogOptions()
o.apply(opts...)
ignoreLogMethods = o.ignoreMethods
if logger == nil {
logger, _ = zap.NewProduction()
@ -187,7 +212,7 @@ func UnaryServerLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryServerInter
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// ignore printing of the specified method
if _, ok := ignoreLogMethods[info.FullMethod]; ok {
if ignoreLogMethods[info.FullMethod] {
return handler(ctx, req)
}
@ -211,8 +236,9 @@ func UnaryServerLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryServerInter
data = append(data[:o.maxLength], contentMark...)
}
statusCode := status.Code(err)
fields = []zap.Field{
zap.String("code", status.Code(err).String()),
zap.String("code", statusCode.String()),
zap.Error(err),
zap.String("type", "unary"),
zap.String("method", info.FullMethod),
@ -222,7 +248,11 @@ func UnaryServerLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryServerInter
if requestID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, requestID))
}
logger.Info(">>>>", fields...)
if err != nil && printErrorBySpecifiedCodes[statusCode] {
logger.WithOptions(zap.AddStacktrace(zapcore.PanicLevel)).Error(">>>>", fields...)
} else {
logger.Info(">>>>", fields...)
}
return resp, err
}
@ -232,7 +262,6 @@ func UnaryServerLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryServerInter
func UnaryServerSimpleLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryServerInterceptor {
o := defaultLogOptions()
o.apply(opts...)
ignoreLogMethods = o.ignoreMethods
if logger == nil {
logger, _ = zap.NewProduction()
@ -243,7 +272,7 @@ func UnaryServerSimpleLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryServe
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// ignore printing of the specified method
if _, ok := ignoreLogMethods[info.FullMethod]; ok {
if ignoreLogMethods[info.FullMethod] {
return handler(ctx, req)
}
@ -252,17 +281,24 @@ func UnaryServerSimpleLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryServe
resp, err := handler(ctx, req)
requestIDField := zap.Skip()
if requestID != "" {
requestIDField = zap.String(ContextRequestIDKey, requestID)
}
statusCode := status.Code(err)
fields := []zap.Field{
zap.String("code", status.Code(err).String()),
zap.String("code", statusCode.String()),
zap.Error(err),
zap.String("type", "unary"),
zap.String("method", info.FullMethod),
zap.Int64("time_us", time.Since(startTime).Microseconds()),
requestIDField,
}
if requestID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, requestID))
if err != nil && printErrorBySpecifiedCodes[statusCode] {
logger.WithOptions(zap.AddStacktrace(zapcore.PanicLevel)).Error("gRPC response error", fields...)
} else {
logger.Info("gRPC response", fields...)
}
logger.Info("[GRPC] response", fields...)
return resp, err
}
@ -272,7 +308,6 @@ func UnaryServerSimpleLog(logger *zap.Logger, opts ...LogOption) grpc.UnaryServe
func StreamServerLog(logger *zap.Logger, opts ...LogOption) grpc.StreamServerInterceptor {
o := defaultLogOptions()
o.apply(opts...)
ignoreLogMethods = o.ignoreMethods
if logger == nil {
logger, _ = zap.NewProduction()
@ -283,34 +318,40 @@ func StreamServerLog(logger *zap.Logger, opts ...LogOption) grpc.StreamServerInt
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// ignore printing of the specified method
if _, ok := ignoreLogMethods[info.FullMethod]; ok {
if ignoreLogMethods[info.FullMethod] {
return handler(srv, stream)
}
startTime := time.Now()
requestID := ServerCtxRequestID(stream.Context())
requestIDField := zap.Skip()
if requestID != "" {
requestIDField = zap.String(ContextRequestIDKey, requestID)
}
fields := []zap.Field{
zap.String("type", "stream"),
zap.String("method", info.FullMethod),
}
if requestID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, requestID))
requestIDField,
}
logger.Info("<<<<", fields...)
err := handler(srv, stream)
statusCode := status.Code(err)
fields = []zap.Field{
zap.String("code", status.Code(err).String()),
zap.String("code", statusCode.String()),
zap.Error(err),
zap.String("type", "stream"),
zap.String("method", info.FullMethod),
zap.Int64("time_us", time.Since(startTime).Microseconds()),
requestIDField,
}
if requestID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, requestID))
if err != nil && printErrorBySpecifiedCodes[statusCode] {
logger.WithOptions(zap.AddStacktrace(zapcore.PanicLevel)).Error(">>>>", fields...)
} else {
logger.Info(">>>>", fields...)
}
logger.Info(">>>>", fields...)
return err
}
@ -320,7 +361,6 @@ func StreamServerLog(logger *zap.Logger, opts ...LogOption) grpc.StreamServerInt
func StreamServerSimpleLog(logger *zap.Logger, opts ...LogOption) grpc.StreamServerInterceptor {
o := defaultLogOptions()
o.apply(opts...)
ignoreLogMethods = o.ignoreMethods
if logger == nil {
logger, _ = zap.NewProduction()
@ -331,25 +371,34 @@ func StreamServerSimpleLog(logger *zap.Logger, opts ...LogOption) grpc.StreamSer
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// ignore printing of the specified method
if _, ok := ignoreLogMethods[info.FullMethod]; ok {
if ignoreLogMethods[info.FullMethod] {
return handler(srv, stream)
}
startTime := time.Now()
requestID := ServerCtxRequestID(stream.Context())
requestIDField := zap.Skip()
if requestID != "" {
requestIDField = zap.String(ContextRequestIDKey, requestID)
}
err := handler(srv, stream)
statusCode := status.Code(err)
fields := []zap.Field{
zap.String("code", status.Code(err).String()),
zap.String("code", statusCode.String()),
zap.Error(err),
zap.String("type", "stream"),
zap.String("method", info.FullMethod),
zap.Int64("time_us", time.Since(startTime).Microseconds()),
requestIDField,
}
if requestID != "" {
fields = append(fields, zap.String(ContextRequestIDKey, requestID))
if err != nil && printErrorBySpecifiedCodes[statusCode] {
logger.WithOptions(zap.AddStacktrace(zapcore.PanicLevel)).Error("gRPC response error", fields...)
} else {
logger.Info("gRPC response", fields...)
}
logger.Info("[GRPC] response", fields...)
return err
}

View File

@ -6,6 +6,7 @@ import (
"testing"
"time"
"github.com/go-dev-frame/sponge/pkg/errcode"
"github.com/go-dev-frame/sponge/pkg/logger"
)
@ -14,7 +15,7 @@ func TestUnaryClientLog(t *testing.T) {
time.Sleep(time.Millisecond * 200)
cli := newUnaryRPCClient(addr,
UnaryClientRequestID(),
UnaryClientLog(logger.Get(), WithReplaceGRPCLogger()),
UnaryClientLog(logger.Get(), WithReplaceGRPCLogger(), WithPrintErrorByCodes(errcode.StatusInvalidParams.Code())),
)
_ = sayHelloMethod(context.Background(), cli)
}
@ -23,6 +24,15 @@ func TestUnaryServerLog(t *testing.T) {
addr := newUnaryRPCServer(
UnaryServerRequestID(),
UnaryServerLog(logger.Get(), WithReplaceGRPCLogger()),
)
time.Sleep(time.Millisecond * 200)
cli := newUnaryRPCClient(addr)
_ = sayHelloMethod(context.Background(), cli)
}
func TestUnaryServerSimpleLog(t *testing.T) {
addr := newUnaryRPCServer(
UnaryServerRequestID(),
UnaryServerSimpleLog(logger.Get(), WithReplaceGRPCLogger()),
)
time.Sleep(time.Millisecond * 200)
@ -45,12 +55,11 @@ func TestUnaryServerLog_ignore(t *testing.T) {
addr := newUnaryRPCServer(
UnaryServerLog(logger.Get(),
WithMaxLen(200),
WithLogFields(map[string]interface{}{"foo": "bar"}),
WithMarshalFn(func(reply interface{}) []byte {
data, _ := json.Marshal(reply)
return data
}),
WithLogIgnoreMethods("/api.user.v1.user/GetByID"),
WithLogIgnoreMethods("/proto.Greeter/SayHello"),
),
)
time.Sleep(time.Millisecond * 200)
@ -63,11 +72,9 @@ func TestStreamServerLog(t *testing.T) {
StreamServerRequestID(),
StreamServerLog(logger.Get(),
WithReplaceGRPCLogger(),
WithLogFields(map[string]interface{}{}),
),
StreamServerSimpleLog(logger.Get(),
WithReplaceGRPCLogger(),
WithLogFields(map[string]interface{}{}),
),
)
time.Sleep(time.Millisecond * 200)