Kern

Recipe - gRPC + HTTP Hybrid Service

Run Kern HTTP alongside a gRPC server with health checks, reflection, and shared structured logging.

gRPC + HTTP Hybrid Service

Run a Kern HTTP API and a gRPC server side-by-side, sharing the same structured logger and lifecycle.

Setup

import (
    "log/slog"
    "os"
    "time"

    "github.com/mobentum/kern"
    "github.com/mobentum/kern/extensions/xgrpc"
    "github.com/mobentum/kern/extensions/xlog"
    "google.golang.org/grpc/health/grpc_health_v1"
)

Shared logger

func main() {
    logger := xlog.NewLogger(xlog.Config{
        Format: "json",
        Level:  slog.LevelInfo,
    })
    slog.SetDefault(logger)

gRPC server with health and reflection

    grpcServer := xgrpc.New(xgrpc.Config{
        Addr:             ":9090",
        Logger:           logger,
        EnableHealth:     true,
        EnableReflection: true,
        ShutdownTimeout:  10 * time.Second,
        UnaryInterceptors: []grpc.UnaryServerInterceptor{
            // custom interceptors can be added here
        },
    }, xgrpc.Registration{
        Name: "UserService",
        Register: func(s grpc.ServiceRegistrar) {
            // pb.RegisterUserServiceServer(s, &userServer{})
        },
    })

    go func() {
        slog.Info("gRPC starting", "addr", ":9090")
        if err := grpcServer.Run(); err != nil {
            slog.Error("gRPC server error", "error", err)
        }
    }()

Kern HTTP server

    app := kern.New(
        kern.WithRecovery(),
        kern.WithSlogLogger(logger),
        kern.WithLogger(),
    )

    app.GET("/health", func(c *kern.Context) {
        healthy := grpcServer.HealthService() != nil
        c.JSON(200, map[string]interface{}{
            "http": "ok",
            "grpc": healthy,
        })
    })

    app.GET("/api/users", func(c *kern.Context) {
        c.JSON(200, map[string]string{"service": "users"})
    })

Coordinated shutdown

    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    go func() {
        <-ctx.Done()
        slog.Info("shutting down")

        shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()

        if err := grpcServer.Shutdown(shutdownCtx); err != nil {
            slog.Error("gRPC shutdown error", "error", err)
        }
    }()

    slog.Info("HTTP starting", "addr", ":8080")
    if err := app.Run(":8080",
        kern.WithGracefulShutdown(10*time.Second),
    ); err != nil {
        slog.Error("HTTP server error", "error", err)
    }
}

Key patterns

  • Shared logger: Both servers use the same xlog structured logger
  • Coordinated lifecycle: gRPC starts first, both shut down gracefully on signal
  • Health endpoint: HTTP health check reflects gRPC server health status
  • Independent ports: HTTP on :8080, gRPC on :9090 — no port conflict