Skip to content

Commit d082557

Browse files
committed
Capture gRPC UNKNOWN requests
1 parent c639fe5 commit d082557

21 files changed

Lines changed: 477 additions & 33 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## Unreleased
44

5+
### ⚠️ Breaking changes to non-stable APIs
6+
7+
- `GrpcRequest.getMethod()` now returns `@Nullable` to support unregistered-service requests where
8+
no `MethodDescriptor` is available
9+
([#16214](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/16214))
10+
511
## Version 2.25.0 (2026-02-13)
612

713
### ⚠️ Breaking changes to non-stable APIs

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesGetter.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ default String getRpcMethod(REQUEST request) {
6868
return null;
6969
}
7070

71+
/**
72+
* Returns the original method name when the method reported via {@link #getRpcMethod(REQUEST)} is
73+
* set to {@code _OTHER} because the method is not recognized by the RPC framework.
74+
*/
75+
@Nullable
76+
default String getRpcMethodOriginal(REQUEST request) {
77+
return null;
78+
}
79+
7180
/**
7281
* Returns a description of a class of error the operation ended with.
7382
*

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcCommonAttributesExtractor.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ abstract class RpcCommonAttributesExtractor<REQUEST, RESPONSE>
1919
implements AttributesExtractor<REQUEST, RESPONSE> {
2020

2121
static final AttributeKey<String> RPC_METHOD = AttributeKey.stringKey("rpc.method");
22+
static final AttributeKey<String> RPC_METHOD_ORIGINAL =
23+
AttributeKey.stringKey("rpc.method_original");
2224

2325
// Stable semconv keys
2426
static final AttributeKey<String> RPC_SYSTEM_NAME = AttributeKey.stringKey("rpc.system.name");
@@ -42,6 +44,7 @@ public final void onStart(AttributesBuilder attributes, Context parentContext, R
4244
if (emitStableRpcSemconv()) {
4345
attributes.put(RPC_SYSTEM_NAME, getter.getRpcSystemName(request));
4446
attributes.put(RPC_METHOD, getter.getRpcMethod(request));
47+
attributes.put(RPC_METHOD_ORIGINAL, getter.getRpcMethodOriginal(request));
4548
}
4649

4750
if (emitOldRpcSemconv()) {

instrumentation/armeria/armeria-grpc-1.14/javaagent/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies {
1818
library("com.linecorp.armeria:armeria-grpc:1.14.0")
1919
implementation(project(":instrumentation:grpc-1.6:library"))
2020

21+
testInstrumentation(project(":instrumentation:armeria:armeria-1.3:javaagent"))
2122
testInstrumentation(project(":instrumentation:netty:netty-4.1:javaagent"))
2223
testInstrumentation(project(":instrumentation:grpc-1.6:javaagent"))
2324

@@ -62,6 +63,10 @@ afterEvaluate {
6263
tasks {
6364
withType<Test>().configureEach {
6465
systemProperty("collectMetadata", findProperty("collectMetadata")?.toString() ?: "false")
66+
// The armeria HTTP instrumentation creates an HTTP server span, and then the gRPC
67+
// interceptor creates a second server span from the same incoming context, which triggers the
68+
// context leak debugger.
69+
jvmArgs("-Dotel.javaagent.experimental.thread-propagation-debugger.enabled=false")
6570
}
6671

6772
val testStableSemconv by registering(Test::class) {

instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcServiceBuilderInstrumentation.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import static net.bytebuddy.matcher.ElementMatchers.named;
1111

1212
import com.linecorp.armeria.server.grpc.GrpcServiceBuilder;
13-
import io.opentelemetry.api.GlobalOpenTelemetry;
14-
import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry;
1513
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
1614
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
1715
import net.bytebuddy.asm.Advice;
@@ -36,7 +34,7 @@ public static class BuildAdvice {
3634

3735
@Advice.OnMethodEnter
3836
public static void onEnter(@Advice.This GrpcServiceBuilder builder) {
39-
builder.intercept(GrpcTelemetry.create(GlobalOpenTelemetry.get()).createServerInterceptor());
37+
builder.intercept(ArmeriaGrpcSingletons.SERVER_INTERCEPTOR);
4038
}
4139
}
4240
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14;
7+
8+
import io.grpc.ServerInterceptor;
9+
import io.opentelemetry.api.GlobalOpenTelemetry;
10+
import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry;
11+
import io.opentelemetry.instrumentation.grpc.v1_6.internal.Internal;
12+
13+
public final class ArmeriaGrpcSingletons {
14+
15+
public static final ServerInterceptor SERVER_INTERCEPTOR;
16+
17+
static {
18+
GrpcTelemetry telemetry = GrpcTelemetry.create(GlobalOpenTelemetry.get());
19+
SERVER_INTERCEPTOR = Internal.createServerInterceptor(telemetry);
20+
}
21+
22+
private ArmeriaGrpcSingletons() {}
23+
}

instrumentation/armeria/armeria-grpc-1.14/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcTest.java

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,19 @@
99
import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableRpcSemconv;
1010
import static io.opentelemetry.instrumentation.testing.junit.rpc.SemconvRpcStabilityUtil.maybeStable;
1111
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
12+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies;
13+
import static io.opentelemetry.semconv.ClientAttributes.CLIENT_ADDRESS;
14+
import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD;
15+
import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE;
16+
import static io.opentelemetry.semconv.HttpAttributes.HTTP_ROUTE;
17+
import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_ADDRESS;
18+
import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PEER_PORT;
19+
import static io.opentelemetry.semconv.NetworkAttributes.NETWORK_PROTOCOL_VERSION;
1220
import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS;
1321
import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT;
22+
import static io.opentelemetry.semconv.UrlAttributes.URL_PATH;
23+
import static io.opentelemetry.semconv.UrlAttributes.URL_SCHEME;
24+
import static io.opentelemetry.semconv.UserAgentAttributes.USER_AGENT_ORIGINAL;
1425
import static io.opentelemetry.semconv.incubating.MessageIncubatingAttributes.MESSAGE_ID;
1526
import static io.opentelemetry.semconv.incubating.MessageIncubatingAttributes.MESSAGE_TYPE;
1627
import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_GRPC_STATUS_CODE;
@@ -19,6 +30,7 @@
1930
import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SERVICE;
2031
import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SYSTEM;
2132
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2234

2335
import com.linecorp.armeria.client.grpc.GrpcClients;
2436
import com.linecorp.armeria.server.ServerBuilder;
@@ -27,9 +39,12 @@
2739
import example.GreeterGrpc;
2840
import example.Helloworld;
2941
import io.grpc.Status;
42+
import io.grpc.StatusRuntimeException;
43+
import io.grpc.protobuf.services.HealthStatusManager;
3044
import io.grpc.stub.StreamObserver;
3145
import io.opentelemetry.api.trace.SpanKind;
3246
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension;
47+
import io.opentelemetry.sdk.trace.data.StatusData;
3348
import org.junit.jupiter.api.Test;
3449
import org.junit.jupiter.api.extension.RegisterExtension;
3550

@@ -62,6 +77,18 @@ public void sayHello(
6277
}
6378
};
6479

80+
@RegisterExtension
81+
static final ServerExtension serverWithoutGreeter =
82+
new ServerExtension() {
83+
@Override
84+
protected void configure(ServerBuilder sb) {
85+
sb.service(
86+
GrpcService.builder()
87+
.addService(new HealthStatusManager().getHealthService())
88+
.build());
89+
}
90+
};
91+
6592
@SuppressWarnings("deprecation") // using deprecated semconv
6693
@Test
6794
void grpcInstrumentation() {
@@ -107,6 +134,23 @@ void grpcInstrumentation() {
107134
.hasAttributesSatisfyingExactly(
108135
equalTo(MESSAGE_TYPE, "RECEIVED"),
109136
equalTo(MESSAGE_ID, 1L))),
137+
span ->
138+
span.hasName("POST /example.Greeter/SayHello")
139+
.hasKind(SpanKind.SERVER)
140+
.hasParent(trace.getSpan(1))
141+
.hasAttributesSatisfyingExactly(
142+
equalTo(HTTP_REQUEST_METHOD, "POST"),
143+
equalTo(HTTP_RESPONSE_STATUS_CODE, 200),
144+
equalTo(HTTP_ROUTE, "/example.Greeter/SayHello"),
145+
equalTo(URL_PATH, "/example.Greeter/SayHello"),
146+
equalTo(URL_SCHEME, "http"),
147+
equalTo(SERVER_ADDRESS, "127.0.0.1"),
148+
equalTo(SERVER_PORT, (long) server.httpPort()),
149+
equalTo(CLIENT_ADDRESS, "127.0.0.1"),
150+
equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"),
151+
satisfies(NETWORK_PEER_PORT, port -> port.isInstanceOf(Long.class)),
152+
equalTo(NETWORK_PROTOCOL_VERSION, "2"),
153+
satisfies(USER_AGENT_ORIGINAL, val -> val.startsWith("armeria/"))),
110154
span ->
111155
span.hasName("example.Greeter/SayHello")
112156
.hasKind(SpanKind.SERVER)
@@ -137,4 +181,66 @@ void grpcInstrumentation() {
137181
.hasAttributesSatisfyingExactly(
138182
equalTo(MESSAGE_TYPE, "SENT"), equalTo(MESSAGE_ID, 1L)))));
139183
}
184+
185+
@SuppressWarnings("deprecation") // using deprecated semconv
186+
@Test
187+
void unknownService() {
188+
GreeterGrpc.GreeterBlockingStub client =
189+
GrpcClients.builder(serverWithoutGreeter.httpUri())
190+
.build(GreeterGrpc.GreeterBlockingStub.class);
191+
192+
Helloworld.Request request = Helloworld.Request.newBuilder().setName("test").build();
193+
assertThatThrownBy(() -> client.sayHello(request)).isInstanceOf(StatusRuntimeException.class);
194+
195+
// Armeria uses exact-route binding for gRPC services, so calling an unregistered service
196+
// results in an HTTP-level response rather than gRPC UNIMPLEMENTED. The armeria HTTP
197+
// instrumentation captures this as an HTTP server span.
198+
testing.waitAndAssertTraces(
199+
trace ->
200+
trace.hasSpansSatisfyingExactly(
201+
span ->
202+
span.hasName("example.Greeter/SayHello")
203+
.hasKind(SpanKind.CLIENT)
204+
.hasNoParent()
205+
.hasStatus(StatusData.error())
206+
.hasAttributesSatisfyingExactly(
207+
equalTo(maybeStable(RPC_SYSTEM), "grpc"),
208+
equalTo(RPC_SERVICE, emitOldRpcSemconv() ? "example.Greeter" : null),
209+
equalTo(
210+
RPC_METHOD,
211+
emitStableRpcSemconv() ? "example.Greeter/SayHello" : "SayHello"),
212+
equalTo(
213+
RPC_GRPC_STATUS_CODE,
214+
emitOldRpcSemconv()
215+
? (long) Status.Code.UNIMPLEMENTED.value()
216+
: null),
217+
equalTo(
218+
RPC_RESPONSE_STATUS_CODE,
219+
emitStableRpcSemconv() ? Status.Code.UNIMPLEMENTED.name() : null),
220+
equalTo(SERVER_ADDRESS, "127.0.0.1"),
221+
equalTo(SERVER_PORT, (long) serverWithoutGreeter.httpPort()))
222+
.hasEventsSatisfyingExactly(
223+
event ->
224+
event
225+
.hasName("message")
226+
.hasAttributesSatisfyingExactly(
227+
equalTo(MESSAGE_TYPE, "SENT"), equalTo(MESSAGE_ID, 1L))),
228+
span ->
229+
span.hasName("POST /*")
230+
.hasKind(SpanKind.SERVER)
231+
.hasParent(trace.getSpan(0))
232+
.hasAttributesSatisfyingExactly(
233+
equalTo(HTTP_REQUEST_METHOD, "POST"),
234+
equalTo(HTTP_RESPONSE_STATUS_CODE, 404),
235+
equalTo(HTTP_ROUTE, "/*"),
236+
equalTo(URL_PATH, "/example.Greeter/SayHello"),
237+
equalTo(URL_SCHEME, "http"),
238+
equalTo(SERVER_ADDRESS, "127.0.0.1"),
239+
equalTo(SERVER_PORT, (long) serverWithoutGreeter.httpPort()),
240+
equalTo(CLIENT_ADDRESS, "127.0.0.1"),
241+
equalTo(NETWORK_PEER_ADDRESS, "127.0.0.1"),
242+
satisfies(NETWORK_PEER_PORT, port -> port.isInstanceOf(Long.class)),
243+
equalTo(NETWORK_PROTOCOL_VERSION, "2"),
244+
satisfies(USER_AGENT_ORIGINAL, val -> val.startsWith("armeria/")))));
245+
}
140246
}

instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcServerBuilderInstrumentation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public static CallDepth onEnter(@Advice.This ServerBuilder<?> serverBuilder) {
5050
return callDepth;
5151
}
5252
if (!Boolean.TRUE.equals(SERVER_BUILDER_INSTRUMENTED.get(serverBuilder))) {
53-
serverBuilder.intercept(GrpcSingletons.SERVER_INTERCEPTOR);
53+
GrpcSingletons.configureServerBuilder(serverBuilder);
5454
SERVER_BUILDER_INSTRUMENTED.set(serverBuilder, true);
5555
}
5656
return callDepth;

instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcSingletons.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import io.grpc.Context;
1212
import io.grpc.ManagedChannelBuilder;
1313
import io.grpc.ServerBuilder;
14-
import io.grpc.ServerInterceptor;
1514
import io.opentelemetry.api.GlobalOpenTelemetry;
1615
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
1716
import io.opentelemetry.instrumentation.api.incubator.config.internal.DeclarativeConfigUtil;
@@ -33,7 +32,7 @@ public final class GrpcSingletons {
3332

3433
public static final ClientInterceptor CLIENT_INTERCEPTOR;
3534

36-
public static final ServerInterceptor SERVER_INTERCEPTOR;
35+
private static final GrpcTelemetry GRPC_TELEMETRY;
3736

3837
private static final AtomicReference<Context.Storage> STORAGE_REFERENCE = new AtomicReference<>();
3938

@@ -56,16 +55,15 @@ public final class GrpcSingletons {
5655
.get("server")
5756
.getScalarList("request", String.class, emptyList());
5857

59-
GrpcTelemetry telemetry =
58+
GRPC_TELEMETRY =
6059
GrpcTelemetry.builder(GlobalOpenTelemetry.get())
6160
.setEmitMessageEvents(emitMessageEvents)
6261
.setCaptureExperimentalSpanAttributes(experimentalSpanAttributes)
6362
.setCapturedClientRequestMetadata(clientRequestMetadata)
6463
.setCapturedServerRequestMetadata(serverRequestMetadata)
6564
.build();
6665

67-
CLIENT_INTERCEPTOR = telemetry.createClientInterceptor();
68-
SERVER_INTERCEPTOR = telemetry.createServerInterceptor();
66+
CLIENT_INTERCEPTOR = GRPC_TELEMETRY.createClientInterceptor();
6967
}
7068

7169
public static Context.Storage getStorage() {
@@ -77,5 +75,9 @@ public static Context.Storage setStorage(Context.Storage storage) {
7775
return getStorage();
7876
}
7977

78+
public static void configureServerBuilder(ServerBuilder<?> serverBuilder) {
79+
GRPC_TELEMETRY.configureServerBuilder(serverBuilder);
80+
}
81+
8082
private GrpcSingletons() {}
8183
}

instrumentation/grpc-1.6/library/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ void configureClientInterceptor(OpenTelemetry openTelemetry, NettyChannelBuilder
3737
nettyChannelBuilder.intercept(grpcTelemetry.createClientInterceptor());
3838
}
3939

40-
// For server-side, attatch the interceptor to your service.
41-
ServerServiceDefinition configureServerInterceptor(OpenTelemetry openTelemetry, ServerServiceDefinition serviceDefinition) {
40+
// For server-side, configure the server builder.
41+
void configureServer(OpenTelemetry openTelemetry, ServerBuilder<?> serverBuilder) {
4242
GrpcTelemetry grpcTelemetry = GrpcTelemetry.create(openTelemetry);
43-
return ServerInterceptors.intercept(serviceDefinition, grpcTelemetry.createServerInterceptor());
43+
grpcTelemetry.configureServerBuilder(serverBuilder);
4444
}
4545
```

0 commit comments

Comments
 (0)