package server import ( "context" "fmt" "log/slog" "net" "net/http" "os" "sync" "time" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/reflection" "go-grpc-gateway-template/internal/config" "go-grpc-gateway-template/internal/health" helloworldpb "go-grpc-gateway-template/proto/helloworld" ) // Server wraps gRPC and HTTP servers with configuration type Server struct { config *config.Config logger *slog.Logger grpcServer *grpc.Server httpServer *http.Server healthHandler *health.Handler } // New creates a new server instance func New(cfg *config.Config) *Server { // Setup logger var handler slog.Handler if cfg.Log.Format == "json" { handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ Level: parseLogLevel(cfg.Log.Level), }) } else { handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ Level: parseLogLevel(cfg.Log.Level), }) } logger := slog.New(handler) // Create health handler healthHandler := health.NewHandler("1.0.0") return &Server{ config: cfg, logger: logger, healthHandler: healthHandler, } } // Start starts both gRPC and HTTP servers func (s *Server) Start(ctx context.Context) error { var wg sync.WaitGroup errChan := make(chan error, 2) // Start gRPC server wg.Add(1) go func() { defer wg.Done() if err := s.startGRPCServer(ctx); err != nil { errChan <- fmt.Errorf("gRPC server error: %w", err) } }() // Start HTTP server wg.Add(1) go func() { defer wg.Done() if err := s.startHTTPServer(ctx); err != nil && err != http.ErrServerClosed { errChan <- fmt.Errorf("HTTP server error: %w", err) } }() // Wait for servers to start go func() { wg.Wait() close(errChan) }() // Return first error if any return <-errChan } func (s *Server) startGRPCServer(ctx context.Context) error { lis, err := net.Listen("tcp", ":"+s.config.Server.GRPCPort) if err != nil { return fmt.Errorf("failed to listen on port %s: %w", s.config.Server.GRPCPort, err) } s.grpcServer = grpc.NewServer() // Register services greeter := &GreeterService{} helloworldpb.RegisterGreeterServer(s.grpcServer, greeter) // Enable reflection for development reflection.Register(s.grpcServer) s.logger.Info("Starting gRPC server", "port", s.config.Server.GRPCPort) return s.grpcServer.Serve(lis) } func (s *Server) startHTTPServer(ctx context.Context) error { // Create gRPC client connection conn, err := grpc.DialContext( ctx, "localhost:"+s.config.Server.GRPCPort, grpc.WithBlock(), grpc.WithTransportCredentials(insecure.NewCredentials()), ) if err != nil { return fmt.Errorf("failed to dial gRPC server: %w", err) } // Create gateway mux gwMux := runtime.NewServeMux() // Register gRPC gateway handlers err = helloworldpb.RegisterGreeterHandler(ctx, gwMux, conn) if err != nil { return fmt.Errorf("failed to register gateway: %w", err) } // Create HTTP mux with health endpoint mux := http.NewServeMux() mux.Handle("/", gwMux) mux.Handle("/health", s.healthHandler) s.httpServer = &http.Server{ Addr: ":" + s.config.Server.HTTPPort, Handler: mux, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, } s.logger.Info("Starting HTTP server", "port", s.config.Server.HTTPPort) return s.httpServer.ListenAndServe() } // Shutdown gracefully shuts down the servers func (s *Server) Shutdown(ctx context.Context) error { s.logger.Info("Shutting down servers...") var wg sync.WaitGroup // Shutdown HTTP server if s.httpServer != nil { wg.Add(1) go func() { defer wg.Done() if err := s.httpServer.Shutdown(ctx); err != nil { s.logger.Error("HTTP server shutdown error", "error", err) } }() } // Shutdown gRPC server if s.grpcServer != nil { wg.Add(1) go func() { defer wg.Done() s.grpcServer.GracefulStop() }() } wg.Wait() s.logger.Info("Servers stopped") return nil } // GreeterService implements the helloworld.Greeter service type GreeterService struct { helloworldpb.UnimplementedGreeterServer } // SayHello implements the SayHello RPC method func (g *GreeterService) SayHello(ctx context.Context, req *helloworldpb.HelloRequest) (*helloworldpb.HelloReply, error) { return &helloworldpb.HelloReply{ Message: "Hello " + req.GetName(), }, nil } func parseLogLevel(level string) slog.Level { switch level { case "debug": return slog.LevelDebug case "info": return slog.LevelInfo case "warn": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } }