From 5e82fa5b893d67135f25fa0d9178add52d8acee7 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Mon, 24 Aug 2020 21:44:29 +0300 Subject: [PATCH] mvc: struct field and method dependency logs on debug level. Read HISTORY.md - remove Party.GetReporter - Read HISTORY.md --- HISTORY.md | 27 +++-- README_ES.md | 4 +- README_FA.md | 2 +- README_FR.md | 4 +- README_GR.md | 2 +- README_KO.md | 4 +- README_RU.md | 4 +- README_ZH.md | 4 +- _examples/mvc/error-handler-http/main.go | 2 +- _examples/mvc/overview/main.go | 4 +- core/router/api_builder.go | 34 ++---- core/router/handler.go | 4 +- core/router/party.go | 3 - core/router/route.go | 38 +++--- core/router/route_register_rule_test.go | 6 +- hero/binding.go | 20 ++- hero/container.go | 86 ++++++++++++- hero/dependency_source.go | 2 +- hero/handler.go | 1 + hero/struct.go | 2 +- iris.go | 15 ++- middleware/pprof/pprof.go | 147 +++++++++++++++-------- mvc/controller.go | 48 ++++---- mvc/mvc.go | 139 +++++++++++++++++++++ 24 files changed, 435 insertions(+), 167 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 2ac3199e..8d080c15 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -363,6 +363,22 @@ Response: Other Improvements: +- Improved tracing (with `app.Logger().SetLevel("debug")`) for routes. Screens: + +#### DBUG Routes (1) + +![DBUG routes 1](https://iris-go.com/images/v12.2.0-dbug.png?v=0) + +#### DBUG Routes (2) + +![DBUG routes 2](https://iris-go.com/images/v12.2.0-dbug2.png?v=0) + +#### DBUG Routes (3) + +![DBUG routes with Controllers](https://iris-go.com/images/v12.2.0-dbug3.png?v=0) + +- Update the [pprof middleware](https://github.com/kataras/iris/tree/master/middleware/pprof). + - New `Controller.HandleHTTPError(mvc.Code) ` optional Controller method to handle http errors as requested at: [MVC - More Elegent OnErrorCode registration?](https://github.com/kataras/iris/issues/1595). Example can be found [here](https://github.com/kataras/iris/tree/master/_examples/mvc/error-handler-http/main.go). ![MVC: HTTP Error Handler Method](https://user-images.githubusercontent.com/22900943/90948989-e04cd300-e44c-11ea-8c97-54d90fb0cbb6.png) @@ -470,16 +486,6 @@ func main() { - New Router [Wrapper](middleware/grpc). - New MVC `.Handle(ctrl, mvc.GRPC{...})` option which allows to register gRPC services per-party (without the requirement of a full wrapper) and optionally strict access to gRPC clients only, see the [example here](_examples/mvc/grpc-compatible). -- Improved tracing (with `app.Logger().SetLevel("debug")`) for routes. Example: - -#### DBUG Routes (1) - -![DBUG routes](https://iris-go.com/images/v12.2.0-dbug.png?v=0) - -#### DBUG Routes (2) - -![DBUG routes](https://iris-go.com/images/v12.2.0-dbug2.png?v=0) - - Add `Configuration.RemoteAddrHeadersForce bool` to force `Context.RemoteAddr() string` to return the first entry of request headers as a fallback instead of the `Request.RemoteAddr` one, as requested at: [1567#issuecomment-663972620](https://github.com/kataras/iris/issues/1567#issuecomment-663972620). - Fix [#1569#issuecomment-663739177](https://github.com/kataras/iris/issues/1569#issuecomment-663739177). @@ -627,6 +633,7 @@ New Context Methods: Breaking Changes: +- `Party.GetReporter()` **removed**. The `Application.Build` returns the first error now and the API's errors are logged, this allows the server to run even if some of the routes are invalid but not fatal to the entire application (it was a request from a company). - `versioning.NewGroup(string)` now accepts a `Party` as its first input argument: `NewGroup(Party, string)`. - `versioning.RegisterGroups` is **removed** as it is no longer necessary. - `Configuration.RemoteAddrHeaders` from `map[string]bool` to `[]string`. If you used `With(out)RemoteAddrHeader` then you are ready to proceed without any code changes for that one. diff --git a/README_ES.md b/README_ES.md index fec5f5e9..d4613de3 100644 --- a/README_ES.md +++ b/README_ES.md @@ -53,11 +53,11 @@ Para obtener una documentación técnica más detallada, puede dirigirse a nuest ### ¿Te gusta leer mientras viajas? - Book cover + Book cover [![follow author](https://img.shields.io/twitter/follow/makismaropoulos.svg?style=for-the-badge)](https://twitter.com/intent/follow?screen_name=makismaropoulos) -Puedes [solicitar](https://bit.ly/iris-req-book) una versión en PDF y acceso en línea del **E-Book** hoy y participar en el desarrollo de Iris. +Puedes [solicitar](https://www.iris-go.com/#ebookDonateForm) una versión en PDF y acceso en línea del **E-Book** hoy y participar en el desarrollo de Iris. ## Contribuir diff --git a/README_FA.md b/README_FA.md index dcdacaf8..8fb01612 100644 --- a/README_FA.md +++ b/README_FA.md @@ -75,7 +75,7 @@ $ go run example.go
- Book cover + Book cover
diff --git a/README_FR.md b/README_FR.md index 1ab7394a..e56fb131 100644 --- a/README_FR.md +++ b/README_FR.md @@ -61,11 +61,11 @@ Pour une documentation encore plus complète vous pouvez visiter notre [godocs]( ### Vous préférez une version PDF? - Book cover + Book cover [![follow author](https://img.shields.io/twitter/follow/makismaropoulos.svg?style=for-the-badge)](https://twitter.com/intent/follow?screen_name=makismaropoulos) -Vous pouvez [demander](https://bit.ly/iris-req-book) une version **E-Book** (en Anglais) de la documentation et contribuer au développement d'Iris. +Vous pouvez [demander](https://www.iris-go.com/#ebookDonateForm) une version **E-Book** (en Anglais) de la documentation et contribuer au développement d'Iris. ## 🙌 Contribuer diff --git a/README_GR.md b/README_GR.md index b7d98ff6..5a94c4ee 100644 --- a/README_GR.md +++ b/README_GR.md @@ -57,7 +57,7 @@ $ go run example.go ### Σας αρέσει να διαβάζετε ενώ ταξιδεύετε; -Μπορείτε να [ζητήσετε](https://bit.ly/iris-req-book) σήμερα την PDF έκδοση και την online πρόσβαση στο Ηλεκτρονικό μας **Βιβλίο(E-Book)** και να συμμετάσχετε στην ανάπτυξη του Iris. +Μπορείτε να [ζητήσετε](https://www.iris-go.com/#ebookDonateForm) σήμερα την PDF έκδοση και την online πρόσβαση στο Ηλεκτρονικό μας **Βιβλίο(E-Book)** και να συμμετάσχετε στην ανάπτυξη του Iris. [![https://iris-go.com/images/iris-book-cover-sm.jpg](https://iris-go.com/images/iris-book-cover-sm.jpg)](https://bit.ly/iris-req-book) diff --git a/README_KO.md b/README_KO.md index 97bd485b..db2e36e2 100644 --- a/README_KO.md +++ b/README_KO.md @@ -53,11 +53,11 @@ Iris는 광범위하고 꼼꼼한 **[wiki](https://github.com/kataras/iris/wiki) ### 여행하면서 독서를 즐기세요? - Book cover + Book cover [![follow author](https://img.shields.io/twitter/follow/makismaropoulos.svg?style=for-the-badge)](https://twitter.com/intent/follow?screen_name=makismaropoulos) -PDF 버전과 **E-Book** 에 대한 온라인 접근을 [요청](https://bit.ly/iris-req-book)하시고 Iris의 개발에 참가하실 수 있습니다. +PDF 버전과 **E-Book** 에 대한 온라인 접근을 [요청](https://www.iris-go.com/#ebookDonateForm)하시고 Iris의 개발에 참가하실 수 있습니다. ## 기여하기 diff --git a/README_RU.md b/README_RU.md index 412456fb..f0a7174b 100644 --- a/README_RU.md +++ b/README_RU.md @@ -54,11 +54,11 @@ $ go run example.go ### Вы любите читать во время путешествий? - Book cover + Book cover -Вы можете [запросить](https://bit.ly/iris-req-book) PDF версию и онлайн-доступ к **E-Book** сегодня и принять участие в разработке Iris. +Вы можете [запросить](https://www.iris-go.com/#ebookDonateForm) PDF версию и онлайн-доступ к **E-Book** сегодня и принять участие в разработке Iris. ## Содействие diff --git a/README_ZH.md b/README_ZH.md index 4494aa5e..8a9ba794 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -189,11 +189,11 @@ Iris 有完整且详尽的 **[使用文档](https://github.com/kataras/iris/wiki ### 你喜欢在旅行时阅读吗? - Book cover + Book cover [![follow Iris web framework on twitter](https://img.shields.io/twitter/follow/iris_framework?color=ee7506&logoColor=ee7506&style=for-the-badge)](https://twitter.com/intent/follow?screen_name=iris_framework) -您可以[获取](https://bit.ly/iris-req-book)PDF版本或在线访问**电子图书**,并参与到Iris的开发中。 +您可以[获取](https://www.iris-go.com/#ebookDonateForm)PDF版本或在线访问**电子图书**,并参与到Iris的开发中。 ## 🙌 贡献 diff --git a/_examples/mvc/error-handler-http/main.go b/_examples/mvc/error-handler-http/main.go index ff148a58..42f7ad43 100644 --- a/_examples/mvc/error-handler-http/main.go +++ b/_examples/mvc/error-handler-http/main.go @@ -31,7 +31,7 @@ func (c *controller) Get() string { // You could register a Context and get its error code through ctx.GetStatusCode(). // // This can accept dependencies and output values like any other Controller Method, -// however be careful if your registered dependencies depend only on succesful(200...) requests. +// however be careful if your registered dependencies depend only on successful(200...) requests. // // Also note that, if you register more than one controller.HandleHTTPError // in the same Party, you need to use the RouteOverlap feature as shown diff --git a/_examples/mvc/overview/main.go b/_examples/mvc/overview/main.go index 7476d9b7..0e1449cf 100644 --- a/_examples/mvc/overview/main.go +++ b/_examples/mvc/overview/main.go @@ -12,13 +12,15 @@ import ( func main() { app := iris.New() + app.Logger().SetLevel("debug") + app.Get("/ping", pong).Describe("healthcheck") mvc.Configure(app.Party("/greet"), setup) // http://localhost:8080/greet?name=kataras addr := ":" + environment.Getenv("PORT", "8080") - app.Listen(addr, iris.WithLogLevel("debug")) + app.Listen(addr) } func pong(ctx iris.Context) { diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 4509ffaa..2e795865 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -11,7 +11,6 @@ import ( "time" "github.com/kataras/iris/v12/context" - "github.com/kataras/iris/v12/core/errgroup" "github.com/kataras/iris/v12/hero" "github.com/kataras/iris/v12/macro" macroHandler "github.com/kataras/iris/v12/macro/handler" @@ -169,12 +168,6 @@ type APIBuilder struct { // the api builder global routes repository routes *repository - // the api builder global errors, can be filled by the Subdomain, WildcardSubdomain, Handle... - // the list of possible errors that can be - // collected on the build state to log - // to the end-user. - errors *errgroup.Group - // the per-party handlers, order // of handlers registration matters. middleware context.Handlers @@ -236,10 +229,9 @@ func NewAPIBuilder(logger *golog.Logger) *APIBuilder { logger: logger, parent: nil, macros: macro.Defaults, - errors: errgroup.New("API Builder"), relativePath: "/", routes: new(repository), - apiBuilderDI: &APIContainer{Container: hero.New()}, + apiBuilderDI: &APIContainer{Container: hero.New().WithLogger(logger)}, routerFilters: make(map[Party]*Filter), partyMatcher: defaultPartyMatcher, } @@ -296,11 +288,6 @@ func (api *APIBuilder) GetRelPath() string { return api.relativePath } -// GetReporter returns the reporter for adding or receiving any errors caused when building the API. -func (api *APIBuilder) GetReporter() *errgroup.Group { - return api.errors -} - // AllowMethods will re-register the future routes that will be registered // via `Handle`, `Get`, `Post`, ... to the given "methods" on that Party and its children "Parties", // duplicates are not registered. @@ -394,7 +381,7 @@ func (api *APIBuilder) handle(errorCode int, method string, relativePath string, route.topLink = api.routes.getRelative(route) if route, err = api.routes.register(route, api.routeRegisterRule); err != nil { - api.errors.Add(err) + api.logger.Error(err) break } } @@ -492,7 +479,7 @@ func (api *APIBuilder) HandleDir(requestPath string, fs http.FileSystem, opts .. } if _, err := api.routes.register(route, api.routeRegisterRule); err != nil { - api.errors.Add(err) + api.logger.Error(err) break } } @@ -533,7 +520,7 @@ func (api *APIBuilder) createRoutes(errorCode int, methods []string, relativePat fullpath := api.relativePath + relativePath // for now, keep the last "/" if any, "/xyz/" if len(handlers) == 0 { - api.errors.Addf("missing handlers for route[%s:%d] %s: %s", filename, line, strings.Join(methods, ", "), fullpath) + api.logger.Errorf("missing handlers for route[%s:%d] %s: %s", filename, line, strings.Join(methods, ", "), fullpath) return nil } @@ -586,7 +573,7 @@ func (api *APIBuilder) createRoutes(errorCode int, methods []string, relativePat for i, m := range methods { // single, empty method for error handlers. route, err := NewRoute(errorCode, m, subdomain, path, routeHandlers, *api.macros) if err != nil { // template path parser errors: - api.errors.Addf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path) + api.logger.Errorf("[%s:%d] %v -> %s:%s:%s", filename, line, err, m, subdomain, path) continue } @@ -678,7 +665,6 @@ func (api *APIBuilder) Party(relativePath string, handlers ...context.Handler) P routes: api.routes, beginGlobalHandlers: api.beginGlobalHandlers, doneGlobalHandlers: api.doneGlobalHandlers, - errors: api.errors, routerFilters: api.routerFilters, // shared. partyMatcher: api.partyMatcher, // shared. // per-party/children @@ -730,7 +716,7 @@ func (api *APIBuilder) PartyFunc(relativePath string, partyBuilderFunc func(p Pa func (api *APIBuilder) Subdomain(subdomain string, middleware ...context.Handler) Party { if api.relativePath == SubdomainWildcardIndicator { // cannot concat wildcard subdomain with something else - api.errors.Addf("cannot concat parent wildcard subdomain with anything else -> %s , %s", + api.logger.Errorf("cannot concat parent wildcard subdomain with anything else -> %s , %s", api.relativePath, subdomain) return api } @@ -750,7 +736,7 @@ func (api *APIBuilder) Subdomain(subdomain string, middleware ...context.Handler func (api *APIBuilder) WildcardSubdomain(middleware ...context.Handler) Party { if hasSubdomain(api.relativePath) { // cannot concat static subdomain with a dynamic one, wildcard should be at the root level - api.errors.Addf("cannot concat static subdomain with a dynamic one. Dynamic subdomains should be at the root level -> %s", + api.logger.Errorf("cannot concat static subdomain with a dynamic one. Dynamic subdomains should be at the root level -> %s", api.relativePath) return api } @@ -1184,7 +1170,7 @@ func (api *APIBuilder) Favicon(favPath string, requestPath ...string) *Route { favPath = Abs(favPath) f, err := os.Open(favPath) if err != nil { - api.errors.Addf("favicon: file or directory %s not found: %w", favPath, err) + api.logger.Errorf("favicon: file or directory %s not found: %w", favPath, err) return nil } @@ -1201,7 +1187,7 @@ func (api *APIBuilder) Favicon(favPath string, requestPath ...string) *Route { // So we could panic but we don't, // we just interrupt with a message // to the (user-defined) logger. - api.errors.Addf("favicon: couldn't read the data bytes for %s: %w", favPath, err) + api.logger.Errorf("favicon: couldn't read the data bytes for %s: %w", favPath, err) return nil } @@ -1261,7 +1247,7 @@ func (api *APIBuilder) OnAnyErrorCode(handlers ...context.Handler) (routes []*Ro // Read `Configuration.ViewEngineContextKey` documentation for more. func (api *APIBuilder) RegisterView(viewEngine context.ViewEngine) { if err := viewEngine.Load(); err != nil { - api.errors.Add(err) + api.logger.Error(err) return } diff --git a/core/router/handler.go b/core/router/handler.go index 5dd51543..271fda76 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -227,7 +227,7 @@ func (h *routerHandler) Build(provider RoutesProvider) error { } // TODO: move this and make it easier to read when all cases are, visually, tested. - if logger := h.logger; logger != nil && logger.Level == golog.DebugLevel { + if logger := h.logger; logger != nil && logger.Level == golog.DebugLevel && noLogCount < len(registeredRoutes) { // group routes by method and print them without the [DBUG] and time info, // the route logs are colorful. // Note: don't use map, we need to keep registered order, use @@ -291,7 +291,7 @@ func (h *routerHandler) Build(provider RoutesProvider) error { m.method = "ERROR" } fmt.Fprintf(logger.Printer, "%d ", len(m.routes)) - pio.WriteRich(logger.Printer, m.method, traceMethodColor(m.method)) + pio.WriteRich(logger.Printer, m.method, TraceTitleColorCode(m.method)) } fmt.Fprint(logger.Printer, ")\n") diff --git a/core/router/party.go b/core/router/party.go index be0d1812..10468e60 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -4,7 +4,6 @@ import ( "net/http" "github.com/kataras/iris/v12/context" - "github.com/kataras/iris/v12/core/errgroup" "github.com/kataras/iris/v12/macro" "github.com/kataras/golog" @@ -36,8 +35,6 @@ type Party interface { // if r := app.Party("/users"), then the `r.GetRelPath()` is the "/users". // if r := app.Party("www.") or app.Subdomain("www") then the `r.GetRelPath()` is the "www.". GetRelPath() string - // GetReporter returns the reporter for adding or receiving any errors caused when building the API. - GetReporter() *errgroup.Group // Macros returns the macro collection that is responsible // to register custom macros with their own parameter types and their macro functions for all routes. // diff --git a/core/router/route.go b/core/router/route.go index 15a89542..bf71db0e 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -19,7 +19,7 @@ import ( // If any of the following fields are changed then the // caller should Refresh the router. type Route struct { - Title string `json"title"` // custom name to replace the method on debug logging. + Title string `json:"title"` // custom name to replace the method on debug logging. Name string `json:"name"` // "userRoute" Description string `json:"description"` // "lists a user" Method string `json:"method"` // "GET" @@ -202,10 +202,10 @@ func (r *Route) BuildHandlers() { // String returns the form of METHOD, SUBDOMAIN, TMPL PATH. func (r *Route) String() string { - start := r.Method - if r.StatusCode > 0 { - start = http.StatusText(r.StatusCode) - } + start := r.GetTitle() + // if r.StatusCode > 0 { + // start = fmt.Sprintf("%d (%s)", r.StatusCode, http.StatusText(r.StatusCode)) + // } return fmt.Sprintf("%s %s%s", start, r.Subdomain, r.Tmpl().Src) @@ -375,7 +375,8 @@ var methodColors = map[string]int{ MethodNone: 203, // orange-red. } -func traceMethodColor(method string) int { +// TraceTitleColorCode returns the color code depending on the method or the status. +func TraceTitleColorCode(method string) int { if color, ok := methodColors[method]; ok { return color } @@ -383,6 +384,20 @@ func traceMethodColor(method string) int { return 131 // for error handlers, of "ERROR [%STATUSCODE]" } +// GetTitle returns the custom Title or the method or the error code. +func (r *Route) GetTitle() string { + title := r.Title + if title == "" { + if r.StatusCode > 0 { + title = fmt.Sprintf("%d", r.StatusCode) // if error code then title is the status code, e.g. 400. + } else { + title = r.Method // else is its method, e.g. GET + } + } + + return title +} + // Trace prints some debug info about the Route to the "w". // Should be called after `Build` state. // @@ -391,17 +406,10 @@ func traceMethodColor(method string) int { // * @second_handler ... // If route and handler line:number locations are equal then the second is ignored. func (r *Route) Trace(w io.Writer, stoppedIndex int) { - title := r.Title - if title == "" { - if r.StatusCode > 0 { - title = fmt.Sprintf("%d", r.StatusCode) // if error code then title is the status code, e.g. 400. - } else { - title = r.Method // else is its method, e.g. GET - } - } + title := r.GetTitle() // Color the method. - color := traceMethodColor(title) + color := TraceTitleColorCode(title) // @method: @path // space := strings.Repeat(" ", len(http.MethodConnect)-len(method)) diff --git a/core/router/route_register_rule_test.go b/core/router/route_register_rule_test.go index 1a581ac2..85668ddd 100644 --- a/core/router/route_register_rule_test.go +++ b/core/router/route_register_rule_test.go @@ -43,9 +43,9 @@ func TestRegisterRule(t *testing.T) { if route := v1.Get("/", getHandler); route != nil { t.Fatalf("expected duplicated route, with RouteError rule, to be nil but got: %#+v", route) } - if expected, got := 1, len(v1.GetReporter().Errors); expected != got { - t.Fatalf("expected api builder's errors length to be: %d but got: %d", expected, got) - } + // if expected, got := 1, len(v1.GetReporter().Errors); expected != got { + // t.Fatalf("expected api builder's errors length to be: %d but got: %d", expected, got) + // } } func testRegisterRule(e *httptest.Expect, expectedGetBody string) { diff --git a/hero/binding.go b/hero/binding.go index 18620cc3..10318282 100644 --- a/hero/binding.go +++ b/hero/binding.go @@ -16,8 +16,9 @@ type binding struct { // Input contains the input reference of which a dependency is binded to. type Input struct { - Index int // for func inputs - StructFieldIndex []int // for struct fields in order to support embedded ones. + Index int // for func inputs + StructFieldIndex []int // for struct fields in order to support embedded ones. + StructFieldName string // the struct field's name. Type reflect.Type selfValue reflect.Value // reflect.ValueOf(*Input) cache. @@ -34,6 +35,12 @@ func newInput(typ reflect.Type, index int, structFieldIndex []int) *Input { return in } +func newStructFieldInput(f reflect.StructField) *Input { + input := newInput(f.Type, f.Index[0], f.Index) + input.StructFieldName = f.Name + return input +} + // String returns the string representation of a binding. func (b *binding) String() string { index := fmt.Sprintf("%d", b.Input.Index) @@ -261,7 +268,7 @@ func getBindingsForStruct(v reflect.Value, dependencies []*Dependency, paramsCou // fmt.Printf("Controller [%s] | NonZero | Field Index: %v | Field Type: %s\n", typ, f.Index, f.Type) bindings = append(bindings, &binding{ Dependency: NewDependency(elem.FieldByIndex(f.Index).Interface()), - Input: newInput(f.Type, f.Index[0], f.Index), + Input: newStructFieldInput(f), }) } @@ -299,9 +306,10 @@ func getBindingsForStruct(v reflect.Value, dependencies []*Dependency, paramsCou // fmt.Printf(""Controller [%s] | Binding: %s\n", typ, binding.String()) if len(binding.Input.StructFieldIndex) == 0 { - // set correctly the input's field index. - structFieldIndex := fields[binding.Input.Index].Index - binding.Input.StructFieldIndex = structFieldIndex + // set correctly the input's field index and name. + f := fields[binding.Input.Index] + binding.Input.StructFieldIndex = f.Index + binding.Input.StructFieldName = f.Name } // fmt.Printf("Controller [%s] | binding Index: %v | binding Type: %s\n", typ, binding.Input.StructFieldIndex, binding.Input.Type) diff --git a/hero/container.go b/hero/container.go index 7471fd5b..86347d4b 100644 --- a/hero/container.go +++ b/hero/container.go @@ -6,6 +6,7 @@ import ( "net" "net/http" "reflect" + "strings" "time" "github.com/kataras/iris/v12/context" @@ -15,7 +16,7 @@ import ( ) // Default is the default container value which can be used for dependencies share. -var Default = New() +var Default = New().WithLogger(golog.Default) // Container contains and delivers the Dependencies that will be binded // to the controller(s) or handler(s) that can be created @@ -28,6 +29,10 @@ var Default = New() // // For a more high-level structure please take a look at the "mvc.go#Application". type Container struct { + // Optional Logger to report dependencies and matched bindings + // per struct, function and method. + // By default it is set by the Party creator of this Container. + Logger *golog.Logger // Sorter specifies how the inputs should be sorted before binded. // Defaults to sort by "thinnest" target empty interface. Sorter Sorter @@ -36,12 +41,83 @@ type Container struct { // GetErrorHandler should return a valid `ErrorHandler` to handle bindings AND handler dispatch errors. // Defaults to a functon which returns the `DefaultErrorHandler`. GetErrorHandler func(*context.Context) ErrorHandler // cannot be nil. + // Reports contains an ordered list of information about bindings for further analysys and testing. + Reports []*Report // resultHandlers is a list of functions that serve the return struct value of a function handler. // Defaults to "defaultResultHandler" but it can be overridden. resultHandlers []func(next ResultHandler) ResultHandler } +// A Report holds meta information about dependency sources and target values per package, +// struct, struct's fields, struct's method, package-level function or closure. +// E.g. main -> (*UserController) -> HandleHTTPError. +type Report struct { + // The name is the last part of the name of a struct or its methods or a function. + // Each name is splited by its package.struct.field or package.funcName or package.func.inlineFunc. + Name string + // If it's a struct or package or function + // then it contains children reports of each one of its methods or input parameters + // respectfully. + Reports []*Report + + Parent *Report + Entries []ReportEntry +} + +// A ReportEntry holds the information about a binding. +type ReportEntry struct { + InputPosition int // struct field position or parameter position. + InputFieldName string // if it's a struct field, then this is its type name (we can't get param names). + InputFieldType reflect.Type // the input's type. + DependencyValue interface{} // the dependency value binded to that InputPosition of Name. + DependencyFile string // the file + DependencyLine int // and line number of the dependency's value. +} + +func (r *Report) fill(bindings []*binding) { + for _, b := range bindings { + inputFieldName := b.Input.StructFieldName + if inputFieldName == "" { + // it's not a struct field, then type. + inputFieldName = b.Input.Type.String() + } + // remove only the main one prefix. + inputFieldName = strings.TrimPrefix(inputFieldName, "main.") + + fieldName := inputFieldName + switch fieldName { + case "*context.Context": + inputFieldName = strings.Replace(inputFieldName, "*context", "iris", 1) + case "hero.Code", "hero.Result", "hero.View", "hero.Response": + inputFieldName = strings.Replace(inputFieldName, "hero", "mvc", 1) + } + + entry := ReportEntry{ + InputPosition: b.Input.Index, + InputFieldName: inputFieldName, + InputFieldType: b.Input.Type, + + DependencyValue: b.Dependency.OriginalValue, + DependencyFile: b.Dependency.Source.File, + DependencyLine: b.Dependency.Source.Line, + } + + r.Entries = append(r.Entries, entry) + } +} + +// fillReport adds a report to the Reports field. +func (c *Container) fillReport(fullName string, bindings []*binding) { + // r := c.getReport(fullName) + + r := &Report{ + Name: fullName, + } + r.fill(bindings) + c.Reports = append(c.Reports, r) +} + // BuiltinDependencies is a list of builtin dependencies that are added on Container's initilization. // Contains the iris context, standard context, iris sessions and time dependencies. var BuiltinDependencies = []*Dependency{ @@ -113,16 +189,24 @@ func New(dependencies ...interface{}) *Container { return c } +// WithLogger injects a logger to use to debug dependencies and bindings. +func (c *Container) WithLogger(logger *golog.Logger) *Container { + c.Logger = logger + return c +} + // Clone returns a new cloned container. // It copies the ErrorHandler, Dependencies and all Options from "c" receiver. func (c *Container) Clone() *Container { cloned := New() + cloned.Logger = c.Logger cloned.GetErrorHandler = c.GetErrorHandler cloned.Sorter = c.Sorter clonedDeps := make([]*Dependency, len(c.Dependencies)) copy(clonedDeps, c.Dependencies) cloned.Dependencies = clonedDeps cloned.resultHandlers = c.resultHandlers + // Reports are not cloned. return cloned } diff --git a/hero/dependency_source.go b/hero/dependency_source.go index 579427a2..66b25f7c 100644 --- a/hero/dependency_source.go +++ b/hero/dependency_source.go @@ -48,7 +48,7 @@ func newSource(fn reflect.Value) Source { } return Source{ - File: callerFileName, + File: filepath.ToSlash(callerFileName), Line: callerLineNumber, Caller: callerName, } diff --git a/hero/handler.go b/hero/handler.go index 3baa8229..85911b8b 100644 --- a/hero/handler.go +++ b/hero/handler.go @@ -82,6 +82,7 @@ func makeHandler(fn interface{}, c *Container, paramsCount int) context.Handler numIn := typ.NumIn() bindings := getBindingsForFunc(v, c.Dependencies, paramsCount) + c.fillReport(context.HandlerName(fn), bindings) resultHandler := defaultResultHandler for i, lidx := 0, len(c.resultHandlers)-1; i <= lidx; i++ { diff --git a/hero/struct.go b/hero/struct.go index 7d537164..09f4d572 100644 --- a/hero/struct.go +++ b/hero/struct.go @@ -84,8 +84,8 @@ func makeStruct(structPtr interface{}, c *Container, partyParamsCount int) *Stru } isErrHandler := isErrorHandler(typ) - newContainer := c.Clone() + newContainer.fillReport(typ.String(), bindings) // Add the controller dependency itself as func dependency but with a known type which should be explicit binding // in order to keep its maximum priority. newContainer.Register(s.Acquire).Explicitly().DestType = typ diff --git a/iris.go b/iris.go index b66b6fdf..0f9b2692 100644 --- a/iris.go +++ b/iris.go @@ -15,7 +15,6 @@ import ( "time" "github.com/kataras/iris/v12/context" - "github.com/kataras/iris/v12/core/errgroup" "github.com/kataras/iris/v12/core/host" "github.com/kataras/iris/v12/core/netutil" "github.com/kataras/iris/v12/core/router" @@ -534,9 +533,6 @@ func (app *Application) Build() error { app.logger.SetLevel(app.config.LogLevel) } - rp := errgroup.New("Application Builder") - rp.Err(app.APIBuilder.GetReporter()) - if app.defaultMode { // the app.I18n and app.View will be not available until Build. if !app.I18n.Loaded() { for _, s := range []string{"./locales/*/*", "./locales/*", "./translations"} { @@ -585,14 +581,16 @@ func (app *Application) Build() error { app.view.AddFunc("urlpath", rv.Path) // app.view.AddFunc("url", rv.URL) if err := app.view.Load(); err != nil { - rp.Group("View Builder").Err(err) + app.logger.Errorf("View Builder: %v", err) + return err } } if !app.Router.Downgraded() { // router if _, err := injectLiveReload(app.ContextPool, app.Router); err != nil { - rp.Errf("LiveReload: init: failed: %v", err) + app.logger.Errorf("LiveReload: init: failed: %v", err) + return err } if app.config.ForceLowercaseRouting { @@ -609,7 +607,8 @@ func (app *Application) Build() error { routerHandler := router.NewDefaultHandler(app.config, app.logger) err := app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder, false) if err != nil { - rp.Err(err) + app.logger.Error(err) + return err } app.HTTPErrorHandler = routerHandler // re-build of the router from outside can be done with @@ -619,7 +618,7 @@ func (app *Application) Build() error { // if end := time.Since(start); end.Seconds() > 5 { // app.logger.Debugf("Application: build took %s", time.Since(start)) - return errgroup.Check(rp) + return nil } // Runner is just an interface which accepts the framework instance diff --git a/middleware/pprof/pprof.go b/middleware/pprof/pprof.go index a71b2b10..f8551f33 100644 --- a/middleware/pprof/pprof.go +++ b/middleware/pprof/pprof.go @@ -5,77 +5,118 @@ import ( "html/template" "net/http/pprof" rpprof "runtime/pprof" - "strings" + "sort" "github.com/kataras/iris/v12/context" - "github.com/kataras/iris/v12/core/handlerconv" ) func init() { context.SetHandlerName("iris/middleware/pprof.*", "iris.profiling") } -// New returns a new pprof (profile, cmdline, symbol, goroutine, heap, threadcreate, debug/block) Middleware. -// Note: Route MUST have the last named parameter wildcard named '{action:path}' -func New() context.Handler { - cmdlineHandler := handlerconv.FromStd(pprof.Cmdline) - profileHandler := handlerconv.FromStd(pprof.Profile) - symbolHandler := handlerconv.FromStd(pprof.Symbol) - goroutineHandler := handlerconv.FromStd(pprof.Handler("goroutine")) - heapHandler := handlerconv.FromStd(pprof.Handler("heap")) - threadcreateHandler := handlerconv.FromStd(pprof.Handler("threadcreate")) - debugBlockHandler := handlerconv.FromStd(pprof.Handler("block")) +// net/http/pprof copy: +var profileDescriptions = map[string]string{ + "allocs": "A sampling of all past memory allocations", + "block": "Stack traces that led to blocking on synchronization primitives", + "cmdline": "The command line invocation of the current program", + "goroutine": "Stack traces of all current goroutines", + "heap": "A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.", + "mutex": "Stack traces of holders of contended mutexes", + "profile": "CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.", + "threadcreate": "Stack traces that led to the creation of new OS threads", + "trace": "A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.", +} +// New returns a new pprof (profile, cmdline, symbol, goroutine, heap, threadcreate, debug/block) Middleware. +// Note: Route MUST have the last named parameter wildcard named '{action:path}'. +// Example: +// app.HandleMany("GET", "/debug/pprof /debug/pprof/{action:path}", pprof.New()) +func New() context.Handler { return func(ctx *context.Context) { - ctx.ContentType("text/html") - action := ctx.Params().Get("action") - if action != "" { - if strings.Contains(action, "cmdline") { - cmdlineHandler((ctx)) - } else if strings.Contains(action, "profile") { - profileHandler(ctx) - } else if strings.Contains(action, "symbol") { - symbolHandler(ctx) - } else if strings.Contains(action, "goroutine") { - goroutineHandler(ctx) - } else if strings.Contains(action, "heap") { - heapHandler(ctx) - } else if strings.Contains(action, "threadcreate") { - threadcreateHandler(ctx) - } else if strings.Contains(action, "debug/block") { - debugBlockHandler(ctx) - } + if action := ctx.Params().Get("action"); action != "" { + pprof.Handler(action).ServeHTTP(ctx.ResponseWriter(), ctx.Request()) return } - profiles := rpprof.Profiles() - data := map[string]interface{}{ - "Profiles": profiles, - "Path": ctx.RequestPath(false), + ctx.Header("X-Content-Type-Options", "nosniff") + ctx.Header("Content-Type", "text/html; charset=utf-8") + + type profile struct { + Name string + Href string + Desc string + Count int + } + type page struct { + Path string + Profiles []profile } - if err := indexTmpl.Execute(ctx, data); err != nil { + var profiles []profile + for _, p := range rpprof.Profiles() { + profiles = append(profiles, profile{ + Name: p.Name(), + Href: p.Name() + "?debug=1", + Desc: profileDescriptions[p.Name()], + Count: p.Count(), + }) + } + + // Adding other profiles exposed from within this package + for _, p := range []string{"cmdline", "profile", "trace"} { + profiles = append(profiles, profile{ + Name: p, + Href: p, + Desc: profileDescriptions[p], + }) + } + + sort.Slice(profiles, func(i, j int) bool { + return profiles[i].Name < profiles[j].Name + }) + + if err := indexTmpl.Execute(ctx, page{ + Path: ctx.Path(), + Profiles: profiles, + }); err != nil { ctx.Application().Logger().Error(err) } } } var indexTmpl = template.Must(template.New("index").Parse(` - - /{{.Path}} - - - {{.Path}}
-
- profiles:
- - {{$path := .Path}} - {{range .Profiles}} -
{{.Count}}{{.Name}} - {{end}} -
-
- full goroutine stack dump
- - - `)) + +{{.Path}} + + + +{{.Path}}
+
+Types of profiles available: + + +{{ $path := .Path}} +{{range .Profiles}} + + + +{{end}} +
CountProfile
{{.Count}}{{.Name}}
+full goroutine stack dump +
+

+Profile Descriptions: +

+

+ + +`)) diff --git a/mvc/controller.go b/mvc/controller.go index d21e1bd0..a8b6edd4 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -163,6 +163,11 @@ func (c *ControllerActivator) Name() string { return c.fullName } +// RelName returns the path relatively to the main package. +func (c *ControllerActivator) RelName() string { + return strings.TrimPrefix(c.fullName, "main.") +} + // Router is the standard Iris router's public API. // With this you can register middleware, view layouts, subdomains, serve static files // and even add custom standard iris handlers as normally. @@ -243,7 +248,7 @@ func (c *ControllerActivator) DependenciesReadOnly() []*hero.Dependency { // Dependencies returns a value which can manage the controller's dependencies. func (c *ControllerActivator) Dependencies() *hero.Container { - return c.app.container + return c.app.container // although the controller's one are: c.injector.Container } // checks if a method is already registered. @@ -293,8 +298,8 @@ func (c *ControllerActivator) activate() { return } - c.parseHTTPErrorHandler() c.parseMethods() + c.parseHTTPErrorHandler() } func (c *ControllerActivator) parseHTTPErrorHandler() { @@ -316,7 +321,7 @@ func (c *ControllerActivator) parseMethod(m reflect.Method) { httpMethod, httpPath, err := parseMethod(c.app.Router.Macros(), m, c.isReservedMethod) if err != nil { if err != errSkip { - c.addErr(fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.fullName, m.Name, err)) + c.logErrorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.fullName, m.Name, err) } return @@ -325,8 +330,8 @@ func (c *ControllerActivator) parseMethod(m reflect.Method) { c.Handle(httpMethod, httpPath, m.Name) } -func (c *ControllerActivator) addErr(err error) bool { - return c.app.Router.GetReporter().Err(err) != nil +func (c *ControllerActivator) logErrorf(format string, args ...interface{}) { + c.Router().Logger().Errorf(format, args...) } // Handle registers a route based on a http method, the route's path @@ -350,7 +355,7 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . // handleHTTPError is called when a controller's method // with the "HandleHTTPError" is found. That method // can accept dependencies like the rest but if it's not called manually -// then any dynamic dependencies depending on succesful requests +// then any dynamic dependencies depending on successful requests // may fail - this is end-developer's job; // to register the correct dependencies or not do it all on that method. // @@ -364,20 +369,11 @@ func (c *ControllerActivator) handleHTTPError(funcName string) *router.Route { routes := c.app.Router.OnAnyErrorCode(handler) if len(routes) == 0 { - err := fmt.Errorf("MVC: unable to register an HTTP error code handler for '%s.%s'", c.fullName, funcName) - c.addErr(err) + c.logErrorf("MVC: unable to register an HTTP error code handler for '%s.%s'", c.fullName, funcName) return nil } - for _, r := range routes { - r.Description = "controller" - r.MainHandlerName = fmt.Sprintf("%s.%s", c.fullName, funcName) - if m, ok := c.Type.MethodByName(funcName); ok { - r.SourceFileName, r.SourceLineNumber = context.HandlerFileLineRel(m.Func) - } - } - - c.routes[funcName] = routes + c.saveRoutes(funcName, routes, true) return routes[0] } @@ -410,16 +406,19 @@ func (c *ControllerActivator) handleMany(method, path, funcName string, override // register the handler now. routes := c.app.Router.HandleMany(method, path, append(middleware, handler)...) if routes == nil { - c.addErr(fmt.Errorf("MVC: unable to register a route for the path for '%s.%s'", c.fullName, funcName)) + c.logErrorf("MVC: unable to register a route for the path for '%s.%s'", c.fullName, funcName) return nil } + c.saveRoutes(funcName, routes, override) + return routes +} + +func (c *ControllerActivator) saveRoutes(funcName string, routes []*router.Route, override bool) { + relName := c.RelName() for _, r := range routes { - // change the main handler's name and file:line - // in order to respect the controller's and give - // a proper debug/log message. - r.Description = "controller" - r.MainHandlerName = fmt.Sprintf("%s.%s", c.fullName, funcName) + r.Description = relName + r.MainHandlerName = fmt.Sprintf("%s.%s", relName, funcName) if m, ok := c.Type.MethodByName(funcName); ok { r.SourceFileName, r.SourceLineNumber = context.HandlerFileLineRel(m.Func) } @@ -428,15 +427,12 @@ func (c *ControllerActivator) handleMany(method, path, funcName string, override // add this as a reserved method name in order to // be sure that the same route // (method is allowed to be registered more than one on different routes - v11.2). - existingRoutes, exist := c.routes[funcName] if override || !exist { c.routes[funcName] = routes } else { c.routes[funcName] = append(existingRoutes, routes...) } - - return routes } func (c *ControllerActivator) handlerOf(relPath, methodName string) context.Handler { diff --git a/mvc/mvc.go b/mvc/mvc.go index 049bf20c..0a319976 100644 --- a/mvc/mvc.go +++ b/mvc/mvc.go @@ -1,6 +1,7 @@ package mvc import ( + "fmt" "reflect" "strings" @@ -10,6 +11,7 @@ import ( "github.com/kataras/iris/v12/websocket" "github.com/kataras/golog" + "github.com/kataras/pio" ) // Application is the high-level component of the "mvc" package. @@ -282,6 +284,10 @@ func (app *Application) handle(controller interface{}, options ...Option) *Contr } app.Controllers = append(app.Controllers, c) + + // Note: log on register-time, so they can catch any failures before build. + logController(app.Router.Logger(), c) + return c } @@ -313,3 +319,136 @@ func (app *Application) Clone(party router.Party) *Application { func (app *Application) Party(relativePath string, middleware ...context.Handler) *Application { return app.Clone(app.Router.Party(relativePath, middleware...)) } + +var childNameReplacer = strings.NewReplacer("*", "", "(", "", ")", "") + +// TODO: instead of this I want to get in touch with tools like "graphviz" +// so we can put all that information (and the API) inside web graphs, +// it will be easier for developers to see the flow of the whole application, +// but probalby I will never find time for that as we have higher priorities...just a reminder though. +func logController(logger *golog.Logger, c *ControllerActivator) { + if logger.Level != golog.DebugLevel { + return + } + + /* + [DBUG] controller.GreetController + ╺ Service → ./service/greet_service.go:16 + ╺ Get + GET /greet + • iris.Context + • service.Other → ./service/other_service.go:11 + */ + + bckpNewLine := logger.NewLine + bckpTimeFormat := logger.TimeFormat + logger.NewLine = false + logger.TimeFormat = "" + + printer := logger.Printer + + reports := c.injector.Container.Reports + ctrlName := c.RelName() + logger.Debugf("%s\n", ctrlName) + + longestNameLen := 0 + for _, report := range reports { + for _, entry := range report.Entries { + if n := len(entry.InputFieldName); n > longestNameLen { + if strings.HasSuffix(entry.InputFieldName, ctrlName) { + continue + } + longestNameLen = n + } + } + } + + longestMethodName := 0 + for methodName := range c.routes { + if n := len(methodName); n > longestMethodName { + longestMethodName = n + } + } + + lastColorCode := -1 + + for _, report := range reports { + + childName := childNameReplacer.Replace(report.Name) + if idx := strings.Index(childName, c.Name()); idx >= 0 { + childName = childName[idx+len(c.Name()):] // it's always +1 otherwise should be reported as BUG. + } + + if childName != "" && childName[0] == '.' { + // It's a struct's method. + + childName = childName[1:] + + for _, route := range c.routes[childName] { + if route.NoLog { + continue + } + + // Let them be logged again with the middlewares, e.g UseRouter or UseGlobal after this MVC app created. + // route.NoLog = true + + colorCode := router.TraceTitleColorCode(route.Method) + + // group same methods (or errors). + if lastColorCode == -1 { + lastColorCode = colorCode + } else if lastColorCode != colorCode { + lastColorCode = colorCode + fmt.Fprintln(printer) + } + + fmt.Fprint(printer, " ╺ ") + pio.WriteRich(printer, childName, colorCode) + + entries := report.Entries[1:] // the ctrl value is always the first input argument so 1:.. + if len(entries) == 0 { + fmt.Print("()") + } + fmt.Fprintln(printer) + + // pio.WriteRich(printer, " "+route.GetTitle(), colorCode) + fmt.Fprintf(printer, " %s\n", route.String()) + + for _, entry := range entries { + fileLine := "" + if !strings.Contains(entry.DependencyFile, "kataras/iris/") { + fileLine = fmt.Sprintf("→ %s:%d", entry.DependencyFile, entry.DependencyLine) + } + + fieldName := entry.InputFieldName + + spaceRequired := longestNameLen - len(fieldName) + if spaceRequired < 0 { + spaceRequired = 0 + } + // → ⊳ ↔ + fmt.Fprintf(printer, " • %s%s %s\n", fieldName, strings.Repeat(" ", spaceRequired), fileLine) + } + } + } else { + // It's a struct's field. + for _, entry := range report.Entries { + fileLine := "" + if !strings.Contains(entry.DependencyFile, "kataras/iris/") { + fileLine = fmt.Sprintf("→ %s:%d", entry.DependencyFile, entry.DependencyLine) + } + + fieldName := entry.InputFieldName + spaceRequired := longestNameLen + 2 - len(fieldName) // plus the two spaces because it's not collapsed. + if spaceRequired < 0 { + spaceRequired = 0 + } + fmt.Fprintf(printer, " ╺ %s%s %s\n", fieldName, strings.Repeat(" ", spaceRequired), fileLine) + } + } + } + // fmt.Fprintln(printer) + + logger.NewLine = bckpNewLine + logger.TimeFormat = bckpTimeFormat +}