grpc

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: AGPL-3.0 Imports: 23 Imported by: 0

Documentation

Overview

Package grpc builds the gRPC server this module's services are served from: the listener, the interceptor chain, TLS, health, and a shutdown that drains before it flushes.

An application supplies its RegistrationFuncs and whatever interceptors it wants and gets back a *Server with tracing and request logging already in the chain. The platform's own interceptors are chained first, so an application interceptor runs inside them and its work is inside the span.

TLS is on only when both files are named

The server enables TLS when a certificate and its key are both configured, and serves plaintext otherwise. That reading is why the config validates the two together and refuses one without the other: naming a certificate and forgetting the key is a configuration that looks like TLS and serves cleartext, and startup is the only place that is cheap to notice.

When TLS is on it is TLS 1.2 or better with a fixed set of ECDHE cipher suites and curves, and no client certificate is requested — this is server authentication, not mutual TLS. A deployment needing mTLS needs its own credentials, not an option here.

A port of zero is not an error: it asks the OS for an ephemeral one, which is what a test wants.

Health and reflection are opt-in

WithHealthRegistry registers grpc_health_v1 backed by a healthcheck.Registry — the same registry the HTTP server answers /readyz from, so both transports report from one set of checkers rather than two that can disagree. Passing it alongside an application's own grpc_health_v1 registration panics, because gRPC rejects a service registered twice.

Reflection is off by default. It enumerates every method and message the server exposes to anyone who can reach the port, which is a convenience in development and an inventory of the attack surface in production.

Message sizes are bounded in both directions

grpc-go bounds a received message at 4 MiB and a sent one at math.MaxInt32, which is no bound at all. That pairing is worse than it looks: a server on those defaults will marshal and send a response no default-configured client can read, and the ResourceExhausted surfaces on the caller — under its own 4 MiB receive default, in a process the service owner may not operate, with nothing in the server's logs or traces to say a response was ever too large.

This package bounds both directions at DefaultMaxMessageSize, so an oversized response fails on the server, attributable to the handler that produced it. Raising the bound is Config.MaxReceiveMessageSize / Config.MaxSendMessageSize — deployment-time, because the right number depends on payloads rather than on code — or WithMaxReceiveMessageSize / WithMaxSendMessageSize, which win over the config the way caller options do everywhere else here. Zero from either source takes the default; UnboundedMessageSize restores grpc-go's send behavior. The bound is per message, so a stream is bounded once per message rather than once per RPC.

A denormalized read model is the usual reason to raise the send bound. A page of records that each embed their related records is larger than it reads: 250 rows of a few dozen fields apiece clears 4 MiB without anything about the query looking unusual, and a page-size ceiling and a message-size ceiling that were chosen independently will disagree.

Raising the server's send bound is only half of it. The bound that actually breaks a consumer is the client's receive bound, which this module does not build and cannot set: a caller dials with grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(n)) to match. A server raised alone just moves the ResourceExhausted back to where it was hardest to attribute.

Shutdown

Shutdown drains in-flight RPCs until ctx is done, then stops hard and reports ctx's error, so a caller can tell a clean drain from a forced one. It flushes the tracer provider afterwards — spans from RPCs that finish during draining would be lost by flushing first — but does not shut the provider down, since the provider belongs to the process rather than to this server.

Index

Constants

View Source
const (

	// DefaultMaxMessageSize bounds a single message in either direction, in
	// bytes. It is grpc-go's receive default applied to send as well.
	//
	// grpc-go's own pair is asymmetric: receive is bounded at 4 MiB and send at
	// math.MaxInt32, which is no bound at all. A server on those defaults will
	// marshal and send a response that no default-configured client can read,
	// and the ResourceExhausted lands on the caller — in a process the service
	// owner may not operate, with nothing in the server's logs or traces to say
	// a response was ever too large. Bounding send at the same 4 MiB moves that
	// failure to the handler that produced the oversized message.
	DefaultMaxMessageSize = 4 << 20

	// UnboundedMessageSize is the largest bound gRPC can be given, and the value
	// to name to opt out of bounding a direction at all. It is what grpc-go uses
	// for its own send default.
	UnboundedMessageSize = math.MaxInt32
)

