package app import ( "context" "fmt" "net" "net/http" "os" "route256/comments/infra/config" "route256/comments/infra/db/postgres" "route256/comments/internal/app/server" "route256/comments/internal/domain/service" "time" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/jackc/pgx/v5/pgxpool" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/reflection" mw "route256/comments/infra/grpc/middleware" repository "route256/comments/infra/repository/sqlc" pb "route256/pkg/api/comments/v1" ) type App struct { config *config.Config controller *server.Server grpcServer *grpc.Server httpServer *http.Server gwConn *grpc.ClientConn } func NewApp(configPath string) (*App, error) { c, err := config.LoadConfig(configPath) if err != nil { return nil, fmt.Errorf("unable to load config: %w", err) } log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) zerolog.SetGlobalLevel(zerolog.InfoLevel) if c.Service.LogLevel != "" { level, logErr := zerolog.ParseLevel(c.Service.LogLevel) if logErr != nil { return nil, fmt.Errorf("unknown log level `%s` provided: %w", c.Service.LogLevel, logErr) } zerolog.SetGlobalLevel(level) } log.WithLevel(zerolog.GlobalLevel()).Msgf("using logging level=`%s`", zerolog.GlobalLevel().String()) shards, err := getPostgresPools(c) if err != nil { return nil, err } repo := repository.NewCommentsRepository(shards[0], shards[1]) service := service.NewCommentsService(repo, c.App.EditInterval) controller := server.NewServer(service) app := &App{ config: c, controller: controller, } return app, nil } func (app *App) Shutdown(ctx context.Context) (err error) { if app.httpServer != nil { err = app.httpServer.Shutdown(ctx) if err != nil { log.Error().Err(err).Msgf("failed http gateway server shutdown") } } done := make(chan struct{}) if app.grpcServer != nil { go func() { app.grpcServer.GracefulStop() close(done) }() } select { case <-done: case <-ctx.Done(): if app.grpcServer != nil { app.grpcServer.Stop() } } if app.gwConn != nil { err2 := app.gwConn.Close() if err2 != nil { err = err2 log.Error().Err(err).Msgf("failed gateway connection close") } } return err } func (app *App) ListenAndServe(ctx context.Context) error { grpcAddr := fmt.Sprintf("%s:%s", app.config.Service.Host, app.config.Service.GRPCPort) l, err := net.Listen("tcp", grpcAddr) if err != nil { return err } app.grpcServer = grpc.NewServer( grpc.ChainUnaryInterceptor( mw.Logging, mw.Validate, ), ) reflection.Register(app.grpcServer) pb.RegisterCommentsServer(app.grpcServer, app.controller) go func() { if err = app.grpcServer.Serve(l); err != nil { log.Fatal().Err(err).Msg("failed to serve") } }() log.Info().Msgf("Serving grpc loms at grpc://%s", l.Addr()) // Setup HTTP gateway conn, err := grpc.NewClient( grpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { return fmt.Errorf("grpc.NewClient: %w", err) } app.gwConn = conn gwmux := runtime.NewServeMux() if err = pb.RegisterCommentsHandler(ctx, gwmux, conn); err != nil { return fmt.Errorf("pb.RegisterLOMSHandler: %w", err) } root := http.NewServeMux() root.Handle("/metrics", promhttp.Handler()) root.Handle("/", gwmux) app.httpServer = &http.Server{ Addr: fmt.Sprintf("%s:%s", app.config.Service.Host, app.config.Service.HTTPPort), Handler: root, ReadTimeout: 10 * time.Second, } log.Info().Msgf("Serving http loms at http://%s", app.httpServer.Addr) return app.httpServer.ListenAndServe() } func getPostgresPools(c *config.Config) ([]*pgxpool.Pool, error) { conns := make([]string, len(c.DbShards)) for i, shard := range c.DbShards { conns[i] = fmt.Sprintf( "postgresql://%s:%s@%s:%s/%s?sslmode=disable", shard.User, shard.Password, shard.Host, shard.Port, shard.DBName, ) } pools, err := postgres.NewPools(context.Background(), conns...) if err != nil { return nil, err } return pools, nil }