diff --git a/app.go b/app.go index f7991c363e..198a87664a 100644 --- a/app.go +++ b/app.go @@ -1356,7 +1356,7 @@ func (app *App) serverErrorHandler(fctx *fasthttp.RequestCtx, err error) { } // startupProcess Is the method which executes all the necessary processes just before the start of the server. -func (app *App) startupProcess() *App { +func (app *App) startupProcess() { app.mutex.Lock() defer app.mutex.Unlock() @@ -1371,12 +1371,10 @@ func (app *App) startupProcess() *App { // build route tree stack app.buildTree() - - return app } // Run onListen hooks. If they return an error, panic. -func (app *App) runOnListenHooks(listenData ListenData) { +func (app *App) runOnListenHooks(listenData *ListenData) { if err := app.hooks.executeOnListenHooks(listenData); err != nil { panic(err) } diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 3173fa7aa1..659d5ce31d 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -168,6 +168,64 @@ func main() { +### ListenData + +`ListenData` exposes runtime metadata about the listener: + +| Field | Type | Description | +| --- | --- | --- | +| `Host` | `string` | Resolved hostname or IP address. | +| `Port` | `string` | The bound port. | +| `TLS` | `bool` | Indicates whether TLS is enabled. | +| `Version` | `string` | Fiber version reported in the startup banner. | +| `AppName` | `string` | Application name from the configuration. | +| `HandlerCount` | `int` | Total registered handler count. | +| `ProcessCount` | `int` | Number of processes Fiber will use. | +| `PID` | `int` | Current process identifier. | +| `Prefork` | `bool` | Whether prefork is enabled. | +| `ChildPIDs` | `[]int` | Child process identifiers when preforking. | +| `ColorScheme` | [`Colors`](https://github.com/gofiber/fiber/blob/main/color.go) | Active color scheme for the startup message. | + +### Startup message customization + +Use `OnPreStartupMessage` to tweak the banner before Fiber prints it, and `OnPostStartupMessage` to run logic after the banner is printed (or skipped): + +- Assign `sm.Header` to override the ASCII art banner. Leave it empty to use the default. +- Provide `sm.PrimaryInfo` and/or `sm.SecondaryInfo` maps to replace the primary (server URL, handler counts, etc.) and secondary (prefork status, PID, process count) sections. +- Set `sm.PreventDefault = true` to suppress the built-in banner without affecting other hooks. +- `PostStartupMessageData` reports whether the banner was skipped via the `Disabled`, `IsChild`, and `Prevented` flags. + +```go title="Customize the startup message" +package main + +import ( + "fmt" + "os" + + "github.com/gofiber/fiber/v3" +) + +func main() { + app := fiber.New() + + app.Hooks().OnPreStartupMessage(func(sm *fiber.PreStartupMessageData) error { + sm.Header = "FOOBER " + sm.Version + "\n-------" + sm.PrimaryInfo = fiber.Map{"Git hash": os.Getenv("GIT_HASH")} + sm.SecondaryInfo = fiber.Map{"Prefork": sm.Prefork} + return nil + }) + + app.Hooks().OnPostStartupMessage(func(sm fiber.PostStartupMessageData) error { + if !sm.Disabled && !sm.IsChild && !sm.Prevented { + fmt.Println("startup completed") + } + return nil + }) + + app.Listen(":5000") +} +``` + ## OnFork Runs in the child process after a fork. diff --git a/docs/whats_new.md b/docs/whats_new.md index 5d8da59eac..db52c0b298 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -295,6 +295,29 @@ app.Listen("app.sock", fiber.ListenerConfig{ }) ``` +- Expanded `ListenData` with versioning, handler, process, and PID metadata, plus dedicated startup message hooks for customization. + +```go +app := fiber.New() + +app.Hooks().OnPreStartupMessage(func(sm *fiber.PreStartupMessageData) error { + sm.Header = "FOOBER " + sm.Version + "\n-------" + sm.PrimaryInfo = fiber.Map{"Git hash": os.Getenv("GIT_HASH")} + sm.SecondaryInfo = fiber.Map{"Process count": sm.ProcessCount} + // Set sm.PreventDefault = true to suppress the default banner entirely. + return nil +}) + +app.Hooks().OnPostStartupMessage(func(sm fiber.PostStartupMessageData) error { + if !sm.Disabled && !sm.IsChild && !sm.Prevented { + log.Println("startup completed") + } + return nil +}) + +go app.Listen(":3000") +``` + ## 🗺 Router We have slightly adapted our router interface diff --git a/hooks.go b/hooks.go index 402ad30be3..7eca7129e1 100644 --- a/hooks.go +++ b/hooks.go @@ -15,6 +15,10 @@ type ( OnGroupNameHandler = OnGroupHandler // OnListenHandler runs when the application begins listening and receives the listener details. OnListenHandler = func(ListenData) error + // OnPreStartupMessageHandler runs before Fiber prints the startup banner. + OnPreStartupMessageHandler = func(*PreStartupMessageData) error + // OnPostStartupMessageHandler runs after Fiber prints (or skips) the startup banner. + OnPostStartupMessageHandler = func(PostStartupMessageData) error // OnPreShutdownHandler runs before the application shuts down. OnPreShutdownHandler = func() error // OnPostShutdownHandler runs after shutdown and receives the shutdown result. @@ -36,17 +40,164 @@ type Hooks struct { onGroup []OnGroupHandler onGroupName []OnGroupNameHandler onListen []OnListenHandler + onPreStartup []OnPreStartupMessageHandler + onPostStartup []OnPostStartupMessageHandler onPreShutdown []OnPreShutdownHandler onPostShutdown []OnPostShutdownHandler onFork []OnForkHandler onMount []OnMountHandler } -// ListenData is a struct to use it with OnListenHandler +type StartupMessageLevel int + +const ( + // StartupMessageLevelInfo represents informational startup message entries. + StartupMessageLevelInfo StartupMessageLevel = iota + // StartupMessageLevelWarning represents warning startup message entries. + StartupMessageLevelWarning + // StartupMessageLevelError represents error startup message entries. + StartupMessageLevelError +) + +// startupMessageEntry represents a single line of startup message information. +type startupMessageEntry struct { + key string + title string + value string + priority int + level StartupMessageLevel +} + +// ListenData contains the listener metadata provided to OnListenHandler. type ListenData struct { - Host string - Port string - TLS bool + ColorScheme Colors + Host string + Port string + Version string + AppName string + + ChildPIDs []int + + HandlerCount int + ProcessCount int + PID int + + TLS bool + Prefork bool +} + +// PreStartupMessageData contains metadata exposed to OnPreStartupMessage hooks. +type PreStartupMessageData struct { + *ListenData + + Header string + + entries []startupMessageEntry + + PreventDefault bool +} + +func (sm *PreStartupMessageData) AddInfo(key, title, value string, priority ...int) { + pri := -1 + if len(priority) > 0 { + pri = priority[0] + } + + sm.addEntry(key, title, value, pri, StartupMessageLevelInfo) +} + +func (sm *PreStartupMessageData) AddWarning(key, title, value string, priority ...int) { + pri := -1 + if len(priority) > 0 { + pri = priority[0] + } + + sm.addEntry(key, title, value, pri, StartupMessageLevelWarning) +} + +func (sm *PreStartupMessageData) AddError(key, title, value string, priority ...int) { + pri := -1 + if len(priority) > 0 { + pri = priority[0] + } + + sm.addEntry(key, title, value, pri, StartupMessageLevelError) +} + +func (sm *PreStartupMessageData) EntryKeys() []string { + keys := make([]string, 0, len(sm.entries)) + for _, entry := range sm.entries { + keys = append(keys, entry.key) + } + return keys +} + +func (sm *PreStartupMessageData) ResetEntries() { + sm.entries = sm.entries[:0] +} + +func (sm *PreStartupMessageData) addEntry(key, title, value string, priority int, level StartupMessageLevel) { + if sm.entries == nil { + sm.entries = make([]startupMessageEntry, 0) + } + + for i, entry := range sm.entries { + if entry.key != key { + continue + } + + sm.entries[i].value = value + sm.entries[i].title = title + sm.entries[i].level = level + sm.entries[i].priority = priority + } + + sm.entries = append(sm.entries, startupMessageEntry{key: key, title: title, value: value, level: level, priority: priority}) +} + +func (sm *PreStartupMessageData) DeleteEntry(key string) { + if sm.entries == nil { + return + } + + for i, entry := range sm.entries { + if entry.key == key { + sm.entries = append(sm.entries[:i], sm.entries[i+1:]...) + return + } + } +} + +func newPreStartupMessageData(listenData *ListenData) *PreStartupMessageData { + clone := listenData + if len(listenData.ChildPIDs) > 0 { + clone.ChildPIDs = append([]int(nil), listenData.ChildPIDs...) + } + + return &PreStartupMessageData{ListenData: clone} +} + +// PostStartupMessageData contains metadata exposed to OnPostStartupMessage hooks. +type PostStartupMessageData struct { + *ListenData + + Disabled bool + IsChild bool + Prevented bool +} + +func newPostStartupMessageData(listenData *ListenData, disabled, isChild, prevented bool) PostStartupMessageData { + clone := listenData + if len(listenData.ChildPIDs) > 0 { + clone.ChildPIDs = append([]int(nil), listenData.ChildPIDs...) + } + + return PostStartupMessageData{ + ListenData: clone, + Disabled: disabled, + IsChild: isChild, + Prevented: prevented, + } } func newHooks(app *App) *Hooks { @@ -57,6 +208,8 @@ func newHooks(app *App) *Hooks { onGroupName: make([]OnGroupNameHandler, 0), onName: make([]OnNameHandler, 0), onListen: make([]OnListenHandler, 0), + onPreStartup: make([]OnPreStartupMessageHandler, 0), + onPostStartup: make([]OnPostStartupMessageHandler, 0), onPreShutdown: make([]OnPreShutdownHandler, 0), onPostShutdown: make([]OnPostShutdownHandler, 0), onFork: make([]OnForkHandler, 0), @@ -107,6 +260,20 @@ func (h *Hooks) OnListen(handler ...OnListenHandler) { h.app.mutex.Unlock() } +// OnPreStartupMessage is a hook to execute user functions before the startup message is printed. +func (h *Hooks) OnPreStartupMessage(handler ...OnPreStartupMessageHandler) { + h.app.mutex.Lock() + h.onPreStartup = append(h.onPreStartup, handler...) + h.app.mutex.Unlock() +} + +// OnPostStartupMessage is a hook to execute user functions after the startup message is printed (or skipped). +func (h *Hooks) OnPostStartupMessage(handler ...OnPostStartupMessageHandler) { + h.app.mutex.Lock() + h.onPostStartup = append(h.onPostStartup, handler...) + h.app.mutex.Unlock() +} + // OnPreShutdown is a hook to execute user functions before Shutdown. func (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler) { h.app.mutex.Lock() @@ -211,9 +378,29 @@ func (h *Hooks) executeOnGroupNameHooks(group Group) error { return nil } -func (h *Hooks) executeOnListenHooks(listenData ListenData) error { +func (h *Hooks) executeOnListenHooks(listenData *ListenData) error { for _, v := range h.onListen { - if err := v(listenData); err != nil { + if err := v(*listenData); err != nil { + return err + } + } + + return nil +} + +func (h *Hooks) executeOnPreStartupMessageHooks(data *PreStartupMessageData) error { + for _, handler := range h.onPreStartup { + if err := handler(data); err != nil { + return err + } + } + + return nil +} + +func (h *Hooks) executeOnPostStartupMessageHooks(data PostStartupMessageData) error { + for _, handler := range h.onPostStartup { + if err := handler(data); err != nil { return err } } diff --git a/hooks_test.go b/hooks_test.go index 307050cb55..fd55ea91cc 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -3,6 +3,8 @@ package fiber import ( "bytes" "errors" + "os" + "runtime" "testing" "time" @@ -287,6 +289,68 @@ func Test_Hook_OnListen(t *testing.T) { require.Equal(t, "ready", buf.String()) } +func Test_ListenDataMetadata(t *testing.T) { + t.Parallel() + + app := New(Config{AppName: "meta"}) + app.handlersCount = 42 + + cfg := ListenConfig{EnablePrefork: true} + childPIDs := []int{11, 22} + listenData := app.prepareListenData(":3030", true, &cfg, childPIDs) + + app.Hooks().OnListen(func(data ListenData) error { + require.Equal(t, globalIpv4Addr, data.Host) + require.Equal(t, "3030", data.Port) + require.True(t, data.TLS) + require.Equal(t, Version, data.Version) + require.Equal(t, "meta", data.AppName) + require.Equal(t, 42, data.HandlerCount) + require.Equal(t, runtime.GOMAXPROCS(0), data.ProcessCount) + require.Equal(t, os.Getpid(), data.PID) + require.True(t, data.Prefork) + require.Equal(t, childPIDs, data.ChildPIDs) + require.Equal(t, app.config.ColorScheme, data.ColorScheme) + + return nil + }) + + app.runOnListenHooks(&listenData) + + app.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error { + require.Equal(t, globalIpv4Addr, data.Host) + require.Equal(t, "3030", data.Port) + require.True(t, data.TLS) + require.Equal(t, Version, data.Version) + require.Equal(t, "meta", data.AppName) + require.Equal(t, 42, data.HandlerCount) + require.Equal(t, runtime.GOMAXPROCS(0), data.ProcessCount) + require.Equal(t, os.Getpid(), data.PID) + require.True(t, data.Prefork) + require.Equal(t, childPIDs, data.ChildPIDs) + require.Equal(t, app.config.ColorScheme, data.ColorScheme) + + data.ResetEntries() + + data.AddInfo("custom", "Custom Info", "value", 3) + data.AddInfo("other", "Other Info", "value", 2) + + return nil + }) + + pre := newPreStartupMessageData(&listenData) + require.NoError(t, app.hooks.executeOnPreStartupMessageHooks(pre)) + + require.Equal(t, "value", pre.entries[0].value) + require.Equal(t, "Custom Info", pre.entries[0].title) + require.Equal(t, 3, pre.entries[0].priority) + + require.Equal(t, "value", pre.entries[1].value) + require.Equal(t, "Other Info", pre.entries[1].title) + require.Equal(t, 2, pre.entries[1].priority) + require.False(t, pre.PreventDefault) +} + func Test_Hook_OnListenPrefork(t *testing.T) { t.Parallel() app := New() @@ -419,7 +483,7 @@ func Test_executeOnListenHooks_Error(t *testing.T) { return errors.New("listen error") }) - err := app.hooks.executeOnListenHooks(ListenData{Host: "127.0.0.1", Port: "0"}) + err := app.hooks.executeOnListenHooks(&ListenData{Host: "127.0.0.1", Port: "0"}) require.EqualError(t, err, "listen error") } diff --git a/listen.go b/listen.go index 5804a5fa75..479aaab076 100644 --- a/listen.go +++ b/listen.go @@ -9,13 +9,13 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "io" "net" "os" "path/filepath" "reflect" "runtime" "sort" - "strconv" "strings" "text/tabwriter" "time" @@ -230,11 +230,13 @@ func (app *App) Listen(addr string, config ...ListenConfig) error { // prepare the server for the start app.startupProcess() + listenData := app.prepareListenData(ln.Addr().String(), getTLSConfig(ln) != nil, &cfg, nil) + // run hooks - app.runOnListenHooks(app.prepareListenData(ln.Addr().String(), getTLSConfig(ln) != nil, &cfg)) + app.runOnListenHooks(&listenData) // Print startup message & routes - app.printMessages(&cfg, ln) + app.printMessages(&cfg, &listenData) // Serve if cfg.BeforeServeFunc != nil { @@ -262,11 +264,13 @@ func (app *App) Listener(ln net.Listener, config ...ListenConfig) error { // prepare the server for the start app.startupProcess() + listenData := app.prepareListenData(ln.Addr().String(), getTLSConfig(ln) != nil, &cfg, nil) + // run hooks - app.runOnListenHooks(app.prepareListenData(ln.Addr().String(), getTLSConfig(ln) != nil, &cfg)) + app.runOnListenHooks(&listenData) // Print startup message & routes - app.printMessages(&cfg, ln) + app.printMessages(&cfg, &listenData) // Serve if cfg.BeforeServeFunc != nil { @@ -323,26 +327,16 @@ func (*App) createListener(addr string, tlsConfig *tls.Config, cfg *ListenConfig return listener, nil } -func (app *App) printMessages(cfg *ListenConfig, ln net.Listener) { - if cfg == nil { - cfg = &ListenConfig{} - } - // Print startup message - if !cfg.DisableStartupMessage { - app.startupMessage(ln.Addr().String(), getTLSConfig(ln) != nil, "", cfg) - } +func (app *App) printMessages(cfg *ListenConfig, listenData *ListenData) { + app.startupMessage(listenData, cfg) - // Print routes if cfg.EnablePrintRoutes { app.printRoutesMessage() } } -// prepareListenData creates a slice of ListenData -func (*App) prepareListenData(addr string, isTLS bool, cfg *ListenConfig) ListenData { //revive:disable-line:flag-parameter // Accepting a bool param named isTLS if fine here - if cfg == nil { - cfg = &ListenConfig{} - } +// prepareListenData creates a ListenData instance populated with the application metadata. +func (app *App) prepareListenData(addr string, isTLS bool, cfg *ListenConfig, childPIDs []int) ListenData { //revive:disable-line:flag-parameter // Accepting a bool param named isTLS is fine here host, port := parseAddr(addr) if host == "" { if cfg.ListenerNetwork == NetworkTCP6 { @@ -352,115 +346,150 @@ func (*App) prepareListenData(addr string, isTLS bool, cfg *ListenConfig) Listen } } - return ListenData{ - Host: host, - Port: port, - TLS: isTLS, + processCount := 1 + if cfg.EnablePrefork { + processCount = runtime.GOMAXPROCS(0) } -} -// startupMessage prepares the startup message with the handler number, port, address and other information -func (app *App) startupMessage(addr string, isTLS bool, pids string, cfg *ListenConfig) { //nolint:revive // Accepting a bool param named isTLS if fine here - // ignore child processes - if IsChild() { - return + var clonedPIDs []int + if len(childPIDs) > 0 { + clonedPIDs = append(clonedPIDs, childPIDs...) } - // Alias colors - colors := app.config.ColorScheme + return ListenData{ + Host: host, + Port: port, + Version: Version, + AppName: app.config.AppName, + ColorScheme: app.config.ColorScheme, + ChildPIDs: clonedPIDs, + HandlerCount: int(app.handlersCount), + ProcessCount: processCount, + PID: os.Getpid(), + TLS: isTLS, + Prefork: cfg.EnablePrefork, + } +} - host, port := parseAddr(addr) - if host == "" { - if cfg.ListenerNetwork == NetworkTCP6 { - host = "[::1]" - } else { - host = globalIpv4Addr - } +// startupMessage renders the startup banner using the provided listener metadata and configuration. +func (app *App) startupMessage(listenData *ListenData, cfg *ListenConfig) { + preData := newPreStartupMessageData(listenData) + colors := listenData.ColorScheme + + out := colorable.NewColorableStdout() + if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") == "1" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) { + out = colorable.NewNonColorable(os.Stdout) } + // Add default entries scheme := schemeHTTP - if isTLS { + if listenData.TLS { scheme = schemeHTTPS } - isPrefork := "Disabled" - if cfg.EnablePrefork { - isPrefork = "Enabled" + if listenData.Host == globalIpv4Addr { + preData.AddInfo("server_address", "Server started on", fmt.Sprintf("%s%s://127.0.0.1:%s%s (bound on host 0.0.0.0 and port %s)", + colors.Blue, scheme, listenData.Port, colors.Reset, listenData.Port), 10) + } else { + preData.AddInfo("server_address", "Server started on", fmt.Sprintf("%s%s://%s:%s%s", + colors.Blue, scheme, listenData.Host, listenData.Port, colors.Reset), 10) } - procs := strconv.Itoa(runtime.GOMAXPROCS(0)) - if !cfg.EnablePrefork { - procs = "1" + if listenData.AppName != "" { + preData.AddInfo("app_name", "Application name", fmt.Sprintf("\t%s%s%s", colors.Blue, listenData.AppName, colors.Reset), 9) } - out := colorable.NewColorableStdout() - if os.Getenv("TERM") == "dumb" || os.Getenv("NO_COLOR") == "1" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) { - out = colorable.NewNonColorable(os.Stdout) - } + preData.AddInfo("total_handlers", "Total handlers", fmt.Sprintf("\t%s%d%s", colors.Blue, listenData.HandlerCount, colors.Reset), 8) - fmt.Fprintf(out, "%s\n", fmt.Sprintf(figletFiberText, colors.Red+"v"+Version+colors.Reset)) - fmt.Fprintln(out, strings.Repeat("-", 50)) - - if host == "0.0.0.0" { - fmt.Fprintf(out, - "%sINFO%s Server started on: \t%s%s://127.0.0.1:%s%s (bound on host 0.0.0.0 and port %s)\n", - colors.Green, colors.Reset, colors.Blue, scheme, port, colors.Reset, port) + if listenData.Prefork { + preData.AddInfo("prefork", "Prefork", fmt.Sprintf("\t\t%sEnabled%s", colors.Blue, colors.Reset), 7) } else { - fmt.Fprintf(out, - "%sINFO%s Server started on: \t%s%s%s\n", - colors.Green, colors.Reset, colors.Blue, fmt.Sprintf("%s://%s:%s", scheme, host, port), colors.Reset) + preData.AddInfo("prefork", "Prefork", fmt.Sprintf("\t\t%sDisabled%s", colors.Red, colors.Reset), 6) } - if app.config.AppName != "" { - fmt.Fprintf(out, "%sINFO%s Application name: \t\t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, app.config.AppName, colors.Reset) + preData.AddInfo("pid", "PID", fmt.Sprintf("\t\t%s%d%s", colors.Blue, listenData.PID, colors.Reset), 5) + + preData.AddInfo("process_count", "Total process count", fmt.Sprintf("%s%d%s", colors.Blue, listenData.ProcessCount, colors.Reset), 4) + + if err := app.hooks.executeOnPreStartupMessageHooks(preData); err != nil { + log.Errorf("failed to call pre startup message hook: %v", err) } - app.logServices(app.servicesStartupCtx(), out, &colors) + disabled := cfg.DisableStartupMessage + isChild := IsChild() + prevented := preData != nil && preData.PreventDefault - fmt.Fprintf(out, - "%sINFO%s Total handlers count: \t%s%s%s\n", - colors.Green, colors.Reset, colors.Blue, strconv.Itoa(int(app.handlersCount)), colors.Reset) + defer func() { + postData := newPostStartupMessageData(listenData, disabled, isChild, prevented) + if err := app.hooks.executeOnPostStartupMessageHooks(postData); err != nil { + log.Errorf("failed to call post startup message hook: %v", err) + } + }() - if isPrefork == "Enabled" { - fmt.Fprintf(out, "%sINFO%s Prefork: \t\t\t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, isPrefork, colors.Reset) + if preData == nil || disabled || isChild || prevented { + return + } + + if preData.Header != "" { + header := preData.Header + fmt.Fprint(out, header) + if !strings.HasSuffix(header, "\n") { + fmt.Fprintln(out) + } } else { - fmt.Fprintf(out, "%sINFO%s Prefork: \t\t\t%s%s%s\n", colors.Green, colors.Reset, colors.Red, isPrefork, colors.Reset) + fmt.Fprintf(out, "%s\n", fmt.Sprintf(figletFiberText, colors.Red+"v"+listenData.Version+colors.Reset)) + fmt.Fprintln(out, strings.Repeat("-", 50)) } - fmt.Fprintf(out, "%sINFO%s PID: \t\t\t%s%v%s\n", colors.Green, colors.Reset, colors.Blue, os.Getpid(), colors.Reset) - fmt.Fprintf(out, "%sINFO%s Total process count: \t%s%s%s\n", colors.Green, colors.Reset, colors.Blue, procs, colors.Reset) + printStartupEntries(out, &colors, preData.entries) - if cfg.EnablePrefork { - // Turn the `pids` variable (in the form ",a,b,c,d,e,f,etc") into a slice of PIDs - pidSlice := make([]string, 0) - for v := range strings.SplitSeq(pids, ",") { - if v != "" { - pidSlice = append(pidSlice, v) - } - } + app.logServices(app.servicesStartupCtx(), out, &colors) + if listenData.Prefork && len(listenData.ChildPIDs) > 0 { fmt.Fprintf(out, "%sINFO%s Child PIDs: \t\t%s", colors.Green, colors.Reset, colors.Blue) - totalPids := len(pidSlice) + + totalPIDs := len(listenData.ChildPIDs) rowTotalPidCount := 10 - for i := 0; i < totalPids; i += rowTotalPidCount { + for i := 0; i < totalPIDs; i += rowTotalPidCount { start := i - end := min(i+rowTotalPidCount, totalPids) + end := min(i+rowTotalPidCount, totalPIDs) - for n, pid := range pidSlice[start:end] { - fmt.Fprintf(out, "%s", pid) - if n+1 != len(pidSlice[start:end]) { - fmt.Fprintf(out, ", ") + for idx, pid := range listenData.ChildPIDs[start:end] { + fmt.Fprintf(out, "%d", pid) + if idx+1 != len(listenData.ChildPIDs[start:end]) { + fmt.Fprint(out, ", ") } } fmt.Fprintf(out, "\n%s", colors.Reset) } } - // add new Line as spacer fmt.Fprintf(out, "\n%s", colors.Reset) } +func printStartupEntries(out io.Writer, colors *Colors, entries []startupMessageEntry) { + // Sort entries by priority (higher priority first) + sort.Slice(entries, func(i, j int) bool { + return entries[i].priority > entries[j].priority + }) + + for _, entry := range entries { + var label string + var color string + switch entry.level { + case StartupMessageLevelWarning: + label, color = "WARN", colors.Yellow + case StartupMessageLevelError: + label, color = "ERROR", colors.Red + default: + label, color = "INFO", colors.Green + } + + fmt.Fprintf(out, "%s%s%s %s: \t%s%s%s\n", color, label, colors.Reset, entry.title, colors.Blue, entry.value, colors.Reset) + } +} + // printRoutesMessage print all routes with method, path, name and handlers // in a format of table, like this: // method | path | name | handlers diff --git a/listen_test.go b/listen_test.go index 0b749c0a84..bdd73eb481 100644 --- a/listen_test.go +++ b/listen_test.go @@ -11,7 +11,6 @@ import ( "net" "os" "path/filepath" - "strings" "sync" "testing" "time" @@ -524,9 +523,17 @@ func Test_Listen_Master_Process_Show_Startup_Message(t *testing.T) { port := addr.Port require.NoError(t, ln.Close()) + childTemplate := []int{11111, 22222, 33333, 44444, 55555, 60000} + childPIDs := make([]int, 0, len(childTemplate)*10) + for range 10 { + childPIDs = append(childPIDs, childTemplate...) + } + + app := New() + listenData := app.prepareListenData(fmt.Sprintf(":%d", port), true, &cfg, childPIDs) + startupMessage := captureOutput(func() { - New(). - startupMessage(fmt.Sprintf(":%d", port), true, strings.Repeat(",11111,22222,33333,44444,55555,60000", 10), &cfg) + app.startupMessage(&listenData, &cfg) }) colors := Colors{} require.Contains(t, startupMessage, fmt.Sprintf("https://127.0.0.1:%d", port)) @@ -550,8 +557,16 @@ func Test_Listen_Master_Process_Show_Startup_MessageWithAppName(t *testing.T) { port := addr.Port require.NoError(t, ln.Close()) + childTemplate := []int{11111, 22222, 33333, 44444, 55555, 60000} + childPIDs := make([]int, 0, len(childTemplate)*10) + for range 10 { + childPIDs = append(childPIDs, childTemplate...) + } + + listenData := app.prepareListenData(fmt.Sprintf(":%d", port), true, &cfg, childPIDs) + startupMessage := captureOutput(func() { - app.startupMessage(fmt.Sprintf(":%d", port), true, strings.Repeat(",11111,22222,33333,44444,55555,60000", 10), &cfg) + app.startupMessage(&listenData, &cfg) }) require.Equal(t, "Test App v3.0.0", app.Config().AppName) require.Contains(t, startupMessage, app.Config().AppName) @@ -573,8 +588,10 @@ func Test_Listen_Master_Process_Show_Startup_MessageWithAppNameNonAscii(t *testi port := addr.Port require.NoError(t, ln.Close()) + listenData := app.prepareListenData(fmt.Sprintf(":%d", port), false, &cfg, nil) + startupMessage := captureOutput(func() { - app.startupMessage(fmt.Sprintf(":%d", port), false, "", &cfg) + app.startupMessage(&listenData, &cfg) }) require.Contains(t, startupMessage, "Serveur de vérification des données") } @@ -594,8 +611,10 @@ func Test_Listen_Master_Process_Show_Startup_MessageWithDisabledPreforkAndCustom port := addr.Port require.NoError(t, ln.Close()) + listenData := app.prepareListenData(fmt.Sprintf("server.com:%d", port), true, &cfg, nil) + startupMessage := captureOutput(func() { - app.startupMessage(fmt.Sprintf("server.com:%d", port), true, strings.Repeat(",11111,22222,33333,44444,55555,60000", 5), &cfg) + app.startupMessage(&listenData, &cfg) }) colors := Colors{} require.Contains(t, startupMessage, fmt.Sprintf("%sINFO%s", colors.Green, colors.Reset)) @@ -605,6 +624,93 @@ func Test_Listen_Master_Process_Show_Startup_MessageWithDisabledPreforkAndCustom require.Contains(t, startupMessage, fmt.Sprintf("Prefork: \t\t\t%sDisabled%s", colors.Red, colors.Reset)) } +func Test_StartupMessageCustomization(t *testing.T) { + cfg := ListenConfig{} + app := New() + listenData := app.prepareListenData(":8080", false, &cfg, nil) + + app.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error { + data.Header = "FOOBER v98\n-------" + + data.ResetEntries() + data.AddInfo("git_hash", "Git hash", "abc123", 3) + data.AddInfo("version", "Version", "v98", 2) + + return nil + }) + + var post PostStartupMessageData + app.Hooks().OnPostStartupMessage(func(data PostStartupMessageData) error { + post = data + + return nil + }) + + startupMessage := captureOutput(func() { + app.startupMessage(&listenData, &cfg) + }) + + require.Contains(t, startupMessage, "FOOBER v98") + require.Contains(t, startupMessage, "Git hash: \tabc123") + require.Contains(t, startupMessage, "Version: \tv98") + require.NotContains(t, startupMessage, "Server started on:") + require.NotContains(t, startupMessage, "Prefork:") + + require.False(t, post.Disabled) + require.False(t, post.IsChild) + require.False(t, post.Prevented) +} + +func Test_StartupMessageDisabledPostHook(t *testing.T) { + cfg := ListenConfig{DisableStartupMessage: true} + app := New() + listenData := app.prepareListenData(":7070", false, &cfg, nil) + + var post PostStartupMessageData + app.Hooks().OnPostStartupMessage(func(data PostStartupMessageData) error { + post = data + + return nil + }) + + startupMessage := captureOutput(func() { + app.startupMessage(&listenData, &cfg) + }) + + require.Empty(t, startupMessage) + require.True(t, post.Disabled) + require.False(t, post.IsChild) + require.False(t, post.Prevented) +} + +func Test_StartupMessagePreventedByHook(t *testing.T) { + cfg := ListenConfig{} + app := New() + listenData := app.prepareListenData(":9090", false, &cfg, nil) + + app.Hooks().OnPreStartupMessage(func(data *PreStartupMessageData) error { + data.PreventDefault = true + + return nil + }) + + var post PostStartupMessageData + app.Hooks().OnPostStartupMessage(func(data PostStartupMessageData) error { + post = data + + return nil + }) + + startupMessage := captureOutput(func() { + app.startupMessage(&listenData, &cfg) + }) + + require.Empty(t, startupMessage) + require.False(t, post.Disabled) + require.False(t, post.IsChild) + require.True(t, post.Prevented) +} + // go test -run Test_Listen_Print_Route func Test_Listen_Print_Route(t *testing.T) { app := New() diff --git a/prefork.go b/prefork.go index d11609be47..e7508e359a 100644 --- a/prefork.go +++ b/prefork.go @@ -8,8 +8,6 @@ import ( "os" "os/exec" "runtime" - "strconv" - "strings" "sync/atomic" "time" @@ -95,7 +93,7 @@ func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg *ListenConfig) e }() // collect child pids - var pids []string + var childPIDs []int // launch child procs for range maxProcs { @@ -121,7 +119,7 @@ func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg *ListenConfig) e // store child process pid := cmd.Process.Pid children[pid] = cmd - pids = append(pids, strconv.Itoa(pid)) + childPIDs = append(childPIDs, pid) // execute fork hook if app.hooks != nil { @@ -140,14 +138,12 @@ func (app *App) prefork(addr string, tlsConfig *tls.Config, cfg *ListenConfig) e // Run onListen hooks // Hooks have to be run here as different as non-prefork mode due to they should run as child or master - app.runOnListenHooks(app.prepareListenData(addr, tlsConfig != nil, cfg)) + listenData := app.prepareListenData(addr, tlsConfig != nil, cfg, childPIDs) - // Print startup message - if !cfg.DisableStartupMessage { - app.startupMessage(addr, tlsConfig != nil, ","+strings.Join(pids, ","), cfg) - } + app.runOnListenHooks(&listenData) + + app.startupMessage(&listenData, cfg) - // Print routes if cfg.EnablePrintRoutes { app.printRoutesMessage() } diff --git a/prefork_test.go b/prefork_test.go index 7b066691fb..0316688c79 100644 --- a/prefork_test.go +++ b/prefork_test.go @@ -87,7 +87,10 @@ func Test_App_Prefork_Child_Process_Never_Show_Startup_Message(t *testing.T) { os.Stdout = w cfg := listenConfigDefault() - New().startupProcess().startupMessage(":0", false, "", &cfg) + app := New() + app.startupProcess() + listenData := app.prepareListenData(":0", false, &cfg, nil) + app.startupMessage(&listenData, &cfg) require.NoError(t, w.Close())