Skip to content

Commit 0a18045

Browse files
authored
Merge pull request #3913 from jooby-project/3912
gRPC: customizer + open-telemetry hook
2 parents f18e69e + 7737a54 commit 0a18045

3 files changed

Lines changed: 132 additions & 1 deletion

File tree

docs/asciidoc/modules/gRPC.adoc

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,52 @@ import io.jooby.kt.Kooby
4444
<1> Enable HTTP/2 on your server.
4545
<2> Install the module and explicitly register your services.
4646

47+
=== Configuration
48+
49+
You can customize the underlying `InProcessServerBuilder` and `InProcessChannelBuilder` used by the module to apply advanced gRPC configurations. This is particularly useful for registering global interceptors (like OpenTelemetry traces), adjusting payload limits, or tweaking executor settings.
50+
51+
Use the `withServer` and `withChannel` callbacks to hook directly into the builders before the server starts:
52+
53+
[source, java, role="primary"]
54+
.Java
55+
----
56+
import io.jooby.Jooby;
57+
import io.jooby.grpc.GrpcModule;
58+
59+
{
60+
install(new GrpcModule(new GreeterService())
61+
.withServer(server -> { // <1>
62+
server.maxInboundMessageSize(1024 * 1024 * 20); // 20MB limit
63+
})
64+
.withChannel(channel -> { // <2>
65+
channel.intercept(new MyCustomClientInterceptor());
66+
})
67+
);
68+
}
69+
----
70+
71+
[source, kotlin, role="secondary"]
72+
.Kotlin
73+
----
74+
import io.jooby.grpc.GrpcModule
75+
import io.jooby.kt.Kooby
76+
77+
{
78+
install(GrpcModule(GreeterService())
79+
.withServer { server -> // <1>
80+
server.maxInboundMessageSize(1024 * 1024 * 20) // 20MB limit
81+
}
82+
.withChannel { channel -> // <2>
83+
channel.intercept(MyCustomClientInterceptor())
84+
}
85+
)
86+
}
87+
----
88+
<1> Customize the internal gRPC server (e.g., adjust max message sizes, add server-side interceptors).
89+
<2> Customize the internal loopback channel (e.g., add client-side interceptors for context propagation).
90+
91+
NOTE: **Size Limits:** By default, Jooby automatically sets the gRPC server's `maxInboundMessageSize` and `maxInboundMetadataSize` to match your web server's `server.maxRequestSize` property (which defaults to `10mb`). If you manually increase these limits on the gRPC server builder, you **must** also increase `server.maxRequestSize`. If an incoming gRPC payload or metadata exceeds the configured web server limit, the request will be rejected before it ever reaches the gRPC layer.
92+
4793
=== Dependency Injection
4894

4995
If your gRPC services require external dependencies (like database repositories), you can register the service classes instead of pre-instantiated objects. The module will automatically provision them using your active Dependency Injection framework (e.g., Guice, Spring).

docs/asciidoc/modules/opentelemetry.adoc

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,55 @@ import io.jooby.opentelemetry.instrumentation.OtelDbScheduler
196196
}
197197
----
198198

