grpcserver

grpcserver assembles a gRPC server from options and a service binder, with trace IDs, request logging, panic recovery, default deadlines, and the standard health service.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/grpcserver"

Builds and runs a gRPC server: your services from a Binder, a default interceptor chain, the standard health service, and the same lifecycle as httpserver.

srv, err := grpcserver.New(ctx, &binder{},
    grpcserver.WithServerAddr(":8074"),
    grpcserver.WithRequestTimeout(30*time.Second),
    grpcserver.WithHealthService(checks),
    grpcserver.WithShutdownWaitGroup(wg),
    grpcserver.WithShutdownSignalChan(sc),
)

srv.StartServer()

Registration errors come from New

grpc.Server.RegisterService terminates the process on a duplicate service or a handler that does not implement its interface. Here the Binder registers through a wrapper that records those cases, and New returns them as wrapped sentinel errors, together with invalid options.

The effect is the one described for httpserver: a broken registration fails at rollout, before the listener serves anything, and the caller can tell the cases apart with errors.Is.

The default interceptor chain

Every unary and streaming RPC passes through, outermost first:

StepWhat it does
Request injectreads the trace ID from the x-request-id metadata, generates a UUIDv7 when it is missing or invalid, and returns it in the response header
Request loggerone entry per RPC with method, peer, user agent, grpc_code and duration
Panic recoverylogs the stack and answers INTERNAL
Default deadlinegives a unary RPC without a deadline the one set by WithRequestTimeout
Your interceptorsWithUnaryInterceptor and WithStreamInterceptor, in order

Streams are exempt from the default deadline, because a long-lived stream is usually intended. WithMethodTimeout sets a deadline for a specific method, streams included, and is applied in the transport before any interceptor runs.

At debug level, unary request and response messages are dumped as JSON, redacted with the redact default, and capped by WithMaxDumpSize. Streams log message counts instead. WithoutMethodLogger silences specific methods, which is what frequently polled health checks usually want.

Panics raised by the request inject or logger step, for example by a custom RedactFn or log handler, are recovered too, so no configuration hook can take the server down.

The health service

WithHealthService registers the standard grpc.health.v1.Health service, backed by the same check type as the healthcheck HTTP endpoint, so one check list serves both transports.

  • The service name "" covers every check, and each check ID is also a service name.
  • Concurrent Check, List and Watch calls on the same service share one run of its checks.
  • A Watch service with open streams is polled once per interval (WithHealthWatchInterval, 5 seconds by default), shared by all its streams, which receive only changes.
  • When Shutdown starts, every service reports NOT_SERVING and open Watch streams end with UNAVAILABLE.

The last point is what lets a load balancer drain the instance before connections close, and it is also why Watch streams never delay the graceful stop.

Checks run on every request, so bound them with WithHealthCheckTimeout.

Authentication and the health service

The health service is not exempt from your interceptors. An authentication interceptor must let Check and Watch through explicitly when probes call them without credentials, and should keep List authenticated, because it returns every check ID and those IDs usually name internal dependencies.

The package ships a JWT example that does exactly this with jwt: it verifies the token from metadata with VerifyToken and stores the claims with NewContextWithClaims.

Lifecycle

The contract is the one of httpserver, built on the same srvlifecycle state machine: a non-blocking, idempotent StartServer, a Shutdown that is safe to call more than once, ServeError for abnormal termination, and Addr for ephemeral ports.

Shutdown calls GracefulStop and waits for in-flight RPCs. When its context expires first, it calls Stop, closing open connections, and returns the context error. Without that fallback, a single client holding a stream open would hold the whole process open.

Passing the same wait group and signal channel to both servers drains every listener on one signal.

A separate listener, on purpose

The server binds its own listener rather than being mounted on the HTTP server. httpserver applies a write timeout and http.TimeoutHandler, which would cut off and buffer streams, and grpc.Server.ServeHTTP is experimental and does not support every grpc-go feature. Keep /metrics, /pprof and /status on the HTTP monitoring server.

Security defaults

  • The listener is plaintext unless WithTLSCertData or WithTLSConfig is set. Use TLS, or a TLS-terminating proxy, on any listener reachable from an untrusted network.
  • WithReflection lets any client list and describe every registered service and message type. Enable it on internal listeners only, or behind an authentication interceptor.
  • Message size, header list size and keepalive enforcement are configurable, with WithMaxRecvMsgSize, WithMaxHeaderListSize and WithKeepaliveEnforcementPolicy.

Instrumentation

WithInstrumenter adds the server options of a grpcutil.ServerInstrumenter. prometheusgrpc and opentelgrpc implement it. Their interceptors run between the request logger and the panic recovery, with a second recovery in front of them, so a panic inside instrumentation is answered like a handler panic.

Dependencies

Reaches google.golang.org/grpc, google.golang.org/protobuf, google.golang.org/genproto/googleapis/rpc, and golang.org/x/net, x/sys and x/text. It does not reach httprouter: make depcheck fails the build if it ever does.