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:
|
||||
|
||||
- `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.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.
|
||||
|
@ -643,6 +645,7 @@ New Context Methods:
|
|||
|
||||
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.
|
||||
- `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).
|
||||
|
|
3
NOTICE
3
NOTICE
|
@ -35,9 +35,6 @@ Revision ID: ab226d925aa394ccecf01e515ea8479367e0961c
|
|||
closestmatch 1fbe626be92eb4c https://github.com/schollz/closestmatch
|
||||
347d182cae9f8f0
|
||||
0a046bf2f4
|
||||
columnize 9e6335e58db3b4c https://github.com/ryanuber/columnize
|
||||
fe3c3c5c881f51f
|
||||
fbc1091b34
|
||||
compress 4a2d40e4b07e5b3 https://github.com/klauspost/compress
|
||||
d333bc0569facd0
|
||||
f2dbf4ef39
|
||||
|
|
|
@ -72,8 +72,7 @@
|
|||
* [Sitemap](routing/sitemap/main.go)
|
||||
* Logging
|
||||
* [Request Logger](logging/request-logger/main.go)
|
||||
* [Log requests and responses to access.log](logging/request-logger/request-logger-access-log-file)
|
||||
* [Log Requests to a File](logging/request-logger/request-logger-file/main.go)
|
||||
* [Log requests and responses to access.log](logging/request-logger/accesslog)
|
||||
* [Log Requests to a JSON File](logging/request-logger/request-logger-file-json/main.go)
|
||||
* [Application File Logger](logging/file-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,
|
||||
// Shows information about the executed route.
|
||||
TraceRoute: true,
|
||||
// Columns: true,
|
||||
|
||||
// if !empty then its contents derives from `ctx.Values().Get("logger_message")
|
||||
// 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.
|
||||
// The body will not be changed and existing data before the
|
||||
// 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"`
|
||||
// FireEmptyFormError returns if set to tue true then the `context.ReadBody/ReadForm`
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func GetBody(r *http.Request, resetBody bool) ([]byte, error) {
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
|
@ -2120,6 +2111,31 @@ func GetBody(r *http.Request, resetBody bool) ([]byte, error) {
|
|||
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
|
||||
// ReadJSON, ReadMsgPack, ReadXML, ReadYAML, ReadForm, ReadQuery, ReadBody and e.t.c.
|
||||
type Validator interface {
|
||||
|
@ -2395,6 +2411,7 @@ func (ctx *Context) ReadBody(ptr interface{}) error {
|
|||
switch ctx.GetContentTypeRequested() {
|
||||
case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue:
|
||||
return ctx.ReadXML(ptr)
|
||||
// "%v reflect.Indirect(reflect.ValueOf(ptr)).Interface())
|
||||
case ContentYAMLHeaderValue:
|
||||
return ctx.ReadYAML(ptr)
|
||||
case ContentFormHeaderValue, ContentFormMultipartHeaderValue:
|
||||
|
|
|
@ -64,9 +64,9 @@ type DirCacheOptions struct {
|
|||
// See `DefaultDirOptions`.
|
||||
type DirOptions struct {
|
||||
// Defaults to "/index.html", if request path is ending with **/*/$IndexName
|
||||
// then it redirects to **/*(/) which another handler is handling it,
|
||||
// that another handler, called index handler, is auto-registered by the framework
|
||||
// if end developer does not managed to handle it by hand.
|
||||
// then it redirects to **/*(/).
|
||||
// That index handler is registered automatically
|
||||
// by the framework unless but it can be overridden.
|
||||
IndexName string
|
||||
// PushTargets filenames (map's value) to
|
||||
// be served without additional client's requests (HTTP/2 Push)
|
||||
|
@ -94,10 +94,12 @@ type DirOptions struct {
|
|||
// When files should served under compression.
|
||||
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
|
||||
// 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.
|
||||
DirList DirListFunc
|
||||
|
||||
|
|
1
go.mod
1
go.mod
|
@ -29,7 +29,6 @@ require (
|
|||
github.com/mediocregopher/radix/v3 v3.5.2
|
||||
github.com/microcosm-cc/bluemonday v1.0.4
|
||||
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/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693
|
||||
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.
|
||||
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,
|
||||
// the middleware will try to fetch
|
||||
// the contents with `ctx.Values().Get(MessageContextKey)`
|
||||
// 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),
|
||||
// if Columns field was set to true then
|
||||
// a new column will be added named 'Message'.
|
||||
//
|
||||
// Defaults to empty.
|
||||
MessageContextKeys []string
|
||||
|
@ -71,8 +62,6 @@ type Config struct {
|
|||
// the contents with `ctx.Values().Get(MessageHeaderKey)`
|
||||
// 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),
|
||||
// if Columns field was set to true then
|
||||
// a new column will be added named 'HeaderMessage'.
|
||||
//
|
||||
// Defaults to empty.
|
||||
MessageHeaderKeys []string
|
||||
|
@ -93,7 +82,7 @@ type Config struct {
|
|||
}
|
||||
|
||||
// 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,
|
||||
// LogFunc and Skippers to nil as well.
|
||||
func DefaultConfig() Config {
|
||||
|
@ -105,7 +94,6 @@ func DefaultConfig() Config {
|
|||
PathAfterHandler: false,
|
||||
Query: false,
|
||||
TraceRoute: false,
|
||||
Columns: false,
|
||||
LogFunc: nil,
|
||||
LogFuncCtx: nil,
|
||||
Skippers: nil,
|
||||
|
|
|
@ -7,8 +7,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12/context"
|
||||
|
||||
"github.com/ryanuber/columnize"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -120,12 +118,6 @@ func (l *requestLoggerMiddleware) ServeHTTP(ctx *context.Context) {
|
|||
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.
|
||||
line := fmt.Sprintf("%v %4v %s %s %s", status, latency, ip, method, path)
|
||||
if message != nil {
|
||||
|
@ -158,26 +150,3 @@ func (l *requestLoggerMiddleware) ServeHTTP(ctx *context.Context) {
|
|||
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