diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesGetter.java index 14de3b4af8e7..e5365006dab1 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesGetter.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesGetter.java @@ -68,6 +68,15 @@ default String getRpcMethod(REQUEST request) { return null; } + /** + * Returns the original method name when the method reported via {@link #getRpcMethod(REQUEST)} is + * set to {@code _OTHER} because the method is not recognized by the RPC framework. + */ + @Nullable + default String getRpcMethodOriginal(REQUEST request) { + return null; + } + /** * Returns a description of a class of error the operation ended with. * diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcCommonAttributesExtractor.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcCommonAttributesExtractor.java index 724738af2196..84c849fc8cda 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcCommonAttributesExtractor.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcCommonAttributesExtractor.java @@ -19,6 +19,8 @@ abstract class RpcCommonAttributesExtractor implements AttributesExtractor { static final AttributeKey RPC_METHOD = AttributeKey.stringKey("rpc.method"); + static final AttributeKey RPC_METHOD_ORIGINAL = + AttributeKey.stringKey("rpc.method_original"); // Stable semconv keys static final AttributeKey RPC_SYSTEM_NAME = AttributeKey.stringKey("rpc.system.name"); @@ -42,6 +44,7 @@ public final void onStart(AttributesBuilder attributes, Context parentContext, R if (emitStableRpcSemconv()) { attributes.put(RPC_SYSTEM_NAME, getter.getRpcSystemName(request)); attributes.put(RPC_METHOD, getter.getRpcMethod(request)); + attributes.put(RPC_METHOD_ORIGINAL, getter.getRpcMethodOriginal(request)); } if (emitOldRpcSemconv()) { diff --git a/instrumentation/armeria/armeria-grpc-1.14/javaagent/build.gradle.kts b/instrumentation/armeria/armeria-grpc-1.14/javaagent/build.gradle.kts index 21d823f76cb1..ee05c04c6b6f 100644 --- a/instrumentation/armeria/armeria-grpc-1.14/javaagent/build.gradle.kts +++ b/instrumentation/armeria/armeria-grpc-1.14/javaagent/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { library("com.linecorp.armeria:armeria-grpc:1.14.0") implementation(project(":instrumentation:grpc-1.6:library")) + testInstrumentation(project(":instrumentation:armeria:armeria-1.3:javaagent")) testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent")) testInstrumentation(project(":instrumentation:grpc-1.6:javaagent")) @@ -61,6 +62,10 @@ afterEvaluate { tasks { withType().configureEach { systemProperty("collectMetadata", otelProps.collectMetadata) + // The armeria HTTP instrumentation creates an HTTP server span, and then the gRPC + // interceptor creates a second server span from the same incoming context, which triggers the + // context leak debugger. + jvmArgs("-Dotel.javaagent.experimental.thread-propagation-debugger.enabled=false") } val testStableSemconv by registering(Test::class) { diff --git a/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcServiceBuilderInstrumentation.java b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcServiceBuilderInstrumentation.java index 9acaa5244254..f9364ea24651 100644 --- a/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcServiceBuilderInstrumentation.java +++ b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcServiceBuilderInstrumentation.java @@ -9,8 +9,6 @@ import static net.bytebuddy.matcher.ElementMatchers.named; import com.linecorp.armeria.server.grpc.GrpcServiceBuilder; -import io.opentelemetry.api.GlobalOpenTelemetry; -import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import net.bytebuddy.asm.Advice; @@ -34,7 +32,7 @@ public static class BuildAdvice { @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) public static void onEnter(@Advice.This GrpcServiceBuilder builder) { - builder.intercept(GrpcTelemetry.create(GlobalOpenTelemetry.get()).createServerInterceptor()); + builder.intercept(ArmeriaGrpcSingletons.SERVER_INTERCEPTOR); } } } diff --git a/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcSingletons.java b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcSingletons.java new file mode 100644 index 000000000000..56a7f90a210a --- /dev/null +++ b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcSingletons.java @@ -0,0 +1,23 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14; + +import io.grpc.ServerInterceptor; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry; +import io.opentelemetry.instrumentation.grpc.v1_6.internal.Internal; + +public final class ArmeriaGrpcSingletons { + + public static final ServerInterceptor SERVER_INTERCEPTOR; + + static { + GrpcTelemetry telemetry = GrpcTelemetry.create(GlobalOpenTelemetry.get()); + SERVER_INTERCEPTOR = Internal.createServerInterceptor(telemetry); + } + + private ArmeriaGrpcSingletons() {} +} diff --git a/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcTest.java b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcTest.java index 4b0b443c463e..4ebdedd9c584 100644 --- a/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcTest.java +++ b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcTest.java @@ -8,8 +8,19 @@ import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitOldRpcSemconv; import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableRpcSemconv; import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.ClientAttributes.CLIENT_ADDRESS; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_PORT; +import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION; import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.UrlAttributes.URL_PATH; +import static io.opentelemetry.semconv.UrlAttributes.URL_SCHEME; +import static io.opentelemetry.semconv.UserAgentAttributes.USER_AGENT_ORIGINAL; import static io.opentelemetry.semconv.incubating.MessageIncubatingAttributes.MESSAGE_ID; import static io.opentelemetry.semconv.incubating.MessageIncubatingAttributes.MESSAGE_TYPE; import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE; @@ -19,6 +30,7 @@ import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SYSTEM; import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SYSTEM_NAME; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.linecorp.armeria.client.grpc.GrpcClients; import com.linecorp.armeria.server.ServerBuilder; @@ -27,9 +39,12 @@ import example.GreeterGrpc; import example.Helloworld; import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.protobuf.services.HealthStatusManager; import io.grpc.stub.StreamObserver; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.sdk.trace.data.StatusData; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -62,6 +77,18 @@ public void sayHello( } }; + @RegisterExtension + static final ServerExtension serverWithoutGreeter = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.service( + GrpcService.builder() + .addService(new HealthStatusManager().getHealthService()) + .build()); + } + }; + @SuppressWarnings("deprecation") // using deprecated semconv @Test void grpcInstrumentation() { @@ -108,6 +135,23 @@ void grpcInstrumentation() { .hasAttributesSatisfyingExactly( equalTo(MESSAGE_TYPE, "RECEIVED"), equalTo(MESSAGE_ID, 1L))), + span -> + span.hasName("POST /example.Greeter/SayHello") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(1)) + .hasAttributesSatisfyingExactly( + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(HTTP_ROUTE, "/example.Greeter/SayHello"), + equalTo(URL_PATH, "/example.Greeter/SayHello"), + equalTo(URL_SCHEME, "http"), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + equalTo(SERVER_PORT, (long) server.httpPort()), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, port -> port.isInstanceOf(Long.class)), + equalTo(NETWORK_PROTOCOL_VERSION, "2"), + satisfies(USER_AGENT_ORIGINAL, val -> val.startsWith("armeria/"))), span -> span.hasName("example.Greeter/SayHello") .hasKind(SpanKind.SERVER) @@ -139,4 +183,70 @@ void grpcInstrumentation() { .hasAttributesSatisfyingExactly( equalTo(MESSAGE_TYPE, "SENT"), equalTo(MESSAGE_ID, 1L))))); } + + @SuppressWarnings("deprecation") // using deprecated semconv + @Test + void unknownService() { + GreeterGrpc.GreeterBlockingStub client = + GrpcClients.builder(serverWithoutGreeter.httpUri()) + .build(GreeterGrpc.GreeterBlockingStub.class); + + Helloworld.Request request = Helloworld.Request.newBuilder().setName("test").build(); + assertThatThrownBy(() -> client.sayHello(request)).isInstanceOf(StatusRuntimeException.class); + + // Armeria uses exact-route binding for gRPC services, so calling an unregistered service + // results in an HTTP 404 rather than the gRPC service layer producing an UNIMPLEMENTED + // response. The gRPC client maps the HTTP 404 to Status.UNIMPLEMENTED (asserted on the + // client span below), and the armeria HTTP instrumentation captures the request as an + // HTTP server span -- there is no gRPC server span because the gRPC service layer is + // never invoked. + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("example.Greeter/SayHello") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, emitOldRpcSemconv() ? "grpc" : null), + equalTo(RPC_SYSTEM_NAME, emitStableRpcSemconv() ? "grpc" : null), + equalTo(RPC_SERVICE, emitOldRpcSemconv() ? "example.Greeter" : null), + equalTo( + RPC_METHOD, + emitStableRpcSemconv() ? "example.Greeter/SayHello" : "SayHello"), + equalTo( + RPC_GRPC_STATUS_CODE, + emitOldRpcSemconv() + ? (long) Status.Code.UNIMPLEMENTED.value() + : null), + equalTo( + RPC_RESPONSE_STATUS_CODE, + emitStableRpcSemconv() ? Status.Code.UNIMPLEMENTED.name() : null), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + equalTo(SERVER_PORT, (long) serverWithoutGreeter.httpPort())) + .hasEventsSatisfyingExactly( + event -> + event + .hasName("message") + .hasAttributesSatisfyingExactly( + equalTo(MESSAGE_TYPE, "SENT"), equalTo(MESSAGE_ID, 1L))), + span -> + span.hasName("POST /*") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(0)) + .hasAttributesSatisfyingExactly( + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 404), + equalTo(HTTP_ROUTE, "/*"), + equalTo(URL_PATH, "/example.Greeter/SayHello"), + equalTo(URL_SCHEME, "http"), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + equalTo(SERVER_PORT, (long) serverWithoutGreeter.httpPort()), + equalTo(CLIENT_ADDRESS, "127.0.0.1"), + equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"), + satisfies(NETWORK_PEER_PORT, port -> port.isInstanceOf(Long.class)), + equalTo(NETWORK_PROTOCOL_VERSION, "2"), + satisfies(USER_AGENT_ORIGINAL, val -> val.startsWith("armeria/"))))); + } } diff --git a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java index 8b4ea428405e..e35e479bb5a3 100644 --- a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java +++ b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java @@ -8,7 +8,6 @@ import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass; import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; import static io.opentelemetry.javaagent.instrumentation.grpc.v1_6.GrpcSingletons.SERVER_BUILDER_INSTRUMENTED; -import static io.opentelemetry.javaagent.instrumentation.grpc.v1_6.GrpcSingletons.serverInterceptor; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.takesArguments; @@ -50,7 +49,7 @@ public static CallDepth onEnter(@Advice.This ServerBuilder serverBuilder) { return callDepth; } if (!Boolean.TRUE.equals(SERVER_BUILDER_INSTRUMENTED.get(serverBuilder))) { - serverBuilder.intercept(serverInterceptor()); + GrpcSingletons.configureServerBuilder(serverBuilder); SERVER_BUILDER_INSTRUMENTED.set(serverBuilder, true); } return callDepth; diff --git a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java index 68ae16349620..f3150a0ab3d8 100644 --- a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java +++ b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java @@ -11,7 +11,6 @@ import io.grpc.Context; import io.grpc.ManagedChannelBuilder; import io.grpc.ServerBuilder; -import io.grpc.ServerInterceptor; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.instrumentation.api.incubator.config.internal.DeclarativeConfigUtil; @@ -34,7 +33,7 @@ public class GrpcSingletons { private static final ClientInterceptor clientInterceptor; - private static final ServerInterceptor serverInterceptor; + private static final GrpcTelemetry grpcTelemetry; private static final AtomicReference storageReference = new AtomicReference<>(); @@ -57,7 +56,7 @@ public class GrpcSingletons { .get("server") .getScalarList("request", String.class, emptyList()); - GrpcTelemetry telemetry = + grpcTelemetry = GrpcTelemetry.builder(GlobalOpenTelemetry.get()) .setEmitMessageEvents(emitMessageEvents) .setCaptureExperimentalSpanAttributes(experimentalSpanAttributes) @@ -65,18 +64,13 @@ public class GrpcSingletons { .setCapturedServerRequestMetadata(serverRequestMetadata) .build(); - clientInterceptor = telemetry.createClientInterceptor(); - serverInterceptor = telemetry.createServerInterceptor(); + clientInterceptor = grpcTelemetry.createClientInterceptor(); } public static ClientInterceptor clientInterceptor() { return clientInterceptor; } - public static ServerInterceptor serverInterceptor() { - return serverInterceptor; - } - @Nullable public static Context.Storage storage() { return storageReference.get(); @@ -87,5 +81,9 @@ public static Context.Storage setStorage(Context.Storage storage) { return storage(); } + public static void configureServerBuilder(ServerBuilder serverBuilder) { + grpcTelemetry.configureServerBuilder(serverBuilder); + } + private GrpcSingletons() {} } diff --git a/instrumentation/grpc-1.6/library/README.md b/instrumentation/grpc-1.6/library/README.md index ecba1980aa12..8644b7447b60 100644 --- a/instrumentation/grpc-1.6/library/README.md +++ b/instrumentation/grpc-1.6/library/README.md @@ -37,9 +37,9 @@ void configureClientInterceptor(OpenTelemetry openTelemetry, NettyChannelBuilder nettyChannelBuilder.intercept(grpcTelemetry.createClientInterceptor()); } -// For server-side, attatch the interceptor to your service. -ServerServiceDefinition configureServerInterceptor(OpenTelemetry openTelemetry, ServerServiceDefinition serviceDefinition) { +// For server-side, configure the server builder. +void configureServer(OpenTelemetry openTelemetry, ServerBuilder serverBuilder) { GrpcTelemetry grpcTelemetry = GrpcTelemetry.create(openTelemetry); - return ServerInterceptors.intercept(serviceDefinition, grpcTelemetry.createServerInterceptor()); + grpcTelemetry.configureServerBuilder(serverBuilder); } ``` diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequest.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequest.java index 88f52ce454ff..1661ef965531 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequest.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequest.java @@ -12,7 +12,9 @@ public final class GrpcRequest { - private final MethodDescriptor method; + @Nullable private final MethodDescriptor method; + private final String fullMethodName; + @Nullable private final String originalFullMethodName; @Nullable private volatile Metadata metadata; @@ -29,11 +31,20 @@ public final class GrpcRequest { @Nullable SocketAddress peerSocketAddress, @Nullable String authority) { this.method = method; + this.fullMethodName = method.getFullMethodName(); + this.originalFullMethodName = null; this.metadata = metadata; this.peerSocketAddress = peerSocketAddress; setLogicalAddress(authority); } + GrpcRequest(String fullMethodName, @Nullable String originalFullMethodName, Metadata metadata) { + this.method = null; + this.fullMethodName = fullMethodName; + this.originalFullMethodName = originalFullMethodName; + this.metadata = metadata; + } + private void setLogicalAddress(@Nullable String authority) { if (authority == null) { return; @@ -51,10 +62,20 @@ private void setLogicalAddress(@Nullable String authority) { } } + @Nullable public MethodDescriptor getMethod() { return method; } + String getFullMethodName() { + return fullMethodName; + } + + @Nullable + String getOriginalFullMethodName() { + return originalFullMethodName; + } + @Nullable public Metadata getMetadata() { return metadata; diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequestGetter.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequestGetter.java index b07e69abd577..94242b9bffe0 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequestGetter.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequestGetter.java @@ -15,6 +15,8 @@ final class GrpcRequestGetter implements TextMapGetter { + static final GrpcRequestGetter INSTANCE = new GrpcRequestGetter(); + @Override public Iterable keys(GrpcRequest request) { // Filter out HTTP/2 pseudo-headers (starting with ':') as they cannot be diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesGetter.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesGetter.java index 8d486f914dbe..3875b735d5e8 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesGetter.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesGetter.java @@ -25,7 +25,7 @@ public String getSystem(GrpcRequest request) { @Override @Nullable public String getService(GrpcRequest request) { - String fullMethodName = request.getMethod().getFullMethodName(); + String fullMethodName = request.getFullMethodName(); int slashIndex = fullMethodName.lastIndexOf('/'); if (slashIndex == -1) { return null; @@ -37,7 +37,7 @@ public String getService(GrpcRequest request) { @Override @Nullable public String getMethod(GrpcRequest request) { - String fullMethodName = request.getMethod().getFullMethodName(); + String fullMethodName = request.getFullMethodName(); int slashIndex = fullMethodName.lastIndexOf('/'); if (slashIndex == -1) { return null; @@ -47,7 +47,7 @@ public String getMethod(GrpcRequest request) { @Override public String getRpcMethod(GrpcRequest request) { - return request.getMethod().getFullMethodName(); + return request.getFullMethodName(); } @Override @@ -62,6 +62,12 @@ public Long getResponseSize(GrpcRequest request) { return request.getResponseSize(); } + @Override + @Nullable + public String getRpcMethodOriginal(GrpcRequest request) { + return request.getOriginalFullMethodName(); + } + List metadataValue(GrpcRequest request, String key) { if (request.getMetadata() == null) { return emptyList(); diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanNameExtractor.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanNameExtractor.java index 35a08ba0fb16..c44310c5e6ee 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanNameExtractor.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcSpanNameExtractor.java @@ -11,6 +11,6 @@ final class GrpcSpanNameExtractor implements SpanNameExtractor { @Override public String extract(GrpcRequest request) { - return request.getMethod().getFullMethodName(); + return request.getFullMethodName(); } } diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetry.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetry.java index b2e1b1728da7..c2e5d5fa9910 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetry.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetry.java @@ -6,11 +6,13 @@ package io.opentelemetry.instrumentation.grpc.v1_6; import io.grpc.ClientInterceptor; +import io.grpc.ServerBuilder; import io.grpc.ServerInterceptor; import io.grpc.Status; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.grpc.v1_6.internal.Internal; /** Entrypoint for instrumenting gRPC servers or clients. */ public final class GrpcTelemetry { @@ -20,6 +22,10 @@ public final class GrpcTelemetry { private final boolean captureExperimentalSpanAttributes; private final boolean emitMessageEvents; + static { + Internal.setServerInterceptorFactory(GrpcTelemetry::buildServerInterceptor); + } + /** Returns a new {@link GrpcTelemetry} configured with the given {@link OpenTelemetry}. */ public static GrpcTelemetry create(OpenTelemetry openTelemetry) { return builder(openTelemetry).build(); @@ -52,11 +58,30 @@ public ClientInterceptor createClientInterceptor() { clientInstrumenter, propagators, captureExperimentalSpanAttributes, emitMessageEvents); } + /** + * Configures a {@link ServerBuilder} with both the server interceptor and the stream tracer + * factory. The interceptor handles registered service methods, while the stream tracer factory + * creates spans for requests to unregistered services that are not seen by server interceptors. + */ + public void configureServerBuilder(ServerBuilder serverBuilder) { + serverBuilder.intercept(buildServerInterceptor()); + serverBuilder.addStreamTracerFactory( + new TracingServerStreamTracerFactory(serverInstrumenter, propagators)); + } + /** * Returns a new {@link ServerInterceptor} for use with methods like {@link * io.grpc.ServerBuilder#intercept(ServerInterceptor)}. + * + * @deprecated Use {@link #configureServerBuilder(ServerBuilder)} instead, which also registers + * the stream tracer factory needed to capture requests to unregistered services. */ + @Deprecated public ServerInterceptor createServerInterceptor() { + return buildServerInterceptor(); + } + + ServerInterceptor buildServerInterceptor() { return new TracingServerInterceptor( serverInstrumenter, captureExperimentalSpanAttributes, emitMessageEvents); } diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetryBuilder.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetryBuilder.java index 9e4b34fc139d..f45ca416756f 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetryBuilder.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetryBuilder.java @@ -201,7 +201,7 @@ public GrpcTelemetry build() { serverInstrumenterBuilder, RpcSizeAttributesExtractor.create(rpcAttributesGetter)); return new GrpcTelemetry( - serverInstrumenterBuilder.buildServerInstrumenter(new GrpcRequestGetter()), + serverInstrumenterBuilder.buildServerInstrumenter(GrpcRequestGetter.INSTANCE), // gRPC client interceptors require two phases, one to set up request and one to execute. // So we go ahead and inject manually in this instrumentation. clientInstrumenterBuilder.buildInstrumenter(SpanKindExtractor.alwaysClient()), diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerInterceptor.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerInterceptor.java index 921b38c2b317..ebf0a3826a05 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerInterceptor.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerInterceptor.java @@ -70,6 +70,14 @@ public ServerCall.Listener interceptCall( // field. authority = GrpcAuthorityStorage.getAuthority(call); } + + // If a ServerStreamTracer is active, mark it as handled so it won't create a span for this + // request in streamClosed(). + TracingServerStreamTracer streamTracer = TracingServerStreamTracer.STREAM_TRACER_KEY.get(); + if (streamTracer != null) { + streamTracer.markInterceptorHandled(); + } + GrpcRequest request = new GrpcRequest( call.getMethodDescriptor(), diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerStreamTracer.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerStreamTracer.java new file mode 100644 index 000000000000..46f6e05600ae --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerStreamTracer.java @@ -0,0 +1,96 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableRpcSemconv; + +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerStreamTracer; +import io.grpc.Status; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil; +import java.net.SocketAddress; +import java.time.Instant; +import javax.annotation.Nullable; + +/** + * A {@link ServerStreamTracer} that detects whether a gRPC server request was handled by the {@link + * TracingServerInterceptor}. If the interceptor does not fire (unregistered method), {@link + * #streamClosed(Status)} creates a span for the unhandled request. + */ +final class TracingServerStreamTracer extends ServerStreamTracer { + + static final io.grpc.Context.Key STREAM_TRACER_KEY = + io.grpc.Context.key("otel-grpc-stream-tracer"); + + private static final String UNKNOWN_METHOD_SPAN_NAME = "_OTHER"; + + private final Instrumenter instrumenter; + private final ContextPropagators propagators; + private final String fullMethodName; + private final Metadata headers; + private final Context parentContext; + private final Instant startTime; + + private volatile boolean interceptorHandled; + @Nullable private volatile SocketAddress peerAddress; + + TracingServerStreamTracer( + Instrumenter instrumenter, + ContextPropagators propagators, + String fullMethodName, + Metadata headers, + Context parentContext) { + this.instrumenter = instrumenter; + this.propagators = propagators; + this.fullMethodName = fullMethodName; + this.headers = headers; + this.parentContext = parentContext; + this.startTime = Instant.now(); + } + + void markInterceptorHandled() { + interceptorHandled = true; + } + + @Override + public io.grpc.Context filterContext(io.grpc.Context context) { + return context.withValue(STREAM_TRACER_KEY, this); + } + + @Override + public void serverCallStarted(ServerCall call) { + if (peerAddress == null) { + SocketAddress addr = call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + if (addr != null) { + peerAddress = addr; + } + } + } + + @Override + public void streamClosed(Status status) { + if (interceptorHandled || !emitStableRpcSemconv()) { + return; + } + // Interceptor did not fire — this is an unregistered method + GrpcRequest request = new GrpcRequest(UNKNOWN_METHOD_SPAN_NAME, fullMethodName, headers); + if (peerAddress != null) { + request.setPeerSocketAddress(peerAddress); + } + // Extract trace context from incoming headers (e.g. W3C traceparent) + Context extracted = + propagators.getTextMapPropagator().extract(parentContext, request, GrpcRequestGetter.INSTANCE); + if (instrumenter.shouldStart(extracted, request)) { + InstrumenterUtil.startAndEnd( + instrumenter, extracted, request, status, status.getCause(), startTime, Instant.now()); + } + } +} diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerStreamTracerFactory.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerStreamTracerFactory.java new file mode 100644 index 000000000000..e79cdd3fb81b --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerStreamTracerFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6; + +import io.grpc.Metadata; +import io.grpc.ServerStreamTracer; +import io.grpc.Status; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; + +final class TracingServerStreamTracerFactory extends ServerStreamTracer.Factory { + + private final Instrumenter instrumenter; + private final ContextPropagators propagators; + + TracingServerStreamTracerFactory( + Instrumenter instrumenter, ContextPropagators propagators) { + this.instrumenter = instrumenter; + this.propagators = propagators; + } + + @Override + public ServerStreamTracer newServerStreamTracer(String fullMethodName, Metadata headers) { + return new TracingServerStreamTracer( + instrumenter, propagators, fullMethodName, headers, Context.current()); + } +} diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/Internal.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/Internal.java new file mode 100644 index 000000000000..318e6f07e974 --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/Internal.java @@ -0,0 +1,36 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6.internal; + +import static java.util.Objects.requireNonNull; + +import io.grpc.ServerInterceptor; +import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry; +import java.util.function.Function; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class Internal { + + @Nullable + private static volatile Function serverInterceptorFactory; + + public static void setServerInterceptorFactory( + Function factory) { + serverInterceptorFactory = factory; + } + + public static ServerInterceptor createServerInterceptor(GrpcTelemetry telemetry) { + // serverInterceptorFactory is guaranteed non-null because GrpcTelemetry registers it during + // static initialization, before a GrpcTelemetry instance can be passed here + return requireNonNull(serverInterceptorFactory, "serverInterceptorFactory").apply(telemetry); + } + + private Internal() {} +} diff --git a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcStreamingTest.java b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcStreamingTest.java index f7323d13e043..ecd04f307b6b 100644 --- a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcStreamingTest.java +++ b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcStreamingTest.java @@ -18,8 +18,8 @@ class GrpcStreamingTest extends AbstractGrpcStreamingTest { @Override protected ServerBuilder configureServer(ServerBuilder server) { - return server.intercept( - GrpcTelemetry.create(testing.getOpenTelemetry()).createServerInterceptor()); + GrpcTelemetry.create(testing.getOpenTelemetry()).configureServerBuilder(server); + return server; } @Override diff --git a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTest.java b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTest.java index c3dacecba415..5497772449ab 100644 --- a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTest.java +++ b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTest.java @@ -41,11 +41,12 @@ class GrpcTest extends AbstractGrpcTest { @Override protected ServerBuilder configureServer(ServerBuilder server) { - return server.intercept( + GrpcTelemetry telemetry = GrpcTelemetry.builder(testing.getOpenTelemetry()) .setCapturedServerRequestMetadata(singletonList(SERVER_REQUEST_METADATA_KEY)) - .build() - .createServerInterceptor()); + .build(); + telemetry.configureServerBuilder(server); + return server; } @Override @@ -81,17 +82,14 @@ public void sayHello( } }; - Server server = - ServerBuilder.forPort(0) - .addService(greeter) - .intercept( - GrpcTelemetry.builder(testing.getOpenTelemetry()) - .addAttributesExtractor(new CustomAttributesExtractor()) - .addServerAttributeExtractor(new CustomAttributesExtractorV2("serverSideValue")) - .build() - .createServerInterceptor()) - .build() - .start(); + GrpcTelemetry serverTelemetry = + GrpcTelemetry.builder(testing.getOpenTelemetry()) + .addAttributesExtractor(new CustomAttributesExtractor()) + .addServerAttributeExtractor(new CustomAttributesExtractorV2("serverSideValue")) + .build(); + ServerBuilder serverBuilder = ServerBuilder.forPort(0).addService(greeter); + serverTelemetry.configureServerBuilder(serverBuilder); + Server server = serverBuilder.build().start(); ManagedChannel channel = createChannel( diff --git a/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcTest.java b/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcTest.java index 00518a652b1d..e2ed56a122a3 100644 --- a/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcTest.java +++ b/instrumentation/grpc-1.6/testing/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/AbstractGrpcTest.java @@ -24,6 +24,7 @@ import static io.opentelemetry.semconv.incubating.MessageIncubatingAttributes.MESSAGE_TYPE; import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE; import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_METHOD; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_METHOD_ORIGINAL; import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_RESPONSE_STATUS_CODE; import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SERVICE; import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SYSTEM; @@ -69,6 +70,7 @@ import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.util.ThrowingRunnable; import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; import io.opentelemetry.sdk.trace.data.StatusData; import java.util.ArrayList; import java.util.List; @@ -79,6 +81,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -810,6 +813,78 @@ private static Stream provideErrorArguments() { arguments(Status.NOT_FOUND.withDescription("some description"))); } + @Test + void unknownService() throws Exception { + // Create a server without GreeterService registered + Server server = configureServer(ServerBuilder.forPort(0)).build().start(); + ManagedChannel channel = createChannel(server); + closer.add(() -> channel.shutdownNow().awaitTermination(10, SECONDS)); + closer.add(() -> server.shutdownNow().awaitTermination()); + + GreeterGrpc.GreeterBlockingStub client = GreeterGrpc.newBlockingStub(channel); + + Helloworld.Request request = Helloworld.Request.newBuilder().setName("test").build(); + assertThatThrownBy(() -> client.sayHello(request)) + .isInstanceOfSatisfying( + StatusRuntimeException.class, + t -> assertThat(t.getStatus().getCode()).isEqualTo(Status.Code.UNIMPLEMENTED)); + + List> spanAsserts = new ArrayList<>(); + spanAsserts.add( + span -> + span.hasName("example.Greeter/SayHello") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + addExtraClientAttributes( + experimentalSatisfies( + GRPC_RECEIVED_MESSAGE_COUNT, v -> assertThat(v).isEqualTo(0)), + experimentalSatisfies( + GRPC_SENT_MESSAGE_COUNT, v -> assertThat(v).isGreaterThan(0)), + equalTo(RPC_SYSTEM, emitOldRpcSemconv() ? "grpc" : null), + equalTo(RPC_SYSTEM_NAME, emitStableRpcSemconv() ? "grpc" : null), + equalTo(RPC_SERVICE, emitOldRpcSemconv() ? "example.Greeter" : null), + equalTo( + RPC_METHOD, + emitStableRpcSemconv() ? "example.Greeter/SayHello" : "SayHello"), + equalTo( + RPC_GRPC_STATUS_CODE, + emitOldRpcSemconv() ? (long) Status.Code.UNIMPLEMENTED.value() : null), + equalTo( + RPC_RESPONSE_STATUS_CODE, + emitStableRpcSemconv() ? Status.Code.UNIMPLEMENTED.name() : null), + equalTo(SERVER_ADDRESS, "localhost"), + equalTo(SERVER_PORT, (long) server.getPort())))); + + testing() + .waitAndAssertTraces( + trace -> { + List> allAsserts = new ArrayList<>(spanAsserts); + if (emitStableRpcSemconv()) { + allAsserts.add( + span -> + span.hasName("_OTHER") + .hasKind(SpanKind.SERVER) + .hasParent(trace.getSpan(0)) + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo(RPC_SYSTEM, emitOldRpcSemconv() ? "grpc" : null), + equalTo(RPC_SYSTEM_NAME, "grpc"), + equalTo( + RPC_GRPC_STATUS_CODE, + emitOldRpcSemconv() + ? (long) Status.Code.UNIMPLEMENTED.value() + : null), + equalTo(RPC_METHOD, "_OTHER"), + equalTo(RPC_METHOD_ORIGINAL, "example.Greeter/SayHello"), + equalTo( + RPC_RESPONSE_STATUS_CODE, Status.Code.UNIMPLEMENTED.name()))); + } + trace.hasSpansSatisfyingExactly(allAsserts); + }); + } + @Test void userContextPreserved() throws Exception { Context.Key key = Context.key("cat");