From 56aa3de645b52f43757d5777610047e8c5f53324 Mon Sep 17 00:00:00 2001 From: kataras Date: Sun, 16 Jul 2017 13:58:10 +0300 Subject: [PATCH] Update to 8.0.3 | Request logger improvements, error handlers improvements. Read HISTORY.md Former-commit-id: fb5eca0dc955d8c07fdba35b47068008565dbbd1 --- HISTORY.md | 26 ++++- README.md | 4 +- _examples/README.md | 16 +-- _examples/http_request/request-logger/main.go | 31 ++--- .../request-logger-file/main.go | 109 ++++++++++++++++++ core/router/api_builder.go | 57 ++++++++- core/router/status.go | 19 +-- doc.go | 2 +- iris.go | 2 +- middleware/logger/config.go | 61 +++++++++- middleware/logger/logger.go | 73 ++++++++---- 11 files changed, 339 insertions(+), 61 deletions(-) create mode 100644 _examples/http_request/request-logger/request-logger-file/main.go diff --git a/HISTORY.md b/HISTORY.md index c3c3d00c..3ce99ed9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -17,7 +17,31 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`. -# Su, 15 July 2017 | v8.0.2 +# Su, 16 July 2017 | v8.0.3 + +No API changes. + +Relative issues: + +- https://github.com/kataras/iris/issues/674 +- https://github.com/kataras/iris/issues/675 +- https://github.com/kataras/iris/issues/676 + +### HTTP Errors + +Able to register a chain of Handlers (and middleware with `ctx.Next()` support like routes) for a specific error code, read more at [issues/674](https://github.com/kataras/iris/issues/674). Usage example can be found at [_examples/http_request/request-logger/main.go#L37](https://github.com/kataras/iris/blob/master/_examples/http_request/request-logger/main.go#L37). + + +New function to register a Handler or a chain of Handlers for all official http error codes, by calling the new `app.OnAnyErrorCode(func(ctx context.Context){})`, read more at [issues/675](https://github.com/kataras/iris/issues/675). Usage example can be found at [_examples/http_request/request-logger/main.go#L42](https://github.com/kataras/iris/blob/master/_examples/http_request/request-logger/main.go#L42). + +### Request Logger + +Add `Configuration#LogFunc` and `Configuration#Columns` fields, read more at [issues/676](https://github.com/kataras/iris/issues/676). Example can be found at [_examples/http_request/request-logger/request-logger-file](https://github.com/kataras/iris/tree/master/_examples/http_request/request-logger/request-logger-file). + + +Have fun and don't forget to [star](https://github.com/kataras/iris/stargazers) the github repository, it gives me power to continue publishing my work! + +# Sa, 15 July 2017 | v8.0.2 Okay my friends, this is a good time to upgrade, I did implement a feature that you were asking many times at the past. diff --git a/README.md b/README.md index 54726c95..c463b714 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ These types of projects need heart and sacrifices to continue offer the best dev * [Installation](#-installation) -* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#su-15-july-2017--v802) +* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#su-16-july-2017--v803) * [Learn](#-learn) * [HTTP Listening](_examples/#http-listening) * [Configuration](_examples/#configuration) @@ -262,7 +262,7 @@ _iris_ does not force you to use any specific ORM. With support for the most pop ### 📌 Version -Current: **8.0.2** +Current: **8.0.3** Each new release is pushed to the master. It stays there until the next version. When a next version is released then the previous version goes to its own branch with `gopkg.in` as its import path (and its own vendor folder), in order to keep it working "for-ever". diff --git a/_examples/README.md b/_examples/README.md index 263d903d..92fc4810 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -11,7 +11,8 @@ It doesn't always contain the "best ways" but it does cover each important featu - [Hello world!](hello-world/main.go) - [Glimpse](overview/main.go) - [Tutorial: Online Visitors](tutorial/online-visitors/main.go) -- [Tutorial: URL Shortener using BoltDB](tutorial/url-shortener/main.go) +- [Tutorial: URL Shortener using BoltDB](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7) +- [Tutorial: How to turn your Android Device into a fully featured Web Server](https://medium.com/@kataras/how-to-turn-an-android-device-into-a-web-server-9816b28ab199) ### HTTP Listening @@ -24,7 +25,7 @@ It doesn't always contain the "best ways" but it does cover each important featu * [common net.Listener](http-listening/custom-listener/main.go) * [SO_REUSEPORT for unix systems](http-listening/custom-listener/unix-reuseport/main.go) - Custom HTTP Server - * [iris way](http-listening/custom-httpserver/easy-way/main.go) + * [easy way](http-listening/custom-httpserver/easy-way/main.go) * [std way](http-listening/custom-httpserver/std-way/main.go) * [multi server instances](http-listening/custom-httpserver/multi/main.go) - Graceful Shutdown @@ -81,12 +82,12 @@ Navigate through examples for a better understanding. - [Basic](routing/basic/main.go) - [Custom HTTP Errors](routing/http-errors/main.go) - [Dynamic Path](routing/dynamic-path/main.go) - * [Root Level Wildcard Path](routing/dynamic-path/root-wildcard/main.go) + * [root level wildcard path](routing/dynamic-path/root-wildcard/main.go) - [Reverse routing](routing/reverse/main.go) - [Custom wrapper](routing/custom-wrapper/main.go) - Custom Context - * [Method Overriding](routing/custom-context/method-overriding/main.go) - * [New Implementation](routing/custom-context/new-implementation/main.go) + * [method overriding](routing/custom-context/method-overriding/main.go) + * [new implementation](routing/custom-context/new-implementation/main.go) - [Route State](routing/route-state/main.go) ### Subdomains @@ -134,8 +135,8 @@ Navigate through examples for a better understanding. - [Embedding Files Into App Executable File](file-server/embedding-files-into-app/main.go) - [Send/Force-Download Files](file-server/send-files/main.go) - Single Page Applications - * [Single Page Application](file-server/single-page-application/basic/main.go) - * [Embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go) + * [single Page Application](file-server/single-page-application/basic/main.go) + * [embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go) ### How to Read from `context.Request() *http.Request` @@ -156,6 +157,7 @@ Navigate through examples for a better understanding. ### Miscellaneous - [Request Logger](http_request/request-logger/main.go) + * [log requests to a file](http_request/request-logger/request-logger-file/main.go) - [Localization and Internationalization](miscellaneous/i18n/main.go) - [Recovery](miscellaneous/recover/main.go) - [Profiling (pprof)](miscellaneous/pprof/main.go) diff --git a/_examples/http_request/request-logger/main.go b/_examples/http_request/request-logger/main.go index 3a7a8197..8f9585d2 100644 --- a/_examples/http_request/request-logger/main.go +++ b/_examples/http_request/request-logger/main.go @@ -22,24 +22,25 @@ func main() { app.Use(customLogger) - app.Get("/", func(ctx context.Context) { - ctx.Writef("hello") - }) + h := func(ctx context.Context) { + ctx.Writef("Hello from %s", ctx.Path()) + } + app.Get("/", h) - app.Get("/1", func(ctx context.Context) { - ctx.Writef("hello") - }) + app.Get("/1", h) - app.Get("/2", func(ctx context.Context) { - ctx.Writef("hello") - }) + app.Get("/2", h) - // log http errors should be done manually - errorLogger := logger.New() - - app.OnErrorCode(iris.StatusNotFound, func(ctx context.Context) { - errorLogger(ctx) - ctx.Writef("My Custom 404 error page ") + // http errors have their own handlers, therefore + // registering a middleare should be done manually. + /* + app.OnErrorCode(404 ,customLogger, func(ctx context.Context) { + ctx.Writef("My Custom 404 error page ") + }) + */ + // or catch all http errors: + app.OnAnyErrorCode(customLogger, func(ctx context.Context) { + ctx.Writef("My Custom error page") }) // http://localhost:8080 diff --git a/_examples/http_request/request-logger/request-logger-file/main.go b/_examples/http_request/request-logger/request-logger-file/main.go new file mode 100644 index 00000000..5edfd11b --- /dev/null +++ b/_examples/http_request/request-logger/request-logger-file/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "os" + "strings" + "time" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/middleware/logger" +) + +const deleteFileOnExit = true + +func main() { + app := iris.New() + r, close := newRequestLogger() + defer close() + + app.Use(r) + app.OnAnyErrorCode(r, func(ctx context.Context) { + ctx.HTML("

