diff --git a/docs/asciidoc/modules/gRPC.adoc b/docs/asciidoc/modules/gRPC.adoc index e96a4aeedf..cc9909efb9 100644 --- a/docs/asciidoc/modules/gRPC.adoc +++ b/docs/asciidoc/modules/gRPC.adoc @@ -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). diff --git a/docs/asciidoc/modules/opentelemetry.adoc b/docs/asciidoc/modules/opentelemetry.adoc index d06e6fc9b4..52828f5def 100644 --- a/docs/asciidoc/modules/opentelemetry.adoc +++ b/docs/asciidoc/modules/opentelemetry.adoc @@ -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). diff --git a/modules/jooby-grpc/src/main/java/io/jooby/grpc/GrpcModule.java b/modules/jooby-grpc/src/main/java/io/jooby/grpc/GrpcModule.java index eda3d86def..5f3623ccfa 100644 --- a/modules/jooby-grpc/src/main/java/io/jooby/grpc/GrpcModule.java +++ b/modules/jooby-grpc/src/main/java/io/jooby/grpc/GrpcModule.java @@ -73,6 +73,8 @@ public class GrpcModule implements Extension { private final List services = new ArrayList<>(); private final List> serviceClasses = new ArrayList<>(); + private SneakyThrows.Consumer serverCustomizer; + private SneakyThrows.Consumer channelCustomizer; static { // Optionally remove existing handlers attached to the j.u.l root logger @@ -111,6 +113,30 @@ public final GrpcModule bind(Class... 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 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 channelConsumer) { + this.channelCustomizer = channelConsumer; + return this; + } + /** * Installs the gRPC extension into the Jooby application. * * @@ -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);