curl --location --request GET 'https://launch.micronaut.io/create/grpc/demo?lang=JAVA&build=GRADLE' --output demo.zip
unzip demo.zip -d demo
cd demo
Micronaut gRPC
Integration between Micronaut and gRPC
Version: 5.2.1-SNAPSHOT
1 Introduction
This project allows building gRPC servers and clients with Micronaut.
Micronaut adds the following features to the gRPC experience:
-
Compile Time Dependency Injection (DI) and Aspect Oriented Programming (AOP)
-
Service Discovery and Registrations
-
Distributed Tracing
-
Cloud Native Configuration
2 Release History
For this project, you can find a list of releases (with release notes) here:
3 Getting Started
To get started you need first create a Micronaut project. The easiest way to do this is with the Micronaut Launch:
-
Go to Micronaut Launch
-
Select "gRPC Application" under "Application Type"
-
Choose a Language / Build System etc.
-
Click Generate
Replace java with kotlin or groovy to change language and the build flag with maven to use Maven instead.
|
Or alternatively you can create a project with curl:
To manually setup gRPC you can create an application:
$ mn create-app helloworld
Then follow the below steps depending on the build system chosen.
Configuring Gradle
To configure Gradle, first apply the com.google.protobuf plugin:
plugins {
...
alias(libs.plugins.protobuf)
}
Then configure the gRPC and protobuf plugins:
ext {
grpcVersion = libs.versions.managed.grpc.asProvider().get()
protobufVersion = libs.versions.managed.protobuf.asProvider().get()
}
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/java'
}
}
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" }
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
If you are using JDK9 or above, in order to avoid issues javax packages, provide the following compiler option to the grpc plugin to skip these stubs: option '@generated=omit' (added on grpc-kava v1.64.0) See discussion here: issue 9179
|
...
generateProtoTasks {
all()*.plugins {
grpc {
option '@generated=omit'
} }
}
Use this configuration for Kotlin projects:
ext {
grpcVersion = libs.versions.managed.grpc.asProvider().get()
grpcKotlinVersion = libs.versions.managed.grpc.kotlin.get()
protobufVersion = libs.versions.managed.protobuf.asProvider().get()
}
dependencies {
...
implementation libs.managed.grpc.kotlin.stub
implementation libs.managed.grpc.services
compileOnly libs.managed.grpc.stub
compileOnly libs.managed.protobuf.java
compileOnly libs.javax.annotation.api
}
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/grpckt'
srcDirs 'build/generated/source/proto/main/java'
srcDirs 'build/generated/source/proto/main/reactor'
}
}
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" }
grpckt { artifact = "io.grpc:protoc-gen-grpc-kotlin:${grpcKotlinVersion}:jdk8@jar" }
reactor { artifact = "com.salesforce.servicelibs:reactor-grpc:$reactiveGrpcVersion" }
}
generateProtoTasks {
all()*.plugins {
grpc {}
grpckt {}
reactor {}
}
}
}
If you want to generate Reactor-based stubs with reactive-grpc, add the generator plugin and stub dependency alongside the standard gRPC code generation:
ext {
reactiveGrpcVersion = libs.versions.reactive.grpc.get()
}
dependencies {
...
implementation platform(libs.micronaut.reactor)
implementation libs.reactive.grpc.stub
implementation "io.projectreactor:reactor-core"
}
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/reactor'
}
}
}
protobuf {
...
reactor { artifact = "com.salesforce.servicelibs:reactor-grpc:$reactiveGrpcVersion" }
...
reactor {}
}
Finally, add the following dependencies to your build:
For gRPC servers:
implementation("io.micronaut.grpc:micronaut-grpc-server-runtime")
<dependency>
<groupId>io.micronaut.grpc</groupId>
<artifactId>micronaut-grpc-server-runtime</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
"io.micronaut.grpc:micronaut-grpc-server-runtime",
]
For gRPC clients:
implementation("io.micronaut.grpc:micronaut-grpc-client-runtime")
<dependency>
<groupId>io.micronaut.grpc</groupId>
<artifactId>micronaut-grpc-client-runtime</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
"io.micronaut.grpc:micronaut-grpc-client-runtime",
]
If you wish to use gRPC standalone without the Micronaut HTTP server you should comment out the micronaut-http-server-netty dependency.
|
You can then run:
$ ./gradlew generateProto
To generate the Java sources from protobuf definitions in src/main/proto.
Configuring Maven
For Maven create a maven project first:
$ mn create-app helloworld --build
Then configure the Protobuf plugin appropriately:
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<goals>
<goal>test-compile</goal>
<goal>test-compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
You can then run:
$ ./mvnw generate-sources
To generate the Java sources from protobuf definitions in src/main/proto.
Defining a Protobuf File
Once you have the build setup you can define a Protobuf file for your gRPC service. For example:
// Copyright 2015 The gRPC Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
With the Micronaut 1.1 or above CLI you can generate a service with mn create-grpc-service helloworld which will create the proto file and class that implements the stub.
|
4 gRPC Server
Writing a Simple gRPC Server
To implement a gRPC server for the previously defined helloworld.proto definition you first need to generate the Java stubs using Gradle or Maven then create a class that extends from GreeterGrpc.GreeterImplBase.
For example:
import io.grpc.stub.StreamObserver;
import jakarta.inject.Singleton;
@Singleton
public class GreetingEndpoint extends GreeterGrpc.GreeterImplBase { // (1)
private final GreetingService greetingService;
// (2)
public GreetingEndpoint(GreetingService greetingService) {
this.greetingService = greetingService;
}
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
// (3)
final String message = greetingService.sayHello(request.getName());
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
import java
from io.grpc.stub import StreamObserver
from jakarta.inject import Singleton
from .GreetingService import GreetingService
# `helloworld` is both the Java package of the generated gRPC classes and the package of these
# Python sources, so the generated types are looked up by name instead of imported.
GreeterGrpc = java.type("helloworld.GreeterGrpc")
HelloReply = java.type("helloworld.HelloReply")
HelloRequest = java.type("helloworld.HelloRequest")
@Singleton
class GreetingEndpoint(GreeterGrpc.GreeterImplBase): # (1)
# (2)
def __init__(self, greeting_service: GreetingService):
self.greeting_service = greeting_service
def sayHello(self, request: HelloRequest, response_observer: StreamObserver[HelloReply]) -> None:
# (3)
message = self.greeting_service.say_hello(request.getName())
reply = HelloReply.newBuilder().setMessage(message).build()
response_observer.onNext(reply)
response_observer.onCompleted()
import jakarta.inject.Singleton
@Singleton // (1)
class GreetingEndpoint(val greetingService: GreetingService) : GreeterGrpcKt.GreeterCoroutineImplBase() { // (2)
override suspend fun sayHello(request: HelloRequest): HelloReply {
// (3)
val message = greetingService.sayHello(request.name)
val reply = HelloReply.newBuilder().setMessage(message).build()
return reply
}
}
import groovy.transform.CompileStatic
import io.grpc.stub.StreamObserver
import jakarta.inject.Singleton
@CompileStatic
@Singleton
class GreetingEndpoint extends GreeterGrpc.GreeterImplBase { // (1)
final GreetingService greetingService
// (2)
GreetingEndpoint(GreetingService greetingService) {
this.greetingService = greetingService
}
@Override
void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
// (3)
HelloReply.newBuilder().with {
message = greetingService.sayHello(request.name)
responseObserver.onNext(build())
responseObserver.onCompleted()
}
}
}
| 1 | The class extends from GreeterGrpc.GreeterImplBase and is annotated with jakarta.inject.Singleton |
| 2 | You can dependency inject other beans into the implementation. In this case GreetingService is dependency injected. |
| 3 | The StreamObserver is used to send a response to the client. |
If you generate Reactor bindings with reactive-grpc, Micronaut can register the generated BindableService implementation in exactly the same way. Implement the generated Reactor base class as a singleton bean:
@Singleton
public class ReactiveGreetingEndpoint extends ReactorReactiveGreeterGrpc.ReactiveGreeterImplBase {
private final GreetingService greetingService;
public ReactiveGreetingEndpoint(GreetingService greetingService) {
this.greetingService = greetingService;
}
@Override
public Mono<HelloReply> sayHello(Mono<HelloRequest> request) {
return request.map(helloRequest ->
HelloReply.newBuilder()
.setMessage(greetingService.sayHello(helloRequest.getName()))
.build()
);
}
}
@Singleton
class ReactiveGreetingEndpoint(ReactorReactiveGreeterGrpc.ReactiveGreeterImplBase):
def __init__(self, greeting_service: GreetingService):
self.greeting_service = greeting_service
def sayHello(self, request: Mono[HelloRequest]) -> Mono[HelloReply]:
return request.map(
lambda hello_request: HelloReply.newBuilder()
.setMessage(self.greeting_service.say_hello(hello_request.getName()))
.build()
)
@Singleton
class ReactiveGreetingEndpoint(private val greetingService: GreetingService) : ReactorReactiveGreeterGrpc.ReactiveGreeterImplBase() {
override fun sayHello(request: Mono<HelloRequest>): Mono<HelloReply> =
request.map { helloRequest ->
HelloReply.newBuilder()
.setMessage(greetingService.sayHello(helloRequest.name))
.build()
}
}
@CompileStatic
@Singleton
class ReactiveGreetingEndpoint extends ReactorReactiveGreeterGrpc.ReactiveGreeterImplBase {
private final GreetingService greetingService
ReactiveGreetingEndpoint(GreetingService greetingService) {
this.greetingService = greetingService
}
@Override
Mono<HelloReply> sayHello(Mono<HelloRequest> request) {
request.map(helloRequest ->
HelloReply.newBuilder()
.setMessage(greetingService.sayHello(helloRequest.name))
.build()
)
}
}
Running the gRPC Server
To run the server use the Application class or run ./gradlew run for Gradle or ./mvnw compile exec:exec for Maven.
The server by default runs on port 50051, however you can configure which port the server runs on by setting grpc.server.port to whichever value you wish (a value of ${random.port} will use a random port).
Configuring the gRPC Server
The server can be configured in a number of different ways. By default, Micronaut configures gRPC’s NettyServerBuilder via application.yml.
For example:
grpc.server.port=8080
grpc.server.keep-alive-time=3h
grpc.server.max-inbound-message-size=1024
grpc.server.ssl.cert-chain=file://path/to/my.cert
grpc.server.ssl.private-key=classpath:my.key
grpc:
server:
port: 8080
keep-alive-time: 3h
max-inbound-message-size: 1024
ssl:
cert-chain: 'file://path/to/my.cert' # (1)
private-key: 'classpath:my.key' # (2)
[grpc.server]
port = 8080
keep-alive-time = "3h"
max-inbound-message-size = 1024
[grpc.server.ssl]
cert-chain = "file://path/to/my.cert"
private-key = "classpath:my.key"
grpc {
server {
port = 8080
keepAliveTime = "3h"
maxInboundMessageSize = 1024
ssl {
certChain = "file://path/to/my.cert"
privateKey = "classpath:my.key"
}
}
}
{
grpc {
server {
port = 8080
keep-alive-time = "3h"
max-inbound-message-size = 1024
ssl {
cert-chain = "file://path/to/my.cert"
private-key = "classpath:my.key"
}
}
}
}
{
"grpc": {
"server": {
"port": 8080,
"keep-alive-time": "3h",
"max-inbound-message-size": 1024,
"ssl": {
"cert-chain": "file://path/to/my.cert",
"private-key": "classpath:my.key"
}
}
}
}
| 1 | Load the certificate from /path/to/my.cert file. |
| 2 | Load the private key from the classpath. The file should be in src/main/resources/my.key. |
By default, the gRPC server will be enabled. To disable the gRPC server, set grpc.server.enabled to false.
For tests, add the io.micronaut.grpc:micronaut-grpc-inprocess module and set grpc.server.in-process-name to switch the embedded server and the injected @GrpcChannel("grpc-server") test channel to gRPC’s in-process transport instead of opening a TCP port.
grpc.server.in-process-name=test-grpc
grpc:
server:
in-process-name: test-grpc
[grpc.server]
in-process-name = "test-grpc"
grpc {
server {
inProcessName = "test-grpc"
}
}
{
grpc {
server {
in-process-name = "test-grpc"
}
}
}
{
"grpc": {
"server": {
"in-process-name": "test-grpc"
}
}
}
The in-process transport avoids port management and is managed by the Micronaut application lifecycle. SSL is not supported in this mode, and the support lives in the optional micronaut-grpc-inprocess module so the Netty-based server runtime does not require the extra dependency.
Alternatively if you prefer programmatic configuration you can write a BeanCreatedEventListener for example:
import io.grpc.ServerBuilder;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import jakarta.inject.Singleton;
@Singleton
public class ServerBuilderListener implements BeanCreatedEventListener<ServerBuilder<?>> {
@Override
public ServerBuilder<?> onCreated(BeanCreatedEvent<ServerBuilder<?>> event) {
final ServerBuilder<?> builder = event.getBean();
builder.maxInboundMessageSize(1024);
return builder;
}
}
from io.grpc import ServerBuilder
from jakarta.inject import Singleton
from micronaut.context.event import BeanCreatedEvent, BeanCreatedEventListener
@Singleton
class ServerBuilderListener(BeanCreatedEventListener[ServerBuilder]):
def onCreated(self, event: BeanCreatedEvent[ServerBuilder]) -> ServerBuilder:
builder = event.getBean()
builder.maxInboundMessageSize(1024)
return builder
import io.grpc.ServerBuilder
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton
@Singleton
class ServerBuilderListener : BeanCreatedEventListener<ServerBuilder<*>> {
override fun onCreated(event: BeanCreatedEvent<ServerBuilder<*>>): ServerBuilder<*> {
val builder = event.bean
builder.maxInboundMessageSize(1024)
return builder
}
}
import groovy.transform.CompileStatic
import io.grpc.ServerBuilder
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton
@CompileStatic
@Singleton
class ServerBuilderListener implements BeanCreatedEventListener<ServerBuilder<?>> {
@Override
ServerBuilder<?> onCreated(BeanCreatedEvent<ServerBuilder<?>> event) {
final ServerBuilder<?> builder = event.bean
builder.maxInboundMessageSize(1024)
builder
}
}
Auto Injected Types
By default, the server will automatically be dependency injected with beans of the following types:
-
io.grpc.BindableService- Any services declared as beans -
io.grpc.ServerInterceptor- Any interceptors declared as beans -
io.grpc.ServerTransportFilter- Any transport filters declared as beans
In addition, by default the server will be setup to use Micronaut’s I/O executor service.
Set grpc.server.executor to the name of an Executor bean to override that default from configuration.
Server Interceptor Ordering
To produce a consistent and predictable order of execution for server interceptors, it is required
for the io.grpc.ServerInterceptor implementation to do one of the following:
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.micronaut.core.order.Ordered;
import jakarta.inject.Singleton;
@Singleton // (1)
public class CustomInterceptor implements ServerInterceptor, Ordered { // (2)
@Override
public <T, R> ServerCall.Listener<T> interceptCall(ServerCall<T, R> call,
Metadata headers,
ServerCallHandler<T, R> next) {
return next.startCall(call, headers);
}
@Override
public int getOrder() {
return 10; // (3)
}
}
from io.grpc import Metadata, ServerCall, ServerCallHandler, ServerInterceptor
from jakarta.inject import Singleton
from micronaut.core.order import Ordered
@Singleton # (1)
class CustomInterceptor(ServerInterceptor, Ordered): # (2)
def interceptCall[T, R](
self,
call: ServerCall[T, R],
headers: Metadata,
next: ServerCallHandler[T, R],
) -> ServerCall.Listener[T]:
return next.startCall(call, headers)
def getOrder(self) -> int:
return 10 # (3)
import io.grpc.Metadata
import io.grpc.ServerCall
import io.grpc.ServerCallHandler
import io.grpc.ServerInterceptor
import io.micronaut.core.order.Ordered
import jakarta.inject.Singleton
@Singleton // (1)
class CustomInterceptor : ServerInterceptor, Ordered { // (2)
override fun <T, R> interceptCall(
call: ServerCall<T, R>,
headers: Metadata,
next: ServerCallHandler<T, R>,
): ServerCall.Listener<T> {
return next.startCall(call, headers)
}
override fun getOrder(): Int {
return 10 // (3)
}
}
import groovy.transform.CompileStatic
import io.grpc.Metadata
import io.grpc.ServerCall
import io.grpc.ServerCallHandler
import io.grpc.ServerInterceptor
import io.micronaut.core.order.Ordered
import jakarta.inject.Singleton
@CompileStatic
@Singleton // (1)
class CustomInterceptor implements ServerInterceptor, Ordered { // (2)
@Override
<T, R> ServerCall.Listener<T> interceptCall(ServerCall<T, R> call,
Metadata headers,
ServerCallHandler<T, R> next) {
next.startCall(call, headers)
}
@Override
int getOrder() {
10 // (3)
}
}
| 1 | Declare the server interceptor as a bean to have it registered automatically |
| 2 | Implement Ordered in addition to ServerInterceptor |
| 3 | Provide the specified order of execution in the server interceptor chain |
import io.grpc.ServerInterceptor;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.grpc.server.interceptor.OrderedServerInterceptor;
import jakarta.inject.Singleton;
@Factory // (1)
public class ServerInterceptorFactory {
@Bean // (2)
@Singleton
public ServerInterceptor customServerInterceptor() {
return new OrderedServerInterceptor(new CustomInterceptor(), 10); // (3)
}
}
from io.grpc import ServerInterceptor
from jakarta.inject import Singleton
from micronaut.context.annotation import Bean, Factory
from micronaut.grpc.server.interceptor import OrderedServerInterceptor
from .CustomInterceptor import CustomInterceptor
@Factory # (1)
class ServerInterceptorFactory:
@Bean # (2)
@Singleton
def custom_server_interceptor(self) -> ServerInterceptor:
return OrderedServerInterceptor(CustomInterceptor(), 10) # (3)
import io.grpc.ServerInterceptor
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.grpc.server.interceptor.OrderedServerInterceptor
import jakarta.inject.Singleton
@Factory // (1)
class ServerInterceptorFactory {
@Bean // (2)
@Singleton
fun customServerInterceptor(): ServerInterceptor {
return OrderedServerInterceptor(CustomInterceptor(), 10) // (3)
}
}
import groovy.transform.CompileStatic
import io.grpc.ServerInterceptor
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.grpc.server.interceptor.OrderedServerInterceptor
import jakarta.inject.Singleton
@CompileStatic
@Factory // (1)
class ServerInterceptorFactory {
@Bean // (2)
@Singleton
ServerInterceptor customServerInterceptor() {
new OrderedServerInterceptor(new CustomInterceptor(), 10) // (3)
}
}
| 1 | Use a @Factory to create an instance of ServerInterceptor bean |
| 2 | Register the created sever interceptor as a bean |
| 3 | Wrap the CustomInterceptor with OrderedServerInterceptor and provide the order of execution |
The order which is provided will dictate the order of execution of the interceptors when receiving the request message, and then the order will be reversed when sending the response message.
1, 2, and 3:Request -> 1 -> 2 -> 3 -> business logic -> 3 -> 2 -> 1 -> Response
Health checks
gRPC Health checks
If the gRPC services dependency (io.grpc:grpc-services) is added, then gRPC health checks will be enabled.
To modify the status of a service, call the setStatus method on an instance of HealthServiceManager, for example:
import io.grpc.health.v1.HealthCheckResponse;
import io.grpc.protobuf.services.HealthStatusManager;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import jakarta.inject.Singleton;
@Singleton
public class HealthService {
private final HealthStatusManager healthStatusManager;
public HealthService(@Nullable HealthStatusManager healthStatusManager) {
this.healthStatusManager = healthStatusManager;
}
public void setStatus(@NonNull String serviceName, HealthCheckResponse.@NonNull ServingStatus status) {
if (healthStatusManager != null) {
healthStatusManager.setStatus(serviceName, status);
}
}
}
from io.grpc.health.v1 import HealthCheckResponse
from io.grpc.protobuf.services import HealthStatusManager
from jakarta.inject import Singleton
@Singleton
class HealthService:
def __init__(self, health_status_manager: HealthStatusManager | None):
self.health_status_manager = health_status_manager
def set_status(self, service_name: str, status: HealthCheckResponse.ServingStatus) -> None:
if self.health_status_manager is not None:
self.health_status_manager.setStatus(service_name, status)
import io.grpc.health.v1.HealthCheckResponse.ServingStatus
import io.grpc.protobuf.services.HealthStatusManager
import jakarta.inject.Singleton
@Singleton
class HealthService(private val healthStatusManager: HealthStatusManager?) {
fun setStatus(serviceName: String, status: ServingStatus) {
healthStatusManager?.setStatus(serviceName, status)
}
}
import io.grpc.health.v1.HealthCheckResponse
import io.grpc.protobuf.services.HealthStatusManager
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.annotation.Nullable
import jakarta.inject.Singleton
@Singleton
class HealthService {
private final HealthStatusManager healthStatusManager
HealthService(@Nullable HealthStatusManager healthStatusManager) {
this.healthStatusManager = healthStatusManager
}
void setStatus(@NonNull String serviceName, @NonNull HealthCheckResponse.ServingStatus status) {
healthStatusManager?.setStatus(serviceName, status)
}
}
If you wish to disable the gRPC health check while still using the services dependency you can set the property grpc.server.health.enabled to false in your application configuration.
Management Health checks
If the management dependency (io.micronaut:micronaut-management) is added, then Micronaut’s Health Endpoint can be used to expose the health status of the gRPC server.
For example, if gRPC is running then the /health endpoint will return:
{
"status": "UP",
"details": {
"grpc-server": {
"name": "your-project-name",
"status": "UP",
"details": {
"host": "localhost",
"port": 5050
}
}
},
...
}
If you wish to disable the Micronaut gRPC server health check while still using the management dependency you can set the property grpc.server.health.enabled to false in your application configuration.
Testing the Server
To test the server it is recommended that you use Micronaut Test.
| For detailed instructions on how to set up Micronaut Test for Spock, JUnit 5, or Kotest see the documentation on the subject. |
You can then define a blocking stub bean in your test sources. For example:
@Factory
class Clients {
@Bean
GreeterGrpc.GreeterBlockingStub blockingStub(
@GrpcChannel(GrpcServerChannel.NAME) ManagedChannel channel) { // (1)
return GreeterGrpc.newBlockingStub( // (2)
channel
);
}
}
@Factory
class Clients:
@Bean
def blocking_stub(self, channel: Annotated[ManagedChannel, GrpcChannel(GrpcServerChannel.NAME)]) -> GreeterGrpc.GreeterBlockingStub: # (1)
return GreeterGrpc.newBlockingStub(channel) # (2)
@Factory
class Clients {
@Bean
fun greetingClient(@GrpcChannel(GrpcServerChannel.NAME) channel: ManagedChannel): GreeterGrpcKt.GreeterCoroutineStub = // (1)
GreeterGrpcKt.GreeterCoroutineStub(channel) // (2)
}
@Factory
class Clients {
@Bean
GreeterGrpc.GreeterBlockingStub blockingStub(
@GrpcChannel(GrpcServerChannel.NAME) ManagedChannel channel) { // (1)
GreeterGrpc.newBlockingStub( // (2)
channel
)
}
}
| 1 | A ManagedChannel is injected that can communicate with the server. |
| 2 | The generated gRPC client blocking stub is created. |
The above example uses the @GrpcChannel annotation to inject a gRPC ManagedChannel that can communicate with the running server. This channel will be automatically be shutdown when the application shuts down.
Now that you have a test client, writing the test becomes trivial:
import io.grpc.ManagedChannel;
import io.micronaut.context.annotation.Bean;
import io.micronaut.context.annotation.Factory;
import io.micronaut.grpc.annotation.GrpcChannel;
import io.micronaut.grpc.server.GrpcServerChannel;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import jakarta.inject.Inject;
import static org.junit.jupiter.api.Assertions.assertEquals;
@MicronautTest // (1)
class GreetingEndpointTest {
@Inject
GreeterGrpc.GreeterBlockingStub blockingStub; // (2)
@Test
void testHelloWorld() {
final HelloRequest request = HelloRequest.newBuilder() // (3)
.setName("Fred")
.build();
assertEquals(
"Hello Fred",
blockingStub.sayHello(request)
.getMessage()
);
}
}
import java
from typing import Annotated
from io.grpc import ManagedChannel
from jakarta.inject import Inject
from micronaut.context.annotation import Bean, Factory
from micronaut.grpc.annotation import GrpcChannel
from micronaut.grpc.server import GrpcServerChannel
from micronaut.test.extensions.junit5.annotation import MicronautTest
from org.junit.jupiter.api import Test
# `helloworld` is both the Java package of the generated gRPC classes and the package of these
# Python sources, so the generated types are looked up by name instead of imported.
GreeterGrpc = java.type("helloworld.GreeterGrpc")
HelloRequest = java.type("helloworld.HelloRequest")
@MicronautTest # (1)
class GreetingEndpointTest:
blocking_stub: Annotated[GreeterGrpc.GreeterBlockingStub, Inject] # (2)
@Test
def test_hello_world(self):
request = HelloRequest.newBuilder().setName("Fred").build() # (3)
assert self.blocking_stub.sayHello(request).getMessage() == "Hello Fred"
import io.grpc.ManagedChannel
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.grpc.annotation.GrpcChannel
import io.micronaut.grpc.server.GrpcServerChannel
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest
@MicronautTest // (1)
class GreetingEndpointTest(
private val greetingClient: GreeterGrpcKt.GreeterCoroutineStub, // (2)
) : StringSpec({
"returns a greeting response" {
greetingClient.sayHello( // (3)
HelloRequest.newBuilder().setName("Fred").build(),
).message shouldBe "Hello Fred"
}
})
import io.grpc.ManagedChannel
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import io.micronaut.grpc.annotation.GrpcChannel
import io.micronaut.grpc.server.GrpcServerChannel
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification
@MicronautTest // (1)
class GreetingEndpointTest extends Specification {
@Inject
GreeterGrpc.GreeterBlockingStub blockingStub // (2)
void "test greeting endpoint"() {
given:
HelloRequest request = HelloRequest.newBuilder().with { // (3)
name = "Fred"
build()
}
expect:
blockingStub.sayHello(
request
).message == 'Hello Fred'
}
}
| 1 | The test is annotated with @MicronautTest |
| 2 | The client stub is injected into the test |
| 3 | A request is sent and the response asserted. |
5 gRPC Clients
Micronaut for gRPC does not create client beans automatically for you. Instead, you must expose which client stubs your application needs using a @Factory.
You can dependency inject a io.grpc.ManagedChannel into the factory. Each injected io.grpc.ManagedChannel will automatically be shutdown when the application shuts down.
Configuring ManagedChannel Instances
The channel can be configured using properties defined under grpc.client by default.
For example, if you wish to disable secure communication:
grpc.client.plaintext=true
grpc.client.max-retry-attempts=10
grpc:
client:
plaintext: true
max-retry-attempts: 10
[grpc.client]
plaintext = true
max-retry-attempts = 10
grpc {
client {
plaintext = true
maxRetryAttempts = 10
}
}
{
grpc {
client {
plaintext = true
max-retry-attempts = 10
}
}
}
{
"grpc": {
"client": {
"plaintext": true,
"max-retry-attempts": 10
}
}
}
Properties under grpc.client are global properties and are the defaults used unless named configuration exists under grpc.channels.[NAME].
Any property of the io.grpc.netty.NettyChannelBuilder type can be configured.
You can also disable all gRPC client channel injection, or disable a specific named client:
grpc.client.enabled=false
grpc:
client:
enabled: false
[grpc.client]
enabled = false
grpc {
client {
enabled = false
}
}
{
grpc {
client {
enabled = false
}
}
}
{
"grpc": {
"client": {
"enabled": false
}
}
}
grpc.client.greeter.enabled=false
grpc:
client:
greeter:
enabled: false
[grpc.client.greeter]
enabled = false
grpc {
client {
greeter {
enabled = false
}
}
}
{
grpc {
client {
greeter {
enabled = false
}
}
}
}
{
"grpc": {
"client": {
"greeter": {
"enabled": false
}
}
}
}
When a client is disabled, optional or nullable @GrpcChannel injection points resolve to an empty value, while required injection points fail with a dependency injection error.
Alternatively if you prefer programmatic configuration you can write a BeanCreatedEventListener for example:
import io.grpc.ManagedChannelBuilder;
import io.micronaut.context.event.BeanCreatedEvent;
import io.micronaut.context.event.BeanCreatedEventListener;
import jakarta.inject.Singleton;
@Singleton
public class ManagedChannelBuilderListener implements BeanCreatedEventListener<ManagedChannelBuilder<?>> {
@Override
public ManagedChannelBuilder<?> onCreated(BeanCreatedEvent<ManagedChannelBuilder<?>> event) {
final ManagedChannelBuilder<?> channelBuilder = event.getBean();
channelBuilder.maxInboundMessageSize(1024);
return channelBuilder;
}
}
from io.grpc import ManagedChannelBuilder
from jakarta.inject import Singleton
from micronaut.context.event import BeanCreatedEvent, BeanCreatedEventListener
@Singleton
class ManagedChannelBuilderListener(BeanCreatedEventListener[ManagedChannelBuilder]):
def onCreated(self, event: BeanCreatedEvent[ManagedChannelBuilder]) -> ManagedChannelBuilder:
channel_builder = event.getBean()
channel_builder.maxInboundMessageSize(1024)
return channel_builder
import io.grpc.ManagedChannelBuilder
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton
@Singleton
class ManagedChannelBuilderListener : BeanCreatedEventListener<ManagedChannelBuilder<*>> {
override fun onCreated(event: BeanCreatedEvent<ManagedChannelBuilder<*>>): ManagedChannelBuilder<*> {
val channelBuilder = event.bean
channelBuilder.maxInboundMessageSize(1024)
return channelBuilder
}
}
import groovy.transform.CompileStatic
import io.grpc.ManagedChannelBuilder
import io.micronaut.context.event.BeanCreatedEvent
import io.micronaut.context.event.BeanCreatedEventListener
import jakarta.inject.Singleton
@CompileStatic
@Singleton
class ManagedChannelBuilderListener implements BeanCreatedEventListener<ManagedChannelBuilder<?>> {
@Override
ManagedChannelBuilder<?> onCreated(BeanCreatedEvent<ManagedChannelBuilder<?>> event) {
final ManagedChannelBuilder<?> channelBuilder = event.bean
channelBuilder.maxInboundMessageSize(1024)
channelBuilder
}
}
Auto Injected Types
By default, each channel will automatically be dependency injected with beans of the following types:
-
io.grpc.ClientInterceptor- Any client interceptors declared as beans -
io.grpc.NameResolver- The configured name resolver
Creating Client Stub Beans
The value of the @GrpcChannel annotation can be used to specify the target server, the configuration for which can also be externalized:
@Factory
class ExternalizedClients {
@Singleton
GreeterGrpc.GreeterStub greeterStub(
@GrpcChannel("https://${my.server}:${my.port}")
ManagedChannel channel) {
return GreeterGrpc.newStub(
channel
);
}
}
@Factory
class ExternalizedClients:
@Singleton
def greeter_stub(
self,
channel: Annotated[ManagedChannel, GrpcChannel("https://${my.server}:${my.port}")]
) -> GreeterGrpc.GreeterStub:
return GreeterGrpc.newStub(
channel
)
@Factory
class ExternalizedClients {
@Singleton
fun greeterStub(
@GrpcChannel("https://\${my.server}:\${my.port}")
channel: ManagedChannel,
): GreeterGrpc.GreeterStub {
return GreeterGrpc.newStub(
channel
)
}
}
@CompileStatic
@Factory
class ExternalizedClients {
@Singleton
GreeterGrpc.GreeterStub greeterStub(
@GrpcChannel('https://${my.server}:${my.port}')
ManagedChannel channel) {
GreeterGrpc.newStub(
channel
)
}
}
The above example requires that my.server and my.port are specified in application.yml (or via environment variables MY_SERVER and MY_PORT). You can also externalize this further into configuration and provide channel specific configuration.
For example given the following configuration:
grpc.channels.greeter.address=${my.server}:${my.port}
grpc.channels.greeter.plaintext=true
grpc.channels.greeter.max-retry-attempts=10
grpc:
channels:
greeter:
address: '${my.server}:${my.port}'
plaintext: true
max-retry-attempts: 10
[grpc.channels.greeter]
address = "${my.server}:${my.port}"
plaintext = true
max-retry-attempts = 10
grpc {
channels {
greeter {
address = "${my.server}:${my.port}"
plaintext = true
maxRetryAttempts = 10
}
}
}
{
grpc {
channels {
greeter {
address = "${my.server}:${my.port}"
plaintext = true
max-retry-attempts = 10
}
}
}
}
{
"grpc": {
"channels": {
"greeter": {
"address": "${my.server}:${my.port}",
"plaintext": true,
"max-retry-attempts": 10
}
}
}
}
You can then define the @GrpcChannel annotation as follows:
@Factory
class NamedChannelClients {
@Singleton
GreeterGrpc.GreeterStub greeterStub(
@GrpcChannel("greeter")
ManagedChannel channel) {
return GreeterGrpc.newStub(
channel
);
}
}
@Factory
class NamedChannelClients:
@Singleton
def greeter_stub(
self,
channel: Annotated[ManagedChannel, GrpcChannel("greeter")]
) -> GreeterGrpc.GreeterStub:
return GreeterGrpc.newStub(
channel
)
@Factory
class NamedChannelClients {
@Singleton
fun greeterStub(
@GrpcChannel("greeter")
channel: ManagedChannel,
): GreeterGrpc.GreeterStub {
return GreeterGrpc.newStub(
channel
)
}
}
@CompileStatic
@Factory
class NamedChannelClients {
@Singleton
GreeterGrpc.GreeterStub greeterStub(
@GrpcChannel("greeter")
ManagedChannel channel) {
GreeterGrpc.newStub(
channel
)
}
}
The ID greeter is used to reference the configuration for grpc.channels.greeter.
Using service IDs in this way is the preferred way to set up gRPC clients, because it works nicely with Service Discovery (see the next section).
If you generate Reactor bindings with reactive-grpc, define the generated Reactor stub in the same kind of factory and inject the ManagedChannel with @GrpcChannel:
@Bean
ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub reactiveStub(
@GrpcChannel(GrpcServerChannel.NAME) ManagedChannel channel) {
return ReactorReactiveGreeterGrpc.newReactorStub(channel);
}
@Bean
def reactive_stub(self, channel: Annotated[ManagedChannel, GrpcChannel(GrpcServerChannel.NAME)]) -> ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub:
return ReactorReactiveGreeterGrpc.newReactorStub(channel)
@Bean
fun reactiveStub(@GrpcChannel(GrpcServerChannel.NAME) channel: ManagedChannel): ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub =
ReactorReactiveGreeterGrpc.newReactorStub(channel)
@Bean
ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub reactiveStub(
@GrpcChannel(GrpcServerChannel.NAME) ManagedChannel channel) {
ReactorReactiveGreeterGrpc.newReactorStub(channel)
}
The resulting stub can then be injected into your application or tests and used directly with Reactor operators:
@MicronautTest
class ReactiveGreetingEndpointTest {
@Inject
ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub reactiveStub;
@Test
void testReactiveHelloWorld() {
String message = Mono.just(HelloRequest.newBuilder().setName("Fred").build())
.transform(reactiveStub::sayHello)
.map(HelloReply::getMessage)
.block(Duration.ofSeconds(5));
assertEquals("Hello Fred", message);
}
}
@MicronautTest
class ReactiveGreetingEndpointTest:
reactive_stub: Annotated[ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub, Inject]
@Test
def test_reactive_hello_world(self):
message = self.reactive_stub.sayHello(Mono.just(HelloRequest.newBuilder().setName("Fred").build())) \
.map(lambda reply: reply.getMessage()) \
.block(Duration.ofSeconds(5))
assert message == "Hello Fred"
@MicronautTest
class ReactiveGreetingEndpointTest {
@Inject
lateinit var reactiveStub: ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub
@Test
fun testReactiveHelloWorld() {
val message = Mono.just(HelloRequest.newBuilder().setName("Fred").build())
.transform(reactiveStub::sayHello)
.map(HelloReply::getMessage)
.block(Duration.ofSeconds(5))
assertEquals("Hello Fred", message)
}
}
@MicronautTest
class ReactiveGreetingEndpointTest extends Specification {
@Inject
ReactorReactiveGreeterGrpc.ReactorReactiveGreeterStub reactiveStub
void "test reactive greeting endpoint"() {
when:
String message = Mono.just(HelloRequest.newBuilder().setName("Fred").build())
.transform(reactiveStub::sayHello)
.map(HelloReply::getMessage)
.block(Duration.ofSeconds(5))
then:
message == "Hello Fred"
}
}
6 Service Discovery
When using @GrpcChannel with a service ID without explicitly configuring the address of the service will trigger gRPC’s NameResolver and attempt to do service discovery.
The default strategy for this is to use DNS based discovery. So for example you can do:
@Factory
class DnsClients {
@Singleton
GreeterGrpc.GreeterStub greeterStub(
@GrpcChannel("dns:///greeter")
ManagedChannel channel) {
return GreeterGrpc.newStub(
channel
);
}
}
@Factory
class DnsClients:
@Singleton
def greeter_stub(
self,
channel: Annotated[ManagedChannel, GrpcChannel("dns:///greeter")]
) -> GreeterGrpc.GreeterStub:
return GreeterGrpc.newStub(
channel
)
@Factory
class DnsClients {
@Singleton
fun greeterStub(
@GrpcChannel("dns:///greeter")
channel: ManagedChannel,
): GreeterGrpc.GreeterStub {
return GreeterGrpc.newStub(
channel
)
}
}
@CompileStatic
@Factory
class DnsClients {
@Singleton
GreeterGrpc.GreeterStub greeterStub(
@GrpcChannel("dns:///greeter")
ManagedChannel channel) {
GreeterGrpc.newStub(
channel
)
}
}
Where DNS has been configured to know the address of the greeter service.
Alternatively, if you prefer to use a service discovery server then you can use integration with Micronaut service discovery.
Service Discovery with Consul
You can use Micronaut’s built-in service discovery features with any supported server (Consul and Eureka currently).
The way in which this is done is the same as a regular Micronaut service.
Registering a gRPC Service with Consul
To register a gRPC service with Consul first add the micronaut-discovery-client dependency:
runtimeOnly("io.micronaut.discovery:micronaut-discovery-client")
<dependency>
<groupId>io.micronaut.discovery</groupId>
<artifactId>micronaut-discovery-client</artifactId>
<scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
"io.micronaut.discovery:micronaut-discovery-client",
]
Then setup Consul correctly:
micronaut.application.name=greeter
consul.client.registration.enabled=true
consul.client.defaultZone=${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}
micronaut:
application:
name: greeter
consul:
client:
registration:
enabled: true
defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
[micronaut.application]
name = "greeter"
[consul.client]
defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
[consul.client.registration]
enabled = true
micronaut {
application {
name = "greeter"
}
}
consul {
client {
registration {
enabled = true
}
defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
}
}
{
micronaut {
application {
name = "greeter"
}
}
consul {
client {
registration {
enabled = true
}
defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
}
}
}
{
"micronaut": {
"application": {
"name": "greeter"
}
},
"consul": {
"client": {
"registration": {
"enabled": true
},
"defaultZone": "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
}
}
}
When using Service Discovery, Micronaut will register the service in Consul using the name defined in micronaut.application.name.
If the application also uses an HTTP Server (Netty, Tomcat,…), Micronaut will register the application with the same
name and a different port in Consul. In case you want to use a different name for the gRPC service in Consul:
micronaut.application.name=greeter
grpc.server.instance-id=hello-grpc
micronaut:
application:
name: greeter # (1)
grpc:
server:
instance-id: 'hello-grpc' # (2)
[micronaut.application]
name = "greeter"
[grpc.server]
instance-id = "hello-grpc"
micronaut {
application {
name = "greeter"
}
}
grpc {
server {
instanceId = "hello-grpc"
}
}
{
micronaut {
application {
name = "greeter"
}
}
grpc {
server {
instance-id = "hello-grpc"
}
}
}
{
"micronaut": {
"application": {
"name": "greeter"
}
},
"grpc": {
"server": {
"instance-id": "hello-grpc"
}
}
}
| 1 | The HTTP port will be registered in Consul with the name greeter |
| 2 | The gRPC port will be registered in Consul with the name hello-grpc |
Discoverying Services via Consul
To discovery services via Consul and the Micronaut DiscoveryClient abstraction enable Consul and gRPC service discovery:
grpc.client.discovery.enabled=true
consul.client.defaultZone=${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}
grpc:
client:
discovery:
enabled: true
consul:
client:
defaultZone: "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
[grpc.client.discovery]
enabled = true
[consul.client]
defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
grpc {
client {
discovery {
enabled = true
}
}
}
consul {
client {
defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
}
}
{
grpc {
client {
discovery {
enabled = true
}
}
}
consul {
client {
defaultZone = "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
}
}
}
{
"grpc": {
"client": {
"discovery": {
"enabled": true
}
}
},
"consul": {
"client": {
"defaultZone": "${CONSUL_HOST:localhost}:${CONSUL_PORT:8500}"
}
}
}
Then use the value greeter to discover the service when injecting the channel:
@Factory
class DiscoveryClients {
@Singleton
@Bean
GreeterGrpc.GreeterStub greeterStub(
@GrpcChannel("greeter")
ManagedChannel channel) {
return GreeterGrpc.newStub(
channel
);
}
}
@Factory
class DiscoveryClients:
@Singleton
@Bean
def greeter_stub(
self,
channel: Annotated[ManagedChannel, GrpcChannel("greeter")]
) -> GreeterGrpc.GreeterStub:
return GreeterGrpc.newStub(
channel
)
@Factory
class DiscoveryClients {
@Singleton
@Bean
fun greeterStub(
@GrpcChannel("greeter")
channel: ManagedChannel,
): GreeterGrpc.GreeterStub {
return GreeterGrpc.newStub(
channel
)
}
}
@CompileStatic
@Factory
class DiscoveryClients {
@Singleton
@Bean
GreeterGrpc.GreeterStub greeterStub(
@GrpcChannel("greeter")
ManagedChannel channel) {
GreeterGrpc.newStub(
channel
)
}
}
7 Distributed Tracing
Micronaut gRPC includes legacy OpenTracing-based tracing support. For new applications using Micronaut Tracing, add the OpenTelemetry gRPC integration:
implementation("io.micronaut.tracing:micronaut-tracing-opentelemetry-grpc")
<dependency>
<groupId>io.micronaut.tracing</groupId>
<artifactId>micronaut-tracing-opentelemetry-grpc</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
"io.micronaut.tracing:micronaut-tracing-opentelemetry-grpc",
]
Micronaut Tracing will then create spans for gRPC client requests, server requests, client responses, and server responses.
To export traces to Google Cloud Trace, add the Google Cloud OpenTelemetry exporter:
runtimeOnly("com.google.cloud.opentelemetry:exporter-auto")
<dependency>
<groupId>com.google.cloud.opentelemetry</groupId>
<artifactId>exporter-auto</artifactId>
<scope>runtime</scope>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
"com.google.cloud.opentelemetry:exporter-auto",
]
Then configure OpenTelemetry to use the google_cloud_trace exporter:
otel.traces.exporter=google_cloud_trace
otel:
traces:
exporter: google_cloud_trace
[otel.traces]
exporter = "google_cloud_trace"
otel {
traces {
exporter = "google_cloud_trace"
}
}
{
otel {
traces {
exporter = "google_cloud_trace"
}
}
}
{
"otel": {
"traces": {
"exporter": "google_cloud_trace"
}
}
}
When running outside Google Cloud, provide Application Default Credentials with the GOOGLE_APPLICATION_CREDENTIALS environment variable (or gcloud auth application-default login) and set GOOGLE_CLOUD_PROJECT if the project cannot be detected automatically.
For additional exporter options and authentication details, see the Micronaut Tracing OpenTelemetry exporter documentation.
8 Protocol Buffers Support
This project also includes a module that adds the ability to encode and decode Protocol buffers messages with the Micronaut HTTP server.
To use this adds the micronaut-protobuff-support dependency:
implementation("io.micronaut.grpc:micronaut-protobuff-support")
<dependency>
<groupId>io.micronaut.grpc</groupId>
<artifactId>micronaut-protobuff-support</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
"io.micronaut.grpc:micronaut-protobuff-support",
]
Micronaut will now support the encoding and decoding requests / responses of type application/x-protobuf.
9 Protocol Buffers Json Support (Experimental)
This project also includes a module that adds the ability to send json-serialized messages via a POST HTTP 1.1 call with the Micronaut HTTP server.
To use this add the micronaut-protobuff-json-support dependency:
implementation("io.micronaut.grpc:micronaut-protobuff-json-support")
<dependency>
<groupId>io.micronaut.grpc</groupId>
<artifactId>micronaut-protobuff-json-support</artifactId>
</dependency>
[tool.pyronaut.dependencies]
runtime = [
"io.micronaut.grpc:micronaut-protobuff-json-support",
]
grpc.rest.json.exposed=true
And then include the @GrpcRestJsonExposed annotation on your gRPC service definition:
import io.grpc.stub.StreamObserver;
import io.micronaut.grpc.annotation.GrpcRestJsonExposed;
import jakarta.inject.Singleton;
import org.example.grpc.GreeterGrpc;
import org.example.grpc.HelloRequest;
import org.example.grpc.HelloResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class GreeterService extends GreeterGrpc.GreeterImplBase {
private static final Logger LOG = LoggerFactory.getLogger(GreeterService.class);
@GrpcRestJsonExposed
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
LOG.debug("Received request: {}", request);
String name = request.getName();
String greeting = "Hello, " + name;
HelloResponse reply = HelloResponse.newBuilder()
.setGreeting(greeting)
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
import logging
from io.grpc.stub import StreamObserver
from jakarta.inject import Singleton
from micronaut.grpc.annotation import GrpcRestJsonExposed
from org.example.grpc import GreeterGrpc, HelloRequest, HelloResponse
LOG = logging.getLogger(__name__)
@Singleton
class GreeterService(GreeterGrpc.GreeterImplBase):
@GrpcRestJsonExposed
def sayHello(self, request: HelloRequest, response_observer: StreamObserver[HelloResponse]) -> None:
LOG.debug("Received request: %s", request)
name = request.getName()
greeting = "Hello, " + name
reply = HelloResponse.newBuilder().setGreeting(greeting).build()
response_observer.onNext(reply)
response_observer.onCompleted()
import io.grpc.stub.StreamObserver
import io.micronaut.grpc.annotation.GrpcRestJsonExposed
import jakarta.inject.Singleton
import org.example.grpc.GreeterGrpc
import org.example.grpc.HelloRequest
import org.example.grpc.HelloResponse
import org.slf4j.LoggerFactory
@Singleton
class GreeterService : GreeterGrpc.GreeterImplBase() {
@GrpcRestJsonExposed
override fun sayHello(request: HelloRequest, responseObserver: StreamObserver<HelloResponse>) {
LOG.debug("Received request: {}", request)
val name = request.name
val greeting = "Hello, $name"
val reply = HelloResponse.newBuilder()
.setGreeting(greeting)
.build()
responseObserver.onNext(reply)
responseObserver.onCompleted()
}
companion object {
private val LOG = LoggerFactory.getLogger(GreeterService::class.java)
}
}
import groovy.transform.CompileStatic
import io.grpc.stub.StreamObserver
import io.micronaut.grpc.annotation.GrpcRestJsonExposed
import jakarta.inject.Singleton
import org.example.grpc.GreeterGrpc
import org.example.grpc.HelloRequest
import org.example.grpc.HelloResponse
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@CompileStatic
@Singleton
class GreeterService extends GreeterGrpc.GreeterImplBase {
private static final Logger LOG = LoggerFactory.getLogger(GreeterService)
@GrpcRestJsonExposed
@Override
void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
LOG.debug("Received request: {}", request)
String name = request.name
String greeting = "Hello, " + name
HelloResponse reply = HelloResponse.newBuilder()
.setGreeting(greeting)
.build()
responseObserver.onNext(reply)
responseObserver.onCompleted()
}
}
Micronaut will now support the encoding and decoding requests / responses of type application/json via http 1.1.
How this works
Controller Creation
-
After the controller is created, the post creation annotation will traverse the beans created with the
@GrpcRestJsonExposedannotation on the service method. -
For each bean, peek into the gRPC allocated methods created.
-
Register the bean in a cache
The controller endpoint will use google’s built-in JSON/ProtocolBuffer serialization. Unfortunately, there is not yet a way to print a
full API schema for a request. However, to create a json request output one can take any gRPC request protocol buffer and convert it to
json using the io.micronaut.protobuf.json.ProtobufJsonTranscoder class.
Usage
Request comes in and matches as:
/grpc-json/{serviceName}/{method}
where the serviceName is the name of the gRPC service and the method is the name of the method.
Here’s an example of a gRPC service definition.
For example:
syntax = "proto3";
package greeter;
option java_package = "org.example.grpc";
option java_outer_classname = "GreeterProto";
option java_multiple_files = true;
message HelloRequest {
string name = 1;
}
message HelloResponse {
string greeting = 1;
}
service Greeter {
rpc sayHello (HelloRequest) returns (HelloResponse);
}
Mapping this to a grpc service, the request would look like this:
Where GreeterService is the service name and sayHello is the method name.
The payload would look like this:
{"name": "YourName"}
Take in a POST as application/json and the request should cleanly map as a grpc response.
Example client usage with cURL
If you run the test-suite-protobuff-json-java project and issue the following cURL call:
curl -X POST http://localhost:8080/grpc-json/GreeterService/sayHello \
-H "Content-Type: application/json" \
-d '{"name": "YourName"}'
10 Repository
You can find the source code of this project in this repository: