From dd571419c854dd0caec618c63d9f9e2ca02e2aff Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Tue, 3 Mar 2026 14:24:18 -0800 Subject: [PATCH 1/7] Capture gRPC UNKNOWN requests --- .../semconv/rpc/RpcAttributesGetter.java | 9 ++ .../rpc/RpcCommonAttributesExtractor.java | 3 + .../javaagent/build.gradle.kts | 7 +- ...eriaGrpcServiceBuilderInstrumentation.java | 4 +- .../grpc/v1_14/ArmeriaGrpcSingletons.java | 23 ++++ .../armeria/grpc/v1_14/ArmeriaGrpcTest.java | 106 ++++++++++++++++++ .../GrpcServerBuilderInstrumentation.java | 2 +- .../grpc/v1_6/GrpcSingletons.java | 12 +- instrumentation/grpc-1.6/library/README.md | 6 +- .../grpc/v1_6/GrpcRequest.java | 23 +++- .../grpc/v1_6/GrpcRpcAttributesGetter.java | 12 +- .../grpc/v1_6/GrpcSpanNameExtractor.java | 2 +- .../grpc/v1_6/GrpcTelemetry.java | 25 +++++ .../grpc/v1_6/TracingServerInterceptor.java | 8 ++ .../grpc/v1_6/TracingServerStreamTracer.java | 98 ++++++++++++++++ .../TracingServerStreamTracerFactory.java | 31 +++++ .../grpc/v1_6/internal/Internal.java | 30 +++++ .../grpc/v1_6/GrpcStreamingTest.java | 4 +- .../instrumentation/grpc/v1_6/GrpcTest.java | 26 ++--- .../grpc/v1_6/AbstractGrpcTest.java | 75 +++++++++++++ 20 files changed, 472 insertions(+), 34 deletions(-) create mode 100644 instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcSingletons.java create mode 100644 instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerStreamTracer.java create mode 100644 instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerStreamTracerFactory.java create mode 100644 instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/Internal.java 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 397cd3711fcf..8d821179afa4 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 f437a74a75dc..4277ffc912b1 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,7 +62,11 @@ afterEvaluate { tasks { withType().configureEach { - systemProperty("collectMetadata", findProperty("collectMetadata")) + systemProperty("collectMetadata", findProperty("collectMetadata")?.toString() ?: "false") + // 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 e9af59b48f6e..521690e4b7b4 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) 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 ba75fa457c17..4e8399b63587 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,66 @@ 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-level response rather than gRPC UNIMPLEMENTED. The armeria HTTP + // instrumentation captures this as an HTTP server span. + testing.waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("example.Greeter/SayHello") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasStatus(StatusData.error()) + .hasAttributesSatisfyingExactly( + equalTo(maybeStable(RPC_SYSTEM), "grpc"), + 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 92b3d1303b59..41cd0117bea7 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 @@ -49,7 +49,7 @@ public static CallDepth onEnter(@Advice.This ServerBuilder serverBuilder) { return callDepth; } if (!Boolean.TRUE.equals(SERVER_BUILDER_INSTRUMENTED.get(serverBuilder))) { - serverBuilder.intercept(GrpcSingletons.SERVER_INTERCEPTOR); + 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 a28f136fa76b..2ce252d56328 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; @@ -33,7 +32,7 @@ public final class GrpcSingletons { public static final ClientInterceptor CLIENT_INTERCEPTOR; - public static final ServerInterceptor SERVER_INTERCEPTOR; + private static final GrpcTelemetry GRPC_TELEMETRY; private static final AtomicReference STORAGE_REFERENCE = new AtomicReference<>(); @@ -56,7 +55,7 @@ public final class GrpcSingletons { .get("server") .getScalarList("request", String.class, emptyList()); - GrpcTelemetry telemetry = + GRPC_TELEMETRY = GrpcTelemetry.builder(GlobalOpenTelemetry.get()) .setEmitMessageEvents(emitMessageEvents) .setCaptureExperimentalSpanAttributes(experimentalSpanAttributes) @@ -64,8 +63,7 @@ public final class GrpcSingletons { .setCapturedServerRequestMetadata(serverRequestMetadata) .build(); - CLIENT_INTERCEPTOR = telemetry.createClientInterceptor(); - SERVER_INTERCEPTOR = telemetry.createServerInterceptor(); + CLIENT_INTERCEPTOR = GRPC_TELEMETRY.createClientInterceptor(); } public static Context.Storage getStorage() { @@ -77,5 +75,9 @@ public static Context.Storage setStorage(Context.Storage storage) { return getStorage(); } + public static void configureServerBuilder(ServerBuilder serverBuilder) { + GRPC_TELEMETRY.configureServerBuilder(serverBuilder); + } + private GrpcSingletons() {} } diff --git a/instrumentation/grpc-1.6/library/README.md b/instrumentation/grpc-1.6/library/README.md index 09674fbb97a5..513baf22cf7e 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 36bf6f433d3a..49d2bc35bf88 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/GrpcRpcAttributesGetter.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRpcAttributesGetter.java index 6099928e2ab4..375221007fc3 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 d4c97c451144..34f2f66470c0 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,15 +6,21 @@ 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 { + 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(); @@ -53,11 +59,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/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..b77b0b3dd07e --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerStreamTracer.java @@ -0,0 +1,98 @@ +/* + * 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..6bc096798134 --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/Internal.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6.internal; + +import io.grpc.ServerInterceptor; +import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry; +import java.util.function.Function; + +/** + * 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 { + + private static volatile Function serverInterceptorFactory; + + public static void setServerInterceptorFactory( + Function factory) { + serverInterceptorFactory = factory; + } + + public static ServerInterceptor createServerInterceptor(GrpcTelemetry telemetry) { + return 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 836afc83edc2..3eccc393b1a0 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 @@ -44,11 +44,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 @@ -84,17 +85,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 493936506493..c57d0045fddb 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 @@ -23,6 +23,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; @@ -68,6 +69,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; @@ -78,6 +80,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; @@ -809,6 +812,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"); From f4b7a3c72c3853ca3d45fe20a0da8137800e7281 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 27 Mar 2026 09:25:35 -0700 Subject: [PATCH 2/7] Fix grpc stream tracer request getter --- .../instrumentation/grpc/v1_6/TracingServerStreamTracer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index b77b0b3dd07e..5f5dc422a4bf 100644 --- 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 @@ -89,7 +89,7 @@ public void streamClosed(Status status) { Context extracted = propagators .getTextMapPropagator() - .extract(parentContext, request, GrpcRequestGetter.INSTANCE); + .extract(parentContext, request, new GrpcRequestGetter()); if (instrumenter.shouldStart(extracted, request)) { InstrumenterUtil.startAndEnd( instrumenter, extracted, request, status, status.getCause(), startTime, Instant.now()); From baa27474be0fbaa964d89aa0c7a134e34f706215 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 27 Mar 2026 09:31:08 -0700 Subject: [PATCH 3/7] spotless --- .../instrumentation/grpc/v1_6/TracingServerStreamTracer.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 index 5f5dc422a4bf..6b4f41c0db58 100644 --- 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 @@ -87,9 +87,7 @@ public void streamClosed(Status status) { } // Extract trace context from incoming headers (e.g. W3C traceparent) Context extracted = - propagators - .getTextMapPropagator() - .extract(parentContext, request, new GrpcRequestGetter()); + propagators.getTextMapPropagator().extract(parentContext, request, new GrpcRequestGetter()); if (instrumenter.shouldStart(extracted, request)) { InstrumenterUtil.startAndEnd( instrumenter, extracted, request, status, status.getCause(), startTime, Instant.now()); From 2368a7cf0b86a17f8d700c0c7ff2b1198c9700c3 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Fri, 27 Mar 2026 10:41:41 -0700 Subject: [PATCH 4/7] Fix armeria grpc test semconv assertion --- .../instrumentation/armeria/grpc/v1_14/ArmeriaGrpcTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 4e8399b63587..01f109eb1f53 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 @@ -206,7 +206,8 @@ void unknownService() { .hasNoParent() .hasStatus(StatusData.error()) .hasAttributesSatisfyingExactly( - equalTo(maybeStable(RPC_SYSTEM), "grpc"), + equalTo(RPC_SYSTEM, emitOldRpcSemconv() ? "grpc" : null), + equalTo(RPC_SYSTEM_NAME, emitStableRpcSemconv() ? "grpc" : null), equalTo(RPC_SERVICE, emitOldRpcSemconv() ? "example.Greeter" : null), equalTo( RPC_METHOD, From 396cbab446f9e720a72a7da6fe5dbabe7da4f4fa Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 11 May 2026 19:09:20 -0700 Subject: [PATCH 5/7] Make Internal.serverInterceptorFactory null-safety explicit Mark the field @Nullable and use requireNonNull at the call site, matching the pattern used in #18291. Add a comment noting that the factory is registered in GrpcTelemetry's static initializer before any GrpcTelemetry instance can be passed here. --- .../instrumentation/grpc/v1_6/internal/Internal.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 index 6bc096798134..318e6f07e974 100644 --- 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 @@ -5,9 +5,12 @@ 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 @@ -15,6 +18,7 @@ */ public final class Internal { + @Nullable private static volatile Function serverInterceptorFactory; public static void setServerInterceptorFactory( @@ -23,7 +27,9 @@ public static void setServerInterceptorFactory( } public static ServerInterceptor createServerInterceptor(GrpcTelemetry telemetry) { - return serverInterceptorFactory.apply(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() {} From 8846d666aa09be9468245c4bb5a9a8e3d2568b97 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 11 May 2026 19:10:18 -0700 Subject: [PATCH 6/7] Clarify unknownService test comment --- .../armeria/grpc/v1_14/ArmeriaGrpcTest.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 671c60361e9b..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 @@ -195,8 +195,11 @@ void unknownService() { assertThatThrownBy(() -> client.sayHello(request)).isInstanceOf(StatusRuntimeException.class); // Armeria uses exact-route binding for gRPC services, so calling an unregistered service - // results in an HTTP-level response rather than gRPC UNIMPLEMENTED. The armeria HTTP - // instrumentation captures this as an HTTP server span. + // 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( From be53a9ae45d942e8fa9ab4cb13a6024c356cffcb Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 11 May 2026 19:57:45 -0700 Subject: [PATCH 7/7] up --- .../instrumentation/grpc/v1_6/GrpcRequestGetter.java | 2 ++ .../instrumentation/grpc/v1_6/GrpcTelemetryBuilder.java | 2 +- .../instrumentation/grpc/v1_6/TracingServerStreamTracer.java | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) 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/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/TracingServerStreamTracer.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingServerStreamTracer.java index 6b4f41c0db58..46f6e05600ae 100644 --- 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 @@ -87,7 +87,7 @@ public void streamClosed(Status status) { } // Extract trace context from incoming headers (e.g. W3C traceparent) Context extracted = - propagators.getTextMapPropagator().extract(parentContext, request, new GrpcRequestGetter()); + propagators.getTextMapPropagator().extract(parentContext, request, GrpcRequestGetter.INSTANCE); if (instrumenter.shouldStart(extracted, request)) { InstrumenterUtil.startAndEnd( instrumenter, extracted, request, status, status.getCause(), startTime, Instant.now());