Skip to content

Commit a1e958a

Browse files
committed
Add dual-semconv support to RPC attributes extractors and span name extractor
RpcCommonAttributesExtractor now conditionally emits old (rpc.system, rpc.service, rpc.method) and/or stable (rpc.system.name, rpc.method, rpc.method_original, error.type) attributes based on SemconvStability flags. RpcSpanNameExtractor uses getRpcMethod() for stable semconv with system name fallback. In dup mode, old rpc.method is omitted from spans to avoid clashing with stable rpc.method. Instead, a ContextCustomizer stores the old method value in context via a ContextKey for metrics to read. Also adds RpcAttributesGetter.isPredefined() for rpc.method vs rpc.method_original handling.
1 parent 02441e8 commit a1e958a

5 files changed

Lines changed: 294 additions & 27 deletions

File tree

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,31 @@ default String getErrorType(
6969
REQUEST request, @Nullable RESPONSE response, @Nullable Throwable error) {
7070
return null;
7171
}
72+
73+
/**
74+
* Returns whether the RPC method is recognized as a predefined method by the RPC framework or
75+
* library.
76+
*
77+
* <p>Some RPC frameworks or libraries provide a fixed set of recognized methods for client stubs
78+
* and server implementations. Instrumentations for such frameworks MUST return {@code true} only
79+
* when the method is recognized by the framework or library.
80+
*
81+
* <p>When the method is not recognized (for example, when the server receives a request for a
82+
* method that is not predefined on the server), or when instrumentation is not able to reliably
83+
* detect if the method is predefined, this method MUST return {@code false}.
84+
*
85+
* <p>When this method returns {@code false}, the {@code rpc.method} attribute will be set to
86+
* {@code "_OTHER"} and the {@code rpc.method_original} attribute will be set to the original
87+
* method name.
88+
*
89+
* <p>Note: If the RPC instrumentation could end up converting valid RPC methods to {@code
90+
* "_OTHER"}, then it SHOULD provide a way to configure the list of recognized RPC methods.
91+
*
92+
* @param request the request object
93+
* @return {@code true} if the method is recognized as predefined by the framework, {@code false}
94+
* otherwise
95+
*/
96+
default boolean isPredefined(REQUEST request) {
97+
return false;
98+
}
7299
}

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

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,51 @@
66
package io.opentelemetry.instrumentation.api.incubator.semconv.rpc;
77

88
import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet;
9+
import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE;
910

1011
import io.opentelemetry.api.common.AttributeKey;
1112
import io.opentelemetry.api.common.AttributesBuilder;
1213
import io.opentelemetry.context.Context;
14+
import io.opentelemetry.context.ContextKey;
1315
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
16+
import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer;
17+
import io.opentelemetry.instrumentation.api.internal.SemconvStability;
1418
import javax.annotation.Nullable;
1519

