gRPC server
The module starts a gRPC server based on grpc-java and connects handlers from the application graph to it.
A handler is a BindableService, usually a class that extends a generated ...ImplBase and implements unary or streaming RPC methods.
Kora creates a NettyServerBuilder, adds server services, user-defined and standard ServerInterceptor, manages the server lifecycle, and participates in application readiness checks.
If configuration parameters are not enough, the resulting NettyServerBuilder can be additionally configured in code through GrpcServerBuilderConfigurer.
For a step-by-step walkthrough before the reference details, see gRPC Server and Advanced gRPC Server.
Dependency¶
Dependency in build.gradle:
implementation "ru.tinkoff.kora:grpc-server"
implementation "io.grpc:grpc-protobuf:1.74.0"
implementation "javax.annotation:javax.annotation-api:1.3.2"
Module:
Dependency in build.gradle.kts:
implementation("ru.tinkoff.kora:grpc-server")
implementation("io.grpc:grpc-protobuf:1.74.0")
implementation("javax.annotation:javax.annotation-api:1.3.2")
Module:
Plugin¶
The code for the gRPC server is generated with the protobuf gradle plugin.
Plugin in build.gradle:
plugins {
id "com.google.protobuf" version "0.9.4"
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.25.3" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" }
}
generateProtoTasks {
all()*.plugins { grpc {} }
}
}
sourceSets {
main.java {
srcDirs "build/generated/source/proto/main/grpc"
srcDirs "build/generated/source/proto/main/java"
}
}
Plugin in build.gradle.kts:
import com.google.protobuf.gradle.id
plugins {
id("com.google.protobuf") version ("0.9.4")
}
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.25.3" }
plugins {
id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.74.0" }
}
generateProtoTasks {
ofSourceSet("main").forEach { it.plugins { id("grpc") { } } }
}
}
kotlin {
sourceSets.main {
kotlin.srcDir("build/generated/source/proto/main/grpc")
kotlin.srcDir("build/generated/source/proto/main/java")
}
}
Configuration¶
Only port typically needs to be set; all other parameters have defaults.
A minimal configuration that binds a port from an environment variable and enables logging looks like this:
Basic configuration parameters:
gRPC serverport (default:8090).- Maximum incoming message size (default:
4MiB). - Enables
gRPC Server Reflectionservice (default:false).
gRPC serverport (default:8090).- Maximum incoming message size (default:
4MiB). - Enables
gRPC Server Reflectionservice (default:false).
Full Configuration
Example of a complete configuration described by GrpcServerConfig:
grpcServer {
port = 8090 //(1)!
maxMessageSize = "4MiB" //(2)!
reflectionEnabled = false //(3)!
shutdownWait = "30s" //(4)!
maxConnectionAge = "0s" //(5)!
maxConnectionAgeGrace = "0s" //(6)!
keepAliveTime = "0s" //(7)!
keepAliveTimeout = "0s" //(8)!
telemetry {
logging {
enabled = false //(9)!
}
metrics {
enabled = true //(10)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(11)!
tags = { // (12)!
"key1" = "value1"
"key2" = "value2"
}
}
tracing {
enabled = true //(13)!
attributes = { // (14)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
gRPC serverport (default:8090).- Maximum size of an incoming message (default:
4MiB). It can be specified as a number of bytes or as4MiB,4MB,1000Kb, and similar values. - Enables the
gRPC Server Reflectionservice (default:false). - Time to wait for processing before shutting down the server during graceful shutdown (default:
30s). - Sets a custom maximum connection age after which the connection is gracefully terminated (default: not specified, optional). A random jitter of +/-10% is added to the value.
- Sets additional time for graceful connection termination after the maximum connection age is reached (default: not specified, optional).
RPCcalls that do not finish in time are cancelled so the connection can terminate. - Sets the interval between
PINGframes (default: not specified, optional). - Timeout for acknowledging a
PINGframe (default: not specified, optional). If no acknowledgement is received within this time, the connection is closed. - Enables module logging (default:
false). - Enables module metrics (default:
true). - Configures SLO for the DistributionSummary metric (default:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO). - Metric tags (default:
{}). - Enables module tracing (default:
true). - Tracing attributes (default:
{}).
grpcServer:
port: 8090 #(1)!
maxMessageSize: "4MiB" #(2)!
reflectionEnabled: false #(3)!
shutdownWait: "30s" #(4)!
maxConnectionAge: "0s" #(5)!
maxConnectionAgeGrace: "0s" #(6)!
keepAliveTime: "0s" #(7)!
keepAliveTimeout: "0s" #(8)!
telemetry:
logging:
enabled: false #(9)!
metrics:
enabled: true #(10)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(11)!
tags: #(12)!
key1: value1
key2: value2
tracing:
enabled: true #(13)!
attributes: #(14)!
key1: value1
key2: value2
gRPC serverport (default:8090).- Maximum size of an incoming message (default:
4MiB). It can be specified as a number of bytes or as4MiB,4MB,1000Kb, and similar values. - Enables the
gRPC Server Reflectionservice (default:false). - Time to wait for processing before shutting down the server during graceful shutdown (default:
30s). - Sets a custom maximum connection age after which the connection is gracefully terminated (default: not specified, optional). A random jitter of +/-10% is added to the value.
- Sets additional time for graceful connection termination after the maximum connection age is reached (default: not specified, optional).
RPCcalls that do not finish in time are cancelled so the connection can terminate. - Sets the interval between
PINGframes (default: not specified, optional). - Timeout for acknowledging a
PINGframe (default: not specified, optional). If no acknowledgement is received within this time, the connection is closed. - Enables module logging (default:
false). - Enables module metrics (default:
true). - Configures SLO for the DistributionSummary metric (default:
ru.tinkoff.kora.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO). - Metric tags (default:
{}). - Enables module tracing (default:
true). - Tracing attributes (default:
{}).
You can also configure Netty transport.
Configuration In Code¶
If configuration parameters are not enough, register a GrpcServerBuilderConfigurer component and additionally configure NettyServerBuilder in code.
This component is called after configuration has been applied and after services, user-defined ServerInterceptor, and standard ServerInterceptor have been added.
Module metrics are described in the Metrics Reference section.
Handlers¶
A handler is a class that extends the generated ...ImplBase and is registered in the application graph with the @Component annotation.
The ...ImplBase class is produced from the proto contract by the protobuf gradle plugin; you override its RPC methods to implement server behavior.
Ordinary Kora components such as services and repositories can be injected into a handler through its constructor.
Consider a proto contract with a single unary method:
syntax = "proto3";
package ru.tinkoff.kora.generated.grpc;
service UserService {
rpc createUser(RequestEvent) returns (ResponseEvent) {} //(1)!
}
message RequestEvent {
string name = 1;
string code = 2;
}
message ResponseEvent {
bytes id = 1;
}
- A unary
RPC: one request message produces one response message.
The plugin generates UserServiceGrpc.UserServiceImplBase, and the handler overrides the generated method.
The generated method receives the request message and a StreamObserver
that is used to send responses back to the client:
@Component
public final class UserService extends UserServiceGrpc.UserServiceImplBase {
@Override
public void createUser(Message.RequestEvent request, StreamObserver<Message.ResponseEvent> responseObserver) { //(1)!
var response = Message.ResponseEvent.newBuilder()
.setId(ByteString.copyFromUtf8(UUID.randomUUID().toString()))
.build();
responseObserver.onNext(response); //(2)!
responseObserver.onCompleted(); //(3)!
}
}
- The generated method receives the request message and a
StreamObserverfor sending the response - Sends a single response message to the client
- Signals that the call is complete; for a unary method it is called exactly once, after a single
onNext
@Component
class UserService : UserServiceGrpc.UserServiceImplBase() {
override fun createUser(request: Message.RequestEvent, responseObserver: StreamObserver<Message.ResponseEvent>) { //(1)!
val response = Message.ResponseEvent.newBuilder()
.setId(ByteString.copyFromUtf8(UUID.randomUUID().toString()))
.build()
responseObserver.onNext(response) //(2)!
responseObserver.onCompleted() //(3)!
}
}
- The generated method receives the request message and a
StreamObserverfor sending the response - Sends a single response message to the client
- Signals that the call is complete; for a unary method it is called exactly once, after a single
onNext
Server streaming¶
For a server-streaming RPC (returns (stream ...) in the proto), the client sends one request and the server sends back many messages.
Call onNext for each message, then onCompleted once at the end:
@Override
public void getAllUsers(Message.RequestEvent request, StreamObserver<Message.ResponseEvent> responseObserver) {
for (var user : userService.findAll()) {
responseObserver.onNext(toResponse(user)); //(1)!
}
responseObserver.onCompleted(); //(2)!
}
- Sends one of several response messages
- Completes the response stream after the last message
override fun getAllUsers(request: Message.RequestEvent, responseObserver: StreamObserver<Message.ResponseEvent>) {
userService.findAll().forEach { responseObserver.onNext(toResponse(it)) } //(1)!
responseObserver.onCompleted() //(2)!
}
- Sends one of several response messages
- Completes the response stream after the last message
Client streaming¶
For a client-streaming RPC (rpc method(stream ...)), the client sends many messages and the server answers once at the end.
The generated method returns a StreamObserver that receives the incoming request messages; the final response is produced from onCompleted:
@Override
public StreamObserver<Message.RequestEvent> createUsers(StreamObserver<Message.ResponseEvent> responseObserver) {
return new StreamObserver<>() {
private final List<Message.RequestEvent> received = new ArrayList<>();
@Override
public void onNext(Message.RequestEvent value) {
received.add(value); //(1)!
}
@Override
public void onError(Throwable t) {
responseObserver.onError(t); //(2)!
}
@Override
public void onCompleted() {
responseObserver.onNext(aggregate(received)); //(3)!
responseObserver.onCompleted();
}
};
}
- Collects each incoming request message
- Propagates a client-side stream error
- Produces the single aggregated response once the client has finished sending
override fun createUsers(responseObserver: StreamObserver<Message.ResponseEvent>): StreamObserver<Message.RequestEvent> {
return object : StreamObserver<Message.RequestEvent> {
private val received = mutableListOf<Message.RequestEvent>()
override fun onNext(value: Message.RequestEvent) {
received += value //(1)!
}
override fun onError(t: Throwable) {
responseObserver.onError(t) //(2)!
}
override fun onCompleted() {
responseObserver.onNext(aggregate(received)) //(3)!
responseObserver.onCompleted()
}
}
}
- Collects each incoming request message
- Propagates a client-side stream error
- Produces the single aggregated response once the client has finished sending
Bidirectional streaming¶
For a bidirectional-streaming RPC (rpc method(stream ...) returns (stream ...)), both sides exchange many messages on the same call.
The method returns a StreamObserver for the incoming requests and can send responses at any time through responseObserver:
@Override
public StreamObserver<Message.RequestEvent> updateUsers(StreamObserver<Message.ResponseEvent> responseObserver) {
return new StreamObserver<>() {
@Override
public void onNext(Message.RequestEvent value) {
responseObserver.onNext(process(value)); //(1)!
}
@Override
public void onError(Throwable t) {
responseObserver.onError(t);
}
@Override
public void onCompleted() {
responseObserver.onCompleted(); //(2)!
}
};
}
- Responds to each incoming message as it arrives
- Completes the response stream when the client stops sending
override fun updateUsers(responseObserver: StreamObserver<Message.ResponseEvent>): StreamObserver<Message.RequestEvent> {
return object : StreamObserver<Message.RequestEvent> {
override fun onNext(value: Message.RequestEvent) {
responseObserver.onNext(process(value)) //(1)!
}
override fun onError(t: Throwable) {
responseObserver.onError(t)
}
override fun onCompleted() {
responseObserver.onCompleted() //(2)!
}
}
}
- Responds to each incoming message as it arrives
- Completes the response stream when the client stops sending
Error handling¶
Description: gRPC represents call errors with an io.grpc.Status
code and an optional description rather than with HTTP response codes.
To fail a call, complete the response observer with responseObserver.onError(status.asRuntimeException()),
or throw a StatusRuntimeException from the handler.
The auto-registered TelemetryInterceptor observes the terminal Status when the call is closed
(on close, onHalfClose, onCancel, and onComplete) and records logging, metrics, and tracing accordingly.
Causes: choose the Status code that matches the failure — for example Status.NOT_FOUND for a missing entity,
Status.INVALID_ARGUMENT for invalid input, Status.UNAUTHENTICATED or Status.PERMISSION_DENIED for authorization failures,
and Status.INTERNAL for unexpected server errors.
Recommendations:
- Attach a human-readable message with
withDescription(...)and keep the original exception withwithCause(...)so telemetry can record it. - Complete a call exactly once: never call
onErrorafteronCompleted, and never call either twice. - Do not leak internal exception details to clients; map them to an appropriate
Statusfirst.
Handling example: a unary handler that returns NOT_FOUND when an entity is missing and maps unexpected failures to INTERNAL:
@Override
public void getUser(Message.RequestEvent request, StreamObserver<Message.ResponseEvent> responseObserver) {
try {
var user = userService.getUser(request.getName())
.orElseThrow(() -> Status.NOT_FOUND
.withDescription("User not found: " + request.getName())
.asRuntimeException()); //(1)!
responseObserver.onNext(toResponse(user));
responseObserver.onCompleted();
} catch (StatusRuntimeException e) {
responseObserver.onError(e); //(2)!
} catch (Exception e) {
responseObserver.onError(Status.INTERNAL
.withDescription("Failed to get user")
.withCause(e) //(3)!
.asRuntimeException());
}
}
- Builds a
NOT_FOUNDerror with a description - Forwards an already-mapped
Statuserror to the client - Keeps the original exception as the cause so telemetry can record it
override fun getUser(request: Message.RequestEvent, responseObserver: StreamObserver<Message.ResponseEvent>) {
try {
val user = userService.getUser(request.name)
?: throw Status.NOT_FOUND
.withDescription("User not found: ${request.name}")
.asRuntimeException() //(1)!
responseObserver.onNext(toResponse(user))
responseObserver.onCompleted()
} catch (e: StatusRuntimeException) {
responseObserver.onError(e) //(2)!
} catch (e: Exception) {
responseObserver.onError(
Status.INTERNAL
.withDescription("Failed to get user")
.withCause(e) //(3)!
.asRuntimeException()
)
}
}
- Builds a
NOT_FOUNDerror with a description - Forwards an already-mapped
Statuserror to the client - Keeps the original exception as the cause so telemetry can record it
Signatures¶
The shape of a handler method is fixed by the proto contract and the generated ...ImplBase:
By Req and Resp we mean the generated request and response message types.
- Unary:
void myMethod(Req request, StreamObserver<Resp> responseObserver) - Server streaming:
void myMethod(Req request, StreamObserver<Resp> responseObserver)(multipleonNext, oneonCompleted) - Client streaming:
StreamObserver<Req> myMethod(StreamObserver<Resp> responseObserver) - Bidirectional streaming:
StreamObserver<Req> myMethod(StreamObserver<Resp> responseObserver)
The generated method returns void (or the request StreamObserver), so results are delivered asynchronously through the StreamObserver callbacks;
responses may be completed from another thread.
By Req and Resp we mean the generated request and response message types.
- Unary:
myMethod(request: Req, responseObserver: StreamObserver<Resp>) - Server streaming:
myMethod(request: Req, responseObserver: StreamObserver<Resp>)(multipleonNext, oneonCompleted) - Client streaming:
myMethod(responseObserver: StreamObserver<Resp>): StreamObserver<Req> - Bidirectional streaming:
myMethod(responseObserver: StreamObserver<Resp>): StreamObserver<Req>
When you generate coroutine stubs with the grpc-kotlin plugin (io.grpc:protoc-gen-grpc-kotlin)
and extend the generated ...CoroutineImplBase, handler methods can be suspend functions (and streaming methods can use Flow).
Kora auto-registers CoroutineContextInjectInterceptor, which injects the Kora Context into the handler's CoroutineContext;
it activates only when kotlinx-coroutines is on the classpath.
Interceptors¶
An io.grpc.ServerInterceptor processes a call before it is passed to a gRPC service.
Interceptors are suitable for cross-cutting logic: logging, authorization, tracing, working with Metadata, and error mapping.
Unlike the HTTP server, the gRPC server module has no @GrpcService or @InterceptWith annotation:
every ServerInterceptor registered as a @Component is applied globally to all services on the server.
To limit an interceptor to a single service or method, inspect the call at runtime — see Scoping and authorization.
Default¶
When the server starts, Kora adds standard interceptors:
TelemetryInterceptor— enables server telemetry (logging, metrics, tracing) depending on connected modules andgrpcServer.telemetrysettings, and maps the terminalStatus/exception when the call closesContextServerInterceptor— propagates the KoraContextinto call processing so it is available inside the handlerCoroutineContextInjectInterceptor— addsCoroutineContextsupport forKotlincoroutine handlers (active only whenkotlinx-coroutinesis on the classpath)
User-defined ServerInterceptor beans from the application graph are added to NettyServerBuilder before the standard interceptors.
For full NettyServerBuilder configuration, use GrpcServerBuilderConfigurer.
Execution order¶
gRPC invokes interceptors in the reverse order of registration, so the last interceptor added runs first (outermost). Because Kora registers user interceptors first and the standard interceptors last, an incoming call is processed in this order:
CoroutineContextInjectInterceptor -> ContextServerInterceptor -> TelemetryInterceptor -> user interceptors -> handler
Consequences of this order:
- The Kora
Contextand KotlinCoroutineContextare established around your interceptors and the handler, so they are available inside the handler's listener callbacks. TelemetryInterceptorwraps your interceptors and the handler, so it observes the finalStatus(including errors thrown or reported through the response observer).- When several user interceptors exist, they run in the reverse of their graph registration order; do not rely on a specific order between them for correctness.
Custom¶
To add a custom interceptor, create a ServerInterceptor implementation with the @Component annotation:
@Component
public class GrpcExceptionHandlerServerInterceptor implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall,
Metadata metadata,
ServerCallHandler<ReqT, RespT> serverCallHandler) {
// do something
return serverCallHandler.startCall(serverCall, metadata);
}
}
@Component
class GrpcExceptionHandlerServerInterceptor : ServerInterceptor {
override fun <ReqT, RespT> interceptCall(
serverCall: ServerCall<ReqT, RespT>,
metadata: Metadata,
serverCallHandler: ServerCallHandler<ReqT, RespT>
): ServerCall.Listener<ReqT> {
// do something
return serverCallHandler.startCall(serverCall, metadata)
}
}
Scoping and authorization¶
Because an interceptor is global, scope it to a specific service or method by inspecting call.getMethodDescriptor():
getServiceName() returns the service name (the generated constant ...Grpc.SERVICE_NAME), and getFullMethodName() returns service/method.
Request headers arrive as Metadata.
Read a header with a Metadata.Key, and reject a call by closing it with a Status and returning an empty listener so the handler is never invoked.
The example below applies API-key authorization to a single service only:
@Component
public final class ApiKeyServerInterceptor implements ServerInterceptor {
private static final Metadata.Key<String> AUTHORIZATION =
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER); //(1)!
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
if (!UserServiceGrpc.SERVICE_NAME.equals(call.getMethodDescriptor().getServiceName())) { //(2)!
return next.startCall(call, headers);
}
var apiKey = headers.get(AUTHORIZATION); //(3)!
if (apiKey == null || !apiKey.equals("secret")) {
call.close(Status.UNAUTHENTICATED.withDescription("Invalid API key"), new Metadata()); //(4)!
return new ServerCall.Listener<>() {}; //(5)!
}
return next.startCall(call, headers);
}
}
Metadata.Keyfor reading theauthorizationheader as an ASCII string- Applies the interceptor only to
UserService; other services pass through untouched - Reads the header value from the request
Metadata - Rejects the call with an
UNAUTHENTICATEDstatus - Returns an empty listener so the handler is never called
@Component
class ApiKeyServerInterceptor : ServerInterceptor {
override fun <ReqT : Any?, RespT : Any?> interceptCall(
call: ServerCall<ReqT, RespT>,
headers: Metadata,
next: ServerCallHandler<ReqT, RespT>
): ServerCall.Listener<ReqT> {
if (UserServiceGrpc.SERVICE_NAME != call.methodDescriptor.serviceName) { //(2)!
return next.startCall(call, headers)
}
val apiKey = headers.get(AUTHORIZATION) //(3)!
if (apiKey != "secret") {
call.close(Status.UNAUTHENTICATED.withDescription("Invalid API key"), Metadata()) //(4)!
return object : ServerCall.Listener<ReqT>() {} //(5)!
}
return next.startCall(call, headers)
}
companion object {
private val AUTHORIZATION: Metadata.Key<String> =
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER) //(1)!
}
}
Metadata.Keyfor reading theauthorizationheader as an ASCII string- Applies the interceptor only to
UserService; other services pass through untouched - Reads the header value from the request
Metadata - Rejects the call with an
UNAUTHENTICATEDstatus - Returns an empty listener so the handler is never called
Lifecycle and readiness¶
The server is managed by the GrpcNettyServer component, which is created as a @Root component
and follows the application lifecycle:
- On startup it builds and starts the
Nettyserver on the configuredport. If the port is already in use, startup fails with a clear error. - On shutdown it performs a graceful shutdown: it stops accepting new calls and waits up to
shutdownWaitfor in-flight calls to finish, then forcibly terminates any remaining calls.
GrpcNettyServer also implements a readiness probe: the server reports not ready while it is starting up or shutting down,
and ready only while it is running. In a Kubernetes deployment this lets the readiness probe reflect the real server state and drain traffic during graceful shutdown.
Reflection¶
gRPC Server Reflection is supported and provides information about available gRPC services on the server.
Reflection helps clients and tools build RPC requests at runtime without precompiled service information.
For example, it is used by gRPC CLI, which can inspect server proto descriptions and send test RPC calls.
gRPC Server Reflection is supported only for proto-based services.
You can learn more about gRPC Server Reflection in the grpc-java guide.
Dependency¶
You must additionally add the gRPC Server Reflection dependency.
Dependency in build.gradle:
Dependency in build.gradle.kts:
Configuration¶
You must also enable the gRPC Server Reflection service in the configuration.
Kora adds it to the server only if the application has the io.grpc.protobuf.services.ProtoReflectionService class, so configuration alone is not enough without the dependency.
Usage¶
With reflection enabled, tools such as grpcurl can discover services and send RPC calls without a precompiled client.
For a server listening on port 8090:
grpcurl -plaintext localhost:8090 list #(1)!
grpcurl -plaintext localhost:8090 describe ru.tinkoff.kora.generated.grpc.UserService #(2)!
grpcurl -plaintext -d '{"name": "Bob", "code": "123"}' \
localhost:8090 ru.tinkoff.kora.generated.grpc.UserService/createUser #(3)!
- Lists the services exposed by the server
- Describes a service and its methods
- Sends a unary
RPC;-plaintextis used because the example server has noTLS
Telemetry¶
gRPC Server uses a telemetry contract for logging, metrics, and tracing of calls.
Telemetry configuration (section telemetry { logging / metrics / tracing }) is described in the Configuration section.
Extension points are located in ru.tinkoff.kora.grpc.server.common.telemetry.
Server observability is driven by the TelemetryInterceptor through the GrpcServerTelemetry facade and is configured under grpcServer.telemetry.
For each gRPC call, a GrpcServerTelemetry.GrpcServerTelemetryContext is created, which is closed upon call completion.
The call is described through telemetry handler parameters, including service, method, response status, and duration.
The default factory DefaultGrpcServerTelemetryFactory combines three factories:
- GrpcServerLoggerFactory builds GrpcServerLogger for logging call start/end;
- GrpcServerMetricsFactory builds GrpcServerMetrics for writing call metrics;
- GrpcServerTracerFactory builds GrpcServerTracer for distributed tracing.
Metrics and tracing are described in the Metrics Reference section.