Error: Please try this instead.

") + }) + + h := func(ctx context.Context) { + ctx.Writef("Hello from %s", ctx.Path()) + } + + app.Get("/", h) + + app.Get("/1", h) + + app.Get("/2", h) + + // http://localhost:8080 + // http://localhost:8080/1 + // http://localhost:8080/2 + // http://lcoalhost:8080/notfoundhere + app.Run(iris.Addr(":8080")) +} + +// get a filename based on the date, file logs works that way the most times +// but these are just a sugar. +func todayFilename() string { + today := time.Now().Format("Jan 02 2006") + return today + ".txt" +} + +func newLogFile() *os.File { + filename := todayFilename() + // open an output file, this will append to the today's file if server restarted. + f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + panic(err) + } + + return f +} + +var excludeExtensions = [...]string{ + ".js", + ".css", + ".jpg", + ".png", + ".ico", + ".svg", +} + +func newRequestLogger() (h context.Handler, close func() error) { + close = func() error { return nil } + + c := logger.Config{ + Status: true, + IP: true, + Method: true, + Path: true, + Columns: true, + } + + logFile := newLogFile() + close = func() error { + err := logFile.Close() + if deleteFileOnExit { + err = os.Remove(logFile.Name()) + } + return err + } + + c.LogFunc = func(now time.Time, latency time.Duration, status, ip, method, path string) { + output := logger.Columnize(now.Format("2006/01/02 - 15:04:05"), latency, status, ip, method, path) + logFile.Write([]byte(output)) + } + + // we don't want to use the logger + // to log requests to assets and etc + c.AddSkipper(func(ctx context.Context) bool { + path := ctx.Path() + for _, ext := range excludeExtensions { + if strings.HasSuffix(path, ext) { + return true + } + } + return false + }) + + h = logger.New(c) + + return +} diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 1eaf819b..74569644 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -620,8 +620,61 @@ func (rb *APIBuilder) StaticWeb(requestPath string, systemPath string) *Route { // the body if recorder was enabled // and/or disable the gzip if gzip response recorder // was active. -func (rb *APIBuilder) OnErrorCode(statusCode int, handler context.Handler) { - rb.errorCodeHandlers.Register(statusCode, handler) +func (rb *APIBuilder) OnErrorCode(statusCode int, handlers ...context.Handler) { + rb.errorCodeHandlers.Register(statusCode, handlers...) +} + +// OnAnyErrorCode registers a handler which called when error status code written. +// Same as `OnErrorCode` but registers all http error codes. +// See: http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml +func (rb *APIBuilder) OnAnyErrorCode(handlers ...context.Handler) { + // we could register all >=400 and <=511 but this way + // could override custom status codes that iris developers can register for their + // web apps whenever needed. + // There fore these are the hard coded http error statuses: + var errStatusCodes = []int{ + http.StatusBadRequest, + http.StatusUnauthorized, + http.StatusPaymentRequired, + http.StatusForbidden, + http.StatusNotFound, + http.StatusMethodNotAllowed, + http.StatusNotAcceptable, + http.StatusProxyAuthRequired, + http.StatusRequestTimeout, + http.StatusConflict, + http.StatusGone, + http.StatusLengthRequired, + http.StatusPreconditionFailed, + http.StatusRequestEntityTooLarge, + http.StatusRequestURITooLong, + http.StatusUnsupportedMediaType, + http.StatusRequestedRangeNotSatisfiable, + http.StatusExpectationFailed, + http.StatusTeapot, + http.StatusUnprocessableEntity, + http.StatusLocked, + http.StatusFailedDependency, + http.StatusUpgradeRequired, + http.StatusPreconditionRequired, + http.StatusTooManyRequests, + http.StatusRequestHeaderFieldsTooLarge, + http.StatusUnavailableForLegalReasons, + http.StatusInternalServerError, + http.StatusNotImplemented, + http.StatusBadGateway, + http.StatusServiceUnavailable, + http.StatusGatewayTimeout, + http.StatusHTTPVersionNotSupported, + http.StatusVariantAlsoNegotiates, + http.StatusInsufficientStorage, + http.StatusLoopDetected, + http.StatusNotExtended, + http.StatusNetworkAuthenticationRequired} + + for _, statusCode := range errStatusCodes { + rb.OnErrorCode(statusCode, handlers...) + } } // FireErrorCode executes an error http status code handler diff --git a/core/router/status.go b/core/router/status.go index 8bfa4cee..0f7c5e99 100644 --- a/core/router/status.go +++ b/core/router/status.go @@ -11,7 +11,7 @@ import ( // of the list of all http error code handlers. type ErrorCodeHandler struct { StatusCode int - Handler context.Handler + Handlers context.Handlers mu sync.Mutex } @@ -41,12 +41,12 @@ func (ch *ErrorCodeHandler) Fire(ctx context.Context) { // i.e // users := app.Party("/users") // users.Done(func(ctx context.Context){ if ctx.StatusCode() == 400 { /* custom error code for /users */ }}) - ch.Handler(ctx) + ctx.Do(ch.Handlers) } -func (ch *ErrorCodeHandler) updateHandler(h context.Handler) { +func (ch *ErrorCodeHandler) updateHandlers(handlers context.Handlers) { ch.mu.Lock() - ch.Handler = h + ch.Handlers = handlers ch.mu.Unlock() } @@ -101,23 +101,24 @@ func (s *ErrorCodeHandlers) Get(statusCode int) *ErrorCodeHandler { // the body if recorder was enabled // and/or disable the gzip if gzip response recorder // was active. -func (s *ErrorCodeHandlers) Register(statusCode int, handler context.Handler) *ErrorCodeHandler { +func (s *ErrorCodeHandlers) Register(statusCode int, handlers ...context.Handler) *ErrorCodeHandler { if statusCode < 400 { return nil } h := s.Get(statusCode) if h == nil { + // create new and add it ch := &ErrorCodeHandler{ StatusCode: statusCode, - Handler: handler, + Handlers: handlers, } s.handlers = append(s.handlers, ch) - // create new and add it + return ch } - // otherwise update the handler - h.updateHandler(handler) + // otherwise update the handlers + h.updateHandlers(handlers) return h } diff --git a/doc.go b/doc.go index be0acdcd..310ccfcb 100644 --- a/doc.go +++ b/doc.go @@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub: Current Version -8.0.2 +8.0.3 Installation diff --git a/iris.go b/iris.go index 952f9cdc..7a700daa 100644 --- a/iris.go +++ b/iris.go @@ -33,7 +33,7 @@ import ( const ( // Version is the current version number of the Iris Web Framework. - Version = "8.0.2" + Version = "8.0.3" ) // HTTP status codes as registered with IANA. diff --git a/middleware/logger/config.go b/middleware/logger/config.go index c6592a6a..8a871e24 100644 --- a/middleware/logger/config.go +++ b/middleware/logger/config.go @@ -1,21 +1,78 @@ package logger +import ( + "time" + + "github.com/kataras/iris/context" +) + +// The SkipperFunc signature, used to serve the main request without logs. +// See `Configuration` too. +type SkipperFunc func(ctx context.Context) bool + // Config are the options of the logger middlweare // contains 4 bools // Status, IP, Method, Path // if set to true then these will print type Config struct { // Status displays status code (bool) + // + // Defaults to true Status bool // IP displays request's remote address (bool) + // + // Defaults to true IP bool // Method displays the http method (bool) + // + // Defaults to true Method bool // Path displays the request path (bool) + // + // Defaults to true Path bool + // Columns will display the logs as well formatted columns (bool) + // If custom `LogFunc` has been provided then this field is useless and users should + // use the `Columinize` function of the logger to get the ouput result as columns. + // + // Defaults to true + Columns bool + // LogFunc is the writer which logs are written to, + // if missing the logger middleware uses the app.Logger().Infof instead. + LogFunc func(now time.Time, latency time.Duration, status, ip, method, path string) + // Skippers used to skip the logging i.e by `ctx.Path()` and serve + // the next/main handler immediately. + Skippers []SkipperFunc + // the Skippers as one function in order to reduce the time needed to + // combine them at serve time. + skip SkipperFunc } -// DefaultConfiguration returns an options which all properties are true except EnableColors +// DefaultConfiguration returns a default configuration +// that have all boolean fields to true, +// LogFunc to nil, +// and Skippers to nil. func DefaultConfiguration() Config { - return Config{true, true, true, true} + return Config{true, true, true, true, true, nil, nil, nil} +} + +// AddSkipper adds a skipper to the configuration. +func (c *Config) AddSkipper(sk SkipperFunc) { + c.Skippers = append(c.Skippers, sk) + c.buildSkipper() +} + +func (c *Config) buildSkipper() { + if len(c.Skippers) == 0 { + return + } + skippersLocked := c.Skippers[0:] + c.skip = func(ctx context.Context) bool { + for _, s := range skippersLocked { + if s(ctx) { + return true + } + } + return false + } } diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index d90edb16..2d940301 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -2,25 +2,49 @@ package logger import ( + "fmt" "strconv" "time" "github.com/kataras/iris/context" + + "github.com/ryanuber/columnize" ) type requestLoggerMiddleware struct { config Config } +// New creates and returns a new request logger middleware. +// Do not confuse it with the framework's Logger. +// This is for the http requests. +// +// Receives an optional configuation. +func New(cfg ...Config) context.Handler { + c := DefaultConfiguration() + if len(cfg) > 0 { + c = cfg[0] + } + c.buildSkipper() + l := &requestLoggerMiddleware{config: c} + + return l.ServeHTTP +} + // Serve serves the middleware func (l *requestLoggerMiddleware) ServeHTTP(ctx context.Context) { + // skip logs and serve the main request immediately + if l.config.skip != nil { + if l.config.skip(ctx) { + ctx.Next() + return + } + } + //all except latency to string var status, ip, method, path string var latency time.Duration var startTime, endTime time.Time - path = ctx.Path() - method = ctx.Method() - startTime = time.Now() ctx.Next() @@ -36,29 +60,36 @@ func (l *requestLoggerMiddleware) ServeHTTP(ctx context.Context) { ip = ctx.RemoteAddr() } - if !l.config.Method { - method = "" + if l.config.Method { + method = ctx.Method() } - if !l.config.Path { - path = "" + if l.config.Path { + path = ctx.Path() } - //finally print the logs, no new line, the framework's logger is responsible how to render each log. - ctx.Application().Logger().Infof("%v %4v %s %s %s", status, latency, ip, method, path) + // print the logs + if logFunc := l.config.LogFunc; logFunc != nil { + logFunc(endTime, latency, status, ip, method, path) + return + } + endTimeFormatted := endTime.Format("2006/01/02 - 15:04:05") + if l.config.Columns { + output := Columnize(endTimeFormatted, latency, status, ip, method, path) + ctx.Application().Logger().Out.Write([]byte(output)) + return + } + // no new line, the framework's logger is responsible how to render each log. + ctx.Application().Logger().Infof("%s | %v %4v %s %s %s", endTimeFormatted, status, latency, ip, method, path) } -// New creates and returns a new request logger middleware. -// Do not confuse it with the framework's Logger. -// This is for the http requests. -// -// Receives an optional configuation. -func New(cfg ...Config) context.Handler { - c := DefaultConfiguration() - if len(cfg) > 0 { - c = cfg[0] +// Columnize formats the given arguments as columns and returns the formatted output, +// note that it appends a new line to the end. +func Columnize(nowFormatted string, latency time.Duration, status, ip, method, path string) string { + outputC := []string{ + "Time | Status | Latency | IP | Method | Path", + fmt.Sprintf("%s | %v | %4v | %s | %s | %s", nowFormatted, status, latency, ip, method, path), } - l := &requestLoggerMiddleware{config: c} - - return l.ServeHTTP + output := columnize.SimpleFormat(outputC) + "\n" + return output }