Variables

This section is empty.

Functions

func LoggingInterceptor

func LoggingInterceptor(logger logging.Logger) grpc.UnaryServerInterceptor

LoggingInterceptor logs every completed unary RPC.

func NewHealthService

func NewHealthService(registry healthcheck.Registry) grpc_health_v1.HealthServer

NewHealthService returns the grpc_health_v1 service backed by registry.

It is exported so an application that builds its own *grpc.Server still gets the platform's health service, registered the ordinary way with grpc_health_v1.RegisterHealthServer. Servers built here get it from WithHealthRegistry instead.

The empty service name is the whole process, per the health protocol: it reports the registry's aggregate. Any other name is looked up among the registered checkers, so a client that cares about one dependency can ask about that one by the name it was registered under. A nil registry has nothing to check, and therefore reports SERVING for the process and SERVICE_UNKNOWN for every name asked of it.

func RegisterGRPCServer

func RegisterGRPCServer(i do.Injector)

RegisterGRPCServer registers a *Server with the injector. Prerequisites: []grpc.UnaryServerInterceptor, []grpc.StreamServerInterceptor, and []RegistrationFunc must be registered in the injector before calling this.

The server it builds serves grpc_health_v1 when a healthcheck.Registry is registered, from the same registry the HTTP server answers /readyz from.

func StreamLoggingInterceptor

func StreamLoggingInterceptor(logger logging.Logger) grpc.StreamServerInterceptor

StreamLoggingInterceptor logs every completed streaming RPC.

The unary side has been logged since this package existed and the stream side was not logged at all, so a service whose API is mostly streams had no record that any of it had been called.

Types

type Config

type Config struct {
	TLSCertificateFile    string `env:"TLS_CERTIFICATE_FILEPATH"     json:"tlsCertificate,omitempty"    yaml:"tlsCertificate,omitempty"`
	TLSCertificateKeyFile string `env:"TLS_CERTIFICATE_KEY_FILEPATH" json:"tlsCertificateKey,omitempty" yaml:"tlsCertificateKey,omitempty"`

	// MaxReceiveMessageSize bounds a single received message, in bytes.
	// Zero takes DefaultMaxMessageSize; UnboundedMessageSize removes the
	// bound. It is a deployment-time number because it depends on the
	// payloads a service actually carries, not on its code.
	MaxReceiveMessageSize int `env:"MAX_RECEIVE_MESSAGE_SIZE" json:"maxReceiveMessageSize,omitempty" yaml:"maxReceiveMessageSize,omitempty"`

	// MaxSendMessageSize bounds a single sent message, in bytes, on the same
	// terms. A denormalized read model is the usual reason to raise it: a
	// full page of embedded records is larger than a client's own 4 MiB
	// receive default long before anything looks wrong on the server.
	MaxSendMessageSize int `env:"MAX_SEND_MESSAGE_SIZE" json:"maxSendMessageSize,omitempty" yaml:"maxSendMessageSize,omitempty"`

	Port uint16 `env:"PORT" json:"port,omitempty" yaml:"port,omitempty"`
}

func (*Config) ValidateWithContext

func (cfg *Config) ValidateWithContext(ctx context.Context) error

ValidateWithContext validates a Config struct.

Port is not Required, because zero is meaningful: it asks the OS for an ephemeral port, the same reading server/http gives it.

The TLS pair is checked together because NewGRPCServer enables TLS only when both files are named. Supplying one of them looks like TLS was configured and serves plaintext, which is the failure worth refusing at startup.

Neither message size is Required, because zero is meaningful for both: it takes DefaultMaxMessageSize. What is rejected is a bound gRPC cannot express — a negative one, or one past UnboundedMessageSize — since the wire's length prefix is what sets that ceiling and no larger number can mean anything.

type Option

type Option func(*options)

Option configures the server this package constructs. The zero configuration works: an absent logger logs nowhere and an absent tracer provider traces nowhere.

func WithHealthRegistry

func WithHealthRegistry(registry healthcheck.Registry) Option

