iris/middleware/accesslog/log.go
Gerasimos (Makis) Maropoulos 4845b77177
accesslog: improvements and new features
relative to: #1601 and #1624
2020-09-13 02:56:22 +03:00

164 lines
4.9 KiB
Go

package accesslog
import (
"fmt"
"io"
"strings"
"time"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/memstore"
)
// Log represents the log data specifically for the accesslog middleware.
//easyjson:json
type Log struct {
// The AccessLog instance this Log was created of.
Logger *AccessLog `json:"-" yaml:"-" toml:"-"`
// The time the log is created.
Now time.Time `json:"-" yaml:"-" toml:"-"`
// TimeFormat selected to print the Time as string,
// useful on Template Formatter.
TimeFormat string `json:"-" yaml:"-" toml:"-"`
// Timestamp the Now's unix timestamp (milliseconds).
Timestamp int64 `json:"timestamp" csv:"timestamp"`
// Request-Response latency.
Latency time.Duration `json:"latency" csv:"latency"`
// The response status code.
Code int `json:"code" csv:"code"`
// Init request's Method and Path.
Method string `json:"method" csv:"method"`
Path string `json:"path" csv:"path"`
// The Remote Address.
IP string `json:"ip,omitempty" csv:"ip,omitempty"`
// Sorted URL Query arguments.
Query []memstore.StringEntry `json:"query,omitempty" csv:"query,omitempty"`
// Dynamic path parameters.
PathParams memstore.Store `json:"params,omitempty" csv:"params,omitempty"`
// Fields any data information useful to represent this Log.
Fields memstore.Store `json:"fields,omitempty" csv:"fields,omitempty"`
// The Request and Response raw bodies.
// If they are escaped (e.g. JSON),
// A third-party software can read it through:
// data, _ := strconv.Unquote(log.Request)
// err := json.Unmarshal([]byte(data), &customStruct)
Request string `json:"request,omitempty" csv:"request,omitempty"`
Response string `json:"response,omitempty" csv:"response,omitempty"`
// The actual number of bytes received and sent on the network (headers + body or body only).
BytesReceived int `json:"bytes_received,omitempty" csv:"bytes_received,omitempty"`
BytesSent int `json:"bytes_sent,omitempty" csv:"bytes_sent,omitempty"`
// A copy of the Request's Context when Async is true (safe to use concurrently),
// otherwise it's the current Context (not safe for concurrent access).
Ctx *context.Context `json:"-" yaml:"-" toml:"-"`
}
// Clone returns a raw copy value of this Log.
func (l *Log) Clone() Log {
return *l
}
// RequestValuesLine returns a string line which
// combines the path parameters, query and custom fields.
func (l *Log) RequestValuesLine() string {
return parseRequestValues(l.Code, l.PathParams, l.Query, l.Fields)
}
// BytesReceivedLine returns the formatted bytes received length.
func (l *Log) BytesReceivedLine() string {
if !l.Logger.BytesReceived && !l.Logger.BytesReceivedBody {
return ""
}
return formatBytes(l.BytesReceived)
}
// BytesSentLine returns the formatted bytes sent length.
func (l *Log) BytesSentLine() string {
if !l.Logger.BytesSent && !l.Logger.BytesSentBody {
return ""
}
return formatBytes(l.BytesSent)
}
func formatBytes(b int) string {
if b <= 0 {
return "0 B"
}
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB",
float64(b)/float64(div), "KMGTPE"[exp])
}
func parseRequestValues(code int, pathParams memstore.Store, query []memstore.StringEntry, fields memstore.Store) (requestValues string) {
var buf strings.Builder
if !context.StatusCodeNotSuccessful(code) {
// collect path parameters on a successful request-response only.
for _, entry := range pathParams {
buf.WriteString(entry.Key)
buf.WriteByte('=')
buf.WriteString(fmt.Sprintf("%v", entry.ValueRaw))
buf.WriteByte(' ')
}
}
for _, entry := range query {
buf.WriteString(entry.Key)
buf.WriteByte('=')
buf.WriteString(entry.Value)
buf.WriteByte(' ')
}
for _, entry := range fields {
buf.WriteString(entry.Key)
buf.WriteByte('=')
buf.WriteString(fmt.Sprintf("%v", entry.ValueRaw))
buf.WriteByte(' ')
}
if n := buf.Len(); n > 1 {
requestValues = buf.String()[0 : n-1] // remove last space.
}
return
}
type (
// Formatter is responsible to print a Log to the accesslog's writer.
Formatter interface {
// SetOutput 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)
// Format should print the Log.
// Returns nil error on handle successfully,
// otherwise the log will be printed using the default formatter
// and the error will be printed to the Iris Application's error log level.
// Should return true if this handled the logging, otherwise false to
// continue with the default print format.
Format(log *Log) (bool, error)
}
// Flusher can be implemented by a Formatter
// to call its Flush method on AccessLog.Close
// and on panic errors.
Flusher interface{ Flush() error }
)
var (
_ Formatter = (*JSON)(nil)
_ Formatter = (*Template)(nil)
_ Formatter = (*CSV)(nil)
)