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
}