new accesslog middleware: require the writer inside its constructor so we can 'detect' if we must force-lock a writer

relative to: https://github.com/kataras/iris/issues/1601
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-09-07 18:04:35 +03:00
parent e96a009d3f
commit dac834cf11
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
4 changed files with 70 additions and 68 deletions

View File

@ -18,8 +18,8 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
ac := accesslog.New() ac := accesslog.New(w)
ac.SetOutput(w) ac.TimeFormat = "2006-01-02 15:04:05"
/* /*
Use a file directly: Use a file directly:
@ -28,12 +28,9 @@ func main() {
Log after the response was sent: Log after the response was sent:
ac.Async = true ac.Async = true
Protect writer with locks. Force-protect writer with locks.
On this example this is not required but enable it on os.Stdout and e.t.c: On this example this is not required:
ac.LockWriter = true ac.LockWriter = true"
Custom Time Format:
ac.TimeFormat = ""
Add second output: Add second output:
ac.AddOutput(app.Logger().Printer) ac.AddOutput(app.Logger().Printer)

View File

@ -1,6 +1,7 @@
package accesslog package accesslog
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -47,18 +48,17 @@ var (
// and its `Handler` method to learn more. // and its `Handler` method to learn more.
type AccessLog struct { type AccessLog struct {
mu sync.Mutex // ensures atomic writes. mu sync.Mutex // ensures atomic writes.
// If not nil then it overrides the Application's Logger. // The destination writer.
// Useful to write to a file.
// 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. // 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 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 // It is enabled when writer is os.Stdout/os.Stderr.
// or when the writer is os.Stdout/os.Stderr and e.t.c. // You should manually set this field to true if you are not sure
// whether the underline Writer is protected.
// //
// Defaults to false, // Defaults to true on *os.File and *bytes.Buffer, otherwise false.
// as the default Iris Application's Logger is protected with mutex.
LockWriter bool 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.
@ -82,7 +82,8 @@ type AccessLog struct {
// //
// Defaults to false. // Defaults to false.
Async bool Async bool
// If not empty then it overrides the Application's configuration's TimeFormat field. // The time format for current time on log print.
// Defaults to the Iris Application's TimeFormat.
TimeFormat string TimeFormat string
// Force minify request and response contents. // Force minify request and response contents.
BodyMinify bool BodyMinify bool
@ -103,20 +104,24 @@ 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. Output be modified through its `SetOutput` method. // Writes to the "w". Output be further modified through its `Set/AddOutput` methods.
// Register by its `Handler` method. // Register by its `Handler` method.
// See `File` package-level function too. // See `File` package-level function too.
// //
// Example: https://github.com/kataras/iris/tree/master/_examples/logging/request-logger/accesslog // Example: https://github.com/kataras/iris/tree/master/_examples/logging/request-logger/accesslog
func New() *AccessLog { func New(w io.Writer) *AccessLog {
return &AccessLog{ ac := &AccessLog{
Async: false,
LockWriter: false,
BodyMinify: true, BodyMinify: true,
RequestBody: true, RequestBody: true,
ResponseBody: true, ResponseBody: true,
TimeFormat: "2006-01-02 15:04:05",
} }
if w == nil {
w = os.Stdout
}
ac.SetOutput(w)
return ac
} }
// File returns a new AccessLog value with the given "path" // File returns a new AccessLog value with the given "path"
@ -133,18 +138,20 @@ func File(path string) *AccessLog {
panic(err) panic(err)
} }
ac := New() return New(f)
ac.SetOutput(f)
return ac
} }
// Write writes to the log destination. // Write writes to the log destination.
// It completes the io.Writer interface. // It completes the io.Writer interface.
// Safe for concurrent use. // Safe for concurrent use.
func (ac *AccessLog) Write(p []byte) (int, error) { func (ac *AccessLog) Write(p []byte) (int, error) {
ac.mu.Lock() if ac.LockWriter {
ac.mu.Lock()
}
n, err := ac.Writer.Write(p) n, err := ac.Writer.Write(p)
ac.mu.Unlock() if ac.LockWriter {
ac.mu.Unlock()
}
return n, err return n, err
} }
@ -152,17 +159,28 @@ func (ac *AccessLog) Write(p []byte) (int, error) {
// Also, if a writer is a Closer, then it is automatically appended to the Closers. // Also, if a writer is a Closer, then it is automatically appended to the Closers.
// Call it before `SetFormatter` and `Handler` methods. // Call it before `SetFormatter` and `Handler` methods.
func (ac *AccessLog) SetOutput(writers ...io.Writer) *AccessLog { func (ac *AccessLog) SetOutput(writers ...io.Writer) *AccessLog {
if len(writers) == 0 {
return ac
}
lockWriter := false
for _, w := range writers { for _, w := range writers {
if closer, ok := w.(io.Closer); ok { if closer, ok := w.(io.Closer); ok {
ac.Closers = append(ac.Closers, closer) ac.Closers = append(ac.Closers, closer)
} }
}
switch len(writers) { if !lockWriter {
case 0: switch w.(type) {
case 1: case *os.File, *bytes.Buffer: // force lock writer.
lockWriter = true
}
}
}
ac.LockWriter = lockWriter
if len(writers) == 1 {
ac.Writer = writers[0] ac.Writer = writers[0]
default: } else {
ac.Writer = io.MultiWriter(writers...) ac.Writer = io.MultiWriter(writers...)
} }
@ -187,6 +205,10 @@ func (ac *AccessLog) AddOutput(writers ...io.Writer) *AccessLog {
// Usage: // Usage:
// ac.SetFormatter(&accesslog.JSON{Indent: " "}) // ac.SetFormatter(&accesslog.JSON{Indent: " "})
func (ac *AccessLog) SetFormatter(f Formatter) *AccessLog { func (ac *AccessLog) SetFormatter(f Formatter) *AccessLog {
if ac.Writer == nil {
panic("accesslog: SetFormatter called with nil Writer")
}
f.SetOutput(ac.Writer) // inject the writer here. f.SetOutput(ac.Writer) // inject the writer here.
ac.formatter = f ac.formatter = f
return ac return ac
@ -220,7 +242,7 @@ func (ac *AccessLog) Close() (err error) {
// It is the main method of the AccessLog middleware. // It is the main method of the AccessLog middleware.
// //
// Usage: // Usage:
// ac := New() or File("access.log") // ac := New(io.Writer) or File("access.log")
// defer ac.Close() // defer ac.Close()
// app.UseRouter(ac.Handler) // app.UseRouter(ac.Handler)
func (ac *AccessLog) Handler(ctx *context.Context) { func (ac *AccessLog) Handler(ctx *context.Context) {
@ -375,23 +397,9 @@ func (ac *AccessLog) Print(ctx *context.Context, latency time.Duration, timeForm
// key=value key2=value2. // key=value key2=value2.
requestValues := parseRequestValues(code, params, query, fields) requestValues := parseRequestValues(code, params, query, fields)
useLocker := ac.LockWriter
w := ac.Writer
if w == nil {
if ctx != nil {
w = ctx.Application().Logger().Printer
} else {
w = os.Stdout
useLocker = true // force lock.
}
}
if useLocker {
ac.mu.Lock()
}
// 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.
_, err := fmt.Fprintf(w, "%s|%s|%s|%s|%s|%d|%s|%s|\n", _, err := fmt.Fprintf(ac, "%s|%s|%s|%s|%s|%d|%s|%s|\n",
now.Format(timeFormat), now.Format(timeFormat),
latency, latency,
method, method,
@ -401,9 +409,6 @@ func (ac *AccessLog) Print(ctx *context.Context, latency time.Duration, timeForm
reqBody, reqBody,
respBody, respBody,
) )
if useLocker {
ac.mu.Unlock()
}
return err return err
} }

View File

@ -15,11 +15,14 @@ func TestAccessLogPrint_Simple(t *testing.T) {
const goroutinesN = 42 const goroutinesN = 42
w := new(bytes.Buffer) w := new(bytes.Buffer)
ac := New() ac := New(w)
ac.Writer = w ac.TimeFormat = "2006-01-02 15:04:05"
ac.LockWriter = true
ac.Clock = TClock(time.Time{}) ac.Clock = TClock(time.Time{})
if !ac.LockWriter { // should be true because we register a *bytes.Buffer.
t.Fatalf("expected LockRriter to be true")
}
var ( var (
expected string expected string
wg = new(sync.WaitGroup) wg = new(sync.WaitGroup)

View File

@ -100,7 +100,9 @@ type Formatter interface {
// otherwise the log will be printed using the default formatter // otherwise the log will be printed using the default formatter
// and the error will be printed to the Iris Application's error log level. // and the error will be printed to the Iris Application's error log level.
Format(log *Log) error Format(log *Log) error
// SetWriter should inject the accesslog's output. // SetWriter should inject the accesslog's direct output,
// if this "dest" is used then the Formatter
// should manually control its concurrent use.
SetOutput(dest io.Writer) SetOutput(dest io.Writer)
} }
@ -133,15 +135,10 @@ func (f *JSON) SetOutput(dest io.Writer) {
} }
// Format prints the logs in JSON format. // Format prints the logs in JSON format.
// Writes to the destination directly,
// locks on each Format call.
func (f *JSON) Format(log *Log) error { func (f *JSON) Format(log *Log) error {
f.mu.Lock() f.mu.Lock()
if f.enc == nil {
// If no custom writer is given then f.enc is nil,
// this code block should only be executed once.
// Also, the app's logger's writer cannot change during serve-time,
// so all logs share the same instance output.
f.SetOutput(log.Ctx.Application().Logger().Printer)
}
err := f.enc.Encode(log) err := f.enc.Encode(log)
f.mu.Unlock() f.mu.Unlock()
@ -162,7 +159,8 @@ type Template struct {
// is response to hold the log result. // is response to hold the log result.
TmplName string TmplName string
mu sync.Mutex dest io.Writer
mu sync.Mutex
} }
// SetOutput creates the default template if missing // SetOutput creates the default template if missing
@ -176,24 +174,23 @@ func (f *Template) SetOutput(dest io.Writer) {
f.Tmpl = template.Must(template.New("").Parse(text)) f.Tmpl = template.Must(template.New("").Parse(text))
} }
f.dest = dest
} }
const defaultTmplText = "{{.Now.Format .TimeFormat}}|{{.Latency}}|{{.Method}}|{{.Path}}|{{.RequestValuesLine}}|{{.Code}}|{{.Request}}|{{.Response}}|\n" const defaultTmplText = "{{.Now.Format .TimeFormat}}|{{.Latency}}|{{.Method}}|{{.Path}}|{{.RequestValuesLine}}|{{.Code}}|{{.Request}}|{{.Response}}|\n"
// Format prints the logs in text/template format. // Format prints the logs in text/template format.
func (f *Template) Format(log *Log) error { func (f *Template) Format(log *Log) error {
var ( var err error
w = log.Logger.Writer
err error
)
// A template may be executed safely in parallel, although if parallel // A template may be executed safely in parallel, although if parallel
// executions share a Writer the output may be interleaved. // executions share a Writer the output may be interleaved.
f.mu.Lock() f.mu.Lock()
if f.TmplName != "" { if f.TmplName != "" {
err = f.Tmpl.ExecuteTemplate(w, f.TmplName, log) err = f.Tmpl.ExecuteTemplate(f.dest, f.TmplName, log)
} else { } else {
err = f.Tmpl.Execute(w, log) err = f.Tmpl.Execute(f.dest, log)
} }
f.mu.Unlock() f.mu.Unlock()