199+
==== gRPC
200+
201+
Provides automatic tracing, metrics, and context propagation for gRPC services. It instruments both the embedded `grpc-java` server and loopback channels to ensure seamless distributed traces across your application.
202+
203+
Required dependency:
204+
[dependency, groupId="io.opentelemetry.instrumentation", artifactId="opentelemetry-grpc-1.6", version="${otel-instrumentation.version}"]
205+
.
206+
207+
.gRPC Integration
208+
[source, java, role = "primary"]
209+
----
210+
import io.jooby.grpc.GrpcModule;
211+
import io.opentelemetry.api.OpenTelemetry;
212+
import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry;
213+
214+
{
215+
install(new OtelModule());
216+
217+
var grpcTelemetry = GrpcTelemetry.create(require(OpenTelemetry.class));
218+
219+
install(new GrpcModule(new GreeterService()
220+
.withServer(server -> server.intercept(grpcTelemetry.newServerInterceptor())) // <1>
221+
.withChannel(channel -> channel.intercept(grpcTelemetry.newClientInterceptor())) // <2>
222+
));
223+
}
224+
----
225+
226+
.Kotlin
227+
[source, kt, role="secondary"]
228+
----
229+
import io.jooby.grpc.GrpcModule
230+
import io.opentelemetry.api.OpenTelemetry
231+
import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry
232+
233+
{
234+
install(OtelModule())
235+
236+
val grpcTelemetry = GrpcTelemetry.create(require(OpenTelemetry::class.java))
237+
238+
install(GrpcModule(GreeterService())
239+
.withServer { server -> server.intercept(grpcTelemetry.newServerInterceptor()) } // <1>
240+
.withChannel { channel -> channel.intercept(grpcTelemetry.newClientInterceptor()) } // <2>
241+
)
242+
}
243+
----
244+
245+
<1> **`newServerInterceptor()`:** Extracts the distributed trace context from incoming gRPC metadata. It creates a dedicated child span for the specific gRPC method execution, automatically recording its duration and status code when the call completes.
246+
<2> **`newClientInterceptor()`:** Grabs the active trace context (typically started by Jooby's underlying HTTP router) and injects it into the outgoing metadata on the internal loopback channel. This bridges the gap between the HTTP pipeline and the gRPC engine, ensuring a single, unbroken distributed trace.
247+
199248
==== HikariCP
200249

201250
Instruments all registered `HikariDataSource` instances to export critical pool metrics (active/idle connections, timeouts).

modules/jooby-grpc/src/main/java/io/jooby/grpc/GrpcModule.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
public class GrpcModule implements Extension {
7474
private final List<BindableService> services = new ArrayList<>();
7575
private final List<Class<? extends BindableService>> serviceClasses = new ArrayList<>();
76+
private SneakyThrows.Consumer<InProcessServerBuilder> serverCustomizer;
77+
private SneakyThrows.Consumer<InProcessChannelBuilder> channelCustomizer;
7678

7779
static {
7880
// Optionally remove existing handlers attached to the j.u.l root logger
@@ -111,6 +113,30 @@ public final GrpcModule bind(Class<? extends BindableService>... serviceClasses)
111113
return this;
112114
}
113115

116+
/**
117+
* Customizes the in-process gRPC server using the provided server customizer. This method accepts
118+
* a consumer that applies custom settings to an {@code InProcessServerBuilder} instance.
119+
*
120+
* @param serverCustomizer a consumer to customize the {@code InProcessServerBuilder}.
121+
* @return this {@code GrpcModule} instance for method chaining.
122+
*/
123+
public GrpcModule withServer(SneakyThrows.Consumer<InProcessServerBuilder> serverCustomizer) {
124+
this.serverCustomizer = serverCustomizer;
125+
return this;
126+
}
127+
128+
/**
129+
* Configures the gRPC channel using a consumer that applies custom settings to an {@code
130+
* InProcessChannelBuilder} instance.
131+
*
132+
* @param channelConsumer a consumer to customize the {@code InProcessChannelBuilder}.
133+
* @return this {@code GrpcModule} instance for method chaining.
134+
*/
135+
public GrpcModule withChannel(SneakyThrows.Consumer<InProcessChannelBuilder> channelConsumer) {
136+
this.channelCustomizer = channelConsumer;
137+
return this;
138+
}
139+
114140
/**
115141
* Installs the gRPC extension into the Jooby application. *
116142
*
@@ -142,12 +168,22 @@ public void install(Jooby app) throws Exception {
142168
var service = app.require(serviceClass);
143169
bindService(app, builder, registry, service);
144170
}
171+
// Sync both
172+
builder.maxInboundMessageSize(app.getServerOptions().getMaxRequestSize());
173+
builder.maxInboundMetadataSize(app.getServerOptions().getMaxRequestSize());
174+
if (serverCustomizer != null) {
175+
serverCustomizer.accept(builder);
176+
}
145177
var grpcServer = builder.build().start();
146178

147179
// KEEP .directExecutor() here!
148180
// This ensures that when the background gRPC worker finishes, it instantly pushes
149181
// the response back to Undertow/Netty without wasting time on another thread hop.
150-
var channel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
182+
var channelBuilder = InProcessChannelBuilder.forName(serverName).directExecutor();
183+
if (channelCustomizer != null) {
184+
channelCustomizer.accept(channelBuilder);
185+
}
186+
var channel = channelBuilder.build();
151187
processor.setChannel(channel);
152188

153189
app.onStop(channel::shutdownNow);

0 commit comments

Comments
 (0)