Table of contents
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:
| Step | What it does |
|---|---|
| Request inject | reads 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 logger | one entry per RPC with method, peer, user agent, grpc_code and duration |
| Panic recovery | logs the stack and answers INTERNAL |
| Default deadline | gives a unary RPC without a deadline the one set by WithRequestTimeout |
| Your interceptors | WithUnaryInterceptor 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,ListandWatchcalls on the same service share one run of its checks. - A
Watchservice with open streams is polled once per interval (WithHealthWatchInterval, 5 seconds by default), shared by all its streams, which receive only changes. - When
Shutdownstarts, every service reportsNOT_SERVINGand openWatchstreams end withUNAVAILABLE.
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
WithTLSCertDataorWithTLSConfigis set. Use TLS, or a TLS-terminating proxy, on any listener reachable from an untrusted network. WithReflectionlets 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,WithMaxHeaderListSizeandWithKeepaliveEnforcementPolicy.
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.
Links
- Reference documentation
- Source code
- The guide: /docs/grpc-services/
- Calling gRPC: grpcclient
- The HTTP counterpart: httpserver
- All nurago packages