diff --git a/HISTORY.md b/HISTORY.md index 25a36692..38f89dd9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -586,6 +586,7 @@ New Package-level Variables: New Context Methods: +- `Context.URLParamsSorted() []memstore.StringEntry` returns a sorted (by key) slice of key-value entries of the URL Query parameters. - `Context.ViewEngine(ViewEngine)` to set a view engine on-fly for the current chain of handlers, responsible to render templates through `ctx.View`. [Example](_examples/view/context-view-engine). - `Context.SetErr(error)` and `Context.GetErr() error` helpers. - `Context.CompressWriter(bool) error` and `Context.CompressReader(bool) error`. diff --git a/_examples/README.md b/_examples/README.md index 507491cf..19935ca0 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -70,6 +70,7 @@ * [Sitemap](routing/sitemap/main.go) * Logging * [Request Logger](logging/request-logger/main.go) + * [Log requests and responses to access.log](logging/request-logger/request-logger-access-log-file) * [Log Requests to a File](logging/request-logger/request-logger-file/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/http-server/README.md b/_examples/http-server/README.md index d588a0c6..10608909 100644 --- a/_examples/http-server/README.md +++ b/_examples/http-server/README.md @@ -11,7 +11,7 @@ we use the `iris.Addr` which is an `iris.Runner` type ```go // Listening on tcp with network address 0.0.0.0:8080 -// app.Listen(":8080") it's a shortcut of: +// app.Listen(":8080") is just a shortcut of: app.Run(iris.Addr(":8080")) ``` diff --git a/_examples/http-server/graceful-shutdown/custom-notifier/main.go b/_examples/http-server/graceful-shutdown/custom-notifier/main.go index 3984aefb..261a46f7 100644 --- a/_examples/http-server/graceful-shutdown/custom-notifier/main.go +++ b/_examples/http-server/graceful-shutdown/custom-notifier/main.go @@ -17,6 +17,7 @@ func main() { ctx.HTML("

hi, I just exist in order to see if the server is closed

") }) + idleConnsClosed := make(chan struct{}) go func() { ch := make(chan os.Signal, 1) signal.Notify(ch, @@ -37,10 +38,12 @@ func main() { ctx, cancel := stdContext.WithTimeout(stdContext.Background(), timeout) defer cancel() app.Shutdown(ctx) + close(idleConnsClosed) } }() // Start the server and disable the default interrupt handler in order to // handle it clear and simple by our own, without any issues. app.Listen(":8080", iris.WithoutInterruptHandler) + <-idleConnsClosed } diff --git a/_examples/http-server/graceful-shutdown/default-notifier/main.go b/_examples/http-server/graceful-shutdown/default-notifier/main.go index 06acf4ab..930d4a26 100644 --- a/_examples/http-server/graceful-shutdown/default-notifier/main.go +++ b/_examples/http-server/graceful-shutdown/default-notifier/main.go @@ -17,12 +17,14 @@ import ( func main() { app := iris.New() + idleConnsClosed := make(chan struct{}) iris.RegisterOnInterrupt(func() { timeout := 10 * time.Second ctx, cancel := stdContext.WithTimeout(stdContext.Background(), timeout) defer cancel() // close all hosts app.Shutdown(ctx) + close(idleConnsClosed) }) app.Get("/", func(ctx iris.Context) { @@ -30,5 +32,6 @@ func main() { }) // http://localhost:8080 - app.Listen(":8080", iris.WithoutInterruptHandler) + app.Listen(":8080", iris.WithoutInterruptHandler, iris.WithoutServerError(iris.ErrServerClosed)) + <-idleConnsClosed } diff --git a/_examples/logging/request-logger/request-logger-access-log-file/access.log.sample b/_examples/logging/request-logger/request-logger-access-log-file/access.log.sample new file mode 100644 index 00000000..316986fe --- /dev/null +++ b/_examples/logging/request-logger/request-logger-access-log-file/access.log.sample @@ -0,0 +1,6 @@ +2020-08-22 00:44:20|993.3µs|POST|/read_body||200|{"id":10,"name":"Tim","age":22}|{"message":"OK"}| +2020-08-22 00:44:30|0s|POST|/read_body||400||error(invalid character 'a' looking for beginning of object key string)|| +2020-08-22 03:02:41|1ms|GET|/|a=1 b=2|200||

Hello index

| +2020-08-22 03:15:29|968.8µs|GET|/public|file=public|404||| +2020-08-22 03:03:42|0s|GET|/user/kataras|username=kataras|200||Hello, kataras!| +2020-08-22 03:05:40|0s|GET|/user/kataras|username=kataras a_query_parameter=name|200||Hello, kataras!| diff --git a/_examples/logging/request-logger/request-logger-access-log-file/main.go b/_examples/logging/request-logger/request-logger-access-log-file/main.go new file mode 100644 index 00000000..892bd140 --- /dev/null +++ b/_examples/logging/request-logger/request-logger-access-log-file/main.go @@ -0,0 +1,174 @@ +package main // See https://github.com/kataras/iris/issues/1601 + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/middleware/logger" +) + +func main() { + // Create or use the ./access.log file. + f, err := os.OpenFile("access.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + panic(err) + } + defer f.Close() + iris.RegisterOnInterrupt(func() { f.Close() }) + // + + app := iris.New() + + // Init the request logger with + // the LogFuncCtx field alone. + reqLogger := logger.New(logger.Config{ + LogFuncCtx: requestLogFunc(f), + }) + // + + // Wrap the request logger middleware + // with a response recorder because + // we want to record the response body + // sent to the client. + reqLoggerWithRecord := func(ctx iris.Context) { + // Store the requested path just in case. + ctx.Values().Set("path", ctx.Path()) + ctx.Record() + reqLogger(ctx) + } + // + + // Register the middleware (UseRouter to catch http errors too). + app.UseRouter(reqLoggerWithRecord) + // + + // Register some routes... + app.HandleDir("/", iris.Dir("./public")) + + app.Get("/user/{username}", userHandler) + app.Post("/read_body", readBodyHandler) + // + + // Start the server with `WithoutBodyConsumptionOnUnmarshal` + // option so the request body can be readen twice: + // one for our handlers and one from inside our request logger middleware. + app.Listen(":8080", iris.WithoutBodyConsumptionOnUnmarshal) +} + +func readBodyHandler(ctx iris.Context) { + var request interface{} + if err := ctx.ReadBody(&request); err != nil { + ctx.StopWithPlainError(iris.StatusBadRequest, err) + return + } + + ctx.JSON(iris.Map{"message": "OK"}) +} + +func userHandler(ctx iris.Context) { + ctx.Writef("Hello, %s!", ctx.Params().Get("username")) +} + +func jsonToString(src []byte) string { + buf := new(bytes.Buffer) + if err := json.Compact(buf, src); err != nil { + return err.Error() + } + + return buf.String() +} + +func requestLogFunc(w io.Writer) func(ctx iris.Context, lat time.Duration) { + return func(ctx iris.Context, lat time.Duration) { + var ( + method = ctx.Method() // request method. + // Use a stored value instead of ctx.Path() + // because some handlers may change the relative path + // to perform some action. + path = ctx.Values().GetString("path") + code = ctx.GetStatusCode() // response status code + // request and response data or error reading them. + requestBody string + responseBody string + + // url parameters and path parameters separated by space, + // key=value key2=value2. + requestValues string + ) + + // any error handler stored ( ctx.SetErr or StopWith(Plain)Error ) + errHandler := ctx.GetErr() + // check if not error and client sent a response with a content-type set-ed. + if errHandler == nil { + if ctx.GetContentTypeRequested() == "application/json" { + // Read and log request body the client sent to the server: + // + // You can use ctx.ReadBody(&body) + // which will decode any body (json, xml, msgpack, protobuf...) + // and use %v inside the fmt.Fprintf to print something like: + // map[age:22 id:10 name:Tim] + // + // But if you want specific to json string, + // then do that: + var tmp json.RawMessage + if err := ctx.ReadJSON(&tmp); err != nil { + requestBody = err.Error() + } else { + requestBody = jsonToString(tmp) + } + // + } else { + // left for exercise. + } + } else { + requestBody = fmt.Sprintf("error(%s)", errHandler.Error()) + } + + responseData := ctx.Recorder().Body() + // check if the server sent any response with content type, + // note that this will return the ;charset too + // so we check for its prefix instead. + if strings.HasPrefix(ctx.GetContentType(), "application/json") { + responseBody = jsonToString(responseData) + } else { + responseBody = string(responseData) + } + + var buf strings.Builder + + ctx.Params().Visit(func(key, value string) { + buf.WriteString(key) + buf.WriteByte('=') + buf.WriteString(value) + buf.WriteByte(' ') + }) + + for _, entry := range ctx.URLParamsSorted() { + buf.WriteString(entry.Key) + buf.WriteByte('=') + buf.WriteString(entry.Value) + buf.WriteByte(' ') + } + + if n := buf.Len(); n > 1 { + requestValues = buf.String()[0 : n-1] // remove last space. + } + + fmt.Fprintf(w, "%s|%s|%s|%s|%s|%d|%s|%s|\n", + time.Now().Format("2006-01-02 15:04:05"), + lat, + method, + path, + requestValues, + code, + requestBody, + responseBody, + ) + } +} diff --git a/_examples/logging/request-logger/request-logger-access-log-file/public/index.html b/_examples/logging/request-logger/request-logger-access-log-file/public/index.html new file mode 100644 index 00000000..06651a45 --- /dev/null +++ b/_examples/logging/request-logger/request-logger-access-log-file/public/index.html @@ -0,0 +1 @@ +

Hello index

\ No newline at end of file diff --git a/context/context.go b/context/context.go index 10205f83..ccf8a774 100644 --- a/context/context.go +++ b/context/context.go @@ -19,6 +19,7 @@ import ( "path/filepath" "reflect" "regexp" + "sort" "strconv" "strings" "sync/atomic" @@ -1504,8 +1505,12 @@ func (ctx *Context) URLParamBool(name string) (bool, error) { return strconv.ParseBool(ctx.URLParam(name)) } -// URLParams returns a map of GET query parameters separated by comma if more than one -// it returns an empty map if nothing found. +// URLParams returns a map of URL Query parameters. +// If the value of a URL parameter is a slice, +// then it is joined as one separated by comma. +// It returns an empty map on empty URL query. +// +// See URLParamsSorted too. func (ctx *Context) URLParams() map[string]string { q := ctx.request.URL.Query() values := make(map[string]string, len(q)) @@ -1517,6 +1522,34 @@ func (ctx *Context) URLParams() map[string]string { return values } +// URLParamsSorted returns a sorted (by key) slice +// of key-value entries of the URL Query parameters. +func (ctx *Context) URLParamsSorted() []memstore.StringEntry { + q := ctx.request.URL.Query() + n := len(q) + if n == 0 { + return nil + } + + keys := make([]string, 0, n) + for key := range q { + keys = append(keys, key) + } + + sort.Strings(keys) + + entries := make([]memstore.StringEntry, 0, n) + for _, key := range keys { + value := q[key] + entries = append(entries, memstore.StringEntry{ + Key: key, + Value: strings.Join(value, ","), + }) + } + + return entries +} + // No need anymore, net/http checks for the Form already. // func (ctx *Context) askParseForm() error { // if ctx.request.Form == nil { diff --git a/context/request_params.go b/context/request_params.go index 4aa4ae78..70661b2c 100644 --- a/context/request_params.go +++ b/context/request_params.go @@ -16,6 +16,18 @@ type RequestParams struct { memstore.Store } +// RequestParamsReadOnly is the read-only access type of RequestParams. +type RequestParamsReadOnly interface { + Get(key string) string + GetEntryAt(index int) memstore.Entry + Visit(visitor func(key string, value string)) + GetTrim(key string) string + GetEscape(key string) string + GetDecoded(key string) string +} // Note: currently unused. + +var _ RequestParamsReadOnly = (*RequestParams)(nil) + // Set inserts a parameter value. // See `Get` too. func (r *RequestParams) Set(key, value string) { diff --git a/core/memstore/memstore.go b/core/memstore/memstore.go index 51d0db42..10ef05c4 100644 --- a/core/memstore/memstore.go +++ b/core/memstore/memstore.go @@ -26,6 +26,13 @@ type ( immutable bool // if true then it can't change by its caller. } + // StringEntry is just a key-value wrapped by a struct. + // See Context.URLParamsSorted method. + StringEntry struct { + Key string `json:"key" msgpack:"key" yaml:"Key" toml:"Value"` + Value string `json:"value" msgpack:"value" yaml:"Value" toml:"Value"` + } + // Store is a collection of key-value entries with immutability capabilities. Store []Entry ) diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go index f2d447e0..1b3aa963 100644 --- a/middleware/logger/logger.go +++ b/middleware/logger/logger.go @@ -80,6 +80,8 @@ func (l *requestLoggerMiddleware) ServeHTTP(ctx *context.Context) { if l.config.PathAfterHandler /* we don't care if Path is disabled */ { path = l.getPath(ctx) + // note: we could just use the r.RequestURI which is the original one, + // but some users may need the stripped one (on HandleDir). } if l.config.Status {