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 {