add accesslog middleware (rel: #1601)

This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-09-06 10:38:48 +03:00
parent bf9f7617e2
commit 0be856e54c
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
16 changed files with 339 additions and 347 deletions

View File

@ -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).

5
NOTICE
View File

@ -34,10 +34,7 @@ Revision ID: ab226d925aa394ccecf01e515ea8479367e0961c
d25082d2d2
closestmatch 1fbe626be92eb4c https://github.com/schollz/closestmatch
347d182cae9f8f0
0a046bf2f4
columnize 9e6335e58db3b4c https://github.com/ryanuber/columnize
fe3c3c5c881f51f
fbc1091b34
0a046bf2f4
compress 4a2d40e4b07e5b3 https://github.com/klauspost/compress
d333bc0569facd0
f2dbf4ef39

View File

@ -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)

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

View File

@ -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.

View File

@ -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,
)
}
}

View File

@ -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
}

View File

@ -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.

View File

@ -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:

View File

@ -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
View File

@ -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

View 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,
)
}

View File

@ -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,

View File

@ -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
}