accesslog: LockWriter (to lock unprotected writers) and Clock (for testing)

relative to: https://github.com/kataras/iris/issues/1601
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-09-07 16:31:43 +03:00
parent 4c6568b874
commit e96a009d3f
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
3 changed files with 173 additions and 27 deletions

View File

@ -20,6 +20,7 @@ func main() {
} }
ac := accesslog.New() ac := accesslog.New()
ac.SetOutput(w) ac.SetOutput(w)
/* /*
Use a file directly: Use a file directly:
ac := accesslog.File("./access.log") ac := accesslog.File("./access.log")
@ -27,11 +28,15 @@ func main() {
Log after the response was sent: Log after the response was sent:
ac.Async = true ac.Async = true
Protect writer with locks.
On this example this is not required but enable it on os.Stdout and e.t.c:
ac.LockWriter = true
Custom Time Format: Custom Time Format:
ac.TimeFormat = "" ac.TimeFormat = ""
Add second output: Add second output:
ac.AddOutput(os.Stdout) ac.AddOutput(app.Logger().Printer)
Change format (after output was set): Change format (after output was set):
ac.SetFormatter(&accesslog.JSON{Indent: " "}) ac.SetFormatter(&accesslog.JSON{Indent: " "})

View File

@ -15,6 +15,28 @@ import (
// If key or value is empty then this field is not printed. // If key or value is empty then this field is not printed.
type FieldExtractor func(*context.Context) (string, interface{}) type FieldExtractor func(*context.Context) (string, interface{})
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 } }
)
// AccessLog is a middleware which prints information // AccessLog is a middleware which prints information
// incoming HTTP requests. // incoming HTTP requests.
// //
@ -30,9 +52,30 @@ type AccessLog struct {
// If multiple output required, then define an `io.MultiWriter`. // If multiple output required, then define an `io.MultiWriter`.
// See `SetOutput` and `AddOutput` methods too. // See `SetOutput` and `AddOutput` methods too.
Writer io.Writer Writer io.Writer
// If enabled, it locks the underline Writer.
// It should be turned off if the given `Writer` is already protected with a locker.
// It should be enabled when you don't know if the writer locks itself
// or when the writer is os.Stdout/os.Stderr and e.t.c.
//
// Defaults to false,
// as the default Iris Application's Logger is protected with mutex.
LockWriter bool
// If not empty then each one of them is called on `Close` method. // If not empty then each one of them is called on `Close` method.
Closers []io.Closer Closers []io.Closer
// 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
// If true then the middleware will fire the logs in a separate // If true then the middleware will fire the logs in a separate
// go routine, making the request to finish first. // go routine, making the request to finish first.
// The log will be printed based on a copy of the Request's Context instead. // The log will be printed based on a copy of the Request's Context instead.
@ -60,7 +103,7 @@ type AccessLog struct {
} }
// New returns a new AccessLog value with the default values. // New returns a new AccessLog value with the default values.
// Writes to the Application's logger. // Writes to the Application's logger. Output be modified through its `SetOutput` method.
// Register by its `Handler` method. // Register by its `Handler` method.
// See `File` package-level function too. // See `File` package-level function too.
// //
@ -68,6 +111,7 @@ type AccessLog struct {
func New() *AccessLog { func New() *AccessLog {
return &AccessLog{ return &AccessLog{
Async: false, Async: false,
LockWriter: false,
BodyMinify: true, BodyMinify: true,
RequestBody: true, RequestBody: true,
ResponseBody: true, ResponseBody: true,
@ -213,15 +257,9 @@ func (ac *AccessLog) Handler(ctx *context.Context) {
func (ac *AccessLog) after(ctx *context.Context, lat time.Duration, method, path string) { func (ac *AccessLog) after(ctx *context.Context, lat time.Duration, method, path string) {
var ( var (
now = time.Now()
code = ctx.GetStatusCode() // response status code
// request and response data or error reading them. // request and response data or error reading them.
requestBody string requestBody string
responseBody 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 ) // any error handler stored ( ctx.SetErr or StopWith(Plain)Error )
@ -281,49 +319,91 @@ func (ac *AccessLog) after(ctx *context.Context, lat time.Duration, method, path
timeFormat = ctx.Application().ConfigurationReadOnly().GetTimeFormat() timeFormat = ctx.Application().ConfigurationReadOnly().GetTimeFormat()
} }
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,
ctx.Params(), ctx.URLParamsSorted(), fields,
); err != nil {
ctx.Application().Logger().Errorf("accesslog: %v", err)
}
}
// Print writes a log manually.
// The `Handler` method calls it.
func (ac *AccessLog) Print(ctx *context.Context, latency time.Duration, timeFormat string, code int, method, path, reqBody, respBody string, params *context.RequestParams, query []memstore.StringEntry, fields []memstore.Entry) error {
var now time.Time
if ac.Clock != nil {
now = ac.Clock.Now()
} else {
now = time.Now()
}
if f := ac.formatter; f != nil { if f := ac.formatter; f != nil {
log := &Log{ log := &Log{
Logger: ac, Logger: ac,
Now: now, Now: now,
TimeFormat: timeFormat, TimeFormat: timeFormat,
Timestamp: now.Unix(), Timestamp: now.Unix(),
Latency: lat, Latency: latency,
Method: method, Method: method,
Path: path, Path: path,
Code: code, Code: code,
Query: ctx.URLParamsSorted(), Query: query,
PathParams: ctx.Params().Store, PathParams: params.Store,
Fields: fields, Fields: fields,
Request: requestBody, Request: reqBody,
Response: responseBody, Response: respBody,
Ctx: ctx, Ctx: ctx, // ctx should only be used here, it may be nil on testing.
} }
if err := f.Format(log); err != nil { if err := f.Format(log); err != nil {
ctx.Application().Logger().Errorf("accesslog: %v", err) return err
}
// OK, it's handled, exit now.
return nil
}
// url parameters, path parameters and custom fields separated by space,
// key=value key2=value2.
requestValues := parseRequestValues(code, params, query, fields)
useLocker := ac.LockWriter
w := ac.Writer
if w == nil {
if ctx != nil {
w = ctx.Application().Logger().Printer
} else { } else {
// OK, it's handled, exit now. w = os.Stdout
return useLocker = true // force lock.
} }
} }
requestValues = parseRequestValues(code, ctx.Params(), ctx.URLParamsSorted(), fields) if useLocker {
ac.mu.Lock()
w := ac.Writer
if w == nil {
w = ctx.Application().Logger().Printer
} }
// the number of separators are the same, in order to be easier // the number of separators are the same, in order to be easier
// for 3rd-party programs to read the result log file. // for 3rd-party programs to read the result log file.
fmt.Fprintf(w, "%s|%s|%s|%s|%s|%d|%s|%s|\n", _, err := fmt.Fprintf(w, "%s|%s|%s|%s|%s|%d|%s|%s|\n",
now.Format(timeFormat), now.Format(timeFormat),
lat, latency,
method, method,
path, path,
requestValues, requestValues,
code, code,
requestBody, reqBody,
responseBody, respBody,
) )
if useLocker {
ac.mu.Unlock()
}
return err
} }

View File

@ -0,0 +1,61 @@
package accesslog
import (
"bytes"
"sync"
"testing"
"time"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/memstore"
)
func TestAccessLogPrint_Simple(t *testing.T) {
t.Parallel()
const goroutinesN = 42
w := new(bytes.Buffer)
ac := New()
ac.Writer = w
ac.LockWriter = true
ac.Clock = TClock(time.Time{})
var (
expected string
wg = new(sync.WaitGroup)
)
for i := 0; i < goroutinesN; i++ {
wg.Add(1)
expected += "0001-01-01 00:00:00|1s|GET|/path_value?url_query=url_query_value|path_param=path_param_value url_query=url_query_value custom=custom_value|200|Incoming|Outcoming|\n"
go func() {
defer wg.Done()
ac.Print(
nil,
1*time.Second,
ac.TimeFormat,
200,
"GET",
"/path_value?url_query=url_query_value",
"Incoming",
"Outcoming",
&context.RequestParams{
Store: []memstore.Entry{
{Key: "path_param", ValueRaw: "path_param_value"},
},
}, []memstore.StringEntry{
{Key: "url_query", Value: "url_query_value"},
}, []memstore.Entry{
{Key: "custom", ValueRaw: "custom_value"},
})
}()
}
wg.Wait()
if got := w.String(); expected != got {
t.Fatalf("expected printed result to be:\n'%s'\n\nbut got:\n'%s'", expected, got)
}
}