WithHealthRegistry registers the grpc_health_v1 service, backed by the given registry. It names the same registry the HTTP sibling's option of the same name takes, so both transports answer from one set of checkers rather than from two that can disagree.

A nil registry registers nothing. A server that registers its own grpc_health_v1 implementation through a RegistrationFunc must not also pass this — gRPC rejects a service registered twice by panicking.

func WithLogger

func WithLogger(logger logging.Logger) Option

WithLogger attaches a logger.

func WithMaxReceiveMessageSize

func WithMaxReceiveMessageSize(size int) Option

WithMaxReceiveMessageSize bounds a single received message, in bytes, overriding Config.MaxReceiveMessageSize.

It is named after gRPC's own vocabulary rather than after routing.WithMaxRequestBody, its HTTP counterpart, because Config spells the same number MaxReceiveMessageSize and one knob with two names in one package is the thing worth avoiding. The bound is per message, not per RPC: a stream of a thousand messages is bounded a thousand times, once each.

Zero leaves the Config field to decide. UnboundedMessageSize removes the bound; anything negative or past it is refused by NewGRPCServer.

func WithMaxSendMessageSize

func WithMaxSendMessageSize(size int) Option

WithMaxSendMessageSize bounds a single sent message, in bytes, overriding Config.MaxSendMessageSize, on the same terms as WithMaxReceiveMessageSize.

This is the direction worth setting deliberately. grpc-go leaves send effectively unbounded, so an oversized response fails on whichever client called — under its own 4 MiB receive default, in a process the service owner may not operate. Platform bounds it at DefaultMaxMessageSize so the failure belongs to the handler instead; raising it here means also raising the calling client's receive bound, which is the one that actually breaks.

func WithReflection

func WithReflection() Option

WithReflection registers the gRPC server reflection service.

It is off by default. Reflection enumerates every method and message the server exposes to anyone who can reach the port, which is a debugging convenience in development and an inventory of the attack surface in production — so it is opted into rather than out of.

func WithServiceName

func WithServiceName(serviceName string) Option

WithServiceName names the server's logger and instrumentation scope. It mirrors the HTTP server's serviceName, which is an option there for the same reason: a hardcoded name makes two servers in one process indistinguishable in logs.

func WithTracerProvider

func WithTracerProvider(tracerProvider tracing.Provider) Option

WithTracerProvider attaches a tracer provider, enabling spans on served RPCs.

type RegistrationFunc

type RegistrationFunc func(*grpc.Server)

RegistrationFunc is i.e. protobuf.RegisterSomeExampleServiceServer(grpcServer, &exampleServiceServerImpl{}).

type Server

type Server struct {
	// contains filtered or unexported fields
}

func NewGRPCServer

func NewGRPCServer(
	ctx context.Context,
	cfg *Config,
	unaryServerInterceptors []grpc.UnaryServerInterceptor,
	streamServerInterceptors []grpc.StreamServerInterceptor,
	registrationFunctions []RegistrationFunc,
	opts ...Option,
) (*Server, error)

NewGRPCServer builds a gRPC server.

It takes a context and validates the config, matching NewHTTPServer. The Config has had a ValidateWithContext for as long as it has had a TLS pair, and nothing called it — so naming a certificate without its key, which this constructor reads as "TLS was not configured", started a plaintext server that looked from its config like a TLS one.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context) error

Serve serves gRPC traffic until Shutdown is called or ctx is done.

A graceful stop reports nil; every other failure is returned. It used to return nothing, and the only sentinel it checked was net/http's ErrServerClosed — which gRPC never returns — so a bind failure or a dead server was completely silent.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown stops the server gracefully, then flushes the spans its RPCs produced — the same order as the HTTP sibling, and for the same reason: spans from RPCs that complete during draining are lost if the flush runs first.

Like the HTTP sibling it flushes the tracer provider without shutting it down. The provider is shared with whatever else the process is still taking down, and closing an exporter one server happens to be finished with would blind all of it. Its owner shuts it down last.

In-flight RPCs are given until ctx is done to finish. If ctx expires first the server is stopped hard and the context's error is returned, so a caller can tell a clean drain from a forced one.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL