Skip to content

Add gRPC payload capture (UnaryServerInterceptor) - #99

Merged
tonyalaribe merged 1 commit into
mainfrom
grpc-support
Aug 27, 2026
Merged

tonyalaribe merged 1 commit into
mainfrom
grpc-support

Conversation

@tonyalaribe

Copy link
Copy Markdown
Contributor

Every integration in this SDK is an HTTP middleware, so a gRPC service gets no request or response payloads at all — and in Go, gRPC is frequently a service's only protocol.

server := grpc.NewServer(grpc.UnaryInterceptor(
    monoscopegrpc.UnaryServerInterceptor(monoscopegrpc.Config{
        ServiceName:        "payment",
        CaptureRequestBody: true,
        RedactRequestBody:  []string{"$.creditCard.creditCardNumber"},
    }),
))

google.golang.org/grpc was already an indirect dependency, so promoting it to direct costs nothing new.

protojson, not encoding/json

For anything that is a proto.Message, marshalling goes through protojson: it applies the field names declared in the .proto and renders 64-bit fields as strings. encoding/json on a generated struct leaks the generator's internal state and produces field names that would not match a redaction path the user wrote against their schema. Same class of problem the JS SDK hit, where protobuf int64 arrived as {low, high, unsigned}.

Attributes

rpc.system / rpc.method / rpc.grpc.status_code are set alongside the HTTP-shaped fields, not instead of them. The server lifts bodies based on the HTTP shape, but a gRPC status says more than ok-or-not — NOT_FOUND and PERMISSION_DENIED both flatten to 500 — so the real code is kept for the UI to filter on natively later.

Scope

Unary only. Streaming has no single message to capture, so a stream interceptor would record nothing while appearing to work — better to say so than to ship a silent no-op.

Tests

Cover the parts that matter, not just the happy path: response and error reach the caller untouched, a sensitive field in a decoded message is redacted, the gRPC code survives unflattened, capture-off records nothing, and an unmarshalable message degrades to no body. That last one caught a real wart — RedactJSON turns nil into the literal null, which would have been captured as a body reading "null".

Cross-language plan: docs/grpc-sdk-support.md in the monoscope repo. The Node equivalent shipped as @monoscopetech/common@1.3.1 and is deployed in the demo's payment service.

Every integration in this SDK is an HTTP middleware, so a gRPC service gets no
request or response payloads at all — and in Go, gRPC is frequently a service's
only protocol. UnaryServerInterceptor closes that.

google.golang.org/grpc was already an indirect dependency, so promoting it to
direct costs nothing new.

Marshalling goes through protojson for anything that is a proto.Message, not
encoding/json. protojson applies the field names declared in the .proto and
renders 64-bit fields as strings; encoding/json on a generated struct leaks the
generator's internal state and produces field names that would not match a
redaction path the user wrote against their schema. This is the same class of
problem the JS SDK hit, where protobuf int64 arrived as {low, high, unsigned}.

rpc.system / rpc.method / rpc.grpc.status_code are set alongside the HTTP-shaped
fields rather than instead of them: the server lifts bodies based on the HTTP
shape, but a gRPC status says more than ok-or-not — NOT_FOUND and
PERMISSION_DENIED both flatten to 500 — so the real code is kept.

Streaming is deliberately out of scope and documented as such; there is no
single message to capture, so a stream interceptor would record nothing while
appearing to work.

Tests cover the parts that matter rather than the happy path alone: the RPC's
response and error reach the caller untouched, a sensitive field in a decoded
message is redacted, the gRPC code survives unflattened, capture off records
nothing, and an unmarshalable message degrades to no body. That last test caught
a real wart — RedactJSON turns nil into the literal 'null', which would have been
captured as a body reading "null".
@tonyalaribe
tonyalaribe merged commit 3fc1764 into main Aug 27, 2026
26 of 28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant