Skip to content

Observability & Testing

Protobuf’s efficiency costs you human-readability. You can’t eyeball a gRPC call the way you can a REST request. So the ecosystem replaces “read the traffic” with reflection, interceptor-based telemetry, and in-process tests.

Server reflection lets a server describe its own services and messages at runtime, so a tool can call methods without having the .proto on hand. With it enabled, grpcurl becomes your curl for gRPC:

Terminal window
# List services on a running server, then call one — no local .proto needed
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext -d '{"id": 42}' localhost:50051 user.v1.UserService/GetUser

Enable reflection in non-production or behind auth — it’s what makes a running server explorable.

You already met interceptors as the place for cross-cutting concerns. Observability is their biggest use: one interceptor chain adds metrics (latency, request counts, status codes), traces (spans that follow a call across services), and structured logs — for every method, without touching handler code.

flowchart LR
  call["incoming RPC"] --> mw["interceptor chain"]
  mw --> m["metrics
(latency, count, code)"]
  mw --> t["trace span
(propagate context)"]
  mw --> l["structured log"]
  mw --> handler["your handler"]
One interceptor chain makes every method observable

The standard path is OpenTelemetry instrumentation, which ships as ready-made gRPC interceptors in every major language — you register them once and get metrics and distributed traces across your whole fleet.

Spinning up real ports makes tests slow and flaky. gRPC lets you run a real server and client in memory, exercising the full serialization and interceptor path with no network:

// bufconn: a real gRPC server over an in-memory listener
lis := bufconn.Listen(1024 * 1024)
s := grpc.NewServer()
userv1.RegisterUserServiceServer(s, &server{})
go s.Serve(lis)
conn, _ := grpc.NewClient("passthrough:///bufnet",
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
return lis.DialContext(ctx)
}),
grpc.WithTransportCredentials(insecure.NewCredentials()))
client := userv1.NewUserServiceClient(conn)
got, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 42})

gRPC is usually faster than REST/JSON for high-volume, small, structured calls — less bytes, cheaper decode, multiplexed connections. But measure your workload: for occasional calls or large already-compressed blobs, the difference can be negligible, and REST’s tooling may win overall. Benchmark with realistic payloads and concurrency, not a toy loop, before making it a selling point.

What does server reflection enable?
Where do metrics, traces, and logs naturally attach in gRPC?
Why test with an in-process server (e.g. Go bufconn or an ephemeral port)?
What is the honest stance on "gRPC is faster than REST"?