package accesslog import ( "encoding/json" "fmt" "io" "strings" "sync" "text/template" "time" "github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/core/memstore" ) // Log represents the log data specifically for the accesslog middleware. 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 (seconds). Timestamp int64 `json:"timestamp"` // Request-Response latency. Latency time.Duration `json:"latency"` // Init request's Method and Path. Method string `json:"method"` Path string `json:"path"` // The response status code. Code int `json:"code"` // Sorted URL Query arguments. Query []memstore.StringEntry `json:"query"` // Dynamic path parameters. PathParams []memstore.Entry `json:"params"` // Fields any data information useful to represent this Log. Fields []memstore.Entry `json:"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"` Response string `json:"response"` // 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:"-"` } // 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) } func parseRequestValues(code int, pathParams *context.RequestParams, 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) buf.WriteByte('=') buf.WriteString(value) 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 } // Formatter is responsible to print a Log to the accesslog's writer. type Formatter interface { // 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. Format(log *Log) error // SetWriter should inject the accesslog's output. SetOutput(dest io.Writer) } var ( _ Formatter = (*JSON)(nil) _ Formatter = (*Template)(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. func (f *JSON) Format(log *Log) error { 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) f.mu.Unlock() return 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 to use, overrides the `Text` field if not nil. Tmpl *template.Template // If not empty then this named template/block // is response to hold the log result. TmplName string 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 { text := f.Text if f.Text == "" { text = defaultTmplText } f.Tmpl = template.Must(template.New("").Parse(text)) } } const defaultTmplText = "{{.Now.Format .TimeFormat}}|{{.Latency}}|{{.Method}}|{{.Path}}|{{.RequestValuesLine}}|{{.Code}}|{{.Request}}|{{.Response}}|\n" // Format prints the logs in text/template format. func (f *Template) Format(log *Log) error { var ( w = log.Logger.Writer 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(w, f.TmplName, log) } else { err = f.Tmpl.Execute(w, log) } f.mu.Unlock() return err }