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") +}