2020-09-06 09:38:48 +02:00
|
|
|
package accesslog
|
|
|
|
|
|
|
|
import (
|
2020-09-07 17:04:35 +02:00
|
|
|
"bytes"
|
2020-09-06 09:38:48 +02:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2020-09-08 12:44:50 +02:00
|
|
|
"net/http/httputil"
|
2020-09-06 09:38:48 +02:00
|
|
|
"os"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/kataras/iris/v12/context"
|
2020-09-07 10:35:27 +02:00
|
|
|
"github.com/kataras/iris/v12/core/memstore"
|
2020-09-06 09:38:48 +02:00
|
|
|
)
|
|
|
|
|
2020-09-07 10:35:27 +02:00
|
|
|
// FieldExtractor extracts a log's entry key with its corresponding value.
|
|
|
|
// If key or value is empty then this field is not printed.
|
|
|
|
type FieldExtractor func(*context.Context) (string, interface{})
|
|
|
|
|
2020-09-07 15:31:43 +02:00
|
|
|
type (
|
|
|
|
// Clock is an interface which contains a single `Now` method.
|
|
|
|
// It can be used to set a static timer on end to end testing.
|
|
|
|
// See `AccessLog.Clock` field.
|
|
|
|
Clock interface{ Now() time.Time }
|
|
|
|
clockFunc func() time.Time
|
|
|
|
)
|
|
|
|
|
|
|
|
// Now completes the `Clock` interface.
|
|
|
|
func (c clockFunc) Now() time.Time {
|
|
|
|
return c()
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
// UTC returns time with UTC based location.
|
|
|
|
UTC = clockFunc(func() time.Time { return time.Now().UTC() })
|
|
|
|
// TClock accepts a static time.Time to use as
|
|
|
|
// accesslog's Now method on current log fired timestamp.
|
|
|
|
// Useful for testing.
|
|
|
|
TClock = func(t time.Time) clockFunc { return func() time.Time { return t } }
|
|
|
|
)
|
|
|
|
|
2020-09-06 09:38:48 +02:00
|
|
|
// 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.
|
2020-09-07 17:04:35 +02:00
|
|
|
// The destination writer.
|
2020-09-06 09:38:48 +02:00
|
|
|
// If multiple output required, then define an `io.MultiWriter`.
|
|
|
|
// See `SetOutput` and `AddOutput` methods too.
|
|
|
|
Writer io.Writer
|
2020-09-07 15:31:43 +02:00
|
|
|
// If enabled, it locks the underline Writer.
|
|
|
|
// It should be turned off if the given `Writer` is already protected with a locker.
|
2020-09-07 17:04:35 +02:00
|
|
|
// It is enabled when writer is os.Stdout/os.Stderr.
|
|
|
|
// You should manually set this field to true if you are not sure
|
|
|
|
// whether the underline Writer is protected.
|
2020-09-07 15:31:43 +02:00
|
|
|
//
|
2020-09-07 17:04:35 +02:00
|
|
|
// Defaults to true on *os.File and *bytes.Buffer, otherwise false.
|
2020-09-07 15:31:43 +02:00
|
|
|
LockWriter bool
|
|
|
|
|
2020-09-06 09:38:48 +02:00
|
|
|
// If not empty then each one of them is called on `Close` method.
|
|
|
|
Closers []io.Closer
|
|
|
|
|
2020-09-07 15:31:43 +02:00
|
|
|
// If not empty then overrides the time.Now to this custom clocker's `Now` method,
|
|
|
|
// useful for testing (see `TClock`) and
|
|
|
|
// on platforms that its internal clock is not compatible by default (advanced case) and
|
|
|
|
// to change the time location (e.g. `UTC`).
|
|
|
|
//
|
|
|
|
// This field is used to set the time the log fired.
|
|
|
|
// By default the middleware is using the local time, however
|
|
|
|
// can be changed to `UTC` too.
|
|
|
|
//
|
|
|
|
// Do NOT touch this field if you don't know what you're doing.
|
|
|
|
Clock Clock
|
|
|
|
|
2020-09-07 05:12:45 +02:00
|
|
|
// If true then the middleware will fire the logs in a separate
|
|
|
|
// go routine, making the request to finish first.
|
|
|
|
// The log will be printed based on a copy of the Request's Context instead.
|
|
|
|
//
|
|
|
|
// Defaults to false.
|
|
|
|
Async bool
|
2020-09-07 17:04:35 +02:00
|
|
|
// The time format for current time on log print.
|
|
|
|
// Defaults to the Iris Application's TimeFormat.
|
2020-09-06 09:38:48 +02:00
|
|
|
TimeFormat string
|
2020-09-08 12:44:50 +02:00
|
|
|
|
|
|
|
// The actual number of bytes received and sent on the network (headers + body).
|
|
|
|
// It is kind of "slow" operation as it uses the httputil to dumb request
|
|
|
|
// and response to get the total amount of bytes (headers + body).
|
|
|
|
BytesReceived bool
|
|
|
|
BytesSent bool
|
|
|
|
// Note: We could calculate only the bodies, which is a fast operation if we already
|
|
|
|
// have RequestBody and ResponseBody set to true but this is not an accurate measurement.
|
|
|
|
|
2020-09-06 09:38:48 +02:00
|
|
|
// 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
|
2020-09-07 05:12:45 +02:00
|
|
|
|
2020-09-07 10:35:27 +02:00
|
|
|
// Map log fields with custom request values.
|
|
|
|
// See `Log.Fields`.
|
|
|
|
Fields []FieldExtractor
|
|
|
|
// Note: We could use a map but that way we lose the
|
|
|
|
// order of registration so use a slice and
|
|
|
|
// take the field key from the extractor itself.
|
2020-09-07 05:12:45 +02:00
|
|
|
formatter Formatter
|
2020-09-06 09:38:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// New returns a new AccessLog value with the default values.
|
2020-09-07 17:04:35 +02:00
|
|
|
// Writes to the "w". Output be further modified through its `Set/AddOutput` methods.
|
2020-09-06 09:38:48 +02:00
|
|
|
// Register by its `Handler` method.
|
|
|
|
// See `File` package-level function too.
|
2020-09-07 05:12:45 +02:00
|
|
|
//
|
|
|
|
// Example: https://github.com/kataras/iris/tree/master/_examples/logging/request-logger/accesslog
|
2020-09-07 17:04:35 +02:00
|
|
|
func New(w io.Writer) *AccessLog {
|
|
|
|
ac := &AccessLog{
|
2020-09-08 12:44:50 +02:00
|
|
|
BytesReceived: true,
|
|
|
|
BytesSent: true,
|
|
|
|
BodyMinify: true,
|
|
|
|
RequestBody: true,
|
|
|
|
ResponseBody: true,
|
2020-09-06 09:38:48 +02:00
|
|
|
}
|
2020-09-07 17:04:35 +02:00
|
|
|
|
|
|
|
if w == nil {
|
|
|
|
w = os.Stdout
|
|
|
|
}
|
|
|
|
ac.SetOutput(w)
|
|
|
|
|
|
|
|
return ac
|
2020-09-06 09:38:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2020-09-07 17:04:35 +02:00
|
|
|
return New(f)
|
2020-09-06 09:38:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Write writes to the log destination.
|
|
|
|
// It completes the io.Writer interface.
|
|
|
|
// Safe for concurrent use.
|
|
|
|
func (ac *AccessLog) Write(p []byte) (int, error) {
|
2020-09-07 17:04:35 +02:00
|
|
|
if ac.LockWriter {
|
|
|
|
ac.mu.Lock()
|
|
|
|
}
|
2020-09-06 09:38:48 +02:00
|
|
|
n, err := ac.Writer.Write(p)
|
2020-09-07 17:04:35 +02:00
|
|
|
if ac.LockWriter {
|
|
|
|
ac.mu.Unlock()
|
|
|
|
}
|
2020-09-06 09:38:48 +02:00
|
|
|
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.
|
2020-09-07 05:12:45 +02:00
|
|
|
// Call it before `SetFormatter` and `Handler` methods.
|
2020-09-06 09:38:48 +02:00
|
|
|
func (ac *AccessLog) SetOutput(writers ...io.Writer) *AccessLog {
|
2020-09-07 17:04:35 +02:00
|
|
|
if len(writers) == 0 {
|
|
|
|
return ac
|
|
|
|
}
|
|
|
|
|
|
|
|
lockWriter := false
|
2020-09-06 09:38:48 +02:00
|
|
|
for _, w := range writers {
|
|
|
|
if closer, ok := w.(io.Closer); ok {
|
|
|
|
ac.Closers = append(ac.Closers, closer)
|
|
|
|
}
|
2020-09-07 17:04:35 +02:00
|
|
|
|
|
|
|
if !lockWriter {
|
|
|
|
switch w.(type) {
|
|
|
|
case *os.File, *bytes.Buffer: // force lock writer.
|
|
|
|
lockWriter = true
|
|
|
|
}
|
|
|
|
}
|
2020-09-06 09:38:48 +02:00
|
|
|
}
|
2020-09-07 17:04:35 +02:00
|
|
|
ac.LockWriter = lockWriter
|
2020-09-06 09:38:48 +02:00
|
|
|
|
2020-09-07 17:04:35 +02:00
|
|
|
if len(writers) == 1 {
|
2020-09-07 13:31:19 +02:00
|
|
|
ac.Writer = writers[0]
|
2020-09-07 17:04:35 +02:00
|
|
|
} else {
|
2020-09-07 13:31:19 +02:00
|
|
|
ac.Writer = io.MultiWriter(writers...)
|
|
|
|
}
|
|
|
|
|
2020-09-06 09:38:48 +02:00
|
|
|
return ac
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddOutput appends an io.Writer value to the existing writer.
|
2020-09-07 05:12:45 +02:00
|
|
|
// Call it before `SetFormatter` and `Handler` methods.
|
2020-09-06 09:38:48 +02:00
|
|
|
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...)
|
|
|
|
}
|
|
|
|
|
2020-09-07 05:12:45 +02:00
|
|
|
// SetFormatter sets a custom formatter to print the logs.
|
|
|
|
// Any custom output writers should be
|
|
|
|
// already registered before calling this method.
|
|
|
|
// Returns this AccessLog instance.
|
|
|
|
//
|
|
|
|
// Usage:
|
|
|
|
// ac.SetFormatter(&accesslog.JSON{Indent: " "})
|
|
|
|
func (ac *AccessLog) SetFormatter(f Formatter) *AccessLog {
|
2020-09-07 17:04:35 +02:00
|
|
|
if ac.Writer == nil {
|
|
|
|
panic("accesslog: SetFormatter called with nil Writer")
|
|
|
|
}
|
|
|
|
|
2020-09-07 05:12:45 +02:00
|
|
|
f.SetOutput(ac.Writer) // inject the writer here.
|
|
|
|
ac.formatter = f
|
|
|
|
return ac
|
|
|
|
}
|
|
|
|
|
2020-09-07 10:35:27 +02:00
|
|
|
// AddField maps a log entry with a value extracted by the Request Context.
|
|
|
|
// Not safe for concurrent use. Call it before `Handler` method.
|
|
|
|
func (ac *AccessLog) AddField(extractors ...FieldExtractor) *AccessLog {
|
|
|
|
ac.Fields = append(ac.Fields, extractors...)
|
|
|
|
return ac
|
|
|
|
}
|
|
|
|
|
2020-09-06 09:38:48 +02:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2020-09-08 12:44:50 +02:00
|
|
|
func (ac *AccessLog) shouldReadRequestBody() bool {
|
|
|
|
return ac.RequestBody || ac.BytesReceived
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ac *AccessLog) shouldReadResponseBody() bool {
|
|
|
|
return ac.ResponseBody || ac.BytesSent
|
|
|
|
}
|
|
|
|
|
2020-09-06 09:38:48 +02:00
|
|
|
// Handler prints request information to the output destination.
|
|
|
|
// It is the main method of the AccessLog middleware.
|
|
|
|
//
|
|
|
|
// Usage:
|
2020-09-07 17:04:35 +02:00
|
|
|
// ac := New(io.Writer) or File("access.log")
|
2020-09-06 09:38:48 +02:00
|
|
|
// 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.
|
2020-09-08 12:44:50 +02:00
|
|
|
if ac.shouldReadResponseBody() {
|
2020-09-06 09:38:48 +02:00
|
|
|
ctx.Record()
|
|
|
|
}
|
|
|
|
// Enable reading the request body
|
|
|
|
// multiple times (route handler and this middleware).
|
2020-09-08 12:44:50 +02:00
|
|
|
if ac.shouldReadRequestBody() {
|
2020-09-06 09:38:48 +02:00
|
|
|
ctx.RecordBody()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Proceed to the handlers chain.
|
|
|
|
ctx.Next()
|
|
|
|
|
|
|
|
latency := time.Since(startTime)
|
2020-09-07 05:12:45 +02:00
|
|
|
if ac.Async {
|
|
|
|
ctxCopy := ctx.Clone()
|
|
|
|
go ac.after(ctxCopy, latency, method, path)
|
|
|
|
} else {
|
|
|
|
// wait to finish before proceed with response end.
|
|
|
|
ac.after(ctx, latency, method, path)
|
|
|
|
}
|
2020-09-06 09:38:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (ac *AccessLog) after(ctx *context.Context, lat time.Duration, method, path string) {
|
|
|
|
var (
|
|
|
|
// request and response data or error reading them.
|
2020-09-08 12:44:50 +02:00
|
|
|
requestBody string
|
|
|
|
responseBody string
|
|
|
|
bytesReceived int
|
|
|
|
bytesSent int
|
2020-09-06 09:38:48 +02:00
|
|
|
)
|
|
|
|
|
2020-09-08 12:44:50 +02:00
|
|
|
if ac.shouldReadRequestBody() {
|
|
|
|
// any error handler stored ( ctx.SetErr or StopWith(Plain)Error )
|
|
|
|
if ctxErr := ctx.GetErr(); ctxErr != nil {
|
|
|
|
// If there is an error here
|
|
|
|
// we may need to NOT read the body for security reasons, e.g.
|
|
|
|
// unauthorized user tries to send a malicious body.
|
2020-09-06 09:38:48 +02:00
|
|
|
requestBody = fmt.Sprintf("error(%s)", ctxErr.Error())
|
|
|
|
} else {
|
2020-09-08 12:44:50 +02:00
|
|
|
requestData, err := ctx.GetBody()
|
|
|
|
requestBodyLength := len(requestData)
|
|
|
|
if err != nil && ac.RequestBody {
|
|
|
|
requestBody = fmt.Sprintf("error(%s)", err)
|
|
|
|
} else if requestBodyLength > 0 {
|
|
|
|
if ac.RequestBody {
|
|
|
|
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)
|
|
|
|
}
|
2020-09-06 09:38:48 +02:00
|
|
|
}
|
|
|
|
}
|
2020-09-08 12:44:50 +02:00
|
|
|
|
|
|
|
if ac.BytesReceived {
|
|
|
|
// Unfortunally the DumpRequest cannot read the body
|
|
|
|
// length as expected (see postman's i/o values)
|
|
|
|
// so we had to read the data length manually even if RequestBody/ResponseBody
|
|
|
|
// are false, extra operation if they are enabled is to minify their log entry representation.
|
|
|
|
|
|
|
|
b, _ := httputil.DumpRequest(ctx.Request(), false)
|
|
|
|
bytesReceived = len(b) + requestBodyLength
|
2020-09-06 09:38:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-08 12:44:50 +02:00
|
|
|
if ac.shouldReadResponseBody() {
|
|
|
|
responseData := ctx.Recorder().Body()
|
|
|
|
responseBodyLength := len(responseData)
|
|
|
|
if ac.ResponseBody && responseBodyLength > 0 {
|
2020-09-06 09:38:48 +02:00
|
|
|
if ac.BodyMinify {
|
2020-09-08 12:44:50 +02:00
|
|
|
if minified, err := ctx.Application().Minifier().Bytes(ctx.GetContentType(), responseData); err == nil {
|
2020-09-06 09:38:48 +02:00
|
|
|
responseBody = string(minified)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if responseBody == "" {
|
|
|
|
responseBody = string(responseData)
|
|
|
|
}
|
|
|
|
}
|
2020-09-08 12:44:50 +02:00
|
|
|
|
|
|
|
if ac.BytesSent {
|
|
|
|
resp := ctx.Recorder().Result()
|
|
|
|
b, _ := httputil.DumpResponse(resp, false)
|
|
|
|
dateLengthProx := 38 /* it's actually ~37 */
|
|
|
|
if resp.Header.Get("Date") != "" {
|
|
|
|
dateLengthProx = 0 // dump response calculated it.
|
|
|
|
}
|
|
|
|
bytesSent = len(b) + responseBodyLength + dateLengthProx
|
|
|
|
}
|
2020-09-06 09:38:48 +02:00
|
|
|
}
|
|
|
|
|
2020-09-07 10:35:27 +02:00
|
|
|
// Grab any custom fields.
|
|
|
|
var fields []memstore.Entry
|
|
|
|
|
|
|
|
if n := len(ac.Fields); n > 0 {
|
2020-09-07 10:53:16 +02:00
|
|
|
fields = make([]memstore.Entry, 0, n)
|
2020-09-07 10:35:27 +02:00
|
|
|
|
|
|
|
for _, extract := range ac.Fields {
|
|
|
|
key, value := extract(ctx)
|
|
|
|
if key == "" || value == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
fields = append(fields, memstore.Entry{Key: key, ValueRaw: value})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
timeFormat := ac.TimeFormat
|
|
|
|
if timeFormat == "" {
|
|
|
|
timeFormat = ctx.Application().ConfigurationReadOnly().GetTimeFormat()
|
|
|
|
}
|
|
|
|
|
2020-09-07 15:31:43 +02:00
|
|
|
if err := ac.Print(ctx,
|
|
|
|
// latency between begin and finish of the handlers chain.
|
|
|
|
lat,
|
|
|
|
timeFormat,
|
|
|
|
// response code.
|
|
|
|
ctx.GetStatusCode(),
|
|
|
|
// original request's method and path.
|
|
|
|
method, path,
|
|
|
|
requestBody, responseBody,
|
2020-09-08 12:44:50 +02:00
|
|
|
bytesReceived, bytesSent,
|
2020-09-07 15:31:43 +02:00
|
|
|
ctx.Params(), ctx.URLParamsSorted(), fields,
|
|
|
|
); err != nil {
|
|
|
|
ctx.Application().Logger().Errorf("accesslog: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Print writes a log manually.
|
|
|
|
// The `Handler` method calls it.
|
2020-09-08 12:44:50 +02:00
|
|
|
func (ac *AccessLog) Print(ctx *context.Context, latency time.Duration, timeFormat string, code int, method, path, reqBody, respBody string, bytesReceived, bytesSent int, params *context.RequestParams, query []memstore.StringEntry, fields []memstore.Entry) error {
|
2020-09-07 15:31:43 +02:00
|
|
|
var now time.Time
|
|
|
|
|
|
|
|
if ac.Clock != nil {
|
|
|
|
now = ac.Clock.Now()
|
|
|
|
} else {
|
|
|
|
now = time.Now()
|
|
|
|
}
|
|
|
|
|
2020-09-07 05:12:45 +02:00
|
|
|
if f := ac.formatter; f != nil {
|
|
|
|
log := &Log{
|
2020-09-08 12:44:50 +02:00
|
|
|
Logger: ac,
|
|
|
|
Now: now,
|
|
|
|
TimeFormat: timeFormat,
|
|
|
|
Timestamp: now.Unix(),
|
|
|
|
Latency: latency,
|
|
|
|
Method: method,
|
|
|
|
Path: path,
|
|
|
|
Code: code,
|
|
|
|
Query: query,
|
|
|
|
PathParams: params.Store,
|
|
|
|
Fields: fields,
|
|
|
|
BytesReceived: bytesReceived,
|
|
|
|
BytesSent: bytesSent,
|
|
|
|
Request: reqBody,
|
|
|
|
Response: respBody,
|
|
|
|
Ctx: ctx, // ctx should only be used here, it may be nil on testing.
|
2020-09-07 05:12:45 +02:00
|
|
|
}
|
|
|
|
|
2020-09-07 10:35:27 +02:00
|
|
|
if err := f.Format(log); err != nil {
|
2020-09-07 15:31:43 +02:00
|
|
|
return err
|
2020-09-07 05:12:45 +02:00
|
|
|
}
|
2020-09-07 15:31:43 +02:00
|
|
|
|
|
|
|
// OK, it's handled, exit now.
|
|
|
|
return nil
|
2020-09-07 05:12:45 +02:00
|
|
|
}
|
|
|
|
|
2020-09-07 15:31:43 +02:00
|
|
|
// url parameters, path parameters and custom fields separated by space,
|
|
|
|
// key=value key2=value2.
|
|
|
|
requestValues := parseRequestValues(code, params, query, fields)
|
2020-09-06 09:38:48 +02:00
|
|
|
|
|
|
|
// the number of separators are the same, in order to be easier
|
|
|
|
// for 3rd-party programs to read the result log file.
|
2020-09-08 12:44:50 +02:00
|
|
|
_, err := fmt.Fprintf(ac, "%s|%s|%s|%s|%s|%d|%s|%s|%s|%s|\n",
|
2020-09-07 05:12:45 +02:00
|
|
|
now.Format(timeFormat),
|
2020-09-07 15:31:43 +02:00
|
|
|
latency,
|
2020-09-06 09:38:48 +02:00
|
|
|
method,
|
|
|
|
path,
|
|
|
|
requestValues,
|
|
|
|
code,
|
2020-09-08 12:44:50 +02:00
|
|
|
formatBytes(bytesReceived),
|
|
|
|
formatBytes(bytesSent),
|
2020-09-07 15:31:43 +02:00
|
|
|
reqBody,
|
|
|
|
respBody,
|
2020-09-06 09:38:48 +02:00
|
|
|
)
|
2020-09-07 15:31:43 +02:00
|
|
|
|
|
|
|
return err
|
2020-09-06 09:38:48 +02:00
|
|
|
}
|