From 2b342a5122a1f5abb8d2dc071e6618640ce162e5 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 12 Sep 2020 12:34:59 +0300 Subject: [PATCH] accesslog: add CSV format relative to: https://github.com/kataras/iris/issues/1601 --- _examples/README.md | 1 + .../accesslog-csv/access_log.csv.sample | 5 + .../request-logger/accesslog-csv/main.go | 34 ++++ go.mod | 10 +- middleware/accesslog/accesslog.go | 33 +++- middleware/accesslog/accesslog_test.go | 14 +- middleware/accesslog/csv.go | 167 ++++++++++++++++++ middleware/accesslog/csv_test.go | 54 ++++++ middleware/accesslog/json.go | 54 ++++++ middleware/accesslog/log.go | 144 +++------------ middleware/accesslog/template.go | 65 +++++++ 11 files changed, 448 insertions(+), 133 deletions(-) create mode 100644 _examples/logging/request-logger/accesslog-csv/access_log.csv.sample create mode 100644 _examples/logging/request-logger/accesslog-csv/main.go create mode 100644 middleware/accesslog/csv.go create mode 100644 middleware/accesslog/csv_test.go create mode 100644 middleware/accesslog/json.go create mode 100644 middleware/accesslog/template.go diff --git a/_examples/README.md b/_examples/README.md index cd04a20b..21f3fa0c 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -74,6 +74,7 @@ * [Request Logger](logging/request-logger/main.go) * [AccessLog: log request & response and more](logging/request-logger/accesslog) * [AccessLog: custom fields and template](logging/request-logger/accesslog-template/main.go) + * [AccessLog: CSV Format](logging/request-logger/accesslog-csv/main.go) * [AccessLog: listen to logs and render them](logging/request-logger/accesslog-broker/main.go) * [Log Requests to a JSON File](logging/request-logger/request-logger-file-json/main.go) * [Application File Logger](logging/file-logger/main.go) diff --git a/_examples/logging/request-logger/accesslog-csv/access_log.csv.sample b/_examples/logging/request-logger/accesslog-csv/access_log.csv.sample new file mode 100644 index 00000000..f13bdd7f --- /dev/null +++ b/_examples/logging/request-logger/accesslog-csv/access_log.csv.sample @@ -0,0 +1,5 @@ +Timestamp,Latency,Code,Method,Path,IP,Req Values,In,Out,Request,Response +1599900695933,0s,200,GET,/,::1,sleep=35ms,575,81,,Index +1599900696207,0s,404,GET,/notfound,::1,,572,92,,Not Found +1599900696693,0s,200,GET,/,::1,,564,81,,Index +1599900697988,1s,200,GET,/,::1,sleep=1s,573,81,,Index diff --git a/_examples/logging/request-logger/accesslog-csv/main.go b/_examples/logging/request-logger/accesslog-csv/main.go new file mode 100644 index 00000000..5b7b1142 --- /dev/null +++ b/_examples/logging/request-logger/accesslog-csv/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "time" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/middleware/accesslog" +) + +func main() { + app := iris.New() + ac := accesslog.File("access_log.csv") + ac.SetFormatter(&accesslog.CSV{ + AutoFlush: true, + Header: true, + // DateScript: "FROM_UNIX", + LatencyRound: time.Second, + }) + + app.UseRouter(ac.Handler) + app.Get("/", index) + + app.Listen(":8080") +} + +func index(ctx iris.Context) { + if sleepDur := ctx.URLParam("sleep"); sleepDur != "" { + if d, err := time.ParseDuration(sleepDur); err == nil { + time.Sleep(d) + } + } + + ctx.WriteString("Index") +} diff --git a/go.mod b/go.mod index 38c836d7..12504bdb 100644 --- a/go.mod +++ b/go.mod @@ -25,22 +25,22 @@ require ( github.com/kataras/pio v0.0.10 github.com/kataras/sitemap v0.0.5 github.com/kataras/tunnel v0.0.1 - github.com/klauspost/compress v1.10.11 + github.com/klauspost/compress v1.11.0 github.com/mediocregopher/radix/v3 v3.5.2 github.com/microcosm-cc/bluemonday v1.0.4 github.com/russross/blackfriday/v2 v2.0.1 github.com/schollz/closestmatch v2.1.0+incompatible github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 - github.com/tdewolff/minify/v2 v2.9.2 + github.com/tdewolff/minify/v2 v2.9.4 github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 github.com/yosssi/ace v0.0.5 go.etcd.io/bbolt v1.3.5 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a - golang.org/x/net v0.0.0-20200822124328-c89045814202 - golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a + golang.org/x/net v0.0.0-20200904194848-62affa334b73 + golang.org/x/sys v0.0.0-20200909081042-eff7692f9009 golang.org/x/text v0.3.3 golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e google.golang.org/protobuf v1.25.0 - gopkg.in/ini.v1 v1.60.2 + gopkg.in/ini.v1 v1.61.0 gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 ) diff --git a/middleware/accesslog/accesslog.go b/middleware/accesslog/accesslog.go index bb5804d1..c0c45fbe 100644 --- a/middleware/accesslog/accesslog.go +++ b/middleware/accesslog/accesslog.go @@ -140,7 +140,7 @@ type AccessLog struct { // Defaults to '|'. Delim rune // The time format for current time on log print. - // Defaults to ""2006-01-02 15:04:05" on `New` function. + // Defaults to "2006-01-02 15:04:05" on `New` function. // Set it to empty to inherit the Iris Application's TimeFormat. TimeFormat string // IP displays the remote address. @@ -249,7 +249,9 @@ func New(w io.Writer) *AccessLog { // // It panics on error. func File(path string) *AccessLog { - f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + // Note: we add os.RDWR in order to be able to read from it, + // some formatters (e.g. CSV) needs that. + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } @@ -347,7 +349,13 @@ func (ac *AccessLog) SetFormatter(f Formatter) *AccessLog { panic("accesslog: SetFormatter called with nil Writer") } - f.SetOutput(ac.Writer) // inject the writer here. + // Inject the writer (AccessLog) here, the writer + // It is a protected with mutex writer to the final output + // when LockWriter field was set to true + // or when the given destination was os.File or bytes.Buffer + // (otherwise we assume it's locked by the end-developer). + f.SetOutput(ac) + ac.formatter = f return ac } @@ -363,6 +371,8 @@ func (ac *AccessLog) AddFields(setters ...FieldSetter) *AccessLog { // Close calls each registered Closer's Close method. // Exits when all close methods have been executed. func (ac *AccessLog) Close() (err error) { + ac.flushFormatter() + for _, closer := range ac.Closers { cErr := closer.Close() if cErr != nil { @@ -377,6 +387,14 @@ func (ac *AccessLog) Close() (err error) { return } +func (ac *AccessLog) flushFormatter() { + if ac.formatter != nil { + if flusher, ok := ac.formatter.(Flusher); ok { + flusher.Flush() + } + } +} + func (ac *AccessLog) shouldReadRequestBody() bool { return ac.RequestBody || ac.BytesReceived @@ -564,7 +582,7 @@ func (ac *AccessLog) after(ctx *context.Context, lat time.Duration, method, path ip, requestBody, responseBody, bytesReceived, bytesSent, - ctx.Params(), ctx.URLParamsSorted(), *fields, + ctx.Params().Store, ctx.URLParamsSorted(), *fields, ); err != nil { ctx.Application().Logger().Errorf("accesslog: %v", err) } @@ -574,7 +592,7 @@ const defaultDelim = '|' // 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, ip, reqBody, respBody string, bytesReceived, bytesSent int, params *context.RequestParams, query []memstore.StringEntry, fields []memstore.Entry) (err error) { +func (ac *AccessLog) Print(ctx *context.Context, latency time.Duration, timeFormat string, code int, method, path, ip, reqBody, respBody string, bytesReceived, bytesSent int, params memstore.Store, query []memstore.StringEntry, fields []memstore.Entry) (err error) { now := ac.Clock.Now() if hasFormatter, hasBroker := ac.formatter != nil, ac.broker != nil; hasFormatter || hasBroker { @@ -589,7 +607,7 @@ func (ac *AccessLog) Print(ctx *context.Context, latency time.Duration, timeForm log.Path = path log.IP = ip log.Query = query - log.PathParams = params.Store + log.PathParams = params log.Fields = fields log.BytesReceived = bytesReceived log.BytesSent = bytesSent @@ -624,6 +642,7 @@ func (ac *AccessLog) Print(ctx *context.Context, latency time.Duration, timeForm // the number of separators are the same, in order to be easier // for 3rd-party programs to read the result log file. builder := ac.bufPool.Get().(*bytes.Buffer) + builder.WriteString(now.Format(timeFormat)) builder.WriteRune(ac.Delim) @@ -670,6 +689,8 @@ var lineBreaksReplacer = strings.NewReplacer("\n\r", " ", "\n", " ") func (ac *AccessLog) getErrorText(err error) (text string) { // caller checks for nil. if errPanic, ok := context.IsErrPanicRecovery(err); ok { + ac.flushFormatter() // flush any buffered formatter's contents to be written to the output. + switch ac.PanicLog { case LogHandler: text = errPanic.CurrentHandler diff --git a/middleware/accesslog/accesslog_test.go b/middleware/accesslog/accesslog_test.go index 040cddb3..1129900c 100644 --- a/middleware/accesslog/accesslog_test.go +++ b/middleware/accesslog/accesslog_test.go @@ -52,13 +52,13 @@ func TestAccessLogPrint_Simple(t *testing.T) { "Outcoming", 0, 0, - &context.RequestParams{ - Store: []memstore.Entry{ - {Key: "path_param", ValueRaw: "path_param_value"}, - }, - }, []memstore.StringEntry{ + memstore.Store{ + {Key: "path_param", ValueRaw: "path_param_value"}, + }, + []memstore.StringEntry{ {Key: "url_query", Value: "url_query_value"}, - }, []memstore.Entry{ + }, + []memstore.Entry{ {Key: "custom", ValueRaw: "custom_value"}, }) }() @@ -126,7 +126,7 @@ func TestAccessLogBroker(t *testing.T) { "", 0, 0, - &context.RequestParams{}, + nil, nil, nil, ) diff --git a/middleware/accesslog/csv.go b/middleware/accesslog/csv.go new file mode 100644 index 00000000..abd09ea4 --- /dev/null +++ b/middleware/accesslog/csv.go @@ -0,0 +1,167 @@ +package accesslog + +import ( + "encoding/csv" + "io" + "strconv" + "time" +) + +// CSV is a Formatter type for csv encoded logs. +type CSV struct { + writer *csv.Writer + ac *AccessLog + + // Add header fields to the first line if it's not exist. + // Note that the destination should be a compatible io.Reader + // with access to write. + Header bool + // Google Spreadsheet's Script to wrap the Timestamp field + // in order to convert it into a readable date. + // Example: "FROM_UNIX" when + // function FROM_UNIX(epoch_in_millis) { + // return new Date(epoch_in_millis); + // } + DateScript string + // Latency Round base, e.g. time.Second. + LatencyRound time.Duration + // Writes immediately every record. + AutoFlush bool + + // TODO: Fields []string // field name, position? +} + +// SetOutput initializes the csv writer. +// It uses the "dest" as AccessLog to +// write the first csv record which +// contains the names of the future log values. +func (f *CSV) SetOutput(dest io.Writer) { + ac, ok := dest.(*AccessLog) + if !ok { + panic("SetOutput with invalid type. Report it as bug.") + } + + w := csv.NewWriter(dest) + f.writer = w + f.ac = ac + + if !f.Header { + return + } + + { + // If the destination is not a reader + // we can't detect if the header already inserted + // so we exit, we dont want to malform the contents. + destReader, ok := ac.Writer.(io.Reader) + if !ok { + return + } + + r := csv.NewReader(destReader) + if header, err := r.Read(); err == nil && len(header) > 0 && header[0] == "Timestamp" { + // we assume header already exists, exit. + return + } + } + + // Write the header. + + keys := []string{"Timestamp", "Latency", "Code", "Method", "Path"} + + if ac.IP { + keys = append(keys, "IP") + } + + // keys = append(keys, []string{"Params", "Query"}...) + keys = append(keys, "Req Values") + + /* + if len(ac.FieldSetters) > 0 { + keys = append(keys, "Fields") + } // Make fields their own headers? + */ + + if ac.BytesReceived { + keys = append(keys, "In") + } + + if ac.BytesSent { + keys = append(keys, "Out") + } + + if ac.RequestBody { + keys = append(keys, "Request") + } + + if ac.ResponseBody { + keys = append(keys, "Response") + } + + w.Write(keys) + w.Flush() +} + +// Format writes an incoming log using CSV encoding. +func (f *CSV) Format(log *Log) (bool, error) { + // Timestamp, Latency, Code, Method, Path, IP, Path Params Query Fields + //|Bytes Received|Bytes Sent|Request|Response| + + timestamp := strconv.FormatInt(log.Timestamp, 10) + + if f.DateScript != "" { + timestamp = "=" + f.DateScript + "(" + timestamp + ")" + } + + lat := "" + if f.LatencyRound > 0 { + lat = log.Latency.Round(f.LatencyRound).String() + } else { + lat = log.Latency.String() + } + + values := []string{ + timestamp, + lat, + strconv.Itoa(log.Code), + log.Method, + log.Path, + } + + if f.ac.IP { + values = append(values, log.IP) + } + + parseRequestValues(log.Code, log.PathParams, log.Query, log.Fields) + values = append(values, log.RequestValuesLine()) + + if f.ac.BytesReceived { + values = append(values, strconv.Itoa(log.BytesReceived)) + } + + if f.ac.BytesSent { + values = append(values, strconv.Itoa(log.BytesSent)) + } + + if f.ac.RequestBody { + values = append(values, log.Request) + } + + if f.ac.ResponseBody { + values = append(values, log.Response) + } + + f.writer.Write(values) + + if f.AutoFlush { + return true, f.Flush() + } + return true, nil +} + +// Flush implements the Fluster interface. +// Flushes any buffered csv records to the destination. +func (f *CSV) Flush() error { + f.writer.Flush() + return f.writer.Error() +} diff --git a/middleware/accesslog/csv_test.go b/middleware/accesslog/csv_test.go new file mode 100644 index 00000000..7216a39f --- /dev/null +++ b/middleware/accesslog/csv_test.go @@ -0,0 +1,54 @@ +package accesslog + +import ( + "bytes" + "testing" + "time" + + "github.com/kataras/iris/v12/core/memstore" +) + +func TestCSV(t *testing.T) { + buf := new(bytes.Buffer) + ac := New(buf) + staticNow, _ := time.Parse(defaultTimeFormat, "1993-01-01 05:00:00") + ac.Clock = TClock(staticNow) + ac.SetFormatter(&CSV{ + Header: true, + LatencyRound: time.Second, + AutoFlush: true, + }) + + lat, _ := time.ParseDuration("1s") + + print := func() { + ac.Print( + nil, + lat, + "", + 200, + "GET", + "/", + "::1", + "", + "Index", + 573, + 81, + nil, + []memstore.StringEntry{{Key: "sleep", Value: "1s"}}, + nil) + } + + // print twice, the header should only be written once. + print() + print() + + expected := `Timestamp,Latency,Code,Method,Path,IP,Req Values,In,Out,Request,Response +725864400000,1s,200,GET,/,::1,sleep=1s,573,81,,Index +725864400000,1s,200,GET,/,::1,sleep=1s,573,81,,Index +` + + if got := buf.String(); expected != got { + t.Fatalf("expected:\n%s\n\nbut got:\n%s", expected, got) + } +} diff --git a/middleware/accesslog/json.go b/middleware/accesslog/json.go new file mode 100644 index 00000000..d28b9942 --- /dev/null +++ b/middleware/accesslog/json.go @@ -0,0 +1,54 @@ +package accesslog + +import ( + "encoding/json" + "io" + "sync" +) + +// JSON is a Formatter type for JSON logs. +type JSON struct { + Prefix, Indent string + EscapeHTML bool + + enc *json.Encoder + mu sync.Mutex + lockEncoder bool +} + +// SetOutput creates the json encoder writes to the "dest". +// It's called automatically by the middleware when this Formatter is used. +func (f *JSON) SetOutput(dest io.Writer) { + if dest == nil { + return + } + + // All logs share the same accesslog's writer and it cannot change during serve-time. + enc := json.NewEncoder(dest) + enc.SetEscapeHTML(f.EscapeHTML) + enc.SetIndent(f.Prefix, f.Indent) + f.lockEncoder = f.Prefix != "" || f.Indent != "" + f.enc = enc +} + +// Format prints the logs in JSON format. +// Writes to the destination directly, +// locks on each Format call. +func (f *JSON) Format(log *Log) (bool, error) { + // f.mu.Lock() + // ^ This lock is not required as the writer is + // protected with mutex if necessary or configurated to do so. + // However, if we navigate through the + // internal encoding's source code we'll see that it + // uses a field for its indent buffer, + // therefore it's only useful when Prefix or Indent was not empty. + if f.lockEncoder { + f.mu.Lock() + } + err := f.enc.Encode(log) + if f.lockEncoder { + f.mu.Unlock() + } + + return true, err +} diff --git a/middleware/accesslog/log.go b/middleware/accesslog/log.go index 00e7d838..44abe4a6 100644 --- a/middleware/accesslog/log.go +++ b/middleware/accesslog/log.go @@ -1,12 +1,9 @@ package accesslog import ( - "encoding/json" "fmt" "io" "strings" - "sync" - "text/template" "time" "github.com/kataras/iris/v12/context" @@ -67,7 +64,7 @@ func (l *Log) Clone() Log { // RequestValuesLine returns a string line which // combines the path parameters, query and custom fields. func (l *Log) RequestValuesLine() string { - return parseRequestValues(l.Code, l.Ctx.Params(), l.Ctx.URLParamsSorted(), l.Fields) + return parseRequestValues(l.Code, l.PathParams, l.Query, l.Fields) } // BytesReceivedLine returns the formatted bytes received length. @@ -98,17 +95,17 @@ func formatBytes(b int) string { float64(b)/float64(div), "KMGTPE"[exp]) } -func parseRequestValues(code int, pathParams *context.RequestParams, query []memstore.StringEntry, fields memstore.Store) (requestValues string) { +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. - pathParams.Visit(func(key, value string) { - buf.WriteString(key) + for _, entry := range pathParams { + buf.WriteString(entry.Key) buf.WriteByte('=') - buf.WriteString(value) + buf.WriteString(fmt.Sprintf("%v", entry.ValueRaw)) buf.WriteByte(' ') - }) + } } for _, entry := range query { @@ -132,114 +129,31 @@ func parseRequestValues(code int, pathParams *context.RequestParams, query []mem return } -// Formatter is responsible to print a Log to the accesslog's writer. -type 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) -} +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) + _ Flusher = (*CSV)(nil) ) - -// JSON is a Formatter type for JSON logs. -type JSON struct { - Prefix, Indent string - EscapeHTML bool - - enc *json.Encoder - mu sync.Mutex -} - -// SetOutput creates the json encoder writes to the "dest". -// It's called automatically by the middleware when this Formatter is used. -func (f *JSON) SetOutput(dest io.Writer) { - if dest == nil { - return - } - - // All logs share the same accesslog's writer and it cannot change during serve-time. - enc := json.NewEncoder(dest) - enc.SetEscapeHTML(f.EscapeHTML) - enc.SetIndent(f.Prefix, f.Indent) - f.enc = enc -} - -// Format prints the logs in JSON format. -// Writes to the destination directly, -// locks on each Format call. -func (f *JSON) Format(log *Log) (bool, error) { - f.mu.Lock() - err := f.enc.Encode(log) - f.mu.Unlock() - - return true, err -} - -// Template is a Formatter. -// It's used to print the Log in a text/template way. -// The caller has full control over the printable result; -// certain fields can be ignored, change the display order and e.t.c. -type Template struct { - // Custom template source. - // Use this or `Tmpl/TmplName` fields. - Text string - // Custom template funcs to used when `Text` is not empty. - Funcs template.FuncMap - - // Custom template to use, overrides the `Text` and `Funcs` fields. - Tmpl *template.Template - // If not empty then this named template/block renders the log line. - TmplName string - - dest io.Writer - mu sync.Mutex -} - -// SetOutput creates the default template if missing -// when this formatter is registered. -func (f *Template) SetOutput(dest io.Writer) { - if f.Tmpl == nil { - tmpl := template.New("") - - text := f.Text - if text != "" { - tmpl.Funcs(f.Funcs) - } else { - text = defaultTmplText - } - - f.Tmpl = template.Must(tmpl.Parse(text)) - } - - f.dest = dest -} - -const defaultTmplText = "{{.Now.Format .TimeFormat}}|{{.Latency}}|{{.Code}}|{{.Method}}|{{.Path}}|{{.IP}}|{{.RequestValuesLine}}|{{.BytesReceivedLine}}|{{.BytesSentLine}}|{{.Request}}|{{.Response}}|\n" - -// Format prints the logs in text/template format. -func (f *Template) Format(log *Log) (bool, error) { - var err error - - // A template may be executed safely in parallel, although if parallel - // executions share a Writer the output may be interleaved. - f.mu.Lock() - if f.TmplName != "" { - err = f.Tmpl.ExecuteTemplate(f.dest, f.TmplName, log) - } else { - err = f.Tmpl.Execute(f.dest, log) - } - f.mu.Unlock() - - return true, err -} diff --git a/middleware/accesslog/template.go b/middleware/accesslog/template.go new file mode 100644 index 00000000..73b5e308 --- /dev/null +++ b/middleware/accesslog/template.go @@ -0,0 +1,65 @@ +package accesslog + +import ( + "io" + "sync" + "text/template" +) + +// Template is a Formatter. +// It's used to print the Log in a text/template way. +// The caller has full control over the printable result; +// certain fields can be ignored, change the display order and e.t.c. +type Template struct { + // Custom template source. + // Use this or `Tmpl/TmplName` fields. + Text string + // Custom template funcs to used when `Text` is not empty. + Funcs template.FuncMap + + // Custom template to use, overrides the `Text` and `Funcs` fields. + Tmpl *template.Template + // If not empty then this named template/block renders the log line. + TmplName string + + dest io.Writer + mu sync.Mutex +} + +// SetOutput creates the default template if missing +// when this formatter is registered. +func (f *Template) SetOutput(dest io.Writer) { + if f.Tmpl == nil { + tmpl := template.New("") + + text := f.Text + if text != "" { + tmpl.Funcs(f.Funcs) + } else { + text = defaultTmplText + } + + f.Tmpl = template.Must(tmpl.Parse(text)) + } + + f.dest = dest +} + +const defaultTmplText = "{{.Now.Format .TimeFormat}}|{{.Latency}}|{{.Code}}|{{.Method}}|{{.Path}}|{{.IP}}|{{.RequestValuesLine}}|{{.BytesReceivedLine}}|{{.BytesSentLine}}|{{.Request}}|{{.Response}}|\n" + +// Format prints the logs in text/template format. +func (f *Template) Format(log *Log) (bool, error) { + var err error + + // A template may be executed safely in parallel, although if parallel + // executions share a Writer the output may be interleaved. + f.mu.Lock() + if f.TmplName != "" { + err = f.Tmpl.ExecuteTemplate(f.dest, f.TmplName, log) + } else { + err = f.Tmpl.Execute(f.dest, log) + } + f.mu.Unlock() + + return true, err +}