mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
add accesslog middleware (rel: #1601)
This commit is contained in:
parent
bf9f7617e2
commit
0be856e54c
|
@ -602,6 +602,8 @@ New Package-level Variables:
|
||||||
|
|
||||||
New Context Methods:
|
New Context Methods:
|
||||||
|
|
||||||
|
- `Context.RecordBody()` same as the Application's `DisableBodyConsumptionOnUnmarshal` configuration field but registers per chain of handlers. It makes the request body readable more than once.
|
||||||
|
- `Context.IsRecordingBody() bool` reports whether the request body can be readen multiple times.
|
||||||
- `Context.ReadHeaders(ptr interface{}) error` binds request headers to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-headers/main.go).
|
- `Context.ReadHeaders(ptr interface{}) error` binds request headers to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-headers/main.go).
|
||||||
- `Context.ReadParams(ptr interface{}) error` binds dynamic path parameters to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-params/main.go).
|
- `Context.ReadParams(ptr interface{}) error` binds dynamic path parameters to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-params/main.go).
|
||||||
- `Context.SaveFormFile(fh *multipart.FileHeader, dest string) (int64, error)` previously unexported. Accepts a result file of `Context.FormFile` and saves it to the disk.
|
- `Context.SaveFormFile(fh *multipart.FileHeader, dest string) (int64, error)` previously unexported. Accepts a result file of `Context.FormFile` and saves it to the disk.
|
||||||
|
@ -643,6 +645,7 @@ New Context Methods:
|
||||||
|
|
||||||
Breaking Changes:
|
Breaking Changes:
|
||||||
|
|
||||||
|
- Request Logger's `Columns bool` field has been removed. Use the new [accesslog](https://github.com/kataras/iris/tree/master/_examples/logging/request-logger/accesslog/main.go) middleware instead.
|
||||||
- The `.Binary` method of all view engines was removed: pass the go-bindata's latest version `AssetFile()` exported function as the first argument instead of string. All examples updated.
|
- The `.Binary` method of all view engines was removed: pass the go-bindata's latest version `AssetFile()` exported function as the first argument instead of string. All examples updated.
|
||||||
- `ContextUploadFormFiles(destDirectory string, before ...func(*Context, *multipart.FileHeader) bool) (uploaded []*multipart.FileHeader, n int64, err error)` now returns the total files uploaded too (as its first parameter) and the "before" variadic option should return a boolean, if false then the specific file is skipped.
|
- `ContextUploadFormFiles(destDirectory string, before ...func(*Context, *multipart.FileHeader) bool) (uploaded []*multipart.FileHeader, n int64, err error)` now returns the total files uploaded too (as its first parameter) and the "before" variadic option should return a boolean, if false then the specific file is skipped.
|
||||||
- `Context.PostValues(name string) ([]string, error)` now returns a second output argument of `error` type too, which reports `ErrEmptyForm` or `ErrNotFound` or `ErrEmptyFormField`. The single post value getters now returns the **last value** if multiple was given instead of the first one (this allows clients to append values on flow updates).
|
- `Context.PostValues(name string) ([]string, error)` now returns a second output argument of `error` type too, which reports `ErrEmptyForm` or `ErrNotFound` or `ErrEmptyFormField`. The single post value getters now returns the **last value** if multiple was given instead of the first one (this allows clients to append values on flow updates).
|
||||||
|
|
3
NOTICE
3
NOTICE
|
@ -35,9 +35,6 @@ Revision ID: ab226d925aa394ccecf01e515ea8479367e0961c
|
||||||
closestmatch 1fbe626be92eb4c https://github.com/schollz/closestmatch
|
closestmatch 1fbe626be92eb4c https://github.com/schollz/closestmatch
|
||||||
347d182cae9f8f0
|
347d182cae9f8f0
|
||||||
0a046bf2f4
|
0a046bf2f4
|
||||||
columnize 9e6335e58db3b4c https://github.com/ryanuber/columnize
|
|
||||||
fe3c3c5c881f51f
|
|
||||||
fbc1091b34
|
|
||||||
compress 4a2d40e4b07e5b3 https://github.com/klauspost/compress
|
compress 4a2d40e4b07e5b3 https://github.com/klauspost/compress
|
||||||
d333bc0569facd0
|
d333bc0569facd0
|
||||||
f2dbf4ef39
|
f2dbf4ef39
|
||||||
|
|
|
@ -72,8 +72,7 @@
|
||||||
* [Sitemap](routing/sitemap/main.go)
|
* [Sitemap](routing/sitemap/main.go)
|
||||||
* Logging
|
* Logging
|
||||||
* [Request Logger](logging/request-logger/main.go)
|
* [Request Logger](logging/request-logger/main.go)
|
||||||
* [Log requests and responses to access.log](logging/request-logger/request-logger-access-log-file)
|
* [Log requests and responses to access.log](logging/request-logger/accesslog)
|
||||||
* [Log Requests to a File](logging/request-logger/request-logger-file/main.go)
|
|
||||||
* [Log Requests to a JSON File](logging/request-logger/request-logger-file-json/main.go)
|
* [Log Requests to a JSON File](logging/request-logger/request-logger-file-json/main.go)
|
||||||
* [Application File Logger](logging/file-logger/main.go)
|
* [Application File Logger](logging/file-logger/main.go)
|
||||||
* [Application JSON Logger](logging/json-logger/main.go)
|
* [Application JSON Logger](logging/json-logger/main.go)
|
||||||
|
|
47
_examples/logging/request-logger/accesslog/main.go
Normal file
47
_examples/logging/request-logger/accesslog/main.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package main // See https://github.com/kataras/iris/issues/1601
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/middleware/accesslog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := iris.New()
|
||||||
|
ac := accesslog.File("./access.log")
|
||||||
|
defer ac.Close()
|
||||||
|
iris.RegisterOnInterrupt(func() {
|
||||||
|
ac.Close()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Register the middleware (UseRouter to catch http errors too).
|
||||||
|
app.UseRouter(ac.Handler)
|
||||||
|
//
|
||||||
|
|
||||||
|
// Register some routes...
|
||||||
|
app.HandleDir("/", iris.Dir("./public"))
|
||||||
|
|
||||||
|
app.Get("/user/{username}", userHandler)
|
||||||
|
app.Post("/read_body", readBodyHandler)
|
||||||
|
app.Get("/html_response", htmlResponse)
|
||||||
|
//
|
||||||
|
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBodyHandler(ctx iris.Context) {
|
||||||
|
var request interface{}
|
||||||
|
if err := ctx.ReadBody(&request); err != nil {
|
||||||
|
ctx.StopWithPlainError(iris.StatusBadRequest, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(iris.Map{"message": "OK", "data": request})
|
||||||
|
}
|
||||||
|
|
||||||
|
func userHandler(ctx iris.Context) {
|
||||||
|
ctx.Writef("Hello, %s!", ctx.Params().Get("username"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func htmlResponse(ctx iris.Context) {
|
||||||
|
ctx.HTML("<h1>HTML Response</h1>")
|
||||||
|
}
|
|
@ -21,7 +21,6 @@ func main() {
|
||||||
Query: true,
|
Query: true,
|
||||||
// Shows information about the executed route.
|
// Shows information about the executed route.
|
||||||
TraceRoute: true,
|
TraceRoute: true,
|
||||||
// Columns: true,
|
|
||||||
|
|
||||||
// if !empty then its contents derives from `ctx.Values().Get("logger_message")
|
// if !empty then its contents derives from `ctx.Values().Get("logger_message")
|
||||||
// will be added to the logs.
|
// will be added to the logs.
|
||||||
|
|
|
@ -1,174 +0,0 @@
|
||||||
package main // See https://github.com/kataras/iris/issues/1601
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
|
||||||
"github.com/kataras/iris/v12/middleware/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Create or use the ./access.log file.
|
|
||||||
f, err := os.OpenFile("access.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
iris.RegisterOnInterrupt(func() { f.Close() })
|
|
||||||
//
|
|
||||||
|
|
||||||
app := iris.New()
|
|
||||||
|
|
||||||
// Init the request logger with
|
|
||||||
// the LogFuncCtx field alone.
|
|
||||||
reqLogger := logger.New(logger.Config{
|
|
||||||
LogFuncCtx: requestLogFunc(f),
|
|
||||||
})
|
|
||||||
//
|
|
||||||
|
|
||||||
// Wrap the request logger middleware
|
|
||||||
// with a response recorder because
|
|
||||||
// we want to record the response body
|
|
||||||
// sent to the client.
|
|
||||||
reqLoggerWithRecord := func(ctx iris.Context) {
|
|
||||||
// Store the requested path just in case.
|
|
||||||
ctx.Values().Set("path", ctx.Path())
|
|
||||||
ctx.Record()
|
|
||||||
reqLogger(ctx)
|
|
||||||
}
|
|
||||||
//
|
|
||||||
|
|
||||||
// Register the middleware (UseRouter to catch http errors too).
|
|
||||||
app.UseRouter(reqLoggerWithRecord)
|
|
||||||
//
|
|
||||||
|
|
||||||
// Register some routes...
|
|
||||||
app.HandleDir("/", iris.Dir("./public"))
|
|
||||||
|
|
||||||
app.Get("/user/{username}", userHandler)
|
|
||||||
app.Post("/read_body", readBodyHandler)
|
|
||||||
//
|
|
||||||
|
|
||||||
// Start the server with `WithoutBodyConsumptionOnUnmarshal`
|
|
||||||
// option so the request body can be readen twice:
|
|
||||||
// one for our handlers and one from inside our request logger middleware.
|
|
||||||
app.Listen(":8080", iris.WithoutBodyConsumptionOnUnmarshal)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBodyHandler(ctx iris.Context) {
|
|
||||||
var request interface{}
|
|
||||||
if err := ctx.ReadBody(&request); err != nil {
|
|
||||||
ctx.StopWithPlainError(iris.StatusBadRequest, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.JSON(iris.Map{"message": "OK"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func userHandler(ctx iris.Context) {
|
|
||||||
ctx.Writef("Hello, %s!", ctx.Params().Get("username"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonToString(src []byte) string {
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
if err := json.Compact(buf, src); err != nil {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestLogFunc(w io.Writer) func(ctx iris.Context, lat time.Duration) {
|
|
||||||
return func(ctx iris.Context, lat time.Duration) {
|
|
||||||
var (
|
|
||||||
method = ctx.Method() // request method.
|
|
||||||
// Use a stored value instead of ctx.Path()
|
|
||||||
// because some handlers may change the relative path
|
|
||||||
// to perform some action.
|
|
||||||
path = ctx.Values().GetString("path")
|
|
||||||
code = ctx.GetStatusCode() // response status code
|
|
||||||
// request and response data or error reading them.
|
|
||||||
requestBody string
|
|
||||||
responseBody string
|
|
||||||
|
|
||||||
// url parameters and path parameters separated by space,
|
|
||||||
// key=value key2=value2.
|
|
||||||
requestValues string
|
|
||||||
)
|
|
||||||
|
|
||||||
// any error handler stored ( ctx.SetErr or StopWith(Plain)Error )
|
|
||||||
errHandler := ctx.GetErr()
|
|
||||||
// check if not error and client sent a response with a content-type set-ed.
|
|
||||||
if errHandler == nil {
|
|
||||||
if ctx.GetContentTypeRequested() == "application/json" {
|
|
||||||
// Read and log request body the client sent to the server:
|
|
||||||
//
|
|
||||||
// You can use ctx.ReadBody(&body)
|
|
||||||
// which will decode any body (json, xml, msgpack, protobuf...)
|
|
||||||
// and use %v inside the fmt.Fprintf to print something like:
|
|
||||||
// map[age:22 id:10 name:Tim]
|
|
||||||
//
|
|
||||||
// But if you want specific to json string,
|
|
||||||
// then do that:
|
|
||||||
var tmp json.RawMessage
|
|
||||||
if err := ctx.ReadJSON(&tmp); err != nil {
|
|
||||||
requestBody = err.Error()
|
|
||||||
} else {
|
|
||||||
requestBody = jsonToString(tmp)
|
|
||||||
}
|
|
||||||
//
|
|
||||||
} else {
|
|
||||||
// left for exercise.
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
requestBody = fmt.Sprintf("error(%s)", errHandler.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
responseData := ctx.Recorder().Body()
|
|
||||||
// check if the server sent any response with content type,
|
|
||||||
// note that this will return the ;charset too
|
|
||||||
// so we check for its prefix instead.
|
|
||||||
if strings.HasPrefix(ctx.GetContentType(), "application/json") {
|
|
||||||
responseBody = jsonToString(responseData)
|
|
||||||
} else {
|
|
||||||
responseBody = string(responseData)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf strings.Builder
|
|
||||||
|
|
||||||
ctx.Params().Visit(func(key, value string) {
|
|
||||||
buf.WriteString(key)
|
|
||||||
buf.WriteByte('=')
|
|
||||||
buf.WriteString(value)
|
|
||||||
buf.WriteByte(' ')
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, entry := range ctx.URLParamsSorted() {
|
|
||||||
buf.WriteString(entry.Key)
|
|
||||||
buf.WriteByte('=')
|
|
||||||
buf.WriteString(entry.Value)
|
|
||||||
buf.WriteByte(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
if n := buf.Len(); n > 1 {
|
|
||||||
requestValues = buf.String()[0 : n-1] // remove last space.
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(w, "%s|%s|%s|%s|%s|%d|%s|%s|\n",
|
|
||||||
time.Now().Format("2006-01-02 15:04:05"),
|
|
||||||
lat,
|
|
||||||
method,
|
|
||||||
path,
|
|
||||||
requestValues,
|
|
||||||
code,
|
|
||||||
requestBody,
|
|
||||||
responseBody,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
|
||||||
"github.com/kataras/iris/v12/middleware/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteFileOnExit = false
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
app := iris.New()
|
|
||||||
r, close := newRequestLogger()
|
|
||||||
defer close()
|
|
||||||
|
|
||||||
app.Use(r)
|
|
||||||
app.OnAnyErrorCode(r, func(ctx iris.Context) {
|
|
||||||
ctx.HTML("<h1> Error: Please try <a href ='/'> this </a> instead.</h1>")
|
|
||||||
})
|
|
||||||
|
|
||||||
h := func(ctx iris.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.Listen(":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 iris.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(endTime time.Time, latency time.Duration, status, ip, method, path string, message interface{}, headerMessage interface{}) {
|
|
||||||
output := logger.Columnize(endTime.Format("2006/01/02 - 15:04:05"), latency, status, ip, method, path, message, headerMessage)
|
|
||||||
logFile.Write([]byte(output))
|
|
||||||
} // or make use of the `LogFuncCtx`, see the '../request-logger-file-json' example for more.
|
|
||||||
|
|
||||||
// when we don't want to use to log requests to assets and etc.
|
|
||||||
c.AddSkipper(func(ctx iris.Context) bool {
|
|
||||||
path := ctx.Path()
|
|
||||||
for _, ext := range excludeExtensions {
|
|
||||||
if strings.HasSuffix(path, ext) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
h = logger.New(c)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -715,6 +715,8 @@ type Configuration struct {
|
||||||
// if this field set to true then a new buffer will be created to read from and the request body.
|
// if this field set to true then a new buffer will be created to read from and the request body.
|
||||||
// The body will not be changed and existing data before the
|
// The body will not be changed and existing data before the
|
||||||
// context.UnmarshalBody/ReadJSON/ReadXML will be not consumed.
|
// context.UnmarshalBody/ReadJSON/ReadXML will be not consumed.
|
||||||
|
//
|
||||||
|
// See `Context.RecordBody` method for the same feature, per-request.
|
||||||
DisableBodyConsumptionOnUnmarshal bool `json:"disableBodyConsumptionOnUnmarshal,omitempty" yaml:"DisableBodyConsumptionOnUnmarshal" toml:"DisableBodyConsumptionOnUnmarshal"`
|
DisableBodyConsumptionOnUnmarshal bool `json:"disableBodyConsumptionOnUnmarshal,omitempty" yaml:"DisableBodyConsumptionOnUnmarshal" toml:"DisableBodyConsumptionOnUnmarshal"`
|
||||||
// FireEmptyFormError returns if set to tue true then the `context.ReadBody/ReadForm`
|
// FireEmptyFormError returns if set to tue true then the `context.ReadBody/ReadForm`
|
||||||
// will return an `iris.ErrEmptyForm` on empty request form data.
|
// will return an `iris.ErrEmptyForm` on empty request form data.
|
||||||
|
|
|
@ -2095,15 +2095,6 @@ func (ctx *Context) SetMaxRequestBodySize(limitOverBytes int64) {
|
||||||
ctx.request.Body = http.MaxBytesReader(ctx.writer, ctx.request.Body, limitOverBytes)
|
ctx.request.Body = http.MaxBytesReader(ctx.writer, ctx.request.Body, limitOverBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBody reads and returns the request body.
|
|
||||||
// The default behavior for the http request reader is to consume the data readen
|
|
||||||
// but you can change that behavior by passing the `WithoutBodyConsumptionOnUnmarshal` iris option.
|
|
||||||
//
|
|
||||||
// However, whenever you can use the `ctx.Request().Body` instead.
|
|
||||||
func (ctx *Context) GetBody() ([]byte, error) {
|
|
||||||
return GetBody(ctx.request, ctx.app.ConfigurationReadOnly().GetDisableBodyConsumptionOnUnmarshal())
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBody reads and returns the request body.
|
// GetBody reads and returns the request body.
|
||||||
func GetBody(r *http.Request, resetBody bool) ([]byte, error) {
|
func GetBody(r *http.Request, resetBody bool) ([]byte, error) {
|
||||||
data, err := ioutil.ReadAll(r.Body)
|
data, err := ioutil.ReadAll(r.Body)
|
||||||
|
@ -2120,6 +2111,31 @@ func GetBody(r *http.Request, resetBody bool) ([]byte, error) {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const disableRequestBodyConsumptionContextKey = "iris.request.body.record"
|
||||||
|
|
||||||
|
// RecordBody same as the Application's DisableBodyConsumptionOnUnmarshal configuration field
|
||||||
|
// but acts for the current request.
|
||||||
|
// It makes the request body readable more than once.
|
||||||
|
func (ctx *Context) RecordBody() {
|
||||||
|
ctx.Values().Set(disableRequestBodyConsumptionContextKey, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRecordingBody reports whether the request body can be readen multiple times.
|
||||||
|
func (ctx *Context) IsRecordingBody() bool {
|
||||||
|
return ctx.Values().GetBoolDefault(disableRequestBodyConsumptionContextKey,
|
||||||
|
ctx.app.ConfigurationReadOnly().GetDisableBodyConsumptionOnUnmarshal())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBody reads and returns the request body.
|
||||||
|
// The default behavior for the http request reader is to consume the data readen
|
||||||
|
// but you can change that behavior by passing the `WithoutBodyConsumptionOnUnmarshal` Iris option
|
||||||
|
// or by calling the `RecordBody` method.
|
||||||
|
//
|
||||||
|
// However, whenever you can use the `ctx.Request().Body` instead.
|
||||||
|
func (ctx *Context) GetBody() ([]byte, error) {
|
||||||
|
return GetBody(ctx.request, ctx.IsRecordingBody())
|
||||||
|
}
|
||||||
|
|
||||||
// Validator is the validator for request body on Context methods such as
|
// Validator is the validator for request body on Context methods such as
|
||||||
// ReadJSON, ReadMsgPack, ReadXML, ReadYAML, ReadForm, ReadQuery, ReadBody and e.t.c.
|
// ReadJSON, ReadMsgPack, ReadXML, ReadYAML, ReadForm, ReadQuery, ReadBody and e.t.c.
|
||||||
type Validator interface {
|
type Validator interface {
|
||||||
|
@ -2395,6 +2411,7 @@ func (ctx *Context) ReadBody(ptr interface{}) error {
|
||||||
switch ctx.GetContentTypeRequested() {
|
switch ctx.GetContentTypeRequested() {
|
||||||
case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue:
|
case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue:
|
||||||
return ctx.ReadXML(ptr)
|
return ctx.ReadXML(ptr)
|
||||||
|
// "%v reflect.Indirect(reflect.ValueOf(ptr)).Interface())
|
||||||
case ContentYAMLHeaderValue:
|
case ContentYAMLHeaderValue:
|
||||||
return ctx.ReadYAML(ptr)
|
return ctx.ReadYAML(ptr)
|
||||||
case ContentFormHeaderValue, ContentFormMultipartHeaderValue:
|
case ContentFormHeaderValue, ContentFormMultipartHeaderValue:
|
||||||
|
|
|
@ -64,9 +64,9 @@ type DirCacheOptions struct {
|
||||||
// See `DefaultDirOptions`.
|
// See `DefaultDirOptions`.
|
||||||
type DirOptions struct {
|
type DirOptions struct {
|
||||||
// Defaults to "/index.html", if request path is ending with **/*/$IndexName
|
// Defaults to "/index.html", if request path is ending with **/*/$IndexName
|
||||||
// then it redirects to **/*(/) which another handler is handling it,
|
// then it redirects to **/*(/).
|
||||||
// that another handler, called index handler, is auto-registered by the framework
|
// That index handler is registered automatically
|
||||||
// if end developer does not managed to handle it by hand.
|
// by the framework unless but it can be overridden.
|
||||||
IndexName string
|
IndexName string
|
||||||
// PushTargets filenames (map's value) to
|
// PushTargets filenames (map's value) to
|
||||||
// be served without additional client's requests (HTTP/2 Push)
|
// be served without additional client's requests (HTTP/2 Push)
|
||||||
|
@ -94,10 +94,12 @@ type DirOptions struct {
|
||||||
// When files should served under compression.
|
// When files should served under compression.
|
||||||
Compress bool
|
Compress bool
|
||||||
|
|
||||||
// List the files inside the current requested directory if `IndexName` not found.
|
// List the files inside the current requested
|
||||||
|
// directory if `IndexName` not found.
|
||||||
ShowList bool
|
ShowList bool
|
||||||
// If `ShowList` is true then this function will be used instead
|
// If `ShowList` is true then this function will be used instead
|
||||||
// of the default one to show the list of files of a current requested directory(dir).
|
// of the default one to show the list of files
|
||||||
|
// of a current requested directory(dir).
|
||||||
// See `DirListRich` package-level function too.
|
// See `DirListRich` package-level function too.
|
||||||
DirList DirListFunc
|
DirList DirListFunc
|
||||||
|
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -29,7 +29,6 @@ require (
|
||||||
github.com/mediocregopher/radix/v3 v3.5.2
|
github.com/mediocregopher/radix/v3 v3.5.2
|
||||||
github.com/microcosm-cc/bluemonday v1.0.4
|
github.com/microcosm-cc/bluemonday v1.0.4
|
||||||
github.com/russross/blackfriday/v2 v2.0.1
|
github.com/russross/blackfriday/v2 v2.0.1
|
||||||
github.com/ryanuber/columnize v2.1.2+incompatible
|
|
||||||
github.com/schollz/closestmatch v2.1.0+incompatible
|
github.com/schollz/closestmatch v2.1.0+incompatible
|
||||||
github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693
|
github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693
|
||||||
github.com/tdewolff/minify/v2 v2.9.2
|
github.com/tdewolff/minify/v2 v2.9.2
|
||||||
|
|
251
middleware/accesslog/accesslog.go
Normal file
251
middleware/accesslog/accesslog.go
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
package accesslog
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccessLog is a middleware which prints information
|
||||||
|
// incoming HTTP requests.
|
||||||
|
//
|
||||||
|
// Sample access log line:
|
||||||
|
// 2020-08-22 00:44:20|1ms|POST|/read_body||200|{"id":10,"name":"Tim","age":22}|{"message":"OK"}|
|
||||||
|
//
|
||||||
|
// Look `New`, `File` package-level functions
|
||||||
|
// and its `Handler` method to learn more.
|
||||||
|
type AccessLog struct {
|
||||||
|
mu sync.Mutex // ensures atomic writes.
|
||||||
|
// If not nil then it overrides the Application's Logger.
|
||||||
|
// Useful to write to a file.
|
||||||
|
// If multiple output required, then define an `io.MultiWriter`.
|
||||||
|
// See `SetOutput` and `AddOutput` methods too.
|
||||||
|
Writer io.Writer
|
||||||
|
// If not empty then each one of them is called on `Close` method.
|
||||||
|
Closers []io.Closer
|
||||||
|
|
||||||
|
// If not empty then it overrides the Application's configuration's TimeFormat field.
|
||||||
|
TimeFormat string
|
||||||
|
// Force minify request and response contents.
|
||||||
|
BodyMinify bool
|
||||||
|
// Enable request body logging.
|
||||||
|
// Note that, if this is true then it modifies the underline request's body type.
|
||||||
|
RequestBody bool
|
||||||
|
// Enable response body logging.
|
||||||
|
// Note that, if this is true then it uses a response recorder.
|
||||||
|
ResponseBody bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new AccessLog value with the default values.
|
||||||
|
// Writes to the Application's logger.
|
||||||
|
// Register by its `Handler` method.
|
||||||
|
// See `File` package-level function too.
|
||||||
|
func New() *AccessLog {
|
||||||
|
return &AccessLog{
|
||||||
|
BodyMinify: true,
|
||||||
|
RequestBody: true,
|
||||||
|
ResponseBody: true,
|
||||||
|
TimeFormat: "2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// File returns a new AccessLog value with the given "path"
|
||||||
|
// as the log's output file destination.
|
||||||
|
// Register by its `Handler` method.
|
||||||
|
//
|
||||||
|
// A call of its `Close` method to unlock the underline
|
||||||
|
// file is required on program termination.
|
||||||
|
//
|
||||||
|
// It panics on error.
|
||||||
|
func File(path string) *AccessLog {
|
||||||
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ac := New()
|
||||||
|
ac.SetOutput(f)
|
||||||
|
return ac
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes to the log destination.
|
||||||
|
// It completes the io.Writer interface.
|
||||||
|
// Safe for concurrent use.
|
||||||
|
func (ac *AccessLog) Write(p []byte) (int, error) {
|
||||||
|
ac.mu.Lock()
|
||||||
|
n, err := ac.Writer.Write(p)
|
||||||
|
ac.mu.Unlock()
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOutput sets the log's output destination. Accepts one or more io.Writer values.
|
||||||
|
// Also, if a writer is a Closer, then it is automatically appended to the Closers.
|
||||||
|
func (ac *AccessLog) SetOutput(writers ...io.Writer) *AccessLog {
|
||||||
|
for _, w := range writers {
|
||||||
|
if closer, ok := w.(io.Closer); ok {
|
||||||
|
ac.Closers = append(ac.Closers, closer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ac.Writer = io.MultiWriter(writers...)
|
||||||
|
return ac
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOutput appends an io.Writer value to the existing writer.
|
||||||
|
func (ac *AccessLog) AddOutput(writers ...io.Writer) *AccessLog {
|
||||||
|
if ac.Writer != nil { // prepend if one exists.
|
||||||
|
writers = append([]io.Writer{ac.Writer}, writers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ac.SetOutput(writers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close calls each registered Closer's Close method.
|
||||||
|
// Exits when all close methods have been executed.
|
||||||
|
func (ac *AccessLog) Close() (err error) {
|
||||||
|
for _, closer := range ac.Closers {
|
||||||
|
cErr := closer.Close()
|
||||||
|
if cErr != nil {
|
||||||
|
if err == nil {
|
||||||
|
err = cErr
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("%v, %v", err, cErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler prints request information to the output destination.
|
||||||
|
// It is the main method of the AccessLog middleware.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// ac := New() or File("access.log")
|
||||||
|
// defer ac.Close()
|
||||||
|
// app.UseRouter(ac.Handler)
|
||||||
|
func (ac *AccessLog) Handler(ctx *context.Context) {
|
||||||
|
var (
|
||||||
|
startTime = time.Now()
|
||||||
|
// Store some values, as future handler chain
|
||||||
|
// can modify those (note: we could clone the request or context object too).
|
||||||
|
method = ctx.Method()
|
||||||
|
path = ctx.Path()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enable response recording.
|
||||||
|
if ac.ResponseBody {
|
||||||
|
ctx.Record()
|
||||||
|
}
|
||||||
|
// Enable reading the request body
|
||||||
|
// multiple times (route handler and this middleware).
|
||||||
|
if ac.RequestBody {
|
||||||
|
ctx.RecordBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed to the handlers chain.
|
||||||
|
ctx.Next()
|
||||||
|
|
||||||
|
latency := time.Since(startTime)
|
||||||
|
ac.after(ctx, latency, method, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AccessLog) after(ctx *context.Context, lat time.Duration, method, path string) {
|
||||||
|
var (
|
||||||
|
code = ctx.GetStatusCode() // response status code
|
||||||
|
// request and response data or error reading them.
|
||||||
|
requestBody string
|
||||||
|
responseBody string
|
||||||
|
|
||||||
|
// url parameters and path parameters separated by space,
|
||||||
|
// key=value key2=value2.
|
||||||
|
requestValues string
|
||||||
|
)
|
||||||
|
|
||||||
|
// any error handler stored ( ctx.SetErr or StopWith(Plain)Error )
|
||||||
|
if ctxErr := ctx.GetErr(); ctxErr != nil {
|
||||||
|
requestBody = fmt.Sprintf("error(%s)", ctxErr.Error())
|
||||||
|
} else if ac.RequestBody {
|
||||||
|
requestData, err := ctx.GetBody()
|
||||||
|
if err != nil {
|
||||||
|
requestBody = fmt.Sprintf("error(%s)", ctxErr.Error())
|
||||||
|
} else {
|
||||||
|
if ac.BodyMinify {
|
||||||
|
if minified, err := ctx.Application().Minifier().Bytes(ctx.GetContentTypeRequested(), requestData); err == nil {
|
||||||
|
requestBody = string(minified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Some content types, like the text/plain,
|
||||||
|
no need minifier. Should be printed with spaces and \n. */
|
||||||
|
if requestBody == "" {
|
||||||
|
requestBody = string(requestData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ac.RequestBody {
|
||||||
|
if responseData := ctx.Recorder().Body(); len(responseData) > 0 {
|
||||||
|
if ac.BodyMinify {
|
||||||
|
if minified, err := ctx.Application().Minifier().Bytes(ctx.GetContentType(), ctx.Recorder().Body()); err == nil {
|
||||||
|
responseBody = string(minified)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if responseBody == "" {
|
||||||
|
responseBody = string(responseData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
|
||||||
|
if !context.StatusCodeNotSuccessful(code) {
|
||||||
|
// collect path parameters on a successful request-response only.
|
||||||
|
ctx.Params().Visit(func(key, value string) {
|
||||||
|
buf.WriteString(key)
|
||||||
|
buf.WriteByte('=')
|
||||||
|
buf.WriteString(value)
|
||||||
|
buf.WriteByte(' ')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range ctx.URLParamsSorted() {
|
||||||
|
buf.WriteString(entry.Key)
|
||||||
|
buf.WriteByte('=')
|
||||||
|
buf.WriteString(entry.Value)
|
||||||
|
buf.WriteByte(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
if n := buf.Len(); n > 1 {
|
||||||
|
requestValues = buf.String()[0 : n-1] // remove last space.
|
||||||
|
}
|
||||||
|
|
||||||
|
timeFormat := ac.TimeFormat
|
||||||
|
if timeFormat == "" {
|
||||||
|
timeFormat = ctx.Application().ConfigurationReadOnly().GetTimeFormat()
|
||||||
|
}
|
||||||
|
|
||||||
|
w := ac.Writer
|
||||||
|
if w == nil {
|
||||||
|
w = ctx.Application().Logger().Printer
|
||||||
|
}
|
||||||
|
|
||||||
|
// the number of separators are the same, in order to be easier
|
||||||
|
// for 3rd-party programs to read the result log file.
|
||||||
|
fmt.Fprintf(w, "%s|%s|%s|%s|%s|%d|%s|%s|\n",
|
||||||
|
time.Now().Format(timeFormat),
|
||||||
|
lat,
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
requestValues,
|
||||||
|
code,
|
||||||
|
requestBody,
|
||||||
|
responseBody,
|
||||||
|
)
|
||||||
|
}
|
|
@ -48,20 +48,11 @@ type Config struct {
|
||||||
// Defaults to false.
|
// Defaults to false.
|
||||||
TraceRoute bool
|
TraceRoute bool
|
||||||
|
|
||||||
// Columns will display the logs as a formatted columns-rows text (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 output result as columns.
|
|
||||||
//
|
|
||||||
// Defaults to false.
|
|
||||||
Columns bool
|
|
||||||
|
|
||||||
// MessageContextKeys if not empty,
|
// MessageContextKeys if not empty,
|
||||||
// the middleware will try to fetch
|
// the middleware will try to fetch
|
||||||
// the contents with `ctx.Values().Get(MessageContextKey)`
|
// the contents with `ctx.Values().Get(MessageContextKey)`
|
||||||
// and if available then these contents will be
|
// and if available then these contents will be
|
||||||
// appended as part of the logs (with `%v`, in order to be able to set a struct too),
|
// appended as part of the logs (with `%v`, in order to be able to set a struct too),
|
||||||
// if Columns field was set to true then
|
|
||||||
// a new column will be added named 'Message'.
|
|
||||||
//
|
//
|
||||||
// Defaults to empty.
|
// Defaults to empty.
|
||||||
MessageContextKeys []string
|
MessageContextKeys []string
|
||||||
|
@ -71,8 +62,6 @@ type Config struct {
|
||||||
// the contents with `ctx.Values().Get(MessageHeaderKey)`
|
// the contents with `ctx.Values().Get(MessageHeaderKey)`
|
||||||
// and if available then these contents will be
|
// and if available then these contents will be
|
||||||
// appended as part of the logs (with `%v`, in order to be able to set a struct too),
|
// appended as part of the logs (with `%v`, in order to be able to set a struct too),
|
||||||
// if Columns field was set to true then
|
|
||||||
// a new column will be added named 'HeaderMessage'.
|
|
||||||
//
|
//
|
||||||
// Defaults to empty.
|
// Defaults to empty.
|
||||||
MessageHeaderKeys []string
|
MessageHeaderKeys []string
|
||||||
|
@ -93,7 +82,7 @@ type Config struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig returns a default config
|
// DefaultConfig returns a default config
|
||||||
// that have all boolean fields to true except `Columns`,
|
// that have all boolean fields to true,
|
||||||
// all strings are empty,
|
// all strings are empty,
|
||||||
// LogFunc and Skippers to nil as well.
|
// LogFunc and Skippers to nil as well.
|
||||||
func DefaultConfig() Config {
|
func DefaultConfig() Config {
|
||||||
|
@ -105,7 +94,6 @@ func DefaultConfig() Config {
|
||||||
PathAfterHandler: false,
|
PathAfterHandler: false,
|
||||||
Query: false,
|
Query: false,
|
||||||
TraceRoute: false,
|
TraceRoute: false,
|
||||||
Columns: false,
|
|
||||||
LogFunc: nil,
|
LogFunc: nil,
|
||||||
LogFuncCtx: nil,
|
LogFuncCtx: nil,
|
||||||
Skippers: nil,
|
Skippers: nil,
|
||||||
|
|
|
@ -7,8 +7,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
"github.com/kataras/iris/v12/context"
|
||||||
|
|
||||||
"github.com/ryanuber/columnize"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -120,12 +118,6 @@ func (l *requestLoggerMiddleware) ServeHTTP(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.config.Columns {
|
|
||||||
endTimeFormatted := endTime.Format("2006/01/02 - 15:04:05")
|
|
||||||
output := Columnize(endTimeFormatted, latency, status, ip, method, path, message, headerMessage)
|
|
||||||
_, _ = ctx.Application().Logger().Printer.Write([]byte(output))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// no new line, the framework's logger is responsible how to render each log.
|
// no new line, the framework's logger is responsible how to render each log.
|
||||||
line := fmt.Sprintf("%v %4v %s %s %s", status, latency, ip, method, path)
|
line := fmt.Sprintf("%v %4v %s %s %s", status, latency, ip, method, path)
|
||||||
if message != nil {
|
if message != nil {
|
||||||
|
@ -158,26 +150,3 @@ func (l *requestLoggerMiddleware) ServeHTTP(ctx *context.Context) {
|
||||||
ctx.GetCurrentRoute().Trace(ctx.Application().Logger().Printer, ctx.HandlerIndex(-1))
|
ctx.GetCurrentRoute().Trace(ctx.Application().Logger().Printer, ctx.HandlerIndex(-1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, message interface{}, headerMessage interface{}) string {
|
|
||||||
titles := "Time | Status | Latency | IP | Method | Path"
|
|
||||||
line := fmt.Sprintf("%s | %v | %4v | %s | %s | %s", nowFormatted, status, latency, ip, method, path)
|
|
||||||
if message != nil {
|
|
||||||
titles += " | Message"
|
|
||||||
line += fmt.Sprintf(" | %v", message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if headerMessage != nil {
|
|
||||||
titles += " | HeaderMessage"
|
|
||||||
line += fmt.Sprintf(" | %v", headerMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
outputC := []string{
|
|
||||||
titles,
|
|
||||||
line,
|
|
||||||
}
|
|
||||||
output := columnize.SimpleFormat(outputC) + "\n"
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user