16-
abstract class RpcCommonAttributesExtractor<REQUEST, RESPONSE>
20+
public abstract class RpcCommonAttributesExtractor<REQUEST, RESPONSE>
1721
implements AttributesExtractor<REQUEST, RESPONSE> {
1822

19-
// copied from RpcIncubatingAttributes
2023
static final AttributeKey<String> RPC_METHOD = AttributeKey.stringKey("rpc.method");
24+
25+
static final ContextKey<String> OLD_RPC_METHOD_CONTEXT_KEY =
26+
ContextKey.named("otel-rpc-old-method");
27+
28+
@SuppressWarnings("deprecation") // for getMethod()
29+
public static <REQUEST> ContextCustomizer<REQUEST> oldMethodContextCustomizer(
30+
RpcAttributesGetter<REQUEST, ?> getter) {
31+
return (context, request, startAttributes) -> {
32+
if (SemconvStability.emitOldRpcSemconv() && SemconvStability.emitStableRpcSemconv()) {
33+
String oldMethod = getter.getMethod(request);
34+
if (oldMethod != null) {
35+
return context.with(OLD_RPC_METHOD_CONTEXT_KEY, oldMethod);
36+
}
37+
}
38+
return context;
39+
};
40+
}
41+
42+
// Stable semconv keys
43+
static final AttributeKey<String> RPC_SYSTEM_NAME = AttributeKey.stringKey("rpc.system.name");
44+
45+
// removed in stable semconv (merged into rpc.method)
2146
static final AttributeKey<String> RPC_SERVICE = AttributeKey.stringKey("rpc.service");
47+
48+
// use RPC_SYSTEM_NAME for stable semconv
2249
static final AttributeKey<String> RPC_SYSTEM = AttributeKey.stringKey("rpc.system");
2350

51+
static final AttributeKey<String> RPC_METHOD_ORIGINAL =
52+
AttributeKey.stringKey("rpc.method_original");
53+
2454
private final RpcAttributesGetter<REQUEST, RESPONSE> getter;
2555

2656
RpcCommonAttributesExtractor(RpcAttributesGetter<REQUEST, RESPONSE> getter) {
@@ -30,9 +60,30 @@ abstract class RpcCommonAttributesExtractor<REQUEST, RESPONSE>
3060
@SuppressWarnings("deprecation") // for getMethod()
3161
@Override
3262
public final void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {
33-
internalSet(attributes, RPC_SYSTEM, getter.getSystem(request));
34-
internalSet(attributes, RPC_SERVICE, getter.getService(request));
35-
internalSet(attributes, RPC_METHOD, getter.getMethod(request));
63+
String system = getter.getSystem(request);
64+
65+
if (SemconvStability.emitStableRpcSemconv()) {
66+
internalSet(
67+
attributes,
68+
RPC_SYSTEM_NAME,
69+
system == null ? null : SemconvStability.stableRpcSystemName(system));
70+
String method = getter.getRpcMethod(request);
71+
if (getter.isPredefined(request)) {
72+
internalSet(attributes, RPC_METHOD, method);
73+
} else {
74+
internalSet(attributes, RPC_METHOD_ORIGINAL, method);
75+
internalSet(attributes, RPC_METHOD, "_OTHER");
76+
}
77+
}
78+
79+
if (SemconvStability.emitOldRpcSemconv()) {
80+
internalSet(attributes, RPC_SYSTEM, system);
81+
internalSet(attributes, RPC_SERVICE, getter.getService(request));
82+
if (!SemconvStability.emitStableRpcSemconv()) {
83+
// only set old rpc.method on spans when there's no clash with stable rpc.method
84+
internalSet(attributes, RPC_METHOD, getter.getMethod(request));
85+
}
86+
}
3687
}
3788

3889
@Override
@@ -42,6 +93,13 @@ public final void onEnd(
4293
REQUEST request,
4394
@Nullable RESPONSE response,
4495
@Nullable Throwable error) {
45-
// No response attributes
96+
if (SemconvStability.emitStableRpcSemconv()) {
97+
String errorType = getter.getErrorType(request, response, error);
98+
// fall back to exception class name & _OTHER
99+
if (errorType == null && error != null) {
100+
errorType = error.getClass().getName();
101+
}
102+
internalSet(attributes, ERROR_TYPE, errorType);
103+
}
46104
}
47105
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package io.opentelemetry.instrumentation.api.incubator.semconv.rpc;
77

88
import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor;
9+
import io.opentelemetry.instrumentation.api.internal.SemconvStability;
910

1011
/** A {@link SpanNameExtractor} for RPC requests. */
1112
public final class RpcSpanNameExtractor<REQUEST> implements SpanNameExtractor<REQUEST> {
@@ -28,6 +29,18 @@ private RpcSpanNameExtractor(RpcAttributesGetter<REQUEST, ?> getter) {
2829
@SuppressWarnings("deprecation") // for getMethod()
2930
@Override
3031
public String extract(REQUEST request) {
32+
if (SemconvStability.emitStableRpcSemconv()) {
33+
String method = getter.getRpcMethod(request);
34+
if (method != null) {
35+
return method;
36+
}
37+
String system = getter.getSystem(request);
38+
if (system != null) {
39+
return system;
40+
}
41+
return "RPC request";
42+
}
43+
3144
String service = getter.getService(request);
3245
String method = getter.getMethod(request);
3346
if (service == null || method == null) {

instrumentation-api-incubator/src/test/java/io/opentelemetry/instrumentation/api/incubator/semconv/rpc/RpcAttributesExtractorTest.java

Lines changed: 153 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,34 @@
55

66
package io.opentelemetry.instrumentation.api.incubator.semconv.rpc;
77

8+
import static io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcCommonAttributesExtractor.RPC_METHOD_ORIGINAL;
89
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
10+
import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE;
911
import static org.assertj.core.api.Assertions.entry;
1012

13+
import io.opentelemetry.api.common.AttributeKey;
1114
import io.opentelemetry.api.common.Attributes;
1215
import io.opentelemetry.api.common.AttributesBuilder;
1316
import io.opentelemetry.context.Context;
1417
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
15-
import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes;
18+
import io.opentelemetry.instrumentation.api.internal.SemconvStability;
19+
import java.util.ArrayList;
1620
import java.util.HashMap;
21+
import java.util.List;
1722
import java.util.Map;
23+
import javax.annotation.Nullable;
1824
import org.junit.jupiter.api.Test;
1925

2026
@SuppressWarnings("deprecation") // using deprecated semconv
2127
class RpcAttributesExtractorTest {
2228

23-
enum TestGetter implements RpcAttributesGetter<Map<String, String>, Void> {
24-
INSTANCE;
29+
private static class TestGetter implements RpcAttributesGetter<Map<String, String>, Void> {
30+
31+
private final boolean predefined;
32+
33+
private TestGetter(boolean predefined) {
34+
this.predefined = predefined;
35+
}
2536

2637
@Override
2738
public String getSystem(Map<String, String> request) {
@@ -38,37 +49,163 @@ public String getService(Map<String, String> request) {
3849
public String getMethod(Map<String, String> request) {
3950
return request.get("method");
4051
}
52+
53+
@Nullable
54+
@Override
55+
public String getRpcMethod(Map<String, String> request) {
56+
String service = getService(request);
57+
String method = getMethod(request);
58+
if (service == null || method == null) {
59+
return null;
60+
}
61+
return service + "/" + method;
62+
}
63+
64+
@Nullable
65+
@Override
66+
public String getErrorType(
67+
Map<String, String> request, @Nullable Void response, @Nullable Throwable error) {
68+
return request.get("errorType");
69+
}
70+
71+
@Override
72+
public boolean isPredefined(Map<String, String> stringStringMap) {
73+
return predefined;
74+
}
4175
}
4276

4377
@Test
4478
void server() {
45-
testExtractor(RpcServerAttributesExtractor.create(TestGetter.INSTANCE));
79+
testExtractor(RpcServerAttributesExtractor.create(new TestGetter(false)), "my.Service/Method");
80+
}
81+
82+
@Test
83+
void serverPredefined() {
84+
testExtractor(RpcServerAttributesExtractor.create(new TestGetter(true)), null);
4685
}
4786

4887
@Test
4988
void client() {
50-
testExtractor(RpcClientAttributesExtractor.create(TestGetter.INSTANCE));
89+
testExtractor(RpcClientAttributesExtractor.create(new TestGetter(false)), "my.Service/Method");
90+
}
91+
92+
@Test
93+
void clientPredefined() {
94+
testExtractor(RpcClientAttributesExtractor.create(new TestGetter(true)), null);
95+
}
96+
97+
// Stable semconv keys
98+
private static final AttributeKey<String> RPC_SYSTEM_NAME =
99+
AttributeKey.stringKey("rpc.system.name");
100+
101+
// Old semconv keys (from RpcIncubatingAttributes)
102+
private static final AttributeKey<String> RPC_SYSTEM =
103+
io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SYSTEM;
104+
105+
private static final AttributeKey<String> RPC_SERVICE =
106+
io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SERVICE;
107+
108+
private static final AttributeKey<String> RPC_METHOD =
109+
io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_METHOD;
110+
111+
private static void testExtractor(
112+
AttributesExtractor<Map<String, String>, Void> extractor, @Nullable String originalMethod) {
113+
Map<String, String> request = new HashMap<>();
114+
request.put("service", "my.Service");
115+
request.put("method", "Method");
116+
117+
Context context = Context.root();
118+
119+
AttributesBuilder attributes = Attributes.builder();
120+
extractor.onStart(attributes, context, request);
121+
122+
// Build expected entries list based on semconv mode
123+
List<Map.Entry<? extends AttributeKey<?>, ?>> expectedEntries = new ArrayList<>();
124+
125+
if (SemconvStability.emitStableRpcSemconv()) {
126+
expectedEntries.add(entry(RPC_SYSTEM_NAME, "test"));
127+
if (originalMethod != null) {
128+
expectedEntries.add(entry(RPC_METHOD_ORIGINAL, originalMethod));
129+
expectedEntries.add(entry(RPC_METHOD, "_OTHER"));
130+
} else {
131+
expectedEntries.add(entry(RPC_METHOD, "my.Service/Method"));
132+
}
133+
}
134+
135+
if (SemconvStability.emitOldRpcSemconv()) {
136+
expectedEntries.add(entry(RPC_SYSTEM, "test"));
137+
expectedEntries.add(entry(RPC_SERVICE, "my.Service"));
138+
if (!SemconvStability.emitStableRpcSemconv()) {
139+
expectedEntries.add(entry(RPC_METHOD, "Method"));
140+
}
141+
}
142+
143+
// safe conversion for test assertions
144+
@SuppressWarnings({"unchecked", "rawtypes"})
145+
Map.Entry<? extends AttributeKey<?>, ?>[] expectedArray =
146+
(Map.Entry<? extends AttributeKey<?>, ?>[]) expectedEntries.toArray(new Map.Entry[0]);
147+
assertThat(attributes.build()).containsOnly(expectedArray);
148+
149+
extractor.onEnd(attributes, context, request, null, null);
150+
assertThat(attributes.build()).containsOnly(expectedArray);
51151
}
52152

53-
private static void testExtractor(AttributesExtractor<Map<String, String>, Void> extractor) {
153+
@Test
154+
void shouldExtractErrorType_getter() {
54155
Map<String, String> request = new HashMap<>();
55156
request.put("service", "my.Service");
56157
request.put("method", "Method");
158+
request.put("errorType", "CANCELLED");
159+
160+
AttributesExtractor<Map<String, String>, Void> extractor =
161+
RpcServerAttributesExtractor.create(new TestGetter(false));
57162

58163
Context context = Context.root();
164+
AttributesBuilder attributes = Attributes.builder();
165+
extractor.onStart(attributes, context, request);
166+
extractor.onEnd(attributes, context, request, null, null);
167+
168+
if (SemconvStability.emitStableRpcSemconv()) {
169+
assertThat(attributes.build()).containsEntry(ERROR_TYPE, "CANCELLED");
170+
}
171+
}
172+
173+
@Test
174+
void shouldExtractErrorType_exceptionClassName() {
175+
Map<String, String> request = new HashMap<>();
176+
request.put("service", "my.Service");
177+
request.put("method", "Method");
59178

179+
AttributesExtractor<Map<String, String>, Void> extractor =
180+
RpcServerAttributesExtractor.create(new TestGetter(false));
181+
182+
Context context = Context.root();
183+
AttributesBuilder attributes = Attributes.builder();
184+
extractor.onStart(attributes, context, request);
185+
extractor.onEnd(attributes, context, request, null, new IllegalArgumentException());
186+
187+
if (SemconvStability.emitStableRpcSemconv()) {
188+
assertThat(attributes.build())
189+
.containsEntry(ERROR_TYPE, "java.lang.IllegalArgumentException");
190+
}
191+
}
192+
193+
@Test
194+
void shouldNotExtractErrorType_noError() {
195+
Map<String, String> request = new HashMap<>();
196+
request.put("service", "my.Service");
197+
request.put("method", "Method");
198+
199+
AttributesExtractor<Map<String, String>, Void> extractor =
200+
RpcServerAttributesExtractor.create(new TestGetter(false));
201+
202+
Context context = Context.root();
60203
AttributesBuilder attributes = Attributes.builder();
61204
extractor.onStart(attributes, context, request);
62-
assertThat(attributes.build())
63-
.containsOnly(
64-
entry(RpcIncubatingAttributes.RPC_SYSTEM, "test"),
65-
entry(RpcIncubatingAttributes.RPC_SERVICE, "my.Service"),
66-
entry(RpcIncubatingAttributes.RPC_METHOD, "Method"));
67205
extractor.onEnd(attributes, context, request, null, null);
68-
assertThat(attributes.build())
69-
.containsOnly(
70-
entry(RpcIncubatingAttributes.RPC_SYSTEM, "test"),
71-
entry(RpcIncubatingAttributes.RPC_SERVICE, "my.Service"),
72-
entry(RpcIncubatingAttributes.RPC_METHOD, "Method"));
206+
207+
if (SemconvStability.emitStableRpcSemconv()) {
208+
assertThat(attributes.build()).doesNotContainKey(ERROR_TYPE);
209+
}
73210
}
74211
}

0 commit comments

Comments
 (0)