package grpcadapter import ( "context" "errors" "fmt" "log/slog" "net" "runtime/debug" "time" ledgerv1 "gl/gen/ledger/v1" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/reflection" "google.golang.org/grpc/status" ) type ServerConfig struct { Host string Port int ShutdownTimeout time.Duration } func Run(ctx context.Context, cfg ServerConfig, handler ledgerv1.GeneralLedgerServiceServer) error { listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)) if err != nil { return fmt.Errorf("listen for grpc: %w", err) } return runWithListener(ctx, cfg.ShutdownTimeout, listener, handler) } func runWithListener(ctx context.Context, shutdownTimeout time.Duration, listener net.Listener, handler ledgerv1.GeneralLedgerServiceServer) error { defer listener.Close() server := grpc.NewServer(grpc.ChainUnaryInterceptor(structuredUnaryLogger(), panicRecovery())) ledgerv1.RegisterGeneralLedgerServiceServer(server, handler) reflection.Register(server) serveErr := make(chan error, 1) go func() { serveErr <- server.Serve(listener) }() select { case err := <-serveErr: if errors.Is(err, grpc.ErrServerStopped) { return nil } return fmt.Errorf("serve grpc: %w", err) case <-ctx.Done(): } stopped := make(chan struct{}) go func() { server.GracefulStop() close(stopped) }() timer := time.NewTimer(shutdownTimeout) defer timer.Stop() select { case <-stopped: case <-timer.C: server.Stop() <-stopped } err := <-serveErr if err != nil && !errors.Is(err, grpc.ErrServerStopped) { return fmt.Errorf("serve grpc: %w", err) } return nil } func structuredUnaryLogger() grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { started := time.Now() response, err := handler(ctx, req) code := status.Code(err) level := slog.LevelInfo if code != codes.OK { level = slog.LevelError } slog.Log(ctx, level, "grpc call finished", "component", "grpc_server", "grpc_method", info.FullMethod, "grpc_code", code.String(), "duration_ms", float64(time.Since(started).Microseconds())/1000, ) return response, err } } func panicRecovery() grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (response any, err error) { defer func() { if recovered := recover(); recovered != nil { slog.ErrorContext(ctx, "grpc panic recovered", "component", "grpc_server", "grpc_method", info.FullMethod, "panic", recovered, "stack", string(debug.Stack()), ) err = status.Error(codes.Internal, "internal server error") } }() return handler(ctx, req) } }