mirror of
https://github.com/kataras/iris.git
synced 2026-01-08 04:21:57 +00:00
add accesslog middleware (rel: #1601)
This commit is contained in:
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user