Nik Afiq 657b6aeb22 feat: implement initial application structure with health and hello endpoints
- Add bootstrap package to initialize application components including logger, tracer, and HTTP server.
- Create config package to load runtime settings from environment variables.
- Implement observability features including logging, metrics, and tracing.
- Add health check and hello use cases with corresponding HTTP handlers.
- Introduce middleware for request ID, access logging, metrics, and recovery.
- Set up HTTP router with defined routes for health and hello endpoints.
- Include tests for health and hello endpoints to ensure proper functionality.
- Add OpenTelemetry collector configuration for trace exporting.
2026-03-05 21:22:43 +09:00

110 lines
2.8 KiB
Go

package bootstrap
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"switchbot-api/internal/app/config"
"switchbot-api/internal/app/observability"
"switchbot-api/internal/domain/health"
"switchbot-api/internal/domain/hello"
httptransport "switchbot-api/internal/transport/http"
"switchbot-api/internal/transport/http/handlers"
)
type App struct {
Config config.Config
Logger *slog.Logger
Server *http.Server
tracerShutdown func(ctx context.Context) error
}
func New(ctx context.Context, cfg config.Config) (*App, error) {
logger, err := observability.NewLogger(cfg.LogLevel)
if err != nil {
return nil, err
}
setGinMode(cfg.Environment)
tp, err := observability.NewTracerProvider(ctx, logger, cfg.ServiceName, cfg.Environment, cfg.OTLPEndpoint)
if err != nil {
return nil, fmt.Errorf("initialize tracing: %w", err)
}
tracer := observability.NewTracer(tp, cfg.ServiceName)
promRegistry := observability.NewRegistry()
httpMetrics := observability.NewHTTPMetrics(promRegistry)
promHandler := observability.NewPrometheusHandler(promRegistry)
helloUsecase := hello.NewService()
readinessUsecase := health.NewReadinessUsecase(nil)
healthHandler := handlers.NewHealthHandler(readinessUsecase)
helloHandler := handlers.NewHelloHandler(helloUsecase)
router := httptransport.NewRouter(httptransport.Dependencies{
Logger: logger,
Tracer: tracer,
HTTPMetrics: httpMetrics,
PrometheusHandler: promHandler,
HealthHandler: healthHandler,
HelloHandler: helloHandler,
})
server := &http.Server{
Addr: cfg.Addr(),
Handler: router,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
return &App{
Config: cfg,
Logger: logger,
Server: server,
tracerShutdown: func(ctx context.Context) error {
return tp.Shutdown(ctx)
},
}, nil
}
func (a *App) Start() error {
a.Logger.Info("http server starting", slog.String("addr", a.Server.Addr), slog.String("service", a.Config.ServiceName), slog.String("env", a.Config.Environment))
if err := a.Server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
func (a *App) Shutdown(ctx context.Context) error {
var errs []error
if err := a.Server.Shutdown(ctx); err != nil {
errs = append(errs, fmt.Errorf("shutdown http server: %w", err))
}
if err := a.tracerShutdown(ctx); err != nil {
errs = append(errs, fmt.Errorf("shutdown tracer provider: %w", err))
}
return errors.Join(errs...)
}
func setGinMode(environment string) {
switch strings.ToLower(strings.TrimSpace(environment)) {
case "local", "dev", "development", "test":
gin.SetMode(gin.DebugMode)
default:
gin.SetMode(gin.ReleaseMode)
}
}