mvc: struct field and method dependency logs on debug level. Read HISTORY.md

- remove Party.GetReporter

- Read HISTORY.md
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-08-24 21:44:29 +03:00
parent ef5685bf7e
commit 5e82fa5b89
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
24 changed files with 435 additions and 167 deletions

View File

@ -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) <T>` 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.

View File

@ -53,11 +53,11 @@ Para obtener una documentación técnica más detallada, puede dirigirse a nuest
### ¿Te gusta leer mientras viajas?
<a href="https://bit.ly/iris-req-book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12" /> </a>
<a href="https://iris-go.com/#book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12" /> </a>
[![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

View File

@ -75,7 +75,7 @@ $ go run example.go
<div dir="ltr" align="left">
<a href="https://bit.ly/iris-req-book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12" /> </a>
<a href="https://iris-go.com/#book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12" /> </a>
</div>

View File

@ -61,11 +61,11 @@ Pour une documentation encore plus complète vous pouvez visiter notre [godocs](
### Vous préférez une version PDF?
<a href="https://bit.ly/iris-req-book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12"/> </a>
<a href="https://iris-go.com/#book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12"/> </a>
[![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

View File

@ -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)

View File

@ -53,11 +53,11 @@ Iris는 광범위하고 꼼꼼한 **[wiki](https://github.com/kataras/iris/wiki)
### 여행하면서 독서를 즐기세요?
<a href="https://bit.ly/iris-req-book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12"/> </a>
<a href="https://iris-go.com/#book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12"/> </a>
[![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의 개발에 참가하실 수 있습니다.
## 기여하기

View File

@ -54,11 +54,11 @@ $ go run example.go
### Вы любите читать во время путешествий?
<a href="https://bit.ly/iris-req-book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12" /> </a>
<a href="https://iris-go.com/#book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12" /> </a>
<!-- [![follow author](https://img.shields.io/twitter/follow/makismaropoulos.svg?style=for-the-badge)](https://twitter.com/intent/follow?screen_name=makismaropoulos) -->
Вы можете [запросить](https://bit.ly/iris-req-book) PDF версию и онлайн-доступ к **E-Book** сегодня и принять участие в разработке Iris.
Вы можете [запросить](https://www.iris-go.com/#ebookDonateForm) PDF версию и онлайн-доступ к **E-Book** сегодня и принять участие в разработке Iris.
## Содействие

View File

@ -189,11 +189,11 @@ Iris 有完整且详尽的 **[使用文档](https://github.com/kataras/iris/wiki
### 你喜欢在旅行时阅读吗?
<a href="https://bit.ly/iris-req-book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12" /> </a>
<a href="https://iris-go.com/#book"> <img alt="Book cover" src="https://iris-go.com/images/iris-book-cover-sm.jpg?v=12" /> </a>
[![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的开发中。
## 🙌 贡献

View File

@ -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

View File

@ -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) {

View File

@ -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
}

View File

@ -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")

View File

@ -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.
//

View File

@ -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))

View File

@ -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) {

View File

@ -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)

View File

@ -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
}

View File

@ -48,7 +48,7 @@ func newSource(fn reflect.Value) Source {
}
return Source{
File: callerFileName,
File: filepath.ToSlash(callerFileName),
Line: callerLineNumber,
Caller: callerName,
}

View File

@ -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++ {

View File

@ -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

15
iris.go
View File

@ -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

View File

@ -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(`<html>
<head>
<title>/{{.Path}}</title>
</head>
<body>
{{.Path}}<br>
<br>
profiles:<br>
<table>
{{$path := .Path}}
{{range .Profiles}}
<tr><td align=right>{{.Count}}<td><a href="{{$path}}/{{.Name}}?debug=1">{{.Name}}</a>
{{end}}
</table>
<br>
<a href="{{$path}}/goroutine?debug=2">full goroutine stack dump</a><br>
</body>
</html>
`))
<head>
<title>{{.Path}}</title>
<style>
.profile-name{
display:inline-block;
width:6rem;
}
</style>
</head>
<body>
{{.Path}}<br>
<br>
Types of profiles available:
<table>
<thead><td>Count</td><td>Profile</td></thead>
{{ $path := .Path}}
{{range .Profiles}}
<tr>
<td>{{.Count}}</td><td><a href={{$path}}/{{.Href}}>{{.Name}}</a></td>
</tr>
{{end}}
</table>
<a href="{{.Path}}/goroutine?debug=2">full goroutine stack dump</a>
<br/>
<p>
Profile Descriptions:
<ul>
{{range .Profiles}}
<li><div class=profile-name>{{.Name}}:</div> {{.Desc}}</li>
{{end}}
</ul>
</p>
</body>
</html>
`))

View File

@ -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 {

View File

@ -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
}