petrbouda/jeffrey/jeffrey-traces-grpc

Claude Code and agent skill by petrbouda/jeffrey

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Instrument gRPC with Jeffrey JFR events — a ServerInterceptor that makes every inbound call the root span of a trace, and a ClientInterceptor that records every outbound call as a leaf under the span that made it, both emitting GrpcServerExchangeEvent/GrpcClientExchangeEvent into Jeffrey Traces. Covers the jeffrey-tracing-grpc module, registration on plain gRPC and on Spring gRPC, and the callback problem that makes gRPC different from a servlet filter. Requires the jeffrey-traces-core skill for the data model, emit rules, recording setup, and verification.

Install

npx vskill@latest install petrbouda/jeffrey/jeffrey-traces-grpc

SKILL.md

Jeffrey Traces — gRPC Instrumentation

Read `jeffrey-traces-core` first — it defines the data model, the emit rules, the dependency, recording configuration, and verification. This skill covers gRPC on both sides of the wire.

Your services and stubs need zero changes. An inbound call becomes the root span of its trace; an outbound call becomes a leaf under whatever the caller was doing.

Rules recap (from the core skill) that this embodies:

  • The inbound exchange event is the root span, so nothing your handler does needs to know it is

being traced — spans opened inside it nest underneath.

  • An outbound call is a leaf: it is committed with commitSpan(), which nests it under the span

in progress. The work it triggers happens in another process this recording cannot see.

  • Names are low-cardinality by construction: package.Service/Method, straight off the

MethodDescriptor. Nothing derived from a message reaches the name.


1. Add the module, register the interceptors

xml
<dependency>
<groupId>cafe.jeffrey-analyst</groupId>
<artifactId>jeffrey-tracing-grpc</artifactId>
<version><!-- latest release --></version>
</dependency>

Next to it, one span-context storage — the Tracer fails at startup without one: jeffrey-tracing-scoped-value (a ScopedValue, Java 25+) or jeffrey-tracing-thread-local (a ThreadLocal, Java 21+). The Spring Boot starter brings the ThreadLocal one; on Java 25 add jeffrey-tracing-scoped-value next to it for ScopedValue.

java
Server server = ServerBuilder.forPort(port)
.intercept(new JfrGrpcServerInterceptor()) // every service on this server
.build();
ManagedChannel channel = ManagedChannelBuilder.forAddress(host, port)
.intercept(new JfrGrpcClientInterceptor()) // every stub on this channel
.build();

Both are stateless and thread-safe: one instance per server or channel is enough, and registering the same instance on several is fine.

On Spring gRPC, the server side is a bean marked global so it applies to every registered service — which is how Jeffrey's own hub registers it:

java
@Bean
@GlobalServerInterceptor
ServerInterceptor jfrGrpcServerInterceptor() {
return new JfrGrpcServerInterceptor();
}

There is no starter for this and no auto-configuration: a channel is built by application code, not handed out as a bean the starter could recognise, so the one line above is the integration.

2. Why gRPC is not a servlet filter

A servlet filter can wrap the whole request in one try/finally on one thread. A gRPC call cannot be wrapped that way: interceptCall returns immediately, and the call then proceeds through callbacks (onMessage, onHalfClose, onClose) that arrive on transport threads the application does not control, possibly long afterwards.

Both interceptors therefore split the span from the thread that holds it:

  • The span is opened without binding — Tracer.openSpanOf(event) — at the point where the trace

identity is known. On the client that is the calling thread, so the outbound call becomes a child of the request being served; on the server there is nothing above it, so it is a root.

  • Every callback runs inside Tracer.reenter(span, …), which re-establishes the span for whatever

the callback does and records a jeffrey.TraceScope event saying which thread it ran on. Work your handler does in a callback still nests under the call.

  • The exchange is committed from onClose/close, the one callback that always arrives — for a

success and for a failure alike.

You get this for free by using the module. It is the reason to use it rather than write your own.

3. What it records

FieldServerClient
span namepackage.Service/Methodpackage.Service/Method
kindSERVER (trace root)CLIENT (leaf)
service / methodfrom the MethodDescriptorfrom the MethodDescriptor
authority—the channel's target authority
statusCodethe gRPC status name (OK, INTERNAL, …)the same
requestSize / responseSizeserialized protobuf sizesserialized protobuf sizes

Status handling escalates only: a call that already recorded a failure is not talked back down to OK by a later status. A Status carrying a cause is recorded with failed(cause), so the span shows red and carries the exception type.

Message sizes are read from MessageLite#getSerializedSize when the message is a protobuf, which is every generated stub. A non-protobuf marshaller records size 0 rather than guessing.

4. gRPC-specific pitfalls

SymptomCauseFix
Server spans present, client spans missingthe client interceptor is not registered — they are two separate registrations.intercept(new JfrGrpcClientInterceptor()) on the channel builder
Outbound calls appear as roots of their own tracesthe call was made outside any bound span (a scheduler thread, an @Async method)wrap the caller with Tracer.run, or hand the context over with Tracer.fork/continueIn
Streaming calls measured as ~0 msa hand-written interceptor that committed from interceptCall instead of onCloseuse the module; it commits from the closing callback
Two events per callthe interceptor registered both globally and per-serviceregister once — globally on the server, once per channel on the client
Handler work not nested under the callthe handler dispatched to an executor without carrying the contextTracer.propagating(executor), or Tracer.fork at the hand-off

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/petrbouda/jeffrey/jeffrey-traces-grpc/badge)](https://verified-skill.com/skills/petrbouda/jeffrey/jeffrey-traces-grpc)

Something wrong with this skill or its page? Tell us.