diff --git a/_examples/logging/request-logger/accesslog-csv/access_log.csv.sample b/_examples/logging/request-logger/accesslog-csv/access_log.csv.sample index 6e17fccf..13fe5734 100644 --- a/_examples/logging/request-logger/accesslog-csv/access_log.csv.sample +++ b/_examples/logging/request-logger/accesslog-csv/access_log.csv.sample @@ -1,7 +1,5 @@ Timestamp,Latency,Code,Method,Path,IP,Req Values,In,Out,Request,Response -1599952696740,0s,200,GET,/,::1,,0,5,,Index -1599952696774,0s,404,GET,/favicon.ico,::1,,0,9,,Not Found -1599952697996,0s,200,GET,/,::1,,0,5,,Index -1599952698130,0s,200,GET,/,::1,,0,5,,Index -1599952698261,0s,200,GET,/,::1,,0,5,,Index -1599952741189,1.01113s,200,GET,/,::1,sleep=1s,0,5,,Index +1599996265254,0s,200,GET,/,::1,a=1 b=2,0,5,,Index +1599996266138,0s,200,GET,/,::1,sleep=32ms,0,5,,Index +1599996266778,1s,200,GET,/,::1,sleep=1s,0,5,,Index +1599996267780,1s,200,GET,/,::1,sleep=1s,0,5,,Index diff --git a/_examples/logging/request-logger/accesslog-csv/main.go b/_examples/logging/request-logger/accesslog-csv/main.go index a80527b8..c156d1a1 100644 --- a/_examples/logging/request-logger/accesslog-csv/main.go +++ b/_examples/logging/request-logger/accesslog-csv/main.go @@ -11,6 +11,7 @@ func main() { app := iris.New() ac := accesslog.File("access_log.csv") ac.ResponseBody = true + ac.LatencyRound = time.Second ac.SetFormatter(&accesslog.CSV{ Header: true, // DateScript: "FROM_UNIX", diff --git a/_examples/logging/request-logger/accesslog-simple/access.log.sample b/_examples/logging/request-logger/accesslog-simple/access.log.sample new file mode 100644 index 00000000..dabdd4ac --- /dev/null +++ b/_examples/logging/request-logger/accesslog-simple/access.log.sample @@ -0,0 +1,2 @@ +{"timestamp":1599993744664,"latency":0,"code":404,"method":"GET","path":"/favicon.ico","ip":"::1","request":"","bytes_sent":9} +{"timestamp":1599993774018,"latency":0,"code":200,"method":"GET","path":"/","ip":"::1","query":[{"key":"a","value":"1"},{"key":"b","value":"2"}],"request":"","bytes_sent":2} diff --git a/_examples/logging/request-logger/accesslog/access.log.sample b/_examples/logging/request-logger/accesslog/access.log.sample index 0b9d9ea0..ae988beb 100644 --- a/_examples/logging/request-logger/accesslog/access.log.sample +++ b/_examples/logging/request-logger/accesslog/access.log.sample @@ -1,9 +1,10 @@ -2020-09-08 13:34:42|0s|200|POST|/read_body|::1||324 B|217 B|{"name":"John","email":"example@example.com"}|{"data":{"email":"example@example.com","name":"John"},"message":"OK"}| -2020-09-08 13:40:28|0s|400|POST|/read_body|::1|||85 B|error(invalid character '\r' in string literal)|| -2020-09-08 13:41:05|1.0024ms|200|GET|/|a=1 b=2|767 B|251 B||

Hello index

| -2020-09-08 13:41:26|0s|404|GET|/public|::1||765 B|83 B||| -2020-09-08 13:41:36|0s|200|GET|/user/kataras|::1|username=kataras|771 B|91 B||Hello, kataras!| -2020-09-08 13:41:47|0s|200|GET|/user/kataras|::1|username=kataras a_query_parameter=name|774 B|91 B||Hello, kataras!| -2020-09-08 20:37:33|0s|200|GET|/admin|::1|auth=admin:admin|608 B|164 B||

Username: admin

Password: admin

| -2020-09-09 07:56:00|738.1µs|200|GET|/session|::1|session_id=23fe763f-c9d5-4d65-9e1a-2cc8d23d1aa3 session_test_key=session_test_value|818 B|302 B||OK| -2020-09-09 08:36:47|2.0095505s|200|GET|/fields|::1|job_latency=2s|544 B|130 B||OK| +2020-09-13 13:37:42 2.0057ms 200 POST /read_body ::1 61 B 94 B {"name":"John","email":"example@example.com"} +2020-09-13 13:37:50 0s 400 POST /read_body ::1 0 B 0 B error(invalid character '\r' in string literal) +2020-09-13 13:38:03 0s 404 GET /favicon.ico ::1 0 B 9 B +2020-09-13 13:38:07 0s 404 GET /public ::1 0 B 9 B +2020-09-13 13:38:09 1.0021ms 200 GET /user/kataras ::1 username=kataras 0 B 15 B +2020-09-13 13:38:14 0s 200 GET /user/kataras ::1 username=kataras a_query_parameter=name 0 B 15 B +2020-09-13 13:38:18 0s 401 GET /admin ::1 0 B 0 B +2020-09-13 13:38:19 0s 200 GET /admin ::1 auth=admin:admin 0 B 48 B +2020-09-13 13:38:22 0s 200 GET /session ::1 session_id=23fe763f-c9d5-4d65-9e1a-2cc8d23d1aa3 session_test_key=session_test_value auth=admin:admin 0 B 2 B +2020-09-13 13:38:25 2.0001204s 200 GET /fields ::1 job_latency=2s auth=admin:admin 0 B 2 B diff --git a/_examples/logging/request-logger/accesslog/main.go b/_examples/logging/request-logger/accesslog/main.go index fe4e3cef..2666509d 100644 --- a/_examples/logging/request-logger/accesslog/main.go +++ b/_examples/logging/request-logger/accesslog/main.go @@ -31,6 +31,7 @@ func makeAccessLog() *accesslog.AccessLog { // Initialize a new access log middleware. // Accepts an `io.Writer`. ac := accesslog.New(bufio.NewWriter(w)) + ac.Delim = ' ' // change the separator from '|' to space. // ac.TimeFormat = "2006-01-02 15:04:05" // default // Example of adding more than one field to the logger. diff --git a/i18n/loader.go b/i18n/loader.go index 948fb8fa..56c1e2c8 100644 --- a/i18n/loader.go +++ b/i18n/loader.go @@ -23,7 +23,7 @@ import ( // See `Glob` and `Assets` package-level functions. type ( LoaderConfig struct { - // Template delimeters, defaults to {{ }}. + // Template delimiters, defaults to {{ }}. Left, Right string // Template functions map per locale, defaults to nil. Funcs func(context.Locale) template.FuncMap diff --git a/middleware/accesslog/accesslog.go b/middleware/accesslog/accesslog.go index 5b9f99e0..99d5831a 100644 --- a/middleware/accesslog/accesslog.go +++ b/middleware/accesslog/accesslog.go @@ -133,11 +133,11 @@ type AccessLog struct { // // Defaults to false. Async bool - // The delimeter between fields when logging with the default format. + // The delimiter between fields when logging with the default format. // See `SetFormatter` to customize the log even further. // // Defaults to '|'. - Delim rune + Delim byte // The time format for current time on log print. // Set it to empty to inherit the Iris Application's TimeFormat. // @@ -221,7 +221,7 @@ type AccessLog struct { // remaining logs when Close is called, we wait for timeout (see CloseContext). remaining uint32 // reports whether the logger is already closed, see `Close` & `CloseContext` methods. - isClosed bool + isClosed uint32 } // PanicLog holds the type for the available panic log levels. @@ -236,7 +236,11 @@ const ( LogStack ) -const defaultTimeFormat = "2006-01-02 15:04:05" +const ( + defaultDelim = '|' + defaultTimeFormat = "2006-01-02 15:04:05" + newLine = '\n' +) // New returns a new AccessLog value with the default values. // Writes to the "w". Output can be further modified through its `Set/AddOutput` methods. @@ -298,7 +302,7 @@ func New(w io.Writer) *AccessLog { // File returns a new AccessLog value with the given "path" // as the log's output file destination. -// The Writer is now a buffered file writer. +// The Writer is now a buffered file writer & reader. // Register by its `Handler` method. // // A call of its `Close` method to unlock the underline @@ -313,7 +317,7 @@ func File(path string) *AccessLog { panic(err) } - return New(bufio.NewWriter(f)) + return New(bufio.NewReadWriter(bufio.NewReader(f), bufio.NewWriter(f))) } // Broker creates or returns the broker. @@ -379,11 +383,17 @@ func (ac *AccessLog) AddOutput(writers ...io.Writer) *AccessLog { // Write writes to the log destination. // It completes the io.Writer interface. // Safe for concurrent use. -func (ac *AccessLog) Write(p []byte) (int, error) { +func (ac *AccessLog) Write(p []byte) (n int, err error) { + if ac.Async { + if atomic.LoadUint32(&ac.isClosed) > 0 { + return 0, io.ErrClosedPipe + } + } + ac.mu.Lock() - n, err := ac.Writer.Write(p) + n, err = ac.Writer.Write(p) ac.mu.Unlock() - return n, err + return } // Flush writes any buffered data to the underlying Fluser Writer. @@ -449,10 +459,6 @@ func (ac *AccessLog) AddFields(setters ...FieldSetter) *AccessLog { // // After Close is called the AccessLog is not accessible. func (ac *AccessLog) Close() (err error) { - if ac.isClosed { - return nil - } - ctx, cancelFunc := stdContext.WithTimeout(stdContext.Background(), 10*time.Second) defer cancelFunc() @@ -461,10 +467,9 @@ func (ac *AccessLog) Close() (err error) { // CloseContext same as `Close` but waits until given "ctx" is done. func (ac *AccessLog) CloseContext(ctx stdContext.Context) (err error) { - if ac.isClosed { - return nil + if !atomic.CompareAndSwapUint32(&ac.isClosed, 0, 1) { + return } - ac.isClosed = true if ac.broker != nil { ac.broker.close <- struct{}{} @@ -717,9 +722,6 @@ func (ac *AccessLog) after(ctx *context.Context, lat time.Duration, method, path } } -const defaultDelim = '|' -const newLine = '\n' - // Print writes a log manually. // The `Handler` method calls it. func (ac *AccessLog) Print(ctx *context.Context, @@ -786,60 +788,60 @@ func (ac *AccessLog) Print(ctx *context.Context, } } - // url parameters, path parameters and custom fields separated by space, - // key=value key2=value2. - requestValues := parseRequestValues(code, params, query, fields) - // the number of separators is 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) + builder.WriteByte(ac.Delim) builder.WriteString(latency.String()) - builder.WriteRune(ac.Delim) + builder.WriteByte(ac.Delim) builder.WriteString(strconv.Itoa(code)) - builder.WriteRune(ac.Delim) + builder.WriteByte(ac.Delim) builder.WriteString(method) - builder.WriteRune(ac.Delim) + builder.WriteByte(ac.Delim) builder.WriteString(path) - builder.WriteRune(ac.Delim) + builder.WriteByte(ac.Delim) if ac.IP { builder.WriteString(ip) - builder.WriteRune(ac.Delim) + builder.WriteByte(ac.Delim) } - builder.WriteString(requestValues) - builder.WriteRune(ac.Delim) + // url parameters, path parameters and custom fields separated by space, + // key=value key2=value2. + if n, all := parseRequestValues(builder, code, params, query, fields); n > 0 { + builder.Truncate(all - 1) // remove the last space. + builder.WriteByte(ac.Delim) + } if ac.BytesReceived || ac.BytesReceivedBody { builder.WriteString(formatBytes(bytesReceived)) - builder.WriteRune(ac.Delim) + builder.WriteByte(ac.Delim) } if ac.BytesSent || ac.BytesSentBody { builder.WriteString(formatBytes(bytesSent)) - builder.WriteRune(ac.Delim) + builder.WriteByte(ac.Delim) } if ac.RequestBody { builder.WriteString(reqBody) - builder.WriteRune(ac.Delim) + builder.WriteByte(ac.Delim) } if ac.ResponseBody { builder.WriteString(respBody) - builder.WriteRune(ac.Delim) + builder.WriteByte(ac.Delim) } - builder.WriteRune(newLine) + builder.WriteByte(newLine) - ac.Write(builder.Bytes()) + _, err = ac.Write(builder.Bytes()) builder.Reset() ac.bufPool.Put(builder) diff --git a/middleware/accesslog/accesslog_test.go b/middleware/accesslog/accesslog_test.go index 1aa83143..612788db 100644 --- a/middleware/accesslog/accesslog_test.go +++ b/middleware/accesslog/accesslog_test.go @@ -18,8 +18,7 @@ import ( ) func TestAccessLogPrint_Simple(t *testing.T) { - t.Parallel() - const goroutinesN = 420 + const goroutinesN = 42 w := new(bytes.Buffer) ac := New(w) @@ -31,14 +30,12 @@ func TestAccessLogPrint_Simple(t *testing.T) { expected string expectedLines int mu sync.Mutex - wg sync.WaitGroup ) - wg.Add(goroutinesN) + now := time.Now() for i := 0; i < goroutinesN; i++ { go func() { - defer wg.Done() - ac.Print( + err := ac.Print( nil, 1*time.Second, ac.TimeFormat, @@ -59,25 +56,38 @@ func TestAccessLogPrint_Simple(t *testing.T) { []memstore.Entry{ {Key: "custom", ValueRaw: "custom_value"}, }) + + if err == nil { + mu.Lock() + expected += "0001-01-01 00:00:00|1s|200|GET|/path_value?url_query=url_query_value|::1|path_param=path_param_value url_query=url_query_value custom=custom_value|0 B|0 B|Incoming|Outcoming|\n" + expectedLines++ + mu.Unlock() + } }() - mu.Lock() - expected += "0001-01-01 00:00:00|1s|200|GET|/path_value?url_query=url_query_value|::1|path_param=path_param_value url_query=url_query_value custom=custom_value|0 B|0 B|Incoming|Outcoming|\n" - expectedLines++ - mu.Unlock() } - wg.Wait() - ac.Close() // TODO: Close waits for current messages but does allow future writes, I should change that. + // give some time to write at least some messages or all + // (depends on the machine the test is running). + time.Sleep(42 * time.Millisecond) + ac.Close() + end := time.Since(now) if got := atomic.LoadUint32(&ac.remaining); got > 0 { // test wait. t.Fatalf("expected remaining: %d but got: %d", 0, got) } - if got := w.String(); expected != got { + mu.Lock() + expectedSoFoar := expected + expectedLinesSoFar := expectedLines + mu.Unlock() + + if got := w.String(); expectedSoFoar != got { gotLines := strings.Count(got, "\n") - t.Logf("expected printed result to be[%d]:\n'%s'\n\nbut got[%d]:\n'%s'", expectedLines, expected, gotLines, got) - t.Fatalf("expected[%d]: %d but got: %d lines", goroutinesN, expectedLines, gotLines) + t.Logf("expected printed result to be[%d]:\n'%s'\n\nbut got[%d]:\n'%s'", expectedLinesSoFar, expectedSoFoar, gotLines, got) + t.Fatalf("expected: %d | got: %d lines", expectedLinesSoFar, gotLines) + } else { + t.Logf("We've got [%d/%d] lines of logs in %s", expectedLinesSoFar, goroutinesN, end.String()) } } @@ -92,31 +102,34 @@ func TestAccessLogBroker(t *testing.T) { n := 4 wg.Add(4) go func() { - defer wg.Done() - i := 0 ln := broker.NewListener() for { select { case log, ok := <-ln: if !ok { - t.Log("Log Listener Closed") + if i != n { + for i < n { + wg.Done() + i++ + } + } + + t.Log("Log Listener Closed: interrupted") return } + lat := log.Latency t.Log(lat.String()) wg.Done() if expected := time.Duration(i) * time.Second; expected != lat { panic(fmt.Sprintf("expected latency: %s but got: %s", expected, lat)) } - i++ time.Sleep(1350 * time.Millisecond) - if i == 2 { - time.Sleep(2 * time.Second) // "random" sleep even more. - } if log.Latency != lat { panic("expected logger to wait for notifier before release the log") } + i++ } } }() @@ -153,10 +166,7 @@ func TestAccessLogBroker(t *testing.T) { // wait for all listeners to finish. wg.Wait() - // wait for close messages. - wg.Add(1) ac.Close() - wg.Wait() } type noOpFormatter struct{} diff --git a/middleware/accesslog/log.go b/middleware/accesslog/log.go index 1a635b0f..684675eb 100644 --- a/middleware/accesslog/log.go +++ b/middleware/accesslog/log.go @@ -63,7 +63,18 @@ 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.PathParams, l.Query, l.Fields) + buf := new(strings.Builder) + _, n := parseRequestValues(buf, l.Code, l.PathParams, l.Query, l.Fields) + if n == 0 { + return "" + } + + requestValues := buf.String() + if n > 1 { + return requestValues[0 : n-1] // remove last space. + } + + return requestValues } // BytesReceivedLine returns the formatted bytes received length. @@ -100,38 +111,46 @@ func formatBytes(b int) string { 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 +const ( + eq = '=' + space = ' ' +) +// parses the request values (params, query and fields). +// returns the length of written bytes for parsing request values +// and the total. +func parseRequestValues(buf interface { + io.StringWriter + io.ByteWriter + Len() int +}, code int, pathParams memstore.Store, query []memstore.StringEntry, fields memstore.Store) (int, int) { + n := buf.Len() if !context.StatusCodeNotSuccessful(code) { // collect path parameters on a successful request-response only. for _, entry := range pathParams { buf.WriteString(entry.Key) - buf.WriteByte('=') + buf.WriteByte(eq) buf.WriteString(fmt.Sprintf("%v", entry.ValueRaw)) - buf.WriteByte(' ') + buf.WriteByte(space) } } for _, entry := range query { buf.WriteString(entry.Key) - buf.WriteByte('=') + buf.WriteByte(eq) buf.WriteString(entry.Value) - buf.WriteByte(' ') + buf.WriteByte(space) } for _, entry := range fields { buf.WriteString(entry.Key) - buf.WriteByte('=') + buf.WriteByte(eq) buf.WriteString(fmt.Sprintf("%v", entry.ValueRaw)) - buf.WriteByte(' ') + buf.WriteByte(space) } - if n := buf.Len(); n > 1 { - requestValues = buf.String()[0 : n-1] // remove last space. - } - - return + total := buf.Len() + return total - n, total } type ( diff --git a/sessions/sessiondb/redis/database.go b/sessions/sessiondb/redis/database.go index 2b1898d6..51a0d94b 100644 --- a/sessions/sessiondb/redis/database.go +++ b/sessions/sessiondb/redis/database.go @@ -44,7 +44,7 @@ type Config struct { Timeout time.Duration // Prefix "myprefix-for-this-website". Defaults to "". Prefix string - // Delim the delimeter for the keys on the sessiondb. Defaults to "-". + // Delim the delimiter for the keys on the sessiondb. Defaults to "-". Delim string // TLSConfig will cause Dial to perform a TLS handshake using the provided