Documentation
¶
Overview ¶
Package interceptor provides the gRPC unary and streaming server interceptor chains that align the gRPC transport with the existing HTTP middleware stack (runtime/http/middleware): RequestID, CellAttribution, Tracing, AccessLog, Metrics, Auth, and Recovery.
The unary interceptors are composed by NewUnaryChain and the streaming interceptors by NewStreamChain, each into a single grpc.ServerOption. bootstrap installs both options on the adapters/grpc server; this package owns no server lifecycle. The cross-cutting cores (request-id derivation, cell attribution, span open/close, access logging, the bearer-auth decision, and panic collapse) are shared between the unary and streaming variants — one source per concern across both transports. The metrics cell-label funnel is the sole intentional exception: it is inlined in each metrics interceptor body per the GRPC-METRICS-LABEL-CELLID-CTXSOURCE-01 per-function provenance binding.
Chain order ¶
NewUnaryChain composes the interceptors in this fixed order (outermost → innermost), where the first argument to grpc.ChainUnaryInterceptor is the outermost wrapper closest to the transport:
RequestID → CellAttribution → Tracing → AccessLog → Metrics → Auth → Recovery → handler
NewStreamChain composes the streaming analogs in the same order plus a stream-only StreamDrain just inside Auth (so the handler's context is bound to the framework drain signal, PR-10 #1153):
RequestID → CellAttribution → Tracing → AccessLog → Metrics → Auth → Drain → Recovery → handler
This mirrors the HTTP listener-root order (CellAttribution → Tracing → AccessLog → Metrics). CellAttribution runs before every interceptor that reads the cell label (AccessLog, Metrics) so the owning cell is in ctx when they observe it; AccessLog runs after Tracing (so a propagated trace_id is in ctx) and OUTER to Auth (so auth rejections are still logged). StreamDrain sits just inside Auth so the drain-bound, principal-enriched context reaches the handler while the outer observability interceptors still see the final status (e.g. codes.Canceled when a drain cuts a stream short).
Recovery is INNERMOST, not outermost. This deliberately diverges from the HTTP middleware order (where Recovery sits inside Tracing/Metrics but its placement is driven by the HTTP ResponseWriter commit-ordering constraint — Recovery must own the writer to emit a 500 body). gRPC has no such constraint: a status code is a return value, so a panic recovered by the innermost interceptor and converted to codes.Internal propagates outward as a normal (resp, err) return that the outer Metrics and Tracing interceptors observe accurately. Placing Recovery innermost is the grpc-ecosystem go-grpc-middleware consensus, which documents recovery-last so metrics and logging see the converted status.
ref: grpc-ecosystem/go-grpc-middleware interceptors/recovery/interceptors.go ref: go-kratos/kratos transport/grpc/interceptor.go
Index ¶
- func NewServerInterceptors(deps Deps) runtimegrpc.ServerInterceptors
- func NewStreamChain(deps Deps) grpc.ServerOption
- func NewUnaryChain(deps Deps) grpc.ServerOption
- func StreamAccessLog(clk clock.Clock) grpc.StreamServerInterceptor
- func StreamAuth(verifier auth.IntentTokenVerifier, opts ...AuthOption) grpc.StreamServerInterceptor
- func StreamCellAttribution(resolve CellResolver) grpc.StreamServerInterceptor
- func StreamDrain(drain *runtimegrpc.DrainSignal) grpc.StreamServerInterceptor
- func StreamMetrics(collector metrics.GRPCCollector, clk clock.Clock, ...) grpc.StreamServerInterceptor
- func StreamRecovery() grpc.StreamServerInterceptor
- func StreamRequestID() grpc.StreamServerInterceptor
- func StreamTracing(tracer wrapper.Tracer) grpc.StreamServerInterceptor
- func UnaryAccessLog(clk clock.Clock) grpc.UnaryServerInterceptor
- func UnaryAuth(verifier auth.IntentTokenVerifier, opts ...AuthOption) grpc.UnaryServerInterceptor
- func UnaryCellAttribution(resolve CellResolver) grpc.UnaryServerInterceptor
- func UnaryMetrics(collector metrics.GRPCCollector, clk clock.Clock, ...) grpc.UnaryServerInterceptor
- func UnaryRecovery() grpc.UnaryServerInterceptor
- func UnaryRequestID() grpc.UnaryServerInterceptor
- func UnaryTracing(tracer wrapper.Tracer) grpc.UnaryServerInterceptor
- type AuthOption
- type CellResolver
- type Deps
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewServerInterceptors ¶
func NewServerInterceptors(deps Deps) runtimegrpc.ServerInterceptors
NewServerInterceptors returns the adapter-consumable bundle for a complete GoCell gRPC server. Both unary and streaming chains are built from the same Deps value, and the same Registrar/Drain instances are carried for the adapter to bind and trigger. This is the normal composition-root entrypoint; it closes the "forgot NewStreamChain" streaming-auth gap while keeping adapters/grpc free of a direct interceptor import.
func NewStreamChain ¶
func NewStreamChain(deps Deps) grpc.ServerOption
NewStreamChain composes the streaming interceptors into a single grpc.ServerOption — the streaming analog of NewUnaryChain, at full parity (including auth) plus the stream-only StreamDrain. This is the single authoritative stream composition point; the order
RequestID → CellAttribution → Tracing → AccessLog → Metrics → Auth → Drain → Recovery
(RequestID outermost, Recovery innermost, Drain just inside Auth so the handler context is drain-bound) is fixed here and guarded by GRPC-STREAM-CHAIN-ORDER-01. The grpc.ChainStreamInterceptor caller is locked to this file by GRPC-CHAIN-STREAM-INTERCEPTOR-CALLER-01 (Medium downstream + Go-ceiling upstream; the single-builder Hard upgrade that also forces both chains to be wired is tracked at #1752, same family as #1394/#851/#1282).
Registrar, CellIDClosedSet, and Drain are required (fail-closed): a chain composed without them would silently relabel every RPC to the runtime sentinel or leave streams un-drainable.
func NewUnaryChain ¶
func NewUnaryChain(deps Deps) grpc.ServerOption
NewUnaryChain composes the unary interceptors into a single grpc.ServerOption. This is the single authoritative composition point; the order is fixed here (RequestID outermost, Recovery innermost) and guarded by archtest GRPC-INTERCEPTOR-CHAIN-ORDER-01. See the package doc for the order rationale.
Registrar and CellIDClosedSet are required (fail-closed): a chain composed without them would silently relabel every RPC to the runtime sentinel.
func StreamAccessLog ¶
func StreamAccessLog(clk clock.Clock) grpc.StreamServerInterceptor
StreamAccessLog is the streaming analog of UnaryAccessLog: it logs one structured slog.Info line per stream at close, with the HTTP-parity field set. clk is required (clock.MustHaveClock).
func StreamAuth ¶
func StreamAuth(verifier auth.IntentTokenVerifier, opts ...AuthOption) grpc.StreamServerInterceptor
StreamAuth is the streaming analog of UnaryAuth: it authenticates the stream at open via the shared authorize core (the single source of the gRPC bearer-auth decision across both transports) and forwards a principal-enriched, context-wrapped stream. The verifier is required (programmer-error panic on nil) — the server must refuse to start rather than expose unauthenticated streams.
func StreamCellAttribution ¶
func StreamCellAttribution(resolve CellResolver) grpc.StreamServerInterceptor
StreamCellAttribution is the streaming analog of UnaryCellAttribution: it attributes the stream to its owning cell (writing kernel/ctxkeys.CellID) before the access-log and metrics interceptors read it.
func StreamDrain ¶
func StreamDrain(drain *runtimegrpc.DrainSignal) grpc.StreamServerInterceptor
StreamDrain binds each in-flight stream's handler context to the shared DrainSignal (PR-10 #1153): when the adapter triggers drain at GracefulStop start, the derived context is canceled, so a handler that selects on ctx.Done() returns promptly instead of blocking GracefulStop until the hard-stop deadline. It runs one bounded goroutine per active stream, freed when the stream ends (defer cancel → ctx.Done) or drain fires. drain is required (programmer-error panic on nil; adapters/grpc.New stores the SAME instance from Config.Interceptors for its graceful-stop trigger).
func StreamMetrics ¶
func StreamMetrics(collector metrics.GRPCCollector, clk clock.Clock, validCellIDs map[string]struct{}) grpc.StreamServerInterceptor
StreamMetrics is the streaming analog of UnaryMetrics: it records grpc_server_requests_total / grpc_server_request_duration_seconds once per stream at close (duration = whole stream lifetime). Ops note: during a graceful shutdown the framework drain (StreamDrain) cancels in-flight streams, so they close with code=Canceled — a Canceled spike on this metric within a drain/deploy window is expected, not an outage. The cell-label funnel (metrics.ResolveCellLabel → GRPCCollector.RecordRPC) is INLINED here — not shared with UnaryMetrics — because GRPC-METRICS-LABEL-CELLID-CTXSOURCE-01 binds the funnel by go/types object identity WITHIN each metrics interceptor body; moving it to a shared helper would defeat that per-function provenance check.
collector and clk are required (programmer-error panic on nil).
func StreamRecovery ¶
func StreamRecovery() grpc.StreamServerInterceptor
StreamRecovery is the INNERMOST streaming interceptor: it recovers a panic from the handler, logs the redacted value via the shared recoverGRPCPanic core, and returns codes.Internal. It never re-panics — the panic is collapsed into a return value so the outer Metrics/Tracing interceptors observe a clean status (outside PANIC-REGISTERED-01, same as the unary path).
func StreamRequestID ¶
func StreamRequestID() grpc.StreamServerInterceptor
StreamRequestID is the streaming analog of UnaryRequestID — the OUTERMOST stream interceptor. It derives the request id into the stream context via the shared deriveRequestIDCtx core and forwards a context-wrapped stream.
func StreamTracing ¶
func StreamTracing(tracer wrapper.Tracer) grpc.StreamServerInterceptor
StreamTracing is the streaming analog of UnaryTracing: the span spans the whole stream lifetime (opened at stream start, closed at stream end), recording the final status. A nil tracer degrades to NoopTracer.
func UnaryAccessLog ¶
func UnaryAccessLog(clk clock.Clock) grpc.UnaryServerInterceptor
UnaryAccessLog returns an interceptor that logs one structured slog.Info line per RPC, mirroring the HTTP access-log middleware (runtime/http/middleware/ access_log.go). It carries the HTTP-parity field set:
method, code, duration_ms, cell_id, request_id, correlation_id, trace_id
Placement (chain order GRPC-INTERCEPTOR-CHAIN-ORDER-01): after RequestID + CellAttribution + Tracing (so request_id / cell_id / trace_id are already in ctx) and OUTER to Auth (so auth rejections are logged too). The auth Principal is deliberately NOT logged: UnaryAuth runs inner to this interceptor, so no auth.Principal is in ctx at log time — emitting it would be permanently dead code. Revisit only if auth ordering changes (e.g. device-token / per-method-public). trace_id is best-effort: UnaryTracing writes it only for a propagated/remote trace, same as the HTTP path. real_ip (an HTTP access-log field) is intentionally absent: gRPC has no X-Real-IP equivalent in this stack.
Redaction is handled fail-closed at the slog sink (SLOG-HANDLER-SEALED-FUNNEL-01); like the HTTP access log, this interceptor does not redact field-side.
clk is required (clock.MustHaveClock), matching the HTTP AccessLog contract.
func UnaryAuth ¶
func UnaryAuth(verifier auth.IntentTokenVerifier, opts ...AuthOption) grpc.UnaryServerInterceptor
UnaryAuth returns an interceptor that extracts a bearer token from the incoming metadata, verifies it via runtime/auth.AuthenticateBearer (the shared transport-agnostic core), applies the password-reset gate, and on success forwards the principal-enriched context to the handler. Failures map directly to gRPC status codes (Unauthenticated / Unavailable / PermissionDenied), mirroring the HTTP handleAuthRequest classification.
The verifier is required: a nil verifier is a wiring bug that fails fast at construction (programmer-error panic). For a security interceptor this is correct — the server must refuse to start rather than boot and reject every request, which would be indistinguishable from an outage.
func UnaryCellAttribution ¶
func UnaryCellAttribution(resolve CellResolver) grpc.UnaryServerInterceptor
UnaryCellAttribution returns the interceptor that attributes each RPC to its owning cell: it resolves info.FullMethod through resolve and, on a match, writes kernel/ctxkeys.CellID into the context BEFORE the access-log and metrics interceptors read it. It is the gRPC analog of the HTTP CellAttribution middleware (runtime/http/middleware/cell_id.go) and the writer for the otherwise-sentinel grpc_server_* cell label (closing #1383).
resolve is required: a nil resolver is a wiring bug that fails fast at construction (programmer-error panic, like UnaryMetrics' nil-collector guard) rather than silently leaving every RPC attributed to the runtime sentinel.
func UnaryMetrics ¶
func UnaryMetrics(collector metrics.GRPCCollector, clk clock.Clock, validCellIDs map[string]struct{}) grpc.UnaryServerInterceptor
UnaryMetrics returns an interceptor that records grpc_server_requests_total and grpc_server_request_duration_seconds via the given collector. The metric labels are: method (info.FullMethod), code (the gRPC status code name derived from the handler's returned error), and cell. The cell label is resolved through the sealed metrics.ResolveCellLabel funnel against validCellIDs (the assembly cell-id set): the cell written by UnaryCellAttribution (which runs outer to this interceptor, #1152) is reflected when it is a member, and an absent or out-of-set cell degrades to metrics.RuntimeCellSentinel ("_runtime"). This mirrors runtime/http/middleware.Metrics.
collector and clk are required: a nil collector or clock is a wiring bug that fails fast at construction (programmer-error panic, like MustHaveClock) rather than nil-dereferencing on the first RPC where UnaryRecovery would mask it as a generic codes.Internal.
func UnaryRecovery ¶
func UnaryRecovery() grpc.UnaryServerInterceptor
UnaryRecovery returns the innermost interceptor: it recovers panics raised by the handler, logs the redacted panic value plus stack via slog.Error, and converts the panic into a codes.Internal status returned to the client. It does NOT re-panic — the panic is collapsed into a return value so the outer Metrics and Tracing interceptors observe a clean codes.Internal. This mirrors runtime/http/middleware.Recovery (and the go-grpc-middleware / Kratos recovery convention); because it never calls panic() itself it is outside the PANIC-REGISTERED-01 funnel.
func UnaryRequestID ¶
func UnaryRequestID() grpc.UnaryServerInterceptor
UnaryRequestID returns the outermost interceptor: it derives a request id (reused from incoming metadata when valid, otherwise freshly generated) and stores it under both ctxkeys.RequestID and ctxkeys.CorrelationID so that the downstream Tracing / Metrics / Recovery interceptors and any errcode emitted by the handler carry a stable correlation id. It mirrors runtime/http/middleware.RequestID; the ctxkeys helpers are shared and carry no funnel restriction.
func UnaryTracing ¶
func UnaryTracing(tracer wrapper.Tracer) grpc.UnaryServerInterceptor
UnaryTracing returns an interceptor that starts a wrapper.Span per RPC named by the full method (leading slash stripped), records rpc.* attributes, and on error records the error and sets the span status. Error redaction is owned by the otelSpan sink (SPAN-RECORD-ERROR-SEAL-01) — call sites pass the raw error. Span attributes go through wrapper.Attr (not raw attribute.String), so the adapters/otel safeStringAttr redaction funnel applies automatically — this interceptor does not touch the SPAN-SETATTR-REDACT-01 callsite set. It mirrors runtime/http/middleware.Tracing. A nil tracer degrades to NoopTracer.
Types ¶
type AuthOption ¶
type AuthOption func(*authConfig)
AuthOption configures the auth interceptor.
func WithPasswordResetExempt ¶
func WithPasswordResetExempt(pred func(fullMethod string) bool) AuthOption
WithPasswordResetExempt installs a predicate marking RPC methods exempt from the password-reset gate (the gRPC analog of the HTTP route-based exempt matcher). The default (nil predicate) is fail-closed — a reset-required token is rejected on every method. Passing a nil predicate is a no-op; any previously installed predicate is retained.
func WithPublicMethod ¶
func WithPublicMethod(pred func(fullMethod string) bool) AuthOption
WithPublicMethod installs a predicate marking RPC methods that bypass authentication (e.g. health checks). The wiring of concrete public-method sets is a later-PR concern (tracked in backlog); the default (nil predicate) is fail-closed — every method requires authentication. Passing a nil predicate is a no-op; any previously installed predicate is retained.
type CellResolver ¶
CellResolver maps a gRPC fullMethod (e.g. "/pkg.Svc/Method") to the cell that owns it. The bool is false for an unregistered method (e.g. the native grpc-health service), so the cell label downstream degrades to the runtime sentinel. The composition root supplies the registrar's CellIDForMethod, which is the single method→cellID source shared by the chain, bootstrap, and adapter (Option 3, #1152).
type Deps ¶
type Deps struct {
// Tracer records one span per RPC. A nil Tracer degrades to NoopTracer.
Tracer wrapper.Tracer
// Collector records grpc_server_* metrics.
Collector metrics.GRPCCollector
// Clock times request duration for the metrics + access-log interceptors (required).
Clock clock.Clock
// Verifier authenticates bearer tokens (required; nil fails closed).
Verifier auth.IntentTokenVerifier
// AuthOptions configures the auth interceptor (public-method and
// password-reset-exempt predicates).
AuthOptions []AuthOption
// Registrar is the gRPC service registrar whose CellIDForMethod feeds the
// cell-attribution interceptor (Option 3, #1152). adapters/grpc.New also
// binds this same instance to the underlying grpc.Server via Config.Interceptors,
// so attribution and registration share one method map. Required: a nil
// Registrar is a wiring bug → NewUnaryChain panics rather than leaving every
// RPC attributed to the sentinel.
Registrar *runtimegrpc.ServiceRegistrar
// CellIDClosedSet is the assembly's cell-id set (asm.CellIDs()) the metrics
// interceptor validates the attributed cell against (M12b defense-in-depth: an
// out-of-set cell degrades to the runtime sentinel, never pollutes the SLO
// series). Required + non-empty: a half-wired chain (resolver set, closed set
// empty) would relabel every cell _runtime — NewUnaryChain fails fast instead.
CellIDClosedSet []string
// Drain is the framework-side gRPC drain signal (PR-10 #1153) the StreamDrain
// interceptor binds each in-flight stream's context to, so GracefulStop
// actively cancels long-lived streams instead of merely waiting for them. It
// is also the instance adapters/grpc.New stores for its GracefulStop trigger
// via Config.Interceptors (same Option-3 instance symmetry as Registrar).
// Required by NewStreamChain (fail-closed); NewUnaryChain ignores it (unary
// RPCs are short-lived, GracefulStop's wait suffices).
Drain *runtimegrpc.DrainSignal
}
Deps holds the dependencies the unary AND streaming interceptor chains need (NewUnaryChain / NewStreamChain share one Deps). They are supplied by the composition root (cmd/ or examples/); this package only composes them. Drain is consumed only by the stream chain (StreamDrain); the unary chain ignores it.