diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..28a804d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..62de7f0
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/mux.iml b/.idea/mux.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/mux.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..b4e00d0
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ direct
+
+
\ No newline at end of file
diff --git a/go.mod b/go.mod
index df170a3..e53cf82 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,5 @@
module github.com/gorilla/mux
go 1.12
+
+require github.com/stretchr/testify v1.4.0
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..e863f51
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,10 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/logger.go b/logger.go
new file mode 100644
index 0000000..0fe2a96
--- /dev/null
+++ b/logger.go
@@ -0,0 +1,154 @@
+package mux
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "os"
+ "time"
+)
+
+const (
+ green = "\033[97;42m"
+ white = "\033[90;47m"
+ yellow = "\033[90;43m"
+ red = "\033[97;41m"
+ blue = "\033[97;44m"
+ magenta = "\033[97;45m"
+ cyan = "\033[97;46m"
+ reset = "\033[0m"
+)
+
+type LogConfig struct {
+ Output io.Writer
+}
+
+type LogFormatterParams struct {
+ Request *http.Request
+
+ // TimeStamp shows the time after the server returns a response.
+ TimeStamp time.Time
+ // StatusCode is HTTP response code.
+ StatusCode int
+ // Latency is how much time the server cost to process a certain request.
+ Latency time.Duration
+ // ClientIP equals Context's ClientIP method.
+ ClientIP string
+ // Method is the HTTP method given to the request.
+ Method string
+ // Path is a path the client requests.
+ Path string
+}
+
+type statusWriter struct {
+ http.ResponseWriter
+ status int
+}
+
+func (w *statusWriter) WriteHeader(s int) {
+ w.status = s
+ w.ResponseWriter.WriteHeader(s)
+}
+
+func (p *LogFormatterParams) StatusCodeColor() string {
+ code := p.StatusCode
+
+ switch {
+ case code >= http.StatusOK && code < http.StatusMultipleChoices:
+ return green
+ case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
+ return white
+ case code >= http.StatusBadRequest && code < http.StatusInternalServerError:
+ return yellow
+ default:
+ return red
+ }
+}
+
+func (p *LogFormatterParams) MethodColor() string {
+ method := p.Method
+
+ switch method {
+ case http.MethodGet:
+ return blue
+ case http.MethodPost:
+ return cyan
+ case http.MethodPut:
+ return yellow
+ case http.MethodDelete:
+ return red
+ case http.MethodPatch:
+ return green
+ case http.MethodHead:
+ return magenta
+ case http.MethodOptions:
+ return white
+ default:
+ return reset
+ }
+}
+
+func (p *LogFormatterParams) ResetColor() string {
+ return reset
+}
+
+func Logger(next http.Handler) http.Handler {
+ return LoggerWithConfig(LogConfig{})(next)
+}
+
+func LoggerWithConfig(c LogConfig) MiddlewareFunc {
+ return func (next http.Handler) http.Handler {
+ out := c.Output
+ if out == nil {
+ out = os.Stdout
+ }
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+ path := r.URL.Path
+ raw := r.URL.RawQuery
+
+ sw := statusWriter{
+ ResponseWriter: w,
+ status: 0,
+ }
+ next.ServeHTTP(&sw, r)
+
+ if raw != "" {
+ path = fmt.Sprintf("%s?%s", path, raw)
+ }
+
+ stop := time.Now()
+ p := LogFormatterParams{
+ Request: r,
+ TimeStamp: stop,
+ Latency: stop.Sub(start),
+ ClientIP: "",
+ Method: r.Method,
+ StatusCode: sw.status,
+ Path: path,
+ }
+
+ fmt.Fprintf(out, formatter(p))
+ })
+ }
+}
+
+func formatter(p LogFormatterParams) string {
+ statusColor := p.StatusCodeColor()
+ methodColor := p.MethodColor()
+ resetColor := p.ResetColor()
+
+ if p.Latency > time.Minute {
+ p.Latency = p.Latency - p.Latency%time.Second
+ }
+
+ return fmt.Sprintf("[MUX] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n",
+ p.TimeStamp.Format("2006/01/02 - 15:04:05"),
+ statusColor, p.StatusCode, resetColor,
+ p.Latency,
+ p.ClientIP,
+ methodColor, p.Method, resetColor,
+ p.Path,
+ )
+}
\ No newline at end of file
diff --git a/logger_test.go b/logger_test.go
new file mode 100644
index 0000000..32b3ced
--- /dev/null
+++ b/logger_test.go
@@ -0,0 +1,58 @@
+package mux
+
+import (
+ "bytes"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "testing"
+)
+
+func testHandler(status int) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(status)
+ }
+}
+
+func TestLogger(t *testing.T) {
+ buffer := new(bytes.Buffer)
+ r := NewRouter()
+ r.Use(LoggerWithConfig(LogConfig{Output:buffer}))
+ r.HandleFunc("/example", testHandler(http.StatusOK)).
+ Methods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+
+ rw := NewRecorder()
+
+ req := newRequest("GET", "/example")
+ r.ServeHTTP(rw, req)
+ assert.Contains(t, buffer.String(), "200")
+ assert.Contains(t, buffer.String(), "GET")
+ assert.Contains(t, buffer.String(), "/example")
+
+ buffer.Reset()
+ req = newRequest("POST", "/example")
+ r.ServeHTTP(rw, req)
+ assert.Contains(t, buffer.String(), "200")
+ assert.Contains(t, buffer.String(), "POST")
+ assert.Contains(t, buffer.String(), "/example")
+
+ buffer.Reset()
+ req = newRequest("PUT", "/example")
+ r.ServeHTTP(rw, req)
+ assert.Contains(t, buffer.String(), "200")
+ assert.Contains(t, buffer.String(), "PUT")
+ assert.Contains(t, buffer.String(), "/example")
+
+ buffer.Reset()
+ req = newRequest("DELETE", "/example")
+ r.ServeHTTP(rw, req)
+ assert.Contains(t, buffer.String(), "200")
+ assert.Contains(t, buffer.String(), "DELETE")
+ assert.Contains(t, buffer.String(), "/example")
+
+ buffer.Reset()
+ req = newRequest("OPTIONS", "/example")
+ r.ServeHTTP(rw, req)
+ assert.Contains(t, buffer.String(), "200")
+ assert.Contains(t, buffer.String(), "OPTIONS")
+ assert.Contains(t, buffer.String(), "/example")
+}