feat(startup): print service banner after listener ready

This commit is contained in:
ryan
2026-07-13 15:49:19 +08:00
parent 26be762c3a
commit 6b6c786cfe
11 changed files with 200 additions and 13 deletions
+3 -1
View File
@@ -31,7 +31,9 @@ var allCmd = &cobra.Command{
go func() {
defer wg.Done()
log.Println("[All] 启动 API 服务")
router.Serve()
router.Serve(func() {
printStartupBanner(startupState{mode: "API + Worker + Scheduler", relationalDB: latestMigrationState.relationalDB, clickHouseDB: latestMigrationState.clickHouseDB, listensForHTTP: true})
})
}()
// 启动 Asynq Worker 任务处理服务
+3 -1
View File
@@ -16,6 +16,8 @@ var apiCmd = &cobra.Command{
Run: func(_ *cobra.Command, _ []string) {
bootstrap.RegisterAPI()
runBootstrap(bootstrap.Options{API: true})
router.Serve()
router.Serve(func() {
printStartupBanner(startupState{mode: "API", relationalDB: latestMigrationState.relationalDB, clickHouseDB: latestMigrationState.clickHouseDB, listensForHTTP: true})
})
},
}
+68
View File
@@ -0,0 +1,68 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"fmt"
"log"
"runtime"
"strings"
"github.com/Rain-kl/Wavelet/internal/buildinfo"
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/Rain-kl/Wavelet/internal/db/migrator"
)
type startupState struct {
mode string
relationalDB migrator.Report
clickHouseDB migrator.Report
listensForHTTP bool
}
func printStartupBanner(state startupState) {
log.Print(formatStartupBanner(state))
}
func formatStartupBanner(state startupState) string {
lines := []string{
"",
" ____ ________ ",
" / __ \\____ ___ ____ / ____/ /___ _________ ",
" / / / / __ \\/ _ \\/ __ \\/ /_ / / __ `/ ___/ _ \\",
"/ /_/ / /_/ / __/ / / / __/ / / /_/ / / / __/",
"\\____/ .___/\\___/_/ /_/_/ /_/\\__,_/_/ \\___/ ",
" /_/ ",
fmt.Sprintf(" OpenFlare %s", buildinfo.Version),
"",
fmt.Sprintf(" Environment: %s", config.Config.App.Env),
fmt.Sprintf(" Runtime: %s/%s (%s)", runtime.GOOS, runtime.GOARCH, runtime.Version()),
fmt.Sprintf(" Build time: %s", buildTime()),
fmt.Sprintf(" Database: %s", formatMigration(state.relationalDB)),
fmt.Sprintf(" Analytics: %s", formatMigration(state.clickHouseDB)),
}
if state.listensForHTTP {
lines = append(lines, fmt.Sprintf(" Listening: http://%s", config.Config.App.Addr))
}
lines = append(lines, fmt.Sprintf(" Mode: %s", state.mode), "")
return strings.Join(lines, "\n")
}
func buildTime() string {
if buildinfo.BuildTime == "" {
return "development build"
}
return buildinfo.BuildTime
}
func formatMigration(report migrator.Report) string {
if !report.Enabled {
return "disabled"
}
state := "up to date"
if report.Applied {
state = "upgraded"
}
return fmt.Sprintf("%s (version %d, %s)", report.Backend, report.Version, state)
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package cmd
import (
"strings"
"testing"
"github.com/Rain-kl/Wavelet/internal/buildinfo"
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/Rain-kl/Wavelet/internal/db/migrator"
)
func TestFormatStartupBanner(t *testing.T) {
previousVersion := buildinfo.Version
previousBuildTime := buildinfo.BuildTime
previousEnv := config.Config.App.Env
previousAddr := config.Config.App.Addr
t.Cleanup(func() {
buildinfo.Version = previousVersion
buildinfo.BuildTime = previousBuildTime
config.Config.App.Env = previousEnv
config.Config.App.Addr = previousAddr
})
buildinfo.Version = "v3.2.1"
buildinfo.BuildTime = "2026-07-13T08:00:00Z"
config.Config.App.Env = "production"
config.Config.App.Addr = ":3000"
banner := formatStartupBanner(startupState{
mode: "API",
relationalDB: migrator.Report{
Backend: "PostgreSQL",
Enabled: true,
Version: 202607150003,
Applied: true,
},
clickHouseDB: migrator.Report{Backend: "ClickHouse"},
listensForHTTP: true,
})
for _, want := range []string{
"OpenFlare v3.2.1",
"Environment: production",
"Build time: 2026-07-13T08:00:00Z",
"Database: PostgreSQL (version 202607150003, upgraded)",
"Analytics: disabled",
"Listening: http://:3000",
"Mode: API",
} {
if !strings.Contains(banner, want) {
t.Errorf("banner missing %q:\n%s", want, banner)
}
}
}
+12 -4
View File
@@ -39,8 +39,7 @@ var rootCmd = &cobra.Command{
})
},
PreRun: func(_ *cobra.Command, _ []string) {
migrator.Migrate()
migrator.MigrateClickHouse()
runMigrations()
},
PersistentPostRun: func(_ *cobra.Command, _ []string) {
shutdownTraceProvider()
@@ -51,6 +50,16 @@ var rootCmd = &cobra.Command{
},
}
var latestMigrationState struct {
relationalDB migrator.Report
clickHouseDB migrator.Report
}
func runMigrations() {
latestMigrationState.relationalDB = migrator.Migrate()
latestMigrationState.clickHouseDB = migrator.MigrateClickHouse()
}
func shutdownTraceProvider() {
ctx, cancel := context.WithTimeout(context.Background(), traceShutdownTimeout)
defer cancel()
@@ -63,8 +72,7 @@ func init() {
// 1. 为需要迁移的子命令动态绑定原先 rootCmd.PreRun 拥有的数据库迁移行为
migratePreRun := func(_ *cobra.Command, _ []string) {
migrator.Migrate()
migrator.MigrateClickHouse()
runMigrations()
}
allCmd.PreRun = migratePreRun
apiCmd.PreRun = migratePreRun
+1
View File
@@ -18,6 +18,7 @@ var schedulerCmd = &cobra.Command{
Short: "wavelet Scheduler",
Run: func(_ *cobra.Command, _ []string) {
runBootstrap(bootstrap.Options{})
printStartupBanner(startupState{mode: "Scheduler", relationalDB: latestMigrationState.relationalDB, clickHouseDB: latestMigrationState.clickHouseDB})
log.Println("[Scheduler] 启动定时任务调度服务")
if err := scheduler.StartScheduler(); err != nil {
log.Fatalf("[调度器] 启动失败: %v", err)
+1
View File
@@ -18,6 +18,7 @@ var workerCmd = &cobra.Command{
Short: "wavelet Worker",
Run: func(_ *cobra.Command, _ []string) {
runBootstrap(bootstrap.Options{})
printStartupBanner(startupState{mode: "Worker", relationalDB: latestMigrationState.relationalDB, clickHouseDB: latestMigrationState.clickHouseDB})
log.Println("[Worker] 启动任务处理服务")
if err := worker.StartWorker(); err != nil {
log.Fatalf("[工作器] 启动失败: %v", err)