Files
go-grpc-gateway-template/internal/server/server.go
2025-08-13 19:36:09 +02:00

205 lines
4.6 KiB
Go

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
}
}