Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/asciidoc/modules/gRPC.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,52 @@ import io.jooby.kt.Kooby
<1> Enable HTTP/2 on your server.
<2> Install the module and explicitly register your services.

=== Configuration

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.

Use the `withServer` and `withChannel` callbacks to hook directly into the builders before the server starts:

[source, java, role="primary"]
.Java
----
import io.jooby.Jooby;
import io.jooby.grpc.GrpcModule;

{
install(new GrpcModule(new GreeterService())
.withServer(server -> { // <1>
server.maxInboundMessageSize(1024 * 1024 * 20); // 20MB limit
})
.withChannel(channel -> { // <2>
channel.intercept(new MyCustomClientInterceptor());
})
);
}
----

[source, kotlin, role="secondary"]
.Kotlin
----
import io.jooby.grpc.GrpcModule
import io.jooby.kt.Kooby

{
install(GrpcModule(GreeterService())
.withServer { server -> // <1>
server.maxInboundMessageSize(1024 * 1024 * 20) // 20MB limit
}
.withChannel { channel -> // <2>
channel.intercept(MyCustomClientInterceptor())
}
)
}
----
<1> Customize the internal gRPC server (e.g., adjust max message sizes, add server-side interceptors).
<2> Customize the internal loopback channel (e.g., add client-side interceptors for context propagation).

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.

=== Dependency Injection

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).
Expand Down
49 changes: 49 additions & 0 deletions docs/asciidoc/modules/opentelemetry.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,55 @@ import io.jooby.opentelemetry.instrumentation.OtelDbScheduler
}
----

==== gRPC

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.

Required dependency:
[dependency, groupId="io.opentelemetry.instrumentation", artifactId="opentelemetry-grpc-1.6", version="${otel-instrumentation.version}"]
.

.gRPC Integration
[source, java, role = "primary"]
----
import io.jooby.grpc.GrpcModule;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry;

{
install(new OtelModule());

var grpcTelemetry = GrpcTelemetry.create(require(OpenTelemetry.class));

install(new GrpcModule(new GreeterService()
.withServer(server -> server.intercept(grpcTelemetry.newServerInterceptor())) // <1>
.withChannel(channel -> channel.intercept(grpcTelemetry.newClientInterceptor())) // <2>
));
}
----

.Kotlin
[source, kt, role="secondary"]
----
import io.jooby.grpc.GrpcModule
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry

{
install(OtelModule())

val grpcTelemetry = GrpcTelemetry.create(require(OpenTelemetry::class.java))

install(GrpcModule(GreeterService())
.withServer { server -> server.intercept(grpcTelemetry.newServerInterceptor()) } // <1>
.withChannel { channel -> channel.intercept(grpcTelemetry.newClientInterceptor()) } // <2>
)
}
----

<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.
<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.

==== HikariCP

Instruments all registered `HikariDataSource` instances to export critical pool metrics (active/idle connections, timeouts).
Expand Down
38 changes: 37 additions & 1 deletion modules/jooby-grpc/src/main/java/io/jooby/grpc/GrpcModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@
public class GrpcModule implements Extension {
private final List<BindableService> services = new ArrayList<>();
private final List<Class<? extends BindableService>> serviceClasses = new ArrayList<>();
private SneakyThrows.Consumer<InProcessServerBuilder> serverCustomizer;
private SneakyThrows.Consumer<InProcessChannelBuilder> channelCustomizer;

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

/**
* Customizes the in-process gRPC server using the provided server customizer. This method accepts
* a consumer that applies custom settings to an {@code InProcessServerBuilder} instance.
*
* @param serverCustomizer a consumer to customize the {@code InProcessServerBuilder}.
* @return this {@code GrpcModule} instance for method chaining.
*/
public GrpcModule withServer(SneakyThrows.Consumer<InProcessServerBuilder> serverCustomizer) {
this.serverCustomizer = serverCustomizer;
return this;
}

/**
* Configures the gRPC channel using a consumer that applies custom settings to an {@code
* InProcessChannelBuilder} instance.
*
* @param channelConsumer a consumer to customize the {@code InProcessChannelBuilder}.
* @return this {@code GrpcModule} instance for method chaining.
*/
public GrpcModule withChannel(SneakyThrows.Consumer<InProcessChannelBuilder> channelConsumer) {
this.channelCustomizer = channelConsumer;
return this;
}

/**
* Installs the gRPC extension into the Jooby application. *
*
Expand Down Expand Up @@ -142,12 +168,22 @@ public void install(Jooby app) throws Exception {
var service = app.require(serviceClass);
bindService(app, builder, registry, service);
}
// Sync both
builder.maxInboundMessageSize(app.getServerOptions().getMaxRequestSize());
builder.maxInboundMetadataSize(app.getServerOptions().getMaxRequestSize());
if (serverCustomizer != null) {
serverCustomizer.accept(builder);
}
var grpcServer = builder.build().start();

// KEEP .directExecutor() here!
// This ensures that when the background gRPC worker finishes, it instantly pushes
// the response back to Undertow/Netty without wasting time on another thread hop.
var channel = InProcessChannelBuilder.forName(serverName).directExecutor().build();
var channelBuilder = InProcessChannelBuilder.forName(serverName).directExecutor();
if (channelCustomizer != null) {
channelCustomizer.accept(channelBuilder);
}
var channel = channelBuilder.build();
processor.setChannel(channel);

app.onStop(channel::shutdownNow);
Expand Down
Loading