From f67bee3ef231244d7ff7618192711cbb550a51c3 Mon Sep 17 00:00:00 2001 From: danysousa Date: Mon, 3 Jul 2023 18:24:50 +0200 Subject: [PATCH 1/5] [WIP] --- config/env.go | 18 +++++++ errors/callstack.go | 78 +++++++++++++++++++++++++++ errors/error.go | 16 +++++- log/client.go | 91 ++++++++++++++++++++++++++++++++ log/{logger.go => zap_logger.go} | 78 +++++++++++++++------------ openapi/builder.go | 38 +++++++++---- server/option.go | 14 ++--- 7 files changed, 275 insertions(+), 58 deletions(-) create mode 100644 config/env.go create mode 100644 errors/callstack.go create mode 100644 log/client.go rename log/{logger.go => zap_logger.go} (51%) diff --git a/config/env.go b/config/env.go new file mode 100644 index 0000000..750cf5f --- /dev/null +++ b/config/env.go @@ -0,0 +1,18 @@ +package config + +import ( + "os" +) + +// PORT is the port used by the server to listen +// default value is 8080 +var PORT string + +// IS_LOCAL is a flag to indicate if the server is running locally +var IS_LOCAL = os.Getenv("IS_LOCAL") == "true" + +func init() { + if PORT = os.Getenv("PORT"); PORT == "" { + PORT = "8080" + } +} diff --git a/errors/callstack.go b/errors/callstack.go new file mode 100644 index 0000000..fbcf71b --- /dev/null +++ b/errors/callstack.go @@ -0,0 +1,78 @@ +package errors + +import ( + "fmt" + "runtime" + "strings" +) + +// GetCallers return the caller of the function and the call stack +func GetCallers() (callerName, caller string, callStack []string) { + // Ask runtime.Callers for up to 10 pcs + pc := make([]uintptr, 10) + n := runtime.Callers(1, pc) + if n == 0 { + // No pcs available. Stop now. + // This can happen if the first argument to runtime.Callers is large. + caller = "unknown" + return + } + + pc = pc[:n] // pass only valid pcs to runtime.CallersFrames + frames := runtime.CallersFrames(pc) + + firstFrame := true + // Loop to get frames. + // A fixed number of pcs can expand to an indefinite number of Frames. + for { + frame, more := frames.Next() + if !more { + break + } + + // we don't want to print the http stack + if !firstFrame && strings.Contains(frame.File, "mwm-io/server") { + break + } + + if strings.Contains(frame.File, "mwm-io/log") || + strings.Contains(frame.File, "mwm-io/errors") { + continue + } + + if firstFrame { + caller = formatFrame(frame) + callerName = frame.Function + firstFrame = false + } else { + callStack = append(callStack, formatFrame(frame)) + } + } + + return +} + +func formatFrame(frame runtime.Frame) string { + file := frame.File + line := frame.Line + function := frame.Function + + return fmt.Sprintf("%s:%s: %s", file, itoa(line, -1), function) +} + +// Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding. +func itoa(i int, wid int) string { + // Assemble decimal in reverse order. + var b [20]byte + bp := len(b) - 1 + for i >= 10 || wid > 1 { + wid-- + q := i / 10 + b[bp] = byte('0' + i - q*10) + bp-- + i = q + } + // i < 10 + b[bp] = byte('0' + i) + return string(b[bp:]) +} diff --git a/errors/error.go b/errors/error.go index d56ab0e..7e564cb 100644 --- a/errors/error.go +++ b/errors/error.go @@ -34,6 +34,9 @@ type FullError struct { status int timestamp time.Time sourceErr error + callerName string + caller string + callstack []string } // Wrap will wrap the given error and return a new Error. @@ -42,19 +45,28 @@ func Wrap(err error) Error { return nil } + if castedErr, ok := err.(Error); ok { + return castedErr + } + for _, builder := range errorBuilders { if gErr := builder(err); gErr != nil { return gErr } } + callerName, caller, callstack := GetCallers() + newErr := &FullError{ userMessage: err.Error(), - kind: "", + kind: "internal_error", errorMessage: err.Error(), - timestamp: time.Now(), status: http.StatusInternalServerError, + timestamp: time.Now(), sourceErr: err, + callerName: callerName, + caller: caller, + callstack: callstack, } return newErr diff --git a/log/client.go b/log/client.go new file mode 100644 index 0000000..4607dac --- /dev/null +++ b/log/client.go @@ -0,0 +1,91 @@ +package log + +import ( + "context" + "fmt" + + "go.uber.org/zap" + + "github.com/mwm-io/gapi/errors" +) + +// Log is a simple client to improve the usability of the zap logger using GAPI +type Log struct { + // Chosen function according to severity + f func(string, ...zap.Field) + fields []zap.Field +} + +// With returns a new Log with additional zap.Field +func (l *Log) With(fields ...zap.Field) *Log { + l.fields = append(l.fields, fields...) + return l +} + +// LogMsg format and log a message +func (l *Log) LogMsg(format string, a ...any) { + msg := fmt.Sprintf(format, a...) + if len(l.fields) > 0 { + l.f(msg, l.fields...) + return + } + + l.f(msg) +} + +// LogError take a GAPI error, format error message and log it +func (l *Log) LogError(err errors.Error) { + l.With( + zap.String("kind", err.Kind()), + zap.String("callstack", err.Callstack()), + ).LogMsg(err.Error()) +} + +// Debug logs a debug message with additional zap.Field +func Debug(ctx context.Context) Log { + return Log{ + f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Debug, + } +} + +// Info logs an info message with additional zap.Field +func Info(ctx context.Context) Log { + return Log{ + f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Info, + } +} + +// Warn logs a warning message with additional zap.Field +func Warn(ctx context.Context) Log { + return Log{ + f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Warn, + } +} + +// Error logs an error message with additional zap.Field +func Error(ctx context.Context) Log { + return Log{ + f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Error, + } +} + +// Critical logs a critical message with additional zap.Field +func Critical(ctx context.Context) Log { + return Log{ + f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Panic, + } +} + +// Alert logs an alert message with additional zap.Field +func Alert(ctx context.Context) Log { + return Log{ + f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Panic, + } +} + +// Emergency logs an emergency message with additional zap.Field +func Emergency(ctx context.Context) Log { + return Log{ + f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Fatal, + } +} diff --git a/log/logger.go b/log/zap_logger.go similarity index 51% rename from log/logger.go rename to log/zap_logger.go index 60a9e30..e801a05 100644 --- a/log/logger.go +++ b/log/zap_logger.go @@ -6,6 +6,8 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" + + "github.com/mwm-io/gapi/config" ) var ( @@ -49,9 +51,14 @@ func encodeLevel() zapcore.LevelEncoder { } func defaultConfig() *zap.Config { + var encoding = "json" + if config.IS_LOCAL { + encoding = "console" + } + return &zap.Config{ Level: zap.NewAtomicLevelAt(zapcore.DebugLevel), - Encoding: "json", + Encoding: encoding, EncoderConfig: encoderConfig, OutputPaths: []string{"stdout"}, ErrorOutputPaths: []string{"stderr"}, @@ -79,37 +86,38 @@ func Logger(ctx context.Context) *zap.Logger { return globalLogger } -// Debug logs a debug message with additional zap.Field -func Debug(ctx context.Context, msg string, fields ...zap.Field) { - Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Debug(msg, fields...) -} - -// Info logs an info message with additional zap.Field -func Info(ctx context.Context, msg string, fields ...zap.Field) { - Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Info(msg, fields...) -} - -// Warn logs a warning message with additional zap.Field -func Warn(ctx context.Context, msg string, fields ...zap.Field) { - Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Warn(msg, fields...) -} - -// Error logs an error message with additional zap.Field -func Error(ctx context.Context, msg string, fields ...zap.Field) { - Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Error(msg, fields...) -} - -// Critical logs a critical message with additional zap.Field -func Critical(ctx context.Context, msg string, fields ...zap.Field) { - Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Panic(msg, fields...) -} - -// Alert logs an alert message with additional zap.Field -func Alert(ctx context.Context, msg string, fields ...zap.Field) { - Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Panic(msg, fields...) -} - -// Emergency logs an emergency message with additional zap.Field -func Emergency(ctx context.Context, msg string, fields ...zap.Field) { - Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Fatal(msg, fields...) -} +// +// // Debug logs a debug message with additional zap.Field +// func Debug(ctx context.Context, msg string, fields ...zap.Field) { +// Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Debug(msg, fields...) +// } +// +// // Info logs an info message with additional zap.Field +// func Info(ctx context.Context, msg string, fields ...zap.Field) { +// Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Info(msg, fields...) +// } +// +// // Warn logs a warning message with additional zap.Field +// func Warn(ctx context.Context, msg string, fields ...zap.Field) { +// Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Warn(msg, fields...) +// } +// +// // Error logs an error message with additional zap.Field +// func Error(ctx context.Context, msg string, fields ...zap.Field) { +// Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Error(msg, fields...) +// } +// +// // Critical logs a critical message with additional zap.Field +// func Critical(ctx context.Context, msg string, fields ...zap.Field) { +// Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Panic(msg, fields...) +// } +// +// // Alert logs an alert message with additional zap.Field +// func Alert(ctx context.Context, msg string, fields ...zap.Field) { +// Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Panic(msg, fields...) +// } +// +// // Emergency logs an emergency message with additional zap.Field +// func Emergency(ctx context.Context, msg string, fields ...zap.Field) { +// Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Fatal(msg, fields...) +// } diff --git a/openapi/builder.go b/openapi/builder.go index 0d7b307..9c3b2c3 100644 --- a/openapi/builder.go +++ b/openapi/builder.go @@ -98,16 +98,23 @@ func (b *DocBuilder) WithBody(body interface{}, options ...BuilderOption) *DocBu b.operation.RequestBodyEns().RequestBodyEns().Description = &c.description } - if len(c.examples) != 0 { - for mimeType, val := range b.operation.RequestBodyEns().RequestBodyEns().Content { - for exampleKey, example := range c.examples { - val.Examples[exampleKey] = openapi3.ExampleOrRef{ - Example: &example, - } - } - - b.operation.RequestBodyEns().RequestBodyEns().Content[mimeType] = val + if len(c.examples) == 0 { + c.examples = make(map[string]openapi3.Example) + exampleName := "default" + c.examples[exampleName] = openapi3.Example{ + Summary: &exampleName, + Value: &body, + } + } + + for mimeType, val := range b.operation.RequestBodyEns().RequestBodyEns().Content { + for exampleKey, example := range c.examples { + val.WithExamplesItem(exampleKey, openapi3.ExampleOrRef{ + Example: &example, + }) } + + b.operation.RequestBodyEns().RequestBodyEns().WithContentItem(mimeType, val) } return b @@ -116,8 +123,7 @@ func (b *DocBuilder) WithBody(body interface{}, options ...BuilderOption) *DocBu // WithBodyExample set an example to request body to the operation func (b *DocBuilder) WithBodyExample(value interface{}) *DocBuilder { for mimeType, val := range b.operation.RequestBodyEns().RequestBodyEns().Content { - val.WithExample(value) - b.operation.RequestBodyEns().RequestBodyEns().Content[mimeType] = val + b.operation.RequestBodyEns().RequestBodyEns().WithContentItem(mimeType, *val.WithExample(value)) } return b @@ -171,6 +177,16 @@ func (b *DocBuilder) WithResponse(output interface{}, options ...BuilderOption) resp.ResponseEns().WithDescription(c.description) } + if len(c.examples) == 0 { + c.examples = make(map[string]openapi3.Example) + exampleName := "default" + c.examples[exampleName] = openapi3.Example{ + Summary: &exampleName, + Description: &exampleName, + Value: &output, + } + } + if len(c.examples) != 0 { contentResp := resp.ResponseEns().Content[c.mimeType] diff --git a/server/option.go b/server/option.go index c92e7b3..46a5877 100644 --- a/server/option.go +++ b/server/option.go @@ -6,6 +6,8 @@ import ( "os" "syscall" "time" + + "github.com/mwm-io/gapi/config" ) // Option is an option to modify the default configuration of the http server. @@ -94,11 +96,7 @@ func (c serverOptions) Addr() string { return fmt.Sprintf(":%s", c.port) } - if port := os.Getenv("PORT"); port != "" { - return fmt.Sprintf(":%s", port) - } - - return ":8080" + return fmt.Sprintf(":%s", config.PORT) } func (c serverOptions) AddrHttps() string { @@ -106,11 +104,7 @@ func (c serverOptions) AddrHttps() string { return fmt.Sprintf(":%s", c.port) } - if port := os.Getenv("PORT"); port != "" { - return fmt.Sprintf(":%s", port) - } - - return ":443" + return fmt.Sprintf(":%s", config.PORT) } func (c serverOptions) CORS() CORS { From f931d914c1557db75d23f8d7dd137401acd2c123 Mon Sep 17 00:00:00 2001 From: danysousa Date: Wed, 5 Jul 2023 16:45:51 +0200 Subject: [PATCH 2/5] Improve logs and errors usage --- README.md | 2 +- errors/callstack.go | 13 +-- errors/error.go | 34 +++++++- errors/error_test.go | 4 +- errors/precomputed.go | 80 +++++++++---------- examples/1-hello-world/go.mod | 2 + examples/1-hello-world/go.sum | 2 - examples/1-hello-world/main.go | 6 +- examples/2-encoders/go.mod | 2 + examples/2-encoders/go.sum | 2 - .../2-encoders/internal/err/auto_response.go | 3 +- .../2-encoders/internal/err/json_response.go | 3 +- .../2-encoders/internal/err/xml_response.go | 3 +- examples/2-encoders/main.go | 6 +- examples/3-params/go.mod | 2 + examples/3-params/go.sum | 2 - .../3-params/internal/body_with_validation.go | 5 +- examples/3-params/main.go | 6 +- examples/4-doc-openapi/go.mod | 2 + examples/4-doc-openapi/go.sum | 2 - examples/4-doc-openapi/internal/user.go | 16 ++-- examples/4-doc-openapi/main.go | 9 +-- handler/middleware.go | 1 + log/client.go | 44 +++++----- log/doc.go | 22 ++--- log/encoder.go | 8 ++ middleware/body_decoder.go | 30 ++++--- middleware/log.go | 2 +- middleware/path_parameters.go | 12 +-- middleware/query_parameters.go | 3 +- middleware/recover.go | 3 +- middleware/response_writer.go | 9 +-- openapi/builder.go | 2 +- server/option.go | 7 ++ 34 files changed, 204 insertions(+), 145 deletions(-) create mode 100644 log/encoder.go diff --git a/README.md b/README.md index f62aa0c..4749bae 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ You can see examples implementations in the examples folder: - a Hello-world example showing you the basics ([examples/hello-world](https://github.com/MWM-io/gapi/tree/main/examples/hello-world)) -- a Google Cloud Platform Logger example showing you how to setup a cloud-logging +- a Google Cloud Platform Logger example showing you how to set up a cloud-logging logger ([examples/gcp-logger](https://github.com/MWM-io/gapi/tree/main/examples/gcp-logger)) - a CRUD example showing you how to make a CRUD with this library ([examples/crud](https://github.com/MWM-io/gapi/tree/main/examples/crud)) diff --git a/errors/callstack.go b/errors/callstack.go index fbcf71b..9834727 100644 --- a/errors/callstack.go +++ b/errors/callstack.go @@ -13,7 +13,7 @@ func GetCallers() (callerName, caller string, callStack []string) { n := runtime.Callers(1, pc) if n == 0 { // No pcs available. Stop now. - // This can happen if the first argument to runtime.Callers is large. + // This can happen if the first argument to runtime.Callers are large. caller = "unknown" return } @@ -30,13 +30,14 @@ func GetCallers() (callerName, caller string, callStack []string) { break } - // we don't want to print the http stack - if !firstFrame && strings.Contains(frame.File, "mwm-io/server") { + // Stop call stack when we reach the handler caller + // every call after this is not relevant + if !firstFrame && strings.Contains(frame.File, "github.com/mwm-io/gapi") && strings.Contains(frame.File, "/handler/") { break } - if strings.Contains(frame.File, "mwm-io/log") || - strings.Contains(frame.File, "mwm-io/errors") { + // Ignore errors package from call trace because all errors was created from this package + if strings.Contains(frame.File, "github.com/mwm-io/gapi") && strings.Contains(frame.File, "/errors/") { continue } @@ -57,7 +58,7 @@ func formatFrame(frame runtime.Frame) string { line := frame.Line function := frame.Function - return fmt.Sprintf("%s:%s: %s", file, itoa(line, -1), function) + return fmt.Sprintf("%s:%s -> %s", file, itoa(line, -1), function) } // Cheap integer to fixed-width decimal ASCII. Give a negative width to avoid zero-padding. diff --git a/errors/error.go b/errors/error.go index 7e564cb..a158c95 100644 --- a/errors/error.go +++ b/errors/error.go @@ -19,6 +19,9 @@ type Error interface { Kind() string StatusCode() int Timestamp() time.Time + CallerName() string + Caller() string + Callstack() []string WithMessage(format string, args ...interface{}) Error WithKind(string) Error @@ -73,15 +76,20 @@ func Wrap(err error) Error { } // Err creates a new Error. -func Err(format string, args ...interface{}) Error { +func Err(kind, format string, args ...interface{}) Error { message := fmt.Sprintf(format, args...) + callerName, caller, callstack := GetCallers() + return &FullError{ userMessage: message, - kind: "", + kind: kind, errorMessage: message, timestamp: time.Now(), status: http.StatusInternalServerError, + callerName: callerName, + caller: caller, + callstack: callstack, } } @@ -147,6 +155,28 @@ func (e *FullError) WithError(err error) Error { return e } +// CallerName implements the error interface. +// It will return the name of the function that created the error +func (e *FullError) CallerName() string { + return e.callerName +} + +// Caller implements the error interface. +// It will return the name of the function that created the error +func (e *FullError) Caller() string { + return e.callerName +} + +// Callstack implements the error interface. +// It will return the complete callstack of the error creation +func (e *FullError) Callstack() []string { + return e.callstack +} + +// CallerName() string +// Caller() string +// Callstack() []string + // HttpError is used to json.Marshal or xml.Marshal FullError. // You can use it to decode an incoming error. type HttpError struct { diff --git a/errors/error_test.go b/errors/error_test.go index 17e4a8a..48f1eb1 100644 --- a/errors/error_test.go +++ b/errors/error_test.go @@ -30,9 +30,7 @@ func TestErr(t *testing.T) { expectedMessage := "this is my error" expectedStatusCode := http.StatusInternalServerError - err := Err("this is my error"). - WithMessage(expectedMessage). - WithKind(expectedKind). + err := Err(expectedKind, expectedMessage). WithStatus(expectedStatusCode) assert.Equal(t, expectedKind, err.Kind()) diff --git a/errors/precomputed.go b/errors/precomputed.go index 995580a..414c80c 100644 --- a/errors/precomputed.go +++ b/errors/precomputed.go @@ -6,200 +6,200 @@ import ( // BadRequest return an error with status code http.StatusBadRequest func BadRequest(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusBadRequest).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusBadRequest) } // Unauthorized return an error with status code http.StatusUnauthorized func Unauthorized(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusUnauthorized).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusUnauthorized) } // PaymentRequired return an error with status code http.StatusPaymentRequired func PaymentRequired(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusPaymentRequired).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusPaymentRequired) } // Forbidden return an error with status code http.StatusForbidden func Forbidden(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusForbidden).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusForbidden) } // NotFound return an error with status code http.StatusNotFound func NotFound(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusNotFound).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusNotFound) } // MethodNotAllowed return an error with status code http.StatusMethodNotAllowed func MethodNotAllowed(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusMethodNotAllowed).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusMethodNotAllowed) } // NotAcceptable return an error with status code http.StatusNotAcceptable func NotAcceptable(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusNotAcceptable).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusNotAcceptable) } // ProxyAuthRequired return an error with status code http.StatusProxyAuthRequired func ProxyAuthRequired(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusProxyAuthRequired).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusProxyAuthRequired) } // RequestTimeout return an error with status code http.StatusRequestTimeout func RequestTimeout(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusRequestTimeout).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusRequestTimeout) } // Conflict return an error with status code http.StatusConflict func Conflict(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusConflict).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusConflict) } // Gone return an error with status code http.StatusGone func Gone(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusGone).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusGone) } // LengthRequired return an error with status code http.StatusLengthRequired func LengthRequired(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusLengthRequired).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusLengthRequired) } // PreconditionFailed return an error with status code http.StatusPreconditionFailed func PreconditionFailed(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusPreconditionFailed).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusPreconditionFailed) } // RequestEntityTooLarge return an error with status code http.StatusRequestEntityTooLarge func RequestEntityTooLarge(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusRequestEntityTooLarge).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusRequestEntityTooLarge) } // RequestURITooLong return an error with status code http.StatusRequestURITooLong func RequestURITooLong(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusRequestURITooLong).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusRequestURITooLong) } // UnsupportedMediaType return an error with status code http.StatusUnsupportedMediaType func UnsupportedMediaType(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusUnsupportedMediaType).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusUnsupportedMediaType) } // RequestedRangeNotSatisfiable return an error with status code http.StatusRequestedRangeNotSatisfiable func RequestedRangeNotSatisfiable(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusRequestedRangeNotSatisfiable).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusRequestedRangeNotSatisfiable) } // ExpectationFailed return an error with status code http.StatusExpectationFailed func ExpectationFailed(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusExpectationFailed).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusExpectationFailed) } // Teapot return an error with status code http.StatusTeapot func Teapot(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusTeapot).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusTeapot) } // MisdirectedRequest return an error with status code http.StatusMisdirectedRequest func MisdirectedRequest(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusMisdirectedRequest).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusMisdirectedRequest) } // UnprocessableEntity return an error with status code http.StatusUnprocessableEntity func UnprocessableEntity(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusUnprocessableEntity).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusUnprocessableEntity) } // Locked return an error with status code http.StatusLocked func Locked(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusLocked).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusLocked) } // FailedDependency return an error with status code http.StatusFailedDependency func FailedDependency(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusFailedDependency).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusFailedDependency) } // TooEarly return an error with status code http.StatusTooEarly func TooEarly(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusTooEarly).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusTooEarly) } // UpgradeRequired return an error with status code http.StatusUpgradeRequired func UpgradeRequired(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusUpgradeRequired).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusUpgradeRequired) } // PreconditionRequired return an error with status code http.StatusPreconditionRequired func PreconditionRequired(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusPreconditionRequired).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusPreconditionRequired) } // TooManyRequests return an error with status code http.StatusTooManyRequests func TooManyRequests(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusTooManyRequests).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusTooManyRequests) } // RequestHeaderFieldsTooLarge return an error with status code http.StatusRequestHeaderFieldsTooLarge func RequestHeaderFieldsTooLarge(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusRequestHeaderFieldsTooLarge).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusRequestHeaderFieldsTooLarge) } // UnavailableForLegalReasons return an error with status code http.StatusUnavailableForLegalReasons func UnavailableForLegalReasons(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusUnavailableForLegalReasons).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusUnavailableForLegalReasons) } // InternalServerError return an error with status code http.StatusInternalServerError func InternalServerError(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusInternalServerError).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusInternalServerError) } // NotImplemented return an error with status code http.StatusNotImplemented func NotImplemented(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusNotImplemented).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusNotImplemented) } // BadGateway return an error with status code http.StatusBadGateway func BadGateway(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusBadGateway).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusBadGateway) } // ServiceUnavailable return an error with status code http.StatusServiceUnavailable func ServiceUnavailable(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusServiceUnavailable).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusServiceUnavailable) } // GatewayTimeout return an error with status code http.StatusGatewayTimeout func GatewayTimeout(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusGatewayTimeout).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusGatewayTimeout) } // HTTPVersionNotSupported return an error with status code http.StatusHTTPVersionNotSupported func HTTPVersionNotSupported(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusHTTPVersionNotSupported).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusHTTPVersionNotSupported) } // VariantAlsoNegotiates return an error with status code http.StatusVariantAlsoNegotiates func VariantAlsoNegotiates(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusVariantAlsoNegotiates).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusVariantAlsoNegotiates) } // InsufficientStorage return an error with status code http.StatusInsufficientStorage func InsufficientStorage(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusInsufficientStorage).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusInsufficientStorage) } // LoopDetected return an error with status code http.StatusLoopDetected func LoopDetected(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusLoopDetected).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusLoopDetected) } // NotExtended return an error with status code http.StatusNotExtended func NotExtended(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusNotExtended).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusNotExtended) } // NetworkAuthenticationRequired return an error with status code http.StatusNetworkAuthenticationRequired func NetworkAuthenticationRequired(kind, msgFormat string, args ...interface{}) Error { - return Err(msgFormat, args...).WithStatus(http.StatusNetworkAuthenticationRequired).WithKind(kind) + return Err(kind, msgFormat, args...).WithStatus(http.StatusNetworkAuthenticationRequired) } diff --git a/examples/1-hello-world/go.mod b/examples/1-hello-world/go.mod index 2fb763e..20b89e6 100644 --- a/examples/1-hello-world/go.mod +++ b/examples/1-hello-world/go.mod @@ -18,3 +18,5 @@ require ( go.uber.org/zap v1.24.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace github.com/mwm-io/gapi => ../../ diff --git a/examples/1-hello-world/go.sum b/examples/1-hello-world/go.sum index b3f536f..bae42e6 100644 --- a/examples/1-hello-world/go.sum +++ b/examples/1-hello-world/go.sum @@ -15,8 +15,6 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= -github.com/mwm-io/gapi v0.2.0 h1:jhpCAHIZkuftPk5nIOISag2nC4Eq4FwjzwSHYIlSib4= -github.com/mwm-io/gapi v0.2.0/go.mod h1:gPTxM9Fhgn7m3+5angXq8+63tTToHbYb9JDTibJJYek= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/examples/1-hello-world/main.go b/examples/1-hello-world/main.go index 83c92d1..31d0e21 100644 --- a/examples/1-hello-world/main.go +++ b/examples/1-hello-world/main.go @@ -15,12 +15,12 @@ func main() { server.AddHandler(r, "GET", "/", handler.Func(HelloWorldHandler)) - gLog.Info(ctx, "Starting http server") + gLog.Info(ctx).LogMsg("Starting http server") if err := server.ServeAndHandleShutdown(r); err != nil { - gLog.Emergency(ctx, err.Error()) + gLog.Error(ctx).LogError(err) } - gLog.Info(ctx, "Server stopped") + gLog.Info(ctx).LogMsg("Server stopped") } // HelloWorldHandler is the simplest handler with core middlewares. diff --git a/examples/2-encoders/go.mod b/examples/2-encoders/go.mod index 934271a..798f250 100644 --- a/examples/2-encoders/go.mod +++ b/examples/2-encoders/go.mod @@ -18,3 +18,5 @@ require ( go.uber.org/zap v1.24.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace github.com/mwm-io/gapi => ../../ diff --git a/examples/2-encoders/go.sum b/examples/2-encoders/go.sum index b3f536f..bae42e6 100644 --- a/examples/2-encoders/go.sum +++ b/examples/2-encoders/go.sum @@ -15,8 +15,6 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= -github.com/mwm-io/gapi v0.2.0 h1:jhpCAHIZkuftPk5nIOISag2nC4Eq4FwjzwSHYIlSib4= -github.com/mwm-io/gapi v0.2.0/go.mod h1:gPTxM9Fhgn7m3+5angXq8+63tTToHbYb9JDTibJJYek= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/examples/2-encoders/internal/err/auto_response.go b/examples/2-encoders/internal/err/auto_response.go index 43279af..19ddd96 100644 --- a/examples/2-encoders/internal/err/auto_response.go +++ b/examples/2-encoders/internal/err/auto_response.go @@ -25,7 +25,6 @@ type autoResponseHandler struct { // Serve implements handler.Handler and is the function called when a request is handled func (h autoResponseHandler) Serve(w http.ResponseWriter, r *http.Request) (interface{}, error) { - return nil, errors.Err("Hello World"). - WithKind("example"). + return nil, errors.Err("example", "Hello World"). WithStatus(http.StatusTeapot) } diff --git a/examples/2-encoders/internal/err/json_response.go b/examples/2-encoders/internal/err/json_response.go index f51639a..04ef59e 100644 --- a/examples/2-encoders/internal/err/json_response.go +++ b/examples/2-encoders/internal/err/json_response.go @@ -26,7 +26,6 @@ type jsonResponseHandler struct { // Serve implements handler.Handler and is the function called when a request is handled func (h jsonResponseHandler) Serve(w http.ResponseWriter, r *http.Request) (interface{}, error) { - return nil, errors.Err("Hello World"). - WithKind("example"). + return nil, errors.Err("example", "Hello World"). WithStatus(http.StatusTeapot) } diff --git a/examples/2-encoders/internal/err/xml_response.go b/examples/2-encoders/internal/err/xml_response.go index 8dc8d1d..efa0d73 100644 --- a/examples/2-encoders/internal/err/xml_response.go +++ b/examples/2-encoders/internal/err/xml_response.go @@ -26,7 +26,6 @@ type xmlResponseHandler struct { // Serve implements handler.Handler and is the function called when a request is handled func (h xmlResponseHandler) Serve(w http.ResponseWriter, r *http.Request) (interface{}, error) { - return nil, errors.Err("Hello World"). - WithKind("example"). + return nil, errors.Err("example", "Hello World"). WithStatus(http.StatusTeapot) } diff --git a/examples/2-encoders/main.go b/examples/2-encoders/main.go index eaf887e..47fdc85 100644 --- a/examples/2-encoders/main.go +++ b/examples/2-encoders/main.go @@ -22,10 +22,10 @@ func main() { server.AddHandler(r, "GET", "/error/xml", err.MakeXMLResponseHandler()) server.AddHandler(r, "GET", "/error/auto", err.MakeAutoResponseHandler()) - gLog.Info(ctx, "Starting http server") + gLog.Info(ctx).LogMsg("Starting http server") if errServe := server.ServeAndHandleShutdown(r); errServe != nil { - gLog.Emergency(ctx, errServe.Error()) + gLog.Error(ctx).LogError(errServe) } - gLog.Info(ctx, "Server stopped") + gLog.Info(ctx).LogMsg("Server stopped") } diff --git a/examples/3-params/go.mod b/examples/3-params/go.mod index 7e99567..663ad8c 100644 --- a/examples/3-params/go.mod +++ b/examples/3-params/go.mod @@ -18,3 +18,5 @@ require ( go.uber.org/zap v1.24.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace github.com/mwm-io/gapi => ../../ diff --git a/examples/3-params/go.sum b/examples/3-params/go.sum index b3f536f..bae42e6 100644 --- a/examples/3-params/go.sum +++ b/examples/3-params/go.sum @@ -15,8 +15,6 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= -github.com/mwm-io/gapi v0.2.0 h1:jhpCAHIZkuftPk5nIOISag2nC4Eq4FwjzwSHYIlSib4= -github.com/mwm-io/gapi v0.2.0/go.mod h1:gPTxM9Fhgn7m3+5angXq8+63tTToHbYb9JDTibJJYek= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/examples/3-params/internal/body_with_validation.go b/examples/3-params/internal/body_with_validation.go index ef79684..f80db94 100644 --- a/examples/3-params/internal/body_with_validation.go +++ b/examples/3-params/internal/body_with_validation.go @@ -25,6 +25,7 @@ type bodyWithValidationHandler struct { body OrderBody } +// OrderBody : struct that represent the body used by bodyWithValidationHandler type OrderBody struct { // The required tag validates that the value is not the data types default zero value // For numbers ensures value is not zero. For strings ensures value is not "". @@ -34,9 +35,11 @@ type OrderBody struct { Quantity int `json:"quantity"` } +// Validate is an implementation of middleware.BodyValidation. If user UserBody is given to +// middleware.Body, this function was called automatically and error handled func (b *OrderBody) Validate() error { if b.Quantity < 5 || b.Quantity > 10 { - return errors.Err("quantity must be between 5 & 10").WithStatus(http.StatusBadRequest) + return errors.BadRequest("invalid_quantity", "quantity must be between 5 & 10") } return nil diff --git a/examples/3-params/main.go b/examples/3-params/main.go index 9560101..ed236d4 100644 --- a/examples/3-params/main.go +++ b/examples/3-params/main.go @@ -19,10 +19,10 @@ func main() { server.AddHandlerFactory(r, "GET", "/path-params/{first}/{second}", internal.NewPathParamsHandler) server.AddHandlerFactory(r, "GET", "/query-params", internal.NewQueryParamsHandler) - gLog.Info(ctx, "Starting http server") + gLog.Info(ctx).LogMsg("Starting http server") if err := server.ServeAndHandleShutdown(r); err != nil { - gLog.Emergency(ctx, err.Error()) + gLog.Error(ctx).LogError(err) } - gLog.Info(ctx, "Server stopped") + gLog.Info(ctx).LogMsg("Server stopped") } diff --git a/examples/4-doc-openapi/go.mod b/examples/4-doc-openapi/go.mod index a857d87..658c4a7 100644 --- a/examples/4-doc-openapi/go.mod +++ b/examples/4-doc-openapi/go.mod @@ -18,3 +18,5 @@ require ( go.uber.org/zap v1.24.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace github.com/mwm-io/gapi => ../../ diff --git a/examples/4-doc-openapi/go.sum b/examples/4-doc-openapi/go.sum index b3f536f..bae42e6 100644 --- a/examples/4-doc-openapi/go.sum +++ b/examples/4-doc-openapi/go.sum @@ -15,8 +15,6 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/iancoleman/orderedmap v0.2.0 h1:sq1N/TFpYH++aViPcaKjys3bDClUEU7s5B+z6jq8pNA= -github.com/mwm-io/gapi v0.2.0 h1:jhpCAHIZkuftPk5nIOISag2nC4Eq4FwjzwSHYIlSib4= -github.com/mwm-io/gapi v0.2.0/go.mod h1:gPTxM9Fhgn7m3+5angXq8+63tTToHbYb9JDTibJJYek= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/examples/4-doc-openapi/internal/user.go b/examples/4-doc-openapi/internal/user.go index 23180fa..4171a7d 100644 --- a/examples/4-doc-openapi/internal/user.go +++ b/examples/4-doc-openapi/internal/user.go @@ -1,8 +1,6 @@ package internal import ( - "fmt" - "net/http" "strings" "sync" @@ -28,7 +26,7 @@ type UserBody struct { // middleware.Body, this function was called automatically and error handled func (u UserBody) Validate() error { if u.Name == "" { - return errors.Err("name required").WithStatus(http.StatusPreconditionFailed) + return errors.PreconditionFailed("missing_name", "name required") } return nil @@ -64,6 +62,7 @@ var users = []User{ }, } +// GetByID returns the user with the given ID, or an error if not found. func GetByID(id int) (User, error) { usersMu.RLock() defer usersMu.RUnlock() @@ -74,11 +73,10 @@ func GetByID(id int) (User, error) { } } - return User{}, errors.Err(fmt.Sprintf("user not found for id %d", id)). - WithKind("not_found"). - WithStatus(http.StatusNotFound) + return User{}, errors.NotFound("user_not_found", "user not found for id %d", id) } +// Search returns the users whose name contains the given string. func Search(name string) ([]User, error) { usersMu.RLock() defer usersMu.RUnlock() @@ -93,6 +91,7 @@ func Search(name string) ([]User, error) { return results, nil } +// Save saves the given user, assigning it a new ID if it is a new user. func Save(user User) (User, error) { usersMu.Lock() defer usersMu.Unlock() @@ -120,6 +119,7 @@ func Save(user User) (User, error) { return user, nil } +// Delete deletes the user with the given ID, or returns an error if not found. func Delete(id int) error { usersMu.RLock() defer usersMu.RUnlock() @@ -132,7 +132,5 @@ func Delete(id int) error { } } - return errors.Err(fmt.Sprintf("user not found for id %d", id)). - WithKind("not_found"). - WithStatus(http.StatusNotFound) + return errors.NotFound("user_not_found", "user not found for id %d", id) } diff --git a/examples/4-doc-openapi/main.go b/examples/4-doc-openapi/main.go index e9dc14f..ccce396 100644 --- a/examples/4-doc-openapi/main.go +++ b/examples/4-doc-openapi/main.go @@ -30,14 +30,13 @@ func main() { // server.AddDocHandlers add handler to expose API documentation. // Go to http://localhost:8080 to see the result if err := server.AddDocHandlers(r); err != nil { - gLog.Error(ctx, err.Error()) + gLog.Error(ctx).LogError(err) } - gLog.Info(ctx, "Starting http server") - + gLog.Info(ctx).LogMsg("Starting http server") if err := server.ServeAndHandleShutdown(r); err != nil { - gLog.Emergency(ctx, err.Error()) + gLog.Error(ctx).LogError(err) } - gLog.Info(ctx, "Server stopped") + gLog.Info(ctx).LogMsg("Server stopped") } diff --git a/handler/middleware.go b/handler/middleware.go index d144299..b7f4d89 100644 --- a/handler/middleware.go +++ b/handler/middleware.go @@ -31,6 +31,7 @@ func (p WithMiddlewares) Middlewares() []Middleware { // ByWeight sorts a list of Middleware by Weight type ByWeight []Middleware +// Len returns the length of the list func (b ByWeight) Len() int { return len(b) } diff --git a/log/client.go b/log/client.go index 4607dac..0be9cc2 100644 --- a/log/client.go +++ b/log/client.go @@ -34,58 +34,66 @@ func (l *Log) LogMsg(format string, a ...any) { } // LogError take a GAPI error, format error message and log it -func (l *Log) LogError(err errors.Error) { +func (l *Log) LogError(err error) { + castedErr, ok := err.(errors.Error) + if !ok { + l.LogMsg(err.Error()) + return + } + l.With( - zap.String("kind", err.Kind()), - zap.String("callstack", err.Callstack()), - ).LogMsg(err.Error()) + zap.String("kind", castedErr.Kind()), + zap.Strings("callstack", castedErr.Callstack()), + zap.String("caller", castedErr.Caller()), + zap.String("caller_name", castedErr.CallerName()), + ).LogMsg(castedErr.Error()) } // Debug logs a debug message with additional zap.Field -func Debug(ctx context.Context) Log { - return Log{ +func Debug(ctx context.Context) *Log { + return &Log{ f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Debug, } } // Info logs an info message with additional zap.Field -func Info(ctx context.Context) Log { - return Log{ +func Info(ctx context.Context) *Log { + return &Log{ f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Info, } } // Warn logs a warning message with additional zap.Field -func Warn(ctx context.Context) Log { - return Log{ +func Warn(ctx context.Context) *Log { + return &Log{ f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Warn, } } // Error logs an error message with additional zap.Field -func Error(ctx context.Context) Log { - return Log{ +func Error(ctx context.Context) *Log { + return &Log{ f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Error, } } // Critical logs a critical message with additional zap.Field -func Critical(ctx context.Context) Log { - return Log{ +func Critical(ctx context.Context) *Log { + return &Log{ f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Panic, } } // Alert logs an alert message with additional zap.Field -func Alert(ctx context.Context) Log { - return Log{ +func Alert(ctx context.Context) *Log { + return &Log{ f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Panic, } } // Emergency logs an emergency message with additional zap.Field -func Emergency(ctx context.Context) Log { - return Log{ +func Emergency(ctx context.Context) *Log { + return &Log{ f: Logger(ctx).WithOptions(zap.AddCallerSkip(1)).Fatal, } } diff --git a/log/doc.go b/log/doc.go index 22252bf..fabec57 100644 --- a/log/doc.go +++ b/log/doc.go @@ -8,14 +8,14 @@ This global logger can be overridden with your own zap.Logger ### Public methods -Gapi logging expose a list of public methods to print logs for various logging levels: -- log.Debug(msg string, fields ...zap.Field) -- log.Info(msg string, fields ...zap.Field) -- log.Warn(msg string, fields ...zap.Field) -- log.Error(msg string, fields ...zap.Field) -- log.Critical(msg string, fields ...zap.Field) -- log.Alert(msg string, fields ...zap.Field) -- log.Emergency(msg string, fields ...zap.Field) +Gapi logging expose a list of public methods to get logger with various logging levels: +- log.Debug(ctx context.Context) +- log.Info(ctx context.Context) +- log.Warn(ctx context.Context) +- log.Error(ctx context.Context) +- log.Critical(ctx context.Context) +- log.Alert(ctx context.Context) +- log.Emergency(ctx context.Context) import ( "github.com/mwm-io/gapi/handler" @@ -28,12 +28,12 @@ Gapi logging expose a list of public methods to print logs for various logging l server.AddHandler(r, "GET", "/", handler.Func(HelloWorldHandler)) - gLog.Info("Starting http server") + gLog.Info(context.Background()).LogMsg("Starting http server") if err := server.ServeAndHandleShutdown(r); err != nil { - gLog.Error(err) + gLog.Error(context.Background()).LogError(err) } - gLog.Info("Server stopped") + gLog.Info(context.Background()).LogMsg("Server stopped") } ### Global instance diff --git a/log/encoder.go b/log/encoder.go new file mode 100644 index 0000000..8d01174 --- /dev/null +++ b/log/encoder.go @@ -0,0 +1,8 @@ +package log + +// TODO : +// Example of how to register custom encoder +// https://github.com/evanj/gcplogs/blob/31e8f302525c3552589072938ab4b2dc885334a7/gcpzap/gcpzap.go#L21 +// How to partial override encoder +// https://github.com/evanj/gcplogs/blob/master/gcpzap/encoder.go#L39 +// TODO 2 : override stracktrace if the logged element is a GAPI error diff --git a/middleware/body_decoder.go b/middleware/body_decoder.go index 9e0ffd2..d76eb70 100644 --- a/middleware/body_decoder.go +++ b/middleware/body_decoder.go @@ -87,12 +87,13 @@ func (m BodyDecoder) Wrap(h handler.Handler) handler.Handler { } if reflect.ValueOf(m.BodyPtr).Kind() != reflect.Ptr { - return nil, errors.Err("BodyPtr must be a pointer") + return nil, errors.Err("invalid_body_receiver", "BodyPtr must be a pointer") } unmarshaler, err := m.resolveContentType(r) if err != nil { - return nil, errors.Wrap(err).WithMessage("unable to resolve content type").WithStatus(http.StatusBadRequest) + return nil, errors.BadRequest("invalid_content_type", "unable to resolve content type"). + WithError(err) } var buffer bytes.Buffer @@ -104,11 +105,13 @@ func (m BodyDecoder) Wrap(h handler.Handler) handler.Handler { body, err := io.ReadAll(reader) if err != nil { - return nil, errors.Wrap(err).WithMessage("failed to read body").WithStatus(http.StatusBadRequest) + return nil, errors.BadRequest("body_error", "failed to read body"). + WithError(err) } if errUnmarshal := unmarshaler.Unmarshal(body, m.BodyPtr); errUnmarshal != nil { - return nil, errors.Wrap(errUnmarshal).WithMessage("failed to unmarshal body").WithStatus(http.StatusBadRequest) + return nil, errors.BadRequest("invalid_body_format", "failed to decode body"). + WithError(errUnmarshal) } r.Body = io.NopCloser(bytes.NewReader(buffer.Bytes())) @@ -132,10 +135,10 @@ func (m BodyDecoder) Wrap(h handler.Handler) handler.Handler { fieldName := typeOfFieldI.Name switch jsonTag := typeOfFieldI.Tag.Get("json"); jsonTag { case "-": - return nil, errors.Err("cannot have an omitted field required").WithStatus(http.StatusBadRequest) + return nil, errors.InternalServerError("invalid_config", "field '%s' is required but json tag value is '-'", fieldName) case "": - return nil, errors.Err("field %s is required", fieldName).WithStatus(http.StatusBadRequest) + return nil, errors.BadRequest("missing_param", "field %s is required", fieldName) default: parts := strings.Split(jsonTag, ",") @@ -144,14 +147,18 @@ func (m BodyDecoder) Wrap(h handler.Handler) handler.Handler { name = fieldName } - return nil, errors.Err("field %s is required", name).WithStatus(http.StatusBadRequest) + return nil, errors.BadRequest("missing_param", "field %s is required", name) } } } if v, ok := m.BodyPtr.(BodyValidation); !m.SkipValidation && ok { if errValidate := v.Validate(); errValidate != nil { - return nil, errors.Wrap(errValidate).WithStatus(http.StatusBadRequest) + if castedErr, casted := errValidate.(errors.Error); casted { + return nil, castedErr + } + + return nil, errors.BadRequest("invalid_body", errValidate.Error()) } } @@ -176,7 +183,7 @@ func (m BodyDecoder) resolveContentType(r *http.Request) (Decoder, error) { if m.ForcedContentType != "" { result, ok := m.Decoders[m.ForcedContentType] if !ok { - return nil, errors.Err("no content decoder found for content type %s", m.ForcedContentType) + return nil, errors.UnsupportedMediaType("unsupported_content_type", "no decoder found for content type %s", m.ForcedContentType) } return result, nil @@ -189,7 +196,8 @@ func (m BodyDecoder) resolveContentType(r *http.Request) (Decoder, error) { wantedType, _, errContent := mime.ParseMediaType(contentType) if errContent != nil { - return nil, errors.Wrap(errContent).WithMessage("unknown content-type %s", contentType) + return nil, errors.UnsupportedMediaType("unsupported_content_type", "unknown content-type %s", contentType). + WithError(errContent) } if wantedType == "" { @@ -198,7 +206,7 @@ func (m BodyDecoder) resolveContentType(r *http.Request) (Decoder, error) { result, ok := m.Decoders[wantedType] if !ok || result == nil { - return nil, errors.Err("unsupported content-type %s", wantedType) + return nil, errors.UnsupportedMediaType("unsupported_content_type", "unsupported content-type %s", wantedType) } return result, nil diff --git a/middleware/log.go b/middleware/log.go index 37eba67..58084fb 100644 --- a/middleware/log.go +++ b/middleware/log.go @@ -23,7 +23,7 @@ func (m Log) Wrap(h handler.Handler) handler.Handler { resp, err := h.Serve(w, r) if err != nil { - gLog.Error(r.Context(), err.Error()) + gLog.Error(r.Context()).LogError(err) } return resp, err diff --git a/middleware/path_parameters.go b/middleware/path_parameters.go index 5b10446..f1f6384 100644 --- a/middleware/path_parameters.go +++ b/middleware/path_parameters.go @@ -31,7 +31,7 @@ func (m PathParameters) Wrap(h handler.Handler) handler.Handler { pathParam := typeOfParameters.Field(i).Tag.Get("path") val, ok := mux.Vars(r)[pathParam] if !ok { - return nil, errors.Err("unknown path params").WithStatus(http.StatusInternalServerError) + return nil, errors.Err("invalid_param_type", "unknown path params").WithStatus(http.StatusInternalServerError) } field := v.FieldByName(typeOfParameters.Field(i).Name) @@ -39,14 +39,16 @@ func (m PathParameters) Wrap(h handler.Handler) handler.Handler { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: x, err := strconv.ParseInt(val, 10, 64) if err != nil { - return nil, errors.Wrap(err).WithMessage("%s must be a number", typeOfParameters.Field(i).Name).WithStatus(http.StatusBadRequest) + return nil, errors.BadRequest("invalid_param_type", "%s must be a number", typeOfParameters.Field(i).Name). + WithError(err) } field.SetInt(x) case reflect.Float64, reflect.Float32: x, err := strconv.ParseFloat(val, 64) if err != nil { - return nil, errors.Wrap(err).WithMessage("%s must be a float", typeOfParameters.Field(i).Name).WithStatus(http.StatusBadRequest) + return nil, errors.BadRequest("invalid_param_type", "%s must be a float", typeOfParameters.Field(i).Name). + WithError(err) } field.SetFloat(x) @@ -57,13 +59,13 @@ func (m PathParameters) Wrap(h handler.Handler) handler.Handler { if reflect.TypeOf(i) == reflect.TypeOf([]byte(nil)) { field.SetBytes([]byte(val)) } else { - return nil, errors.Err("cannot have a slice in parameters").WithStatus(http.StatusBadRequest) + return nil, errors.PreconditionFailed("invalid_param_type", "path params can't be a slice. Caused by parameter '%s'", pathParam) } case reflect.String: field.SetString(val) default: - return nil, errors.Err("cannot have a parameter with %q type", field.Kind().String()).WithStatus(http.StatusBadRequest) + return nil, errors.PreconditionFailed("invalid_param_type", "type %q isn't supported. Caused by parameter '%s'", pathParam, field.Kind().String()).WithStatus(http.StatusBadRequest) } } diff --git a/middleware/query_parameters.go b/middleware/query_parameters.go index 01f816d..f04d309 100644 --- a/middleware/query_parameters.go +++ b/middleware/query_parameters.go @@ -28,7 +28,8 @@ func (m QueryParameters) Wrap(h handler.Handler) handler.Handler { decoder.SetAliasTag("query") err := decoder.Decode(m.Parameters, r.URL.Query()) if err != nil { - return nil, errors.Wrap(err).WithMessage("decoder.Decode() failed") + return nil, errors.UnprocessableEntity("query_params_encoding", "failed to decode query params"). + WithError(err) } return h.Serve(w, r) diff --git a/middleware/recover.go b/middleware/recover.go index 308af39..122ad61 100644 --- a/middleware/recover.go +++ b/middleware/recover.go @@ -1,7 +1,6 @@ package middleware import ( - "fmt" "net/http" "github.com/mwm-io/gapi/errors" @@ -16,7 +15,7 @@ func (r Recover) Wrap(h handler.Handler) handler.Handler { return handler.Func(func(w http.ResponseWriter, r *http.Request) (result interface{}, err error) { defer func() { if rec := recover(); rec != nil { - err = errors.Err(fmt.Sprintf("Panic: %v", rec)).WithStatus(http.StatusInternalServerError) + err = errors.InternalServerError("panic", "Panic: %v", rec) } }() diff --git a/middleware/response_writer.go b/middleware/response_writer.go index 1ca51ad..4182cca 100644 --- a/middleware/response_writer.go +++ b/middleware/response_writer.go @@ -1,7 +1,6 @@ package middleware import ( - "fmt" "io" "net/http" @@ -175,7 +174,7 @@ func (m ResponseWriter) resolveContentType(r *http.Request) (string, Encoder, er if m.ForcedContentType != "" { encoder, ok := m.Encoders[m.ForcedContentType] if !ok { - return "", nil, errors.Err(fmt.Sprintf("no content encoder found for content type %s", m.ForcedContentType)) + return "", nil, errors.Err("missing_encoder", "no content encoder found for content type %s", m.ForcedContentType) } return m.ForcedContentType, encoder, nil @@ -193,7 +192,7 @@ func (m ResponseWriter) resolveContentType(r *http.Request) (string, Encoder, er for mediaType := range m.Encoders { parsedMediaType, err := contenttype.ParseMediaType(mediaType) if err != nil { - return "", nil, errors.Wrap(err).WithMessage(fmt.Sprintf("invalid mediaType %s", mediaType)).WithStatus(http.StatusInternalServerError) + return "", nil, errors.UnsupportedMediaType("unsupported_content_type", "invalid mediaType %s", mediaType).WithError(err) } availableTypes = append(availableTypes, parsedMediaType) @@ -201,12 +200,12 @@ func (m ResponseWriter) resolveContentType(r *http.Request) (string, Encoder, er accepted, _, err := contenttype.GetAcceptableMediaType(r, availableTypes) if err != nil { - return "", nil, errors.Wrap(err).WithMessage(fmt.Sprintf("no content-type found to match the accept header %s", r.Header.Get("Accept"))).WithStatus(http.StatusUnsupportedMediaType) + return "", nil, errors.UnsupportedMediaType("unsupported_content_type", "no content-type found to match the accept header %s", r.Header.Get("Accept")).WithError(err) } encoder, ok := m.Encoders[accepted.String()] if !ok { - return "", nil, errors.Err(fmt.Sprintf("no content encoder found for content type %s", accepted.String())) + return "", nil, errors.ExpectationFailed("unsupported_accepted_response_types", "no content encoder found for content type %s", accepted.String()) } return accepted.String(), encoder, nil diff --git a/openapi/builder.go b/openapi/builder.go index 9c3b2c3..27ad910 100644 --- a/openapi/builder.go +++ b/openapi/builder.go @@ -216,7 +216,7 @@ func (b *DocBuilder) WithError(statusCode int, kind, message string, options ... c.applyOptions(options...) - exampleValue := errors.Err(message).WithKind(kind).WithStatus(statusCode) + exampleValue := errors.Err(kind, message).WithStatus(statusCode) if err := b.reflector.SetJSONResponse(b.operation, exampleValue, statusCode); err != nil { b.err = append(b.err, err) diff --git a/server/option.go b/server/option.go index 46a5877..1d82385 100644 --- a/server/option.go +++ b/server/option.go @@ -91,6 +91,7 @@ func newOptions(opts ...Option) serverOptions { return c } +// Addr returns the server address to listen to. func (c serverOptions) Addr() string { if c.port != "" { return fmt.Sprintf(":%s", c.port) @@ -99,6 +100,7 @@ func (c serverOptions) Addr() string { return fmt.Sprintf(":%s", config.PORT) } +// AddrHttps returns the server address to listen to. func (c serverOptions) AddrHttps() string { if c.port != "" { return fmt.Sprintf(":%s", c.port) @@ -107,6 +109,7 @@ func (c serverOptions) AddrHttps() string { return fmt.Sprintf(":%s", config.PORT) } +// CORS returns the CORS configuration. func (c serverOptions) CORS() CORS { if c.cors != nil { return *c.cors @@ -119,6 +122,7 @@ func (c serverOptions) CORS() CORS { } } +// StopTimeout returns the time to wait for before shutting down the server. func (c serverOptions) StopTimeout() time.Duration { if c.stopTimeout != nil { return *c.stopTimeout @@ -127,6 +131,7 @@ func (c serverOptions) StopTimeout() time.Duration { return 3 * time.Second } +// StopSignals returns the signals to listen to for shutting down the server. func (c serverOptions) StopSignals() []os.Signal { if len(c.stopSignals) != 0 { return c.stopSignals @@ -135,10 +140,12 @@ func (c serverOptions) StopSignals() []os.Signal { return []os.Signal{os.Interrupt, syscall.SIGINT, syscall.SIGTERM} } +// StrictSlash returns the strictSlash configuration. func (c serverOptions) StrictSlash() bool { return !c.withoutStrictSlash } +// Context returns the parent context for the *http.Server. func (c serverOptions) Context() context.Context { if c.context != nil { return c.context From e145cfef9d6b899955c15a32b005db855436fbd7 Mon Sep 17 00:00:00 2001 From: danysousa Date: Thu, 6 Jul 2023 10:40:25 +0200 Subject: [PATCH 3/5] [errors] don't override error message in WithError function if already filled --- errors/error.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/errors/error.go b/errors/error.go index a158c95..16ea11d 100644 --- a/errors/error.go +++ b/errors/error.go @@ -150,7 +150,9 @@ func (e *FullError) WithKind(kind string) Error { // WithError wrap source error. func (e *FullError) WithError(err error) Error { e.sourceErr = err - e.errorMessage = err.Error() + if e.errorMessage == "" { + e.errorMessage = err.Error() + } return e } From 92966c4031d6be3db4048e7184db10beabd3725c Mon Sep 17 00:00:00 2001 From: danysousa Date: Thu, 6 Jul 2023 10:40:58 +0200 Subject: [PATCH 4/5] [log] add original_error field in LogErr --- log/client.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/log/client.go b/log/client.go index 0be9cc2..b996963 100644 --- a/log/client.go +++ b/log/client.go @@ -46,6 +46,8 @@ func (l *Log) LogError(err error) { zap.Strings("callstack", castedErr.Callstack()), zap.String("caller", castedErr.Caller()), zap.String("caller_name", castedErr.CallerName()), + zap.String("original_error", castedErr.Unwrap().Error()), + // TODO : if original_error is a GAPI error, log with call original callstack ).LogMsg(castedErr.Error()) } From 964dbc3fa5de28611258e0c9b66fdaa96b7b59a8 Mon Sep 17 00:00:00 2001 From: danysousa Date: Thu, 6 Jul 2023 14:03:55 +0200 Subject: [PATCH 5/5] [errors] clean code & fix callerName --- errors/callstack.go | 2 +- errors/error.go | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/errors/callstack.go b/errors/callstack.go index 9834727..9df7b37 100644 --- a/errors/callstack.go +++ b/errors/callstack.go @@ -43,7 +43,7 @@ func GetCallers() (callerName, caller string, callStack []string) { if firstFrame { caller = formatFrame(frame) - callerName = frame.Function + callerName = frame.Func.Name() firstFrame = false } else { callStack = append(callStack, formatFrame(frame)) diff --git a/errors/error.go b/errors/error.go index 16ea11d..66626f6 100644 --- a/errors/error.go +++ b/errors/error.go @@ -175,10 +175,6 @@ func (e *FullError) Callstack() []string { return e.callstack } -// CallerName() string -// Caller() string -// Callstack() []string - // HttpError is used to json.Marshal or xml.Marshal FullError. // You can use it to decode an incoming error. type HttpError struct {