mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-22 17:42:14 +08:00
Merge branch 'master' into fci
This commit is contained in:
commit
370c4e3f26
42
README.md
42
README.md
@ -35,6 +35,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
|
|||||||
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
|
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
|
||||||
- [Using middleware](#using-middleware)
|
- [Using middleware](#using-middleware)
|
||||||
- [How to write log file](#how-to-write-log-file)
|
- [How to write log file](#how-to-write-log-file)
|
||||||
|
- [Custom Log Format](#custom-log-format)
|
||||||
- [Model binding and validation](#model-binding-and-validation)
|
- [Model binding and validation](#model-binding-and-validation)
|
||||||
- [Custom Validators](#custom-validators)
|
- [Custom Validators](#custom-validators)
|
||||||
- [Only Bind Query String](#only-bind-query-string)
|
- [Only Bind Query String](#only-bind-query-string)
|
||||||
@ -363,6 +364,10 @@ ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
|
|||||||
|
|
||||||
References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single).
|
References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single).
|
||||||
|
|
||||||
|
`file.Filename` **SHOULD NOT** be trusted. See [`Content-Disposition` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives) and [#1693](https://github.com/gin-gonic/gin/issues/1693)
|
||||||
|
|
||||||
|
> The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func main() {
|
func main() {
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
@ -528,6 +533,43 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom Log Format
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
router := gin.New()
|
||||||
|
|
||||||
|
// LoggerWithFormatter middleware will write the logs to gin.DefaultWriter
|
||||||
|
// By default gin.DefaultWriter = os.Stdout
|
||||||
|
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||||
|
|
||||||
|
// your custom format
|
||||||
|
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
|
||||||
|
param.ClientIP,
|
||||||
|
param.TimeStamp.Format(time.RFC1123),
|
||||||
|
param.Method,
|
||||||
|
param.Path,
|
||||||
|
param.Request.Proto,
|
||||||
|
param.StatusCode,
|
||||||
|
param.Latency,
|
||||||
|
param.Request.UserAgent(),
|
||||||
|
param.ErrorMessage,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
router.Use(gin.Recovery())
|
||||||
|
|
||||||
|
router.GET("/ping", func(c *gin.Context) {
|
||||||
|
c.String(200, "pong")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.Run(":8080")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sample Output**
|
||||||
|
```
|
||||||
|
::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" "
|
||||||
|
```
|
||||||
|
|
||||||
### Model binding and validation
|
### Model binding and validation
|
||||||
|
|
||||||
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).
|
To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz).
|
||||||
|
46
context.go
46
context.go
@ -530,15 +530,25 @@ func (c *Context) BindYAML(obj interface{}) error {
|
|||||||
return c.MustBindWith(obj, binding.YAML)
|
return c.MustBindWith(obj, binding.YAML)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BindUri binds the passed struct pointer using binding.Uri.
|
||||||
|
// It will abort the request with HTTP 400 if any error occurs.
|
||||||
|
func (c *Context) BindUri(obj interface{}) error {
|
||||||
|
if err := c.ShouldBindUri(obj); err != nil {
|
||||||
|
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// MustBindWith binds the passed struct pointer using the specified binding engine.
|
// MustBindWith binds the passed struct pointer using the specified binding engine.
|
||||||
// It will abort the request with HTTP 400 if any error occurs.
|
// It will abort the request with HTTP 400 if any error occurs.
|
||||||
// See the binding package.
|
// See the binding package.
|
||||||
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
|
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {
|
||||||
if err = c.ShouldBindWith(obj, b); err != nil {
|
if err := c.ShouldBindWith(obj, b); err != nil {
|
||||||
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
|
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShouldBind checks the Content-Type to select a binding engine automatically,
|
// ShouldBind checks the Content-Type to select a binding engine automatically,
|
||||||
@ -932,34 +942,6 @@ func (c *Context) SetAccepted(formats ...string) {
|
|||||||
c.Accepted = formats
|
c.Accepted = formats
|
||||||
}
|
}
|
||||||
|
|
||||||
/************************************/
|
|
||||||
/***** GOLANG.ORG/X/NET/CONTEXT *****/
|
|
||||||
/************************************/
|
|
||||||
|
|
||||||
// Deadline returns the time when work done on behalf of this context
|
|
||||||
// should be canceled. Deadline returns ok==false when no deadline is
|
|
||||||
// set. Successive calls to Deadline return the same results.
|
|
||||||
func (c *Context) Deadline() (deadline time.Time, ok bool) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Done returns a channel that's closed when work done on behalf of this
|
|
||||||
// context should be canceled. Done may return nil if this context can
|
|
||||||
// never be canceled. Successive calls to Done return the same value.
|
|
||||||
func (c *Context) Done() <-chan struct{} {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Err returns a non-nil error value after Done is closed,
|
|
||||||
// successive calls to Err return the same error.
|
|
||||||
// If Done is not yet closed, Err returns nil.
|
|
||||||
// If Done is closed, Err returns a non-nil error explaining why:
|
|
||||||
// Canceled if the context was canceled
|
|
||||||
// or DeadlineExceeded if the context's deadline passed.
|
|
||||||
func (c *Context) Err() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Value returns the value associated with this context for key, or nil
|
// Value returns the value associated with this context for key, or nil
|
||||||
// if no value is associated with key. Successive calls to Value with
|
// if no value is associated with key. Successive calls to Value with
|
||||||
// the same key returns the same result.
|
// the same key returns the same result.
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/render"
|
"github.com/gin-gonic/gin/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,3 +17,31 @@ import (
|
|||||||
func (c *Context) PureJSON(code int, obj interface{}) {
|
func (c *Context) PureJSON(code int, obj interface{}) {
|
||||||
c.Render(code, render.PureJSON{Data: obj})
|
c.Render(code, render.PureJSON{Data: obj})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************************************/
|
||||||
|
/***** GOLANG.ORG/X/NET/CONTEXT *****/
|
||||||
|
/************************************/
|
||||||
|
|
||||||
|
// Deadline returns the time when work done on behalf of this context
|
||||||
|
// should be canceled. Deadline returns ok==false when no deadline is
|
||||||
|
// set. Successive calls to Deadline return the same results.
|
||||||
|
func (c *Context) Deadline() (time.Time, bool) {
|
||||||
|
return c.Request.Context().Deadline()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done returns a channel that's closed when work done on behalf of this
|
||||||
|
// context should be canceled. Done may return nil if this context can
|
||||||
|
// never be canceled. Successive calls to Done return the same value.
|
||||||
|
func (c *Context) Done() <-chan struct{} {
|
||||||
|
return c.Request.Context().Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns a non-nil error value after Done is closed,
|
||||||
|
// successive calls to Err return the same error.
|
||||||
|
// If Done is not yet closed, Err returns nil.
|
||||||
|
// If Done is closed, Err returns a non-nil error explaining why:
|
||||||
|
// Canceled if the context was canceled
|
||||||
|
// or DeadlineExceeded if the context's deadline passed.
|
||||||
|
func (c *Context) Err() error {
|
||||||
|
return c.Request.Context().Err()
|
||||||
|
}
|
||||||
|
@ -7,9 +7,12 @@
|
|||||||
package gin
|
package gin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -25,3 +28,49 @@ func TestContextRenderPureJSON(t *testing.T) {
|
|||||||
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
|
||||||
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextHTTPContext(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
req, _ := http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||||
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||||
|
defer cancelFunc()
|
||||||
|
c.Request = req.WithContext(ctx)
|
||||||
|
|
||||||
|
assert.NoError(t, c.Err())
|
||||||
|
assert.NotNil(t, c.Done())
|
||||||
|
select {
|
||||||
|
case <-c.Done():
|
||||||
|
assert.Fail(t, "context should not be canceled")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
ti, ok := c.Deadline()
|
||||||
|
assert.Equal(t, ti, time.Time{})
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Equal(t, c.Value(0), c.Request)
|
||||||
|
|
||||||
|
cancelFunc()
|
||||||
|
assert.NotNil(t, c.Done())
|
||||||
|
select {
|
||||||
|
case <-c.Done():
|
||||||
|
default:
|
||||||
|
assert.Fail(t, "context should be canceled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextHTTPContextWithDeadline(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
req, _ := http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
|
||||||
|
location, _ := time.LoadLocation("Europe/Paris")
|
||||||
|
assert.NotNil(t, location)
|
||||||
|
date := time.Date(2031, 12, 27, 16, 00, 00, 00, location)
|
||||||
|
ctx, cancelFunc := context.WithDeadline(context.Background(), date)
|
||||||
|
defer cancelFunc()
|
||||||
|
c.Request = req.WithContext(ctx)
|
||||||
|
|
||||||
|
assert.NoError(t, c.Err())
|
||||||
|
|
||||||
|
ti, ok := c.Deadline()
|
||||||
|
assert.Equal(t, ti, date)
|
||||||
|
assert.True(t, ok)
|
||||||
|
}
|
||||||
|
39
context_pre17.go
Normal file
39
context_pre17.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2018 Gin Core Team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build !go1.7
|
||||||
|
|
||||||
|
package gin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
/************************************/
|
||||||
|
/***** GOLANG.ORG/X/NET/CONTEXT *****/
|
||||||
|
/************************************/
|
||||||
|
|
||||||
|
// Deadline returns the time when work done on behalf of this context
|
||||||
|
// should be canceled. Deadline returns ok==false when no deadline is
|
||||||
|
// set. Successive calls to Deadline return the same results.
|
||||||
|
func (c *Context) Deadline() (deadline time.Time, ok bool) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done returns a channel that's closed when work done on behalf of this
|
||||||
|
// context should be canceled. Done may return nil if this context can
|
||||||
|
// never be canceled. Successive calls to Done return the same value.
|
||||||
|
func (c *Context) Done() <-chan struct{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Err returns a non-nil error value after Done is closed,
|
||||||
|
// successive calls to Err return the same error.
|
||||||
|
// If Done is not yet closed, Err returns nil.
|
||||||
|
// If Done is closed, Err returns a non-nil error explaining why:
|
||||||
|
// Canceled if the context was canceled
|
||||||
|
// or DeadlineExceeded if the context's deadline passed.
|
||||||
|
func (c *Context) Err() error {
|
||||||
|
return nil
|
||||||
|
}
|
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -25,7 +26,8 @@ func main() {
|
|||||||
files := form.File["files"]
|
files := form.File["files"]
|
||||||
|
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if err := c.SaveUploadedFile(file, file.Filename); err != nil {
|
filename := filepath.Base(file.Filename)
|
||||||
|
if err := c.SaveUploadedFile(file, filename); err != nil {
|
||||||
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
|
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -23,7 +24,8 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.SaveUploadedFile(file, file.Filename); err != nil {
|
filename := filepath.Base(file.Filename)
|
||||||
|
if err := c.SaveUploadedFile(file, filename); err != nil {
|
||||||
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
|
c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -290,8 +290,8 @@ func TestShouldBindUri(t *testing.T) {
|
|||||||
router := Default()
|
router := Default()
|
||||||
|
|
||||||
type Person struct {
|
type Person struct {
|
||||||
Name string `uri:"name"`
|
Name string `uri:"name" binding:"required"`
|
||||||
Id string `uri:"id"`
|
Id string `uri:"id" binding:"required"`
|
||||||
}
|
}
|
||||||
router.Handle("GET", "/rest/:name/:id", func(c *Context) {
|
router.Handle("GET", "/rest/:name/:id", func(c *Context) {
|
||||||
var person Person
|
var person Person
|
||||||
@ -304,6 +304,46 @@ func TestShouldBindUri(t *testing.T) {
|
|||||||
path, _ := exampleFromPath("/rest/:name/:id")
|
path, _ := exampleFromPath("/rest/:name/:id")
|
||||||
w := performRequest(router, "GET", path)
|
w := performRequest(router, "GET", path)
|
||||||
assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
|
assert.Equal(t, "ShouldBindUri test OK", w.Body.String())
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindUri(t *testing.T) {
|
||||||
|
DefaultWriter = os.Stdout
|
||||||
|
router := Default()
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Name string `uri:"name" binding:"required"`
|
||||||
|
Id string `uri:"id" binding:"required"`
|
||||||
|
}
|
||||||
|
router.Handle("GET", "/rest/:name/:id", func(c *Context) {
|
||||||
|
var person Person
|
||||||
|
assert.NoError(t, c.BindUri(&person))
|
||||||
|
assert.True(t, "" != person.Name)
|
||||||
|
assert.True(t, "" != person.Id)
|
||||||
|
c.String(http.StatusOK, "BindUri test OK")
|
||||||
|
})
|
||||||
|
|
||||||
|
path, _ := exampleFromPath("/rest/:name/:id")
|
||||||
|
w := performRequest(router, "GET", path)
|
||||||
|
assert.Equal(t, "BindUri test OK", w.Body.String())
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBindUriError(t *testing.T) {
|
||||||
|
DefaultWriter = os.Stdout
|
||||||
|
router := Default()
|
||||||
|
|
||||||
|
type Member struct {
|
||||||
|
Number string `uri:"num" binding:"required,uuid"`
|
||||||
|
}
|
||||||
|
router.Handle("GET", "/new/rest/:num", func(c *Context) {
|
||||||
|
var m Member
|
||||||
|
c.BindUri(&m)
|
||||||
|
})
|
||||||
|
|
||||||
|
path1, _ := exampleFromPath("/new/rest/:num")
|
||||||
|
w1 := performRequest(router, "GET", path1)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w1.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func githubConfigRouter(router *Engine) {
|
func githubConfigRouter(router *Engine) {
|
||||||
|
125
logger.go
125
logger.go
@ -26,6 +26,65 @@ var (
|
|||||||
disableColor = false
|
disableColor = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoggerConfig defines the config for Logger middleware.
|
||||||
|
type LoggerConfig struct {
|
||||||
|
// Optional. Default value is gin.defaultLogFormatter
|
||||||
|
Formatter LogFormatter
|
||||||
|
|
||||||
|
// Output is a writer where logs are written.
|
||||||
|
// Optional. Default value is gin.DefaultWriter.
|
||||||
|
Output io.Writer
|
||||||
|
|
||||||
|
// SkipPaths is a url path array which logs are not written.
|
||||||
|
// Optional.
|
||||||
|
SkipPaths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogFormatter gives the signature of the formatter function passed to LoggerWithFormatter
|
||||||
|
type LogFormatter func(params LogFormatterParams) string
|
||||||
|
|
||||||
|
// LogFormatterParams is the structure any formatter will be handed when time to log comes
|
||||||
|
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
|
||||||
|
// ErrorMessage is set if error has occurred in processing the request.
|
||||||
|
ErrorMessage string
|
||||||
|
// IsTerm shows whether does gin's output descriptor refers to a terminal.
|
||||||
|
IsTerm bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultLogFormatter is the default log format function Logger middleware uses.
|
||||||
|
var defaultLogFormatter = func(param LogFormatterParams) string {
|
||||||
|
var statusColor, methodColor, resetColor string
|
||||||
|
if param.IsTerm {
|
||||||
|
statusColor = colorForStatus(param.StatusCode)
|
||||||
|
methodColor = colorForMethod(param.Method)
|
||||||
|
resetColor = reset
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
|
||||||
|
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||||
|
statusColor, param.StatusCode, resetColor,
|
||||||
|
param.Latency,
|
||||||
|
param.ClientIP,
|
||||||
|
methodColor, param.Method, resetColor,
|
||||||
|
param.Path,
|
||||||
|
param.ErrorMessage,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// DisableConsoleColor disables color output in the console.
|
// DisableConsoleColor disables color output in the console.
|
||||||
func DisableConsoleColor() {
|
func DisableConsoleColor() {
|
||||||
disableColor = true
|
disableColor = true
|
||||||
@ -50,12 +109,39 @@ func ErrorLoggerT(typ ErrorType) HandlerFunc {
|
|||||||
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
|
// Logger instances a Logger middleware that will write the logs to gin.DefaultWriter.
|
||||||
// By default gin.DefaultWriter = os.Stdout.
|
// By default gin.DefaultWriter = os.Stdout.
|
||||||
func Logger() HandlerFunc {
|
func Logger() HandlerFunc {
|
||||||
return LoggerWithWriter(DefaultWriter)
|
return LoggerWithConfig(LoggerConfig{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerWithFormatter instance a Logger middleware with the specified log format function.
|
||||||
|
func LoggerWithFormatter(f LogFormatter) HandlerFunc {
|
||||||
|
return LoggerWithConfig(LoggerConfig{
|
||||||
|
Formatter: f,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoggerWithWriter instance a Logger middleware with the specified writer buffer.
|
// LoggerWithWriter instance a Logger middleware with the specified writer buffer.
|
||||||
// Example: os.Stdout, a file opened in write mode, a socket...
|
// Example: os.Stdout, a file opened in write mode, a socket...
|
||||||
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
|
func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
|
||||||
|
return LoggerWithConfig(LoggerConfig{
|
||||||
|
Output: out,
|
||||||
|
SkipPaths: notlogged,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerWithConfig instance a Logger middleware with config.
|
||||||
|
func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
||||||
|
formatter := conf.Formatter
|
||||||
|
if formatter == nil {
|
||||||
|
formatter = defaultLogFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
out := conf.Output
|
||||||
|
if out == nil {
|
||||||
|
out = DefaultWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
notlogged := conf.SkipPaths
|
||||||
|
|
||||||
isTerm := true
|
isTerm := true
|
||||||
|
|
||||||
if w, ok := out.(*os.File); !ok ||
|
if w, ok := out.(*os.File); !ok ||
|
||||||
@ -85,34 +171,27 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
|
|||||||
|
|
||||||
// Log only when path is not being skipped
|
// Log only when path is not being skipped
|
||||||
if _, ok := skip[path]; !ok {
|
if _, ok := skip[path]; !ok {
|
||||||
// Stop timer
|
param := LogFormatterParams{
|
||||||
end := time.Now()
|
Request: c.Request,
|
||||||
latency := end.Sub(start)
|
IsTerm: isTerm,
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
|
||||||
method := c.Request.Method
|
|
||||||
statusCode := c.Writer.Status()
|
|
||||||
var statusColor, methodColor, resetColor string
|
|
||||||
if isTerm {
|
|
||||||
statusColor = colorForStatus(statusCode)
|
|
||||||
methodColor = colorForMethod(method)
|
|
||||||
resetColor = reset
|
|
||||||
}
|
}
|
||||||
comment := c.Errors.ByType(ErrorTypePrivate).String()
|
|
||||||
|
// Stop timer
|
||||||
|
param.TimeStamp = time.Now()
|
||||||
|
param.Latency = param.TimeStamp.Sub(start)
|
||||||
|
|
||||||
|
param.ClientIP = c.ClientIP()
|
||||||
|
param.Method = c.Request.Method
|
||||||
|
param.StatusCode = c.Writer.Status()
|
||||||
|
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
|
||||||
|
|
||||||
if raw != "" {
|
if raw != "" {
|
||||||
path = path + "?" + raw
|
path = path + "?" + raw
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
|
param.Path = path
|
||||||
end.Format("2006/01/02 - 15:04:05"),
|
|
||||||
statusColor, statusCode, resetColor,
|
fmt.Fprintf(out, formatter(param))
|
||||||
latency,
|
|
||||||
clientIP,
|
|
||||||
methodColor, method, resetColor,
|
|
||||||
path,
|
|
||||||
comment,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
194
logger_test.go
194
logger_test.go
@ -7,8 +7,10 @@ package gin
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -79,7 +81,179 @@ func TestLogger(t *testing.T) {
|
|||||||
assert.Contains(t, buffer.String(), "404")
|
assert.Contains(t, buffer.String(), "404")
|
||||||
assert.Contains(t, buffer.String(), "GET")
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
assert.Contains(t, buffer.String(), "/notfound")
|
assert.Contains(t, buffer.String(), "/notfound")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithConfig(t *testing.T) {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
router := New()
|
||||||
|
router.Use(LoggerWithConfig(LoggerConfig{Output: buffer}))
|
||||||
|
router.GET("/example", func(c *Context) {})
|
||||||
|
router.POST("/example", func(c *Context) {})
|
||||||
|
router.PUT("/example", func(c *Context) {})
|
||||||
|
router.DELETE("/example", func(c *Context) {})
|
||||||
|
router.PATCH("/example", func(c *Context) {})
|
||||||
|
router.HEAD("/example", func(c *Context) {})
|
||||||
|
router.OPTIONS("/example", func(c *Context) {})
|
||||||
|
|
||||||
|
performRequest(router, "GET", "/example?a=100")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "a=100")
|
||||||
|
|
||||||
|
// I wrote these first (extending the above) but then realized they are more
|
||||||
|
// like integration tests because they test the whole logging process rather
|
||||||
|
// than individual functions. Im not sure where these should go.
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "POST", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "POST")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "PUT", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "PUT")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "DELETE", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "DELETE")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "PATCH", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "PATCH")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "HEAD", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "HEAD")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "OPTIONS", "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "OPTIONS")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "GET", "/notfound")
|
||||||
|
assert.Contains(t, buffer.String(), "404")
|
||||||
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
|
assert.Contains(t, buffer.String(), "/notfound")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithFormatter(t *testing.T) {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
|
d := DefaultWriter
|
||||||
|
DefaultWriter = buffer
|
||||||
|
defer func() {
|
||||||
|
DefaultWriter = d
|
||||||
|
}()
|
||||||
|
|
||||||
|
router := New()
|
||||||
|
router.Use(LoggerWithFormatter(func(param LogFormatterParams) string {
|
||||||
|
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
|
||||||
|
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||||
|
param.StatusCode,
|
||||||
|
param.Latency,
|
||||||
|
param.ClientIP,
|
||||||
|
param.Method,
|
||||||
|
param.Path,
|
||||||
|
param.ErrorMessage,
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
router.GET("/example", func(c *Context) {})
|
||||||
|
performRequest(router, "GET", "/example?a=100")
|
||||||
|
|
||||||
|
// output test
|
||||||
|
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "a=100")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithConfigFormatting(t *testing.T) {
|
||||||
|
var gotParam LogFormatterParams
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
|
||||||
|
router := New()
|
||||||
|
router.Use(LoggerWithConfig(LoggerConfig{
|
||||||
|
Output: buffer,
|
||||||
|
Formatter: func(param LogFormatterParams) string {
|
||||||
|
// for assert test
|
||||||
|
gotParam = param
|
||||||
|
|
||||||
|
return fmt.Sprintf("[FORMATTER TEST] %v | %3d | %13v | %15s | %-7s %s\n%s",
|
||||||
|
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
|
||||||
|
param.StatusCode,
|
||||||
|
param.Latency,
|
||||||
|
param.ClientIP,
|
||||||
|
param.Method,
|
||||||
|
param.Path,
|
||||||
|
param.ErrorMessage,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
router.GET("/example", func(c *Context) {
|
||||||
|
// set dummy ClientIP
|
||||||
|
c.Request.Header.Set("X-Forwarded-For", "20.20.20.20")
|
||||||
|
})
|
||||||
|
performRequest(router, "GET", "/example?a=100")
|
||||||
|
|
||||||
|
// output test
|
||||||
|
assert.Contains(t, buffer.String(), "[FORMATTER TEST]")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
assert.Contains(t, buffer.String(), "GET")
|
||||||
|
assert.Contains(t, buffer.String(), "/example")
|
||||||
|
assert.Contains(t, buffer.String(), "a=100")
|
||||||
|
|
||||||
|
// LogFormatterParams test
|
||||||
|
assert.NotNil(t, gotParam.Request)
|
||||||
|
assert.NotEmpty(t, gotParam.TimeStamp)
|
||||||
|
assert.Equal(t, 200, gotParam.StatusCode)
|
||||||
|
assert.NotEmpty(t, gotParam.Latency)
|
||||||
|
assert.Equal(t, "20.20.20.20", gotParam.ClientIP)
|
||||||
|
assert.Equal(t, "GET", gotParam.Method)
|
||||||
|
assert.Equal(t, "/example?a=100", gotParam.Path)
|
||||||
|
assert.Empty(t, gotParam.ErrorMessage)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultLogFormatter(t *testing.T) {
|
||||||
|
timeStamp := time.Unix(1544173902, 0).UTC()
|
||||||
|
|
||||||
|
termFalseParam := LogFormatterParams{
|
||||||
|
TimeStamp: timeStamp,
|
||||||
|
StatusCode: 200,
|
||||||
|
Latency: time.Second * 5,
|
||||||
|
ClientIP: "20.20.20.20",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/",
|
||||||
|
ErrorMessage: "",
|
||||||
|
IsTerm: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
termTrueParam := LogFormatterParams{
|
||||||
|
TimeStamp: timeStamp,
|
||||||
|
StatusCode: 200,
|
||||||
|
Latency: time.Second * 5,
|
||||||
|
ClientIP: "20.20.20.20",
|
||||||
|
Method: "GET",
|
||||||
|
Path: "/",
|
||||||
|
ErrorMessage: "",
|
||||||
|
IsTerm: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam))
|
||||||
|
|
||||||
|
assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestColorForMethod(t *testing.T) {
|
func TestColorForMethod(t *testing.T) {
|
||||||
@ -127,7 +301,7 @@ func TestErrorLogger(t *testing.T) {
|
|||||||
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
|
assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSkippingPaths(t *testing.T) {
|
func TestLoggerWithWriterSkippingPaths(t *testing.T) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
router := New()
|
router := New()
|
||||||
router.Use(LoggerWithWriter(buffer, "/skipped"))
|
router.Use(LoggerWithWriter(buffer, "/skipped"))
|
||||||
@ -142,6 +316,24 @@ func TestSkippingPaths(t *testing.T) {
|
|||||||
assert.Contains(t, buffer.String(), "")
|
assert.Contains(t, buffer.String(), "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoggerWithConfigSkippingPaths(t *testing.T) {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
router := New()
|
||||||
|
router.Use(LoggerWithConfig(LoggerConfig{
|
||||||
|
Output: buffer,
|
||||||
|
SkipPaths: []string{"/skipped"},
|
||||||
|
}))
|
||||||
|
router.GET("/logged", func(c *Context) {})
|
||||||
|
router.GET("/skipped", func(c *Context) {})
|
||||||
|
|
||||||
|
performRequest(router, "GET", "/logged")
|
||||||
|
assert.Contains(t, buffer.String(), "200")
|
||||||
|
|
||||||
|
buffer.Reset()
|
||||||
|
performRequest(router, "GET", "/skipped")
|
||||||
|
assert.Contains(t, buffer.String(), "")
|
||||||
|
}
|
||||||
|
|
||||||
func TestDisableConsoleColor(t *testing.T) {
|
func TestDisableConsoleColor(t *testing.T) {
|
||||||
New()
|
New()
|
||||||
assert.False(t, disableColor)
|
assert.False(t, disableColor)
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
// +build tools
|
// +build tools
|
||||||
|
|
||||||
// This file exists to cause `go mod` and `go get` to believe these tools
|
// This package exists to cause `go mod` and `go get` to believe these tools
|
||||||
// are dependencies, even though they are not runtime dependencies of any
|
// are dependencies, even though they are not runtime dependencies of any
|
||||||
// gin package. This means they will appear in `go.mod` file, but will not
|
// gin package. This means they will appear in `go.mod` file, but will not
|
||||||
// be a part of the build.
|
// be a part of the build.
|
||||||
|
|
||||||
package gin
|
package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/campoy/embedmd"
|
_ "github.com/campoy/embedmd"
|
Loading…
x
Reference in New Issue
Block a user