feat: 添加应用骨架,配置化 zap 日志贯穿全链路
- cmd/server/main.go: 使用 logger.NewLogger(cfg.Log) 替代 zap.NewProduction() - internal/app: 依赖注入组装 DB/Slurm/Service/Handler,传递 logger - internal/middleware: RequestLogger 请求日志中间件 - internal/server: 统一响应格式和路由注册 - go.mod: module 更名为 gcy_hpc_server,添加 gin/zap/lumberjack/gorm 依赖 - 日志初始化失败时 fail fast (os.Exit(1)) - GormLevel 从配置传递到 NewGormDB,支持 YAML 独立配置 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
159
internal/app/app.go
Normal file
159
internal/app/app.go
Normal file
@@ -0,0 +1,159 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gcy_hpc_server/internal/config"
|
||||
"gcy_hpc_server/internal/handler"
|
||||
"gcy_hpc_server/internal/server"
|
||||
"gcy_hpc_server/internal/service"
|
||||
"gcy_hpc_server/internal/slurm"
|
||||
"gcy_hpc_server/internal/store"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// App encapsulates the entire application lifecycle.
|
||||
type App struct {
|
||||
cfg *config.Config
|
||||
logger *zap.Logger
|
||||
db *gorm.DB
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// NewApp initializes all application dependencies: DB, Slurm client, services, handlers, router.
|
||||
func NewApp(cfg *config.Config, logger *zap.Logger) (*App, error) {
|
||||
gormDB, err := initDB(cfg, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slurmClient, err := initSlurmClient(cfg)
|
||||
if err != nil {
|
||||
closeDB(gormDB)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srv := initHTTPServer(cfg, gormDB, slurmClient, logger)
|
||||
|
||||
return &App{
|
||||
cfg: cfg,
|
||||
logger: logger,
|
||||
db: gormDB,
|
||||
server: srv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run starts the HTTP server and blocks until a shutdown signal or server error.
|
||||
func (a *App) Run() error {
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
a.logger.Info("starting server", zap.String("addr", a.server.Addr))
|
||||
if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
errCh <- fmt.Errorf("server listen: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
// Server crashed before receiving a signal — clean up resources before
|
||||
// returning, because the caller may call os.Exit and skip deferred Close().
|
||||
a.logger.Error("server exited unexpectedly", zap.Error(err))
|
||||
_ = a.Close()
|
||||
return err
|
||||
case sig := <-quit:
|
||||
a.logger.Info("received shutdown signal", zap.String("signal", sig.String()))
|
||||
}
|
||||
|
||||
a.logger.Info("shutting down server...")
|
||||
return a.Close()
|
||||
}
|
||||
|
||||
// Close cleans up all resources: HTTP server and database connections.
|
||||
func (a *App) Close() error {
|
||||
var errs []error
|
||||
|
||||
if a.server != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := a.server.Shutdown(ctx); err != nil && err != http.ErrServerClosed {
|
||||
errs = append(errs, fmt.Errorf("shutdown http server: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
if a.db != nil {
|
||||
sqlDB, err := a.db.DB()
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("get underlying sql.DB: %w", err))
|
||||
} else if err := sqlDB.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("close database: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Initialization helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func initDB(cfg *config.Config, logger *zap.Logger) (*gorm.DB, error) {
|
||||
gormDB, err := store.NewGormDB(cfg.MySQLDSN, logger, cfg.Log.GormLevel)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init database: %w", err)
|
||||
}
|
||||
|
||||
if err := store.AutoMigrate(gormDB); err != nil {
|
||||
closeDB(gormDB)
|
||||
return nil, fmt.Errorf("run migrations: %w", err)
|
||||
}
|
||||
|
||||
return gormDB, nil
|
||||
}
|
||||
|
||||
func closeDB(db *gorm.DB) {
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
if sqlDB, err := db.DB(); err == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func initSlurmClient(cfg *config.Config) (*slurm.Client, error) {
|
||||
client, err := service.NewSlurmClient(cfg.SlurmAPIURL, cfg.SlurmUserName, cfg.SlurmJWTKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("init slurm client: %w", err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func initHTTPServer(cfg *config.Config, db *gorm.DB, slurmClient *slurm.Client, logger *zap.Logger) *http.Server {
|
||||
jobSvc := service.NewJobService(slurmClient, logger)
|
||||
clusterSvc := service.NewClusterService(slurmClient, logger)
|
||||
templateStore := store.NewTemplateStore(db)
|
||||
|
||||
jobH := handler.NewJobHandler(jobSvc, logger)
|
||||
clusterH := handler.NewClusterHandler(clusterSvc, logger)
|
||||
templateH := handler.NewTemplateHandler(templateStore, logger)
|
||||
|
||||
router := server.NewRouter(jobH, clusterH, templateH, logger)
|
||||
|
||||
addr := ":" + cfg.ServerPort
|
||||
|
||||
return &http.Server{
|
||||
Addr: addr,
|
||||
Handler: router,
|
||||
}
|
||||
}
|
||||
25
internal/app/app_test.go
Normal file
25
internal/app/app_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gcy_hpc_server/internal/config"
|
||||
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
func TestNewApp_InvalidDB(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
ServerPort: "8080",
|
||||
MySQLDSN: "invalid:dsn@tcp(localhost:99999)/nonexistent?parseTime=true",
|
||||
SlurmAPIURL: "http://localhost:6820",
|
||||
SlurmUserName: "root",
|
||||
SlurmJWTKeyPath: "/nonexistent/jwt.key",
|
||||
}
|
||||
logger := zaptest.NewLogger(t)
|
||||
|
||||
_, err := NewApp(cfg, logger)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid DSN, got nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user