diff --git a/.gitignore b/.gitignore index 462f0d0e..f4787c15 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vscode .directory +coverage.out package-lock.json go.sum node_modules diff --git a/.travis.yml b/.travis.yml index e87b8cd3..89e6e3f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ os: - linux - osx go: +# We support the latest two major Go versions: +# https://golang.org/doc/devel/release.html - 1.14.x - 1.15.x # - master @@ -12,6 +14,15 @@ go_import_path: github.com/kataras/iris/v12 env: global: - GO111MODULE=on +addons: + hosts: + - mydomain.com + - www.mydomain.com + - myotherdomain.com + - mymy.com + - testdomain.com + - testdomain1.com + - testdomain2.com install: - go get ./... script: diff --git a/apps/apps.go b/apps/apps.go new file mode 100644 index 00000000..dd492c5f --- /dev/null +++ b/apps/apps.go @@ -0,0 +1,4 @@ +// Package apps is responsible to control many Iris Applications. +// This package directly imports the iris root package and cannot be used +// inside Iris' codebase itself. Only external packages/programs can make use of it. +package apps diff --git a/apps/switch.go b/apps/switch.go new file mode 100644 index 00000000..5d7aa233 --- /dev/null +++ b/apps/switch.go @@ -0,0 +1,127 @@ +package apps + +import ( + "github.com/kataras/iris/v12" +) + +// Switch returns a new Application +// with the sole purpose of routing the +// matched Applications through the provided "cases". +// +// The cases are filtered in order of register. +// +// Example: +// switcher := Switch(Hosts{ +// "mydomain.com": app, +// "test.mydomain.com": testSubdomainApp, +// "otherdomain.com": "appName", +// }) +// switcher.Listen(":80") +// +// Note that this is NOT a load balancer. The filters +// are executed by registration order and matched Application +// handles the request. +// +// The returned Switch Application can register routes that will run +// if no application is matched against the given filters. +// The returned Switch Application can also register custom error code handlers, +// e.g. to inject the 404 on not application found. +// It can also be wrapped with its `WrapRouter` method, +// which is really useful for logging and statistics. +func Switch(providers ...SwitchProvider) *iris.Application { + if len(providers) == 0 { + panic("iris: switch: empty providers") + } + + var cases []SwitchCase + for _, p := range providers { + for _, c := range p.GetSwitchCases() { + cases = append(cases, c) + } + } + + if len(cases) == 0 { + panic("iris: switch: empty cases") + } + + app := iris.New() + // Try to build the cases apps on app.Build/Listen/Run so + // end-developers don't worry about it. + app.OnBuild = func() error { + for _, c := range cases { + if err := c.App.Build(); err != nil { + return err + } + } + return nil + } + // If we have a request to support + // middlewares in that switcher app then + // we can use app.Get("{p:path}"...) instead. + app.UseRouter(func(ctx iris.Context) { + for _, c := range cases { + if c.Filter(ctx) { + c.App.ServeHTTP(ctx.ResponseWriter().Naive(), ctx.Request()) + + // if c.App.Downgraded() { + // c.App.ServeHTTP(ctx.ResponseWriter(), ctx.Request()) + // } else { + // Note(@kataras): don't ever try something like that; + // the context pool is the switcher's one. + // ctx.SetApplication(c.App) + // c.App.ServeHTTPC(ctx) + // ctx.SetApplication(app) + // } + return + } + } + + // let the "switch app" handle it or fire a custom 404 error page, + // next is the switch app's router. + ctx.Next() + }) + + return app +} + +type ( + // SwitchCase contains the filter + // and the matched Application instance. + SwitchCase struct { + Filter iris.Filter + App *iris.Application + } + + // A SwitchProvider should return the switch cases. + // It's an interface instead of a direct slice because + // we want to make available different type of structures + // without wrapping. + SwitchProvider interface { + GetSwitchCases() []SwitchCase + } + + // Join returns a new slice which joins different type of switch cases. + Join []SwitchProvider +) + +var _ SwitchProvider = SwitchCase{} + +// GetSwitchCases completes the SwitchProvider, it returns itself. +func (sc SwitchCase) GetSwitchCases() []SwitchCase { + return []SwitchCase{sc} +} + +var _ SwitchProvider = Join{} + +// GetSwitchCases completes the switch provider. +func (j Join) GetSwitchCases() (cases []SwitchCase) { + for _, p := range j { + if p == nil { + continue + } + + cases = append(cases, p.GetSwitchCases()...) + } + + return +} diff --git a/apps/switch_go_test.go b/apps/switch_go_test.go new file mode 100644 index 00000000..71780bd3 --- /dev/null +++ b/apps/switch_go_test.go @@ -0,0 +1,37 @@ +package apps + +import ( + "fmt" + "testing" + + "github.com/kataras/iris/v12" +) + +func TestSwitchJoin(t *testing.T) { + myapp := iris.New() + customFilter := func(ctx iris.Context) bool { + pass, _ := ctx.URLParamBool("filter") + return pass + } + + joinedCases := Join{ + SwitchCase{ + Filter: customFilter, + App: myapp, + }, + Hosts{{Pattern: "^test.*$", Target: myapp}}, + } + + cases := []SwitchCase{ + { + Filter: customFilter, + App: myapp, + }, + {Filter: hostFilter("^test.*$"), App: myapp}, + } + + if expected, got := fmt.Sprintf("%#+v", cases), fmt.Sprintf("%#+v", joinedCases.GetSwitchCases()); expected != got { + t.Fatalf("join does not match with the expected slice of cases, expected:\n%s\nbut got:\n%s", expected, got) + } + +} diff --git a/apps/switch_hosts.go b/apps/switch_hosts.go new file mode 100644 index 00000000..f143d4f7 --- /dev/null +++ b/apps/switch_hosts.go @@ -0,0 +1,120 @@ +package apps + +import ( + "fmt" + "net/http" + "net/url" + "regexp" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/context" +) + +type ( + // Host holds the pattern for the SwitchCase filter + // and the Target host or application. + Host struct { + // Pattern is the incoming host matcher regexp or a literal. + Pattern string + // Target is the target Host that incoming requests will be redirected on pattern match + // or an Application's Name that will handle the incoming request matched the Pattern. + Target interface{} // It was a string in my initial design but let's do that interface{}, we may support more types here in the future, until generics are in, keep it interface{}. + } + // Hosts is a switch provider. + // It can be used as input argument to the `Switch` function + // to map host to existing Iris Application instances, e.g. + // { "www.mydomain.com": "mydomainApp" } . + // It can accept regexp as a host too, e.g. + // { "^my.*$": "mydomainApp" } . + Hosts []Host + + // Good by we need order and map can't provide it for us + // (e.g. "fallback" regexp } + // Hosts map[string]*iris.Application +) + +var _ SwitchProvider = Hosts{} + +// GetSwitchCases completes the SwitchProvider. +// It returns a slice of SwitchCase which +// if passed on `Switch` function, they act +// as a router between matched domains and subdomains +// between existing Iris Applications. +func (hosts Hosts) GetSwitchCases() []SwitchCase { + cases := make([]SwitchCase, 0, len(hosts)) + + for _, host := range hosts { + cases = append(cases, SwitchCase{ + Filter: hostFilter(host.Pattern), + App: hostApp(host), + }) + } + + return cases +} + +func hostApp(host Host) *iris.Application { + if host.Target == nil { + return nil + } + + switch target := host.Target.(type) { + case context.Application: + return target.(*iris.Application) + case string: + // Check if the given target is an application name, if so + // we must not redirect (loop) we must serve the request + // using that app. + if targetApp, ok := context.GetApplication(target); ok { + // It's always iris.Application so we are totally safe here. + return targetApp.(*iris.Application) + } + // If it's a real host, warn the user of invalid input. + u, err := url.Parse(target) + if err == nil && u.IsAbs() { + // remember, we redirect hosts, not full URLs here. + panic(fmt.Sprintf(`iris: switch: hosts: invalid target host: "%s"`, target)) + } + + if regex := regexp.MustCompile(host.Pattern); regex.MatchString(target) { + panic(fmt.Sprintf(`iris: switch: hosts: loop detected between expression: "%s" and target host: "%s"`, host.Pattern, host.Target)) + } + + return newHostRedirectApp(target, HostsRedirectCode) + default: + panic(fmt.Sprintf("iris: switch: hosts: invalid target type: %T", target)) + } +} + +func hostFilter(expr string) iris.Filter { + regex := regexp.MustCompile(expr) + return func(ctx iris.Context) bool { + return regex.MatchString(ctx.Host()) + } +} + +// HostsRedirectCode is the default status code is used +// to redirect a matching host to a url. +var HostsRedirectCode = iris.StatusMovedPermanently + +func newHostRedirectApp(targetHost string, code int) *iris.Application { + app := iris.New() + app.Downgrade(func(w http.ResponseWriter, r *http.Request) { + if targetHost == context.GetHost(r) { + // Note(@kataras): + // this should never happen as the HostsRedirect + // carefully checks if the expression already matched the "redirectTo" + // to avoid the redirect loops at all. + // iris: switch: hosts redirect: loop detected between expression: "^my.*$" and target host: "mydomain.com" + http.Error(w, http.StatusText(iris.StatusLoopDetected), iris.StatusLoopDetected) + return + } + + r.Host = targetHost + r.URL.Host = targetHost + + // r.URL.User = nil + http.Redirect(w, r, r.URL.String(), code) + }) + return app +} diff --git a/apps/switch_hosts_test.go b/apps/switch_hosts_test.go new file mode 100644 index 00000000..da9cc1f7 --- /dev/null +++ b/apps/switch_hosts_test.go @@ -0,0 +1,211 @@ +package apps + +import ( + "fmt" + "net/url" + "testing" + + "github.com/kataras/iris/v12" + "github.com/kataras/iris/v12/context" + "github.com/kataras/iris/v12/httptest" +) + +type testRequests map[string]map[string]int // url -> path -> status code + +func TestSwitchHosts(t *testing.T) { + var ( + expected = func(app context.Application, host string) string { + return fmt.Sprintf("App Name: %s\nHost: %s", app, host) + } + index = func(ctx iris.Context) { + ctx.WriteString(expected(ctx.Application(), ctx.Host())) + } + ) + + testdomain1 := iris.New().SetName("test 1 domain") + testdomain1.Get("/", index) // should match host matching with "testdomain1.com". + + testdomain2 := iris.New().SetName("test 2 domain") + testdomain2.Get("/", index) // should match host matching with "testdomain2.com". + + mydomain := iris.New().SetName("my domain") + mydomain.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { + ctx.WriteString(ctx.Host() + " custom not found") + }) + mydomain.Get("/", index) // should match ALL hosts starting with "my". + + tests := []struct { + Pattern string + Target *iris.Application + Requests testRequests + }{ + { + "testdomain1.com", + testdomain1, + testRequests{ + "http://testdomain1.com": { + "/": iris.StatusOK, + }, + }, + }, + { + "testdomain2.com", + testdomain2, + testRequests{ + "http://testdomain2.com": { + "/": iris.StatusOK, + }, + }, + }, + { + "^my.*$", + mydomain, + testRequests{ + "http://mydomain.com": { + "/": iris.StatusOK, + "/nf": iris.StatusNotFound, + }, + "http://myotherdomain.com": { + "/": iris.StatusOK, + }, + "http://mymy.com": { + "/": iris.StatusOK, + }, + "http://nmy.com": { + "/": iris.StatusBadGateway, /* 404 hijacked by switch.OnErrorCode */ + }, + }, + }, + } + + var hosts Hosts + for _, tt := range tests { + hosts = append(hosts, Host{tt.Pattern, tt.Target}) + } + switcher := Switch(hosts) + switcher.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { + // inject the 404 to 502. + // tests the ctx.Next inside the Hosts switch provider. + ctx.StatusCode(iris.StatusBadGateway) + ctx.WriteString("Switcher: Bad Gateway") + }) + + e := httptest.New(t, switcher) + for i, tt := range tests { + for URL, paths := range tt.Requests { + u, err := url.Parse(URL) + if err != nil { + t.Fatalf("[%d] %v", i, err) + } + targetHost := u.Host + for requestPath, statusCode := range paths { + // url := fmt.Sprintf("http://%s", requestHost) + body := expected(tt.Target, targetHost) + switch statusCode { + case 404: + body = targetHost + " custom not found" + case 502: + body = "Switcher: Bad Gateway" + } + + e.GET(requestPath).WithURL(URL).Expect().Status(statusCode).Body().Equal(body) + } + } + } +} + +func TestSwitchHostsRedirect(t *testing.T) { + var ( + expected = func(appName, host, path string) string { + return fmt.Sprintf("App Name: %s\nHost: %s\nPath: %s", appName, host, path) + } + index = func(ctx iris.Context) { + ctx.WriteString(expected(ctx.Application().String(), ctx.Host(), ctx.Path())) + } + ) + + mydomain := iris.New().SetName("mydomain") + mydomain.OnAnyErrorCode(func(ctx iris.Context) { + ctx.WriteString("custom: " + iris.StatusText(ctx.GetStatusCode())) + }) + mydomain.Get("/", index) + mydomain.Get("/f", index) + + tests := []struct { + Pattern string + Target string + Requests testRequests + }{ + { + "www.mydomain.com", + "mydomain", + testRequests{ + "http://www.mydomain.com": { + "/": iris.StatusOK, + "/f": iris.StatusOK, + "/nf": iris.StatusNotFound, + }, + }, + }, + { + "^test.*$", + "mydomain", + testRequests{ + "http://testdomain.com": { + "/": iris.StatusOK, + "/f": iris.StatusOK, + "/nf": iris.StatusNotFound, + }, + }, + }, + // Something like this will panic to protect users: + // { + // ..., + // "^my.*$", + // "mydomain.com", + // ... + // + { + "^www.*$", + "google.com", + testRequests{ + "http://www.mydomain.com": { + "/": iris.StatusOK, + }, + "http://www.golang.org": { + "/": iris.StatusNotFound, // should give not found because this is not a switcher's web app. + }, + }, + }, + } + + var hostsRedirect Hosts + for _, tt := range tests { + hostsRedirect = append(hostsRedirect, Host{tt.Pattern, tt.Target}) + } + + switcher := Switch(hostsRedirect) + e := httptest.New(t, switcher) + + for i, tt := range tests { + for requestURL, paths := range tt.Requests { + u, err := url.Parse(requestURL) + if err != nil { + t.Fatalf("[%d] %v", i, err) + } + targetHost := u.Host + for requestPath, statusCode := range paths { + body := expected(mydomain.String(), targetHost, requestPath) + if statusCode != 200 { + if tt.Target != mydomain.String() { // it's external. + body = "Not Found" + } else { + body = "custom: " + iris.StatusText(statusCode) + } + } + + e.GET(requestPath).WithURL(requestURL).Expect().Status(statusCode).Body().Equal(body) + } + } + } +} diff --git a/apps/switch_scheme.go b/apps/switch_scheme.go new file mode 100644 index 00000000..cff2ab90 --- /dev/null +++ b/apps/switch_scheme.go @@ -0,0 +1 @@ +package apps diff --git a/context/application.go b/context/application.go index ee0fbe4f..e2f52874 100644 --- a/context/application.go +++ b/context/application.go @@ -84,9 +84,30 @@ type Application interface { // // Order may change. FindClosestPaths(subdomain, searchPath string, n int) []string + + // String returns the Application's Name. + String() string } +// Notes(@kataras): +// Alternative places... +// 1. in apps/store, but it would require an empty `import _ "....apps/store" +// from end-developers, to avoid the import cycle and *iris.Application access. +// 2. in root package level, that could be the best option, it has access to the *iris.Application +// instead of the context.Application interface, but we try to keep the root package +// as minimum as possible, however: if in the future, those Application instances +// can be registered through network instead of same-process then we must think of that choice. +// 3. this is the best possible place, as the root package and all subpackages +// have access to this context package without import cycles and they already using it, +// the only downside is that we don't have access to the *iris.Application instance +// but this context.Application is designed that way that can execute all important methods +// as the whole Iris code base is so well written. + var ( + // registerApps holds all the created iris Applications by this process. + // It's slice instead of map because if IRIS_APP_NAME env var exists, + // by-default all applications running on the same machine + // will have the same name unless `Application.SetName` is called. registeredApps []Application mu sync.RWMutex ) @@ -107,8 +128,8 @@ func RegisterApplication(app Application) { // use `Context.Application()` instead. func LastApplication() Application { mu.RLock() - if n := len(registeredApps); n > 0 { - if app := registeredApps[n-1]; app != nil { + for i := len(registeredApps) - 1; i >= 0; i-- { + if app := registeredApps[i]; app != nil { mu.RUnlock() return app } @@ -117,6 +138,34 @@ func LastApplication() Application { return nil } +// GetApplication returns a registered Application +// based on its name. If the "appName" is not unique +// across Applications, then it will return the newest one. +func GetApplication(appName string) (Application, bool) { + mu.RLock() + + for i := len(registeredApps) - 1; i >= 0; i-- { + if app := registeredApps[i]; app != nil && app.String() == appName { + mu.RUnlock() + return app, true + } + } + + mu.RUnlock() + return nil, false +} + +// MustGetApplication same as `GetApplication` but it +// panics if "appName" is not a registered Application's name. +func MustGetApplication(appName string) Application { + app, ok := GetApplication(appName) + if !ok || app == nil { + panic(appName + " is not a registered Application") + } + + return app +} + // DefaultLogger returns a Logger instance for an Iris module. // If the program contains at least one registered Iris Application // before this call then it will return a child of that Application's Logger diff --git a/context/context.go b/context/context.go index b8084379..b2d9de41 100644 --- a/context/context.go +++ b/context/context.go @@ -135,6 +135,14 @@ func NewContext(app Application) *Context { return &Context{app: app} } +/* Not required, unless requested. +// SetApplication sets an Iris Application on-fly. +// Do NOT use it after ServeHTTPC is fired. +func (ctx *Context) SetApplication(app Application) { + ctx.app = app +} +*/ + // Clone returns a copy of the context that // can be safely used outside the request's scope. // Note that if the request-response lifecycle terminated @@ -898,8 +906,22 @@ func (ctx *Context) Domain() string { return GetDomain(ctx.Host()) } -// SubdomainFull returnst he full subdomain level, e.g. +// GetSubdomainFull returns the full subdomain level, e.g. // [test.user.]mydomain.com. +func GetSubdomainFull(r *http.Request) string { + host := GetHost(r) // host:port + rootDomain := GetDomain(host) // mydomain.com + rootDomainIdx := strings.Index(host, rootDomain) + if rootDomainIdx == -1 { + return "" + } + + return host[0:rootDomainIdx] +} + +// SubdomainFull returns the full subdomain level, e.g. +// [test.user.]mydomain.com. +// Note that HostProxyHeaders are being respected here. func (ctx *Context) SubdomainFull() string { host := ctx.Host() // host:port rootDomain := GetDomain(host) // mydomain.com diff --git a/context/handler.go b/context/handler.go index 6e653536..f4d7056c 100644 --- a/context/handler.go +++ b/context/handler.go @@ -19,7 +19,7 @@ var ( ) var ( - handlerNames = make(map[*nameExpr]string) + handlerNames = make(map[*NameExpr]string) handlerNamesMu sync.RWMutex ) @@ -44,19 +44,22 @@ func SetHandlerName(original string, replacement string) { // when a handler name is declared as it's and cause regex parsing expression error, // e.g. `iris/cache/client.(*Handler).ServeHTTP-fm` regex, _ := regexp.Compile(original) - handlerNames[&nameExpr{ + handlerNames[&NameExpr{ literal: original, regex: regex, }] = replacement handlerNamesMu.Unlock() } -type nameExpr struct { +// NameExpr regex or literal comparison through `MatchString`. +type NameExpr struct { regex *regexp.Regexp literal string } -func (expr *nameExpr) MatchString(s string) bool { +// MatchString reports whether "s" is literal of "literal" +// or it matches the regex expression at "regex". +func (expr *NameExpr) MatchString(s string) bool { if expr.literal == s { // if matches as string, as it's. return true } diff --git a/core/host/supervisor.go b/core/host/supervisor.go index 3dc044b5..546f35d2 100644 --- a/core/host/supervisor.go +++ b/core/host/supervisor.go @@ -492,6 +492,9 @@ func (su *Supervisor) RegisterOnShutdown(cb func()) { // for them to close, if desired. func (su *Supervisor) Shutdown(ctx context.Context) error { atomic.StoreUint32(&su.closedManually, 1) // future-use + if ctx == nil { + ctx = context.Background() + } return su.Server.Shutdown(ctx) } diff --git a/core/router/router_subdomain_redirect.go b/core/router/router_subdomain_redirect.go index 2dbcdb53..ed3fe86e 100644 --- a/core/router/router_subdomain_redirect.go +++ b/core/router/router_subdomain_redirect.go @@ -169,27 +169,6 @@ func (s *subdomainRedirectWrapper) Wrapper(w http.ResponseWriter, r *http.Reques router(w, r) } -func redirectAbsolute(w http.ResponseWriter, r *http.Request, url string, code int) { - h := w.Header() - - // RFC 7231 notes that a short HTML body is usually included in - // the response because older user agents may not understand 301/307. - // Do it only if the request didn't already have a Content-Type header. - _, hadCT := h[context.ContentTypeHeaderKey] - - h.Set("Location", url) - if !hadCT && (r.Method == http.MethodGet || r.Method == http.MethodHead) { - h.Set(context.ContentTypeHeaderKey, "text/html; charset=utf-8") - } - w.WriteHeader(code) - - // Shouldn't send the body for POST or HEAD; that leaves GET. - if !hadCT && r.Method == "GET" { - body := "" + http.StatusText(code) + ".\n" - fmt.Fprintln(w, body) - } -} - // NewSubdomainPartyRedirectHandler returns a handler which can be registered // through `UseRouter` or `Use` to redirect from the current request's // subdomain to the one which the given `to` Party can handle. @@ -210,9 +189,34 @@ func NewSubdomainRedirectHandler(toSubdomain string) context.Handler { // en-us.test.mydomain.com host := ctx.Host() fullSubdomain := ctx.SubdomainFull() - newHost := strings.Replace(host, fullSubdomain, toSubdomain, 1) - resturi := ctx.Request().URL.RequestURI() - urlToRedirect := ctx.Scheme() + newHost + resturi - redirectAbsolute(ctx.ResponseWriter(), ctx.Request(), urlToRedirect, http.StatusMovedPermanently) + targetHost := strings.Replace(host, fullSubdomain, toSubdomain, 1) + // resturi := ctx.Request().URL.RequestURI() + // urlToRedirect := ctx.Scheme() + newHost + resturi + r := ctx.Request() + r.Host = targetHost + r.URL.Host = targetHost + urlToRedirect := r.URL.String() + redirectAbsolute(ctx.ResponseWriter(), r, urlToRedirect, http.StatusMovedPermanently) + } +} + +func redirectAbsolute(w http.ResponseWriter, r *http.Request, url string, code int) { + h := w.Header() + + // RFC 7231 notes that a short HTML body is usually included in + // the response because older user agents may not understand 301/307. + // Do it only if the request didn't already have a Content-Type header. + _, hadCT := h[context.ContentTypeHeaderKey] + + h.Set("Location", url) + if !hadCT && (r.Method == http.MethodGet || r.Method == http.MethodHead) { + h.Set(context.ContentTypeHeaderKey, "text/html; charset=utf-8") + } + w.WriteHeader(code) + + // Shouldn't send the body for POST or HEAD; that leaves GET. + if !hadCT && r.Method == "GET" { + body := "" + http.StatusText(code) + ".\n" + fmt.Fprintln(w, body) } } diff --git a/iris.go b/iris.go index f86555d5..b99efb95 100644 --- a/iris.go +++ b/iris.go @@ -82,8 +82,20 @@ type Application struct { // used for build builded bool defaultMode bool + // OnBuild is a single function which + // is fired on the first `Build` method call. + // If reports an error then the execution + // is stopped and the error is logged. + // It's nil by default except when `Switch` instead of `New` or `Default` + // is used to initialize the Application. + // Users can wrap it to accept more events. + OnBuild func() error mu sync.Mutex + // name is the application name and the log prefix for + // that Application instance's Logger. See `SetName` and `String`. + // Defaults to IRIS_APP_NAME envrinoment variable otherwise empty. + name string // Hosts contains a list of all servers (Host Supervisors) that this app is running on. // // Hosts may be empty only if application ran(`app.Run`) with `iris.Raw` option runner, @@ -131,6 +143,41 @@ func Default() *Application { return app } +func newLogger(app *Application) *golog.Logger { + logger := golog.Default.Child(app) + if name := os.Getenv("IRIS_APP_NAME"); name != "" { + app.name = name + logger.SetChildPrefix(name) + } + + return logger +} + +// SetName sets a unique name to this Iris Application. +// It sets a child prefix for the current Application's Logger. +// Look `String` method too. +// +// It returns this Application. +func (app *Application) SetName(appName string) *Application { + app.mu.Lock() + defer app.mu.Unlock() + + if app.name == "" { + app.logger.SetChildPrefix(appName) + } + app.name = appName + + return app +} + +// String completes the fmt.Stringer interface and it returns +// the application's name. +// If name was not set by `SetName` or `IRIS_APP_NAME` environment variable +// then this will return an empty string. +func (app *Application) String() string { + return app.name +} + // WWW creates and returns a "www." subdomain. // The difference from `app.Subdomain("www")` or `app.Party("www.")` is that the `app.WWW()` method // wraps the router so all http(s)://mydomain.com will be redirect to http(s)://www.mydomain.com. @@ -187,22 +234,6 @@ func (app *Application) ConfigurationReadOnly() context.ConfigurationReadOnly { return app.config } -// Maybe, if it's requested: -// func (app *Application) SetName(appName string) *iris.Application { -// app.config.name = appName -// app.logger.SetChildPrefix(appName) -// return app -// } - -func newLogger(app *Application) *golog.Logger { - logger := golog.Default.Child(app) - if prefix := os.Getenv("IRIS_APP_NAME"); prefix != "" { - logger.SetChildPrefix(prefix) - } - - return logger -} - // Logger returns the golog logger instance(pointer) that is being used inside the "app". // // Available levels: @@ -488,6 +519,12 @@ func (app *Application) Build() error { return nil } + if cb := app.OnBuild; cb != nil { + if err := cb(); err != nil { + return err + } + } + // start := time.Now() app.builded = true // even if fails.