diff --git a/README.adoc b/README.adoc index 58fb0643..effff870 100644 --- a/README.adoc +++ b/README.adoc @@ -315,6 +315,29 @@ image:./images/interceptors_003.png[] This started is *natively* supported by `spring-cloud-sleuth` project. + Please continue to https://docs.spring.io/spring-cloud-sleuth/docs/current/reference/html/integrations.html#sleuth-rpc-grpc-integration[sleuth grpc integration]. +=== Distributed tracing support (OpenTelemetry) + +OpenTelemetry distributed tracing can enabled by including: +[source,groovy] +---- +implementation 'io.opentelemetry:opentelemetry-exporter-otlp:[version]' +implementation 'io.micrometer:micrometer-tracing-bridge-otel:[version]' +---- + +Enable tracing for client by adding `TracingClientInterceptor` to `ManagedChannelBuilder`: +[source,java] +---- +ManagedChannelBuilder.forAddress(host, port) + ... +.intercept(TracingClientInterceptor()) +.build() +---- + +Add tracing interceptor `TracingServerInterceptor` for your `GRpcService`: +[source,java] +---- +@GRpcService(interceptors = {LogInterceptor.class, TracingServerInterceptor.class }) +---- === GRPC server metrics (Micrometer.io integration) diff --git a/grpc-client-spring-boot-starter/build.gradle b/grpc-client-spring-boot-starter/build.gradle index 33fbd86d..212a1d1f 100644 --- a/grpc-client-spring-boot-starter/build.gradle +++ b/grpc-client-spring-boot-starter/build.gradle @@ -114,6 +114,7 @@ signing { dependencies { api "io.grpc:grpc-api:${grpcVersion}" + compileOnly 'io.opentelemetry:opentelemetry-exporter-otlp:1.29.0' } compileJava.dependsOn(processResources) diff --git a/grpc-client-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/tracing/TracingClientInterceptor.java b/grpc-client-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/tracing/TracingClientInterceptor.java new file mode 100644 index 00000000..8f62abbf --- /dev/null +++ b/grpc-client-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/tracing/TracingClientInterceptor.java @@ -0,0 +1,33 @@ +package org.lognet.springboot.grpc.tracing; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.TraceId; + +public class TracingClientInterceptor implements ClientInterceptor { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + + return new ForwardingClientCall.SimpleForwardingClientCall(next.newCall(method, callOptions)) { + @Override + public void start(ClientCall.Listener responseListener, Metadata headers) { + Span currentSpan = Span.current(); + String traceId = currentSpan.getSpanContext().getTraceId(); + String spanId = currentSpan.getSpanContext().getSpanId(); + + if (!traceId.equals(TraceId.getInvalid())) { + headers.put(Metadata.Key.of("traceId", Metadata.ASCII_STRING_MARSHALLER), traceId); + headers.put(Metadata.Key.of("spanId", Metadata.ASCII_STRING_MARSHALLER), spanId); + } + super.start(responseListener, headers); + } + }; + } +} diff --git a/grpc-spring-boot-starter/build.gradle b/grpc-spring-boot-starter/build.gradle index 187fb4d3..4ba5e4ec 100644 --- a/grpc-spring-boot-starter/build.gradle +++ b/grpc-spring-boot-starter/build.gradle @@ -286,6 +286,8 @@ dependencies { compileOnly "org.springframework.boot:spring-boot-starter-actuator" compileOnly "org.springframework.boot:spring-boot-starter-validation" compileOnly 'org.springframework.cloud:spring-cloud-starter-consul-discovery' + compileOnly 'io.opentelemetry:opentelemetry-exporter-otlp:1.29.0' + compileOnly 'io.micrometer:micrometer-tracing-bridge-otel:1.1.4' testImplementation "javax.annotation:javax.annotation-api:1.3.2" diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/tracing/OtelConfiguration.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/tracing/OtelConfiguration.java new file mode 100644 index 00000000..708dbeac --- /dev/null +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/autoconfigure/tracing/OtelConfiguration.java @@ -0,0 +1,44 @@ +package org.lognet.springboot.grpc.autoconfigure.tracing; + +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import org.lognet.springboot.grpc.tracing.TracingServerInterceptor; +import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass({OpenTelemetrySdk.class}) +@EnableConfigurationProperties(OtlpProperties.class) +public class OtelConfiguration { + + @Bean + public OtlpGrpcSpanExporter otlpExporter(OtlpProperties properties) { + OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder().setEndpoint(properties.getEndpoint()); + return builder.build(); + } + + @Bean + public Tracer tracer(OtlpGrpcSpanExporter otlpExporter) { + SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(BatchSpanProcessor.builder(otlpExporter).build()) + .build(); + + OpenTelemetrySdk openTelemetry = OpenTelemetrySdk.builder() + .setTracerProvider(tracerProvider) + .buildAndRegisterGlobal(); + + return openTelemetry.getTracerProvider().get("grpc-spring-boot-starter"); + } + + @Bean + public TracingServerInterceptor tracingServerInterceptor(Tracer tracer) { + return new TracingServerInterceptor(tracer); + } +} diff --git a/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/tracing/TracingServerInterceptor.java b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/tracing/TracingServerInterceptor.java new file mode 100644 index 00000000..3d6dddb5 --- /dev/null +++ b/grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/tracing/TracingServerInterceptor.java @@ -0,0 +1,61 @@ +package org.lognet.springboot.grpc.tracing; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.opentelemetry.api.trace.*; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.springframework.core.Ordered; + +import java.util.Optional; + +public class TracingServerInterceptor implements ServerInterceptor, Ordered { + + @Setter + @Accessors(fluent = true) + private Integer order; + + private final Tracer tracer; + + public TracingServerInterceptor(Tracer tracer) { + this.tracer = tracer; + } + + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + + String traceId = headers.get(Metadata.Key.of("traceId", Metadata.ASCII_STRING_MARSHALLER)); + String spanId = headers.get(Metadata.Key.of("spanId", Metadata.ASCII_STRING_MARSHALLER)); + + Context spanContext = createSpanContext(traceId, spanId); + SpanBuilder spanBuilder = tracer.spanBuilder("grpc-spring-boot-starter-span").setParent(spanContext); + Span span = spanBuilder.startSpan(); + Context scopedContext = Context.current().with(span); + + try (Scope scope = scopedContext.makeCurrent()) { + return next.startCall(call, headers); + } finally { + span.end(); + } + } + + private Context createSpanContext(String traceId, String spanId) { + SpanContext spanContext = SpanContext.createFromRemoteParent( + traceId != null ? traceId : "", + spanId != null ? spanId : "", + TraceFlags.getDefault(), + TraceState.getDefault() + ); + return Context.current().with(Span.wrap(spanContext)); + } + + @Override + public int getOrder() { + return Optional.ofNullable(order).orElse(HIGHEST_PRECEDENCE); + } +}