From 9ae10a5df3b07fd921623c6b674193ae92785ce2 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Thu, 26 Feb 2026 15:14:45 -0800 Subject: [PATCH 1/9] Retrieve gRPC server.address/server.port from target --- ...meriaGrpcClientBuilderInstrumentation.java | 14 +- ...GrpcClientBuilderBuildInstrumentation.java | 10 +- .../grpc/v1_6/GrpcSingletons.java | 10 +- instrumentation/grpc-1.6/library/README.md | 2 +- .../GrpcNetworkServerAttributesGetter.java | 4 +- .../grpc/v1_6/GrpcRequest.java | 134 +++++++++++++--- .../grpc/v1_6/GrpcTelemetry.java | 88 ++++++++++- .../grpc/v1_6/TracingClientInterceptor.java | 10 +- .../grpc/v1_6/TracingServerInterceptor.java | 2 +- .../GrpcClientNetworkAttributesGetter.java | 4 +- .../grpc/v1_6/internal/GrpcTargetParser.java | 147 ++++++++++++++++++ .../grpc/v1_6/internal/Internal.java | 33 ++++ .../grpc/v1_6/internal/ParsedTarget.java | 34 ++++ .../grpc/v1_6/GrpcStreamingTest.java | 5 +- .../instrumentation/grpc/v1_6/GrpcTest.java | 28 ++-- .../v1_6/internal/GrpcTargetParserTest.java | 70 +++++++++ 16 files changed, 537 insertions(+), 58 deletions(-) create mode 100644 instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java create mode 100644 instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/Internal.java create mode 100644 instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/ParsedTarget.java create mode 100644 instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java diff --git a/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcClientBuilderInstrumentation.java b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcClientBuilderInstrumentation.java index 288d5f523fed..acf48b5a24bf 100644 --- a/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcClientBuilderInstrumentation.java +++ b/instrumentation/armeria/armeria-grpc-1.14/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/armeria/grpc/v1_14/ArmeriaGrpcClientBuilderInstrumentation.java @@ -5,6 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableRpcSemconv; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; @@ -12,8 +13,11 @@ import com.linecorp.armeria.client.grpc.GrpcClientBuilder; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry; +import io.opentelemetry.instrumentation.grpc.v1_6.internal.Internal; import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.net.URI; +import javax.annotation.Nullable; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.matcher.ElementMatcher; @@ -36,8 +40,14 @@ public void transform(TypeTransformer transformer) { public static class BuildAdvice { @Advice.OnMethodEnter - public static void onEnter(@Advice.This GrpcClientBuilder builder) { - builder.intercept(GrpcTelemetry.create(GlobalOpenTelemetry.get()).createClientInterceptor()); + public static void onEnter( + @Advice.This GrpcClientBuilder builder, @Advice.FieldValue("uri") @Nullable URI uri) { + String target = null; + if (emitStableRpcSemconv() && uri != null) { + target = uri.getAuthority(); + } + GrpcTelemetry telemetry = GrpcTelemetry.create(GlobalOpenTelemetry.get()); + builder.intercept(Internal.createClientInterceptor(telemetry, target)); } } } diff --git a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java index 42bd46faf66f..643624a87f5b 100644 --- a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java +++ b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java @@ -5,6 +5,7 @@ package io.opentelemetry.javaagent.instrumentation.grpc.v1_6; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableRpcSemconv; 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.MANAGED_CHANNEL_BUILDER_INSTRUMENTED; @@ -30,7 +31,8 @@ public ElementMatcher classLoaderOptimization() { @Override public ElementMatcher typeMatcher() { return extendsClass(named("io.grpc.ManagedChannelBuilder")) - .and(declaresField(named("interceptors"))); + .and(declaresField(named("interceptors"))) + .and(declaresField(named("target"))); } @Override @@ -46,9 +48,11 @@ public static class AddInterceptorAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static void addInterceptor( @Advice.This ManagedChannelBuilder builder, - @Advice.FieldValue("interceptors") List interceptors) { + @Advice.FieldValue("interceptors") List interceptors, + @Advice.FieldValue("target") String target) { if (!Boolean.TRUE.equals(MANAGED_CHANNEL_BUILDER_INSTRUMENTED.get(builder))) { - interceptors.add(0, GrpcSingletons.CLIENT_INTERCEPTOR); + String effectiveTarget = emitStableRpcSemconv() ? target : null; + interceptors.add(0, GrpcSingletons.createClientInterceptor(effectiveTarget)); MANAGED_CHANNEL_BUILDER_INSTRUMENTED.set(builder, true); } } 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..fe73e62be9cb 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 @@ -18,8 +18,10 @@ import io.opentelemetry.instrumentation.api.util.VirtualField; import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry; import io.opentelemetry.instrumentation.grpc.v1_6.internal.ContextStorageBridge; +import io.opentelemetry.instrumentation.grpc.v1_6.internal.Internal; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.Nullable; // Holds singleton references. public final class GrpcSingletons { @@ -31,7 +33,7 @@ public final class GrpcSingletons { public static final VirtualField, Boolean> SERVER_BUILDER_INSTRUMENTED = VirtualField.find(ServerBuilder.class, Boolean.class); - public static final ClientInterceptor CLIENT_INTERCEPTOR; + private static final GrpcTelemetry TELEMETRY; public static final ServerInterceptor SERVER_INTERCEPTOR; @@ -64,10 +66,14 @@ public final class GrpcSingletons { .setCapturedServerRequestMetadata(serverRequestMetadata) .build(); - CLIENT_INTERCEPTOR = telemetry.createClientInterceptor(); + TELEMETRY = telemetry; SERVER_INTERCEPTOR = telemetry.createServerInterceptor(); } + public static ClientInterceptor createClientInterceptor(@Nullable String target) { + return Internal.createClientInterceptor(TELEMETRY, target); + } + public static Context.Storage getStorage() { return STORAGE_REFERENCE.get(); } diff --git a/instrumentation/grpc-1.6/library/README.md b/instrumentation/grpc-1.6/library/README.md index 09674fbb97a5..c07d20d411b1 100644 --- a/instrumentation/grpc-1.6/library/README.md +++ b/instrumentation/grpc-1.6/library/README.md @@ -34,7 +34,7 @@ The instrumentation library provides the implementation of `ClientInterceptor` a // For client-side, attach the interceptor to your channel builder. void configureClientInterceptor(OpenTelemetry openTelemetry, NettyChannelBuilder nettyChannelBuilder) { GrpcTelemetry grpcTelemetry = GrpcTelemetry.create(openTelemetry); - nettyChannelBuilder.intercept(grpcTelemetry.createClientInterceptor()); + grpcTelemetry.addClientInterceptor(nettyChannelBuilder); } // For server-side, attatch the interceptor to your service. diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcNetworkServerAttributesGetter.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcNetworkServerAttributesGetter.java index a7f3701c9c60..8c66d0e69059 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcNetworkServerAttributesGetter.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcNetworkServerAttributesGetter.java @@ -18,12 +18,12 @@ final class GrpcNetworkServerAttributesGetter @Nullable @Override public String getServerAddress(GrpcRequest grpcRequest) { - return grpcRequest.getLogicalHost(); + return grpcRequest.getServerAddress(); } @Override public Integer getServerPort(GrpcRequest grpcRequest) { - return grpcRequest.getLogicalPort(); + return grpcRequest.getServerPort(); } @Nullable 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 926799191bb2..e68eb40b146d 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 @@ -7,6 +7,7 @@ import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.opentelemetry.instrumentation.grpc.v1_6.internal.ParsedTarget; import java.net.SocketAddress; import javax.annotation.Nullable; @@ -15,40 +16,65 @@ public final class GrpcRequest { private final MethodDescriptor method; @Nullable private volatile Metadata metadata; - - @Nullable private volatile String logicalHost; - private volatile int logicalPort = -1; + @Nullable private final String serverAddress; + @Nullable private final Integer serverPort; @Nullable private volatile SocketAddress peerSocketAddress; private Long requestSize; private Long responseSize; - GrpcRequest( + /** + * Creates a client-side gRPC request. + * + * @param method the gRPC method descriptor + * @param authority the channel authority (host:port) + * @param parsedTarget the pre-parsed gRPC target (from {@link + * io.opentelemetry.instrumentation.grpc.v1_6.internal.GrpcTargetParser#parse}), or {@code + * null} if unavailable + */ + public static GrpcRequest createClientRequest( + MethodDescriptor method, + @Nullable String authority, + @Nullable ParsedTarget parsedTarget) { + if (parsedTarget != null) { + return new GrpcRequest(method, null, null, parsedTarget.getAddress(), parsedTarget.getPort()); + } + return new GrpcRequest( + method, null, null, hostFromAuthority(authority), portFromAuthority(authority)); + } + + /** + * Creates a server-side gRPC request. + * + * @param method the gRPC method descriptor + * @param metadata the request metadata + * @param peerSocketAddress the peer socket address + * @param authority the request authority + */ + public static GrpcRequest createServerRequest( MethodDescriptor method, @Nullable Metadata metadata, @Nullable SocketAddress peerSocketAddress, @Nullable String authority) { + return new GrpcRequest( + method, + metadata, + peerSocketAddress, + hostFromAuthority(authority), + portFromAuthority(authority)); + } + + private GrpcRequest( + MethodDescriptor method, + @Nullable Metadata metadata, + @Nullable SocketAddress peerSocketAddress, + @Nullable String serverAddress, + @Nullable Integer serverPort) { this.method = method; this.metadata = metadata; this.peerSocketAddress = peerSocketAddress; - setLogicalAddress(authority); - } - - private void setLogicalAddress(@Nullable String authority) { - if (authority == null) { - return; - } - int index = authority.indexOf(':'); - if (index == -1) { - logicalHost = authority; - } else { - logicalHost = authority.substring(0, index); - try { - logicalPort = Integer.parseInt(authority.substring(index + 1)); - } catch (NumberFormatException e) { - // ignore - } - } + this.serverAddress = serverAddress; + this.serverPort = serverPort; } public MethodDescriptor getMethod() { @@ -64,13 +90,45 @@ void setMetadata(Metadata metadata) { this.metadata = metadata; } + /** + * Returns the server address. + * + *

When a target string is available (from gRPC channel configuration), the server address is + * extracted per the gRPC Name + * Resolution spec. Otherwise, falls back to the authority (host portion). + */ + @Nullable + public String getServerAddress() { + return serverAddress; + } + + /** + * Returns the server port. + * + *

When a target string is available (from gRPC channel configuration), the server port is + * extracted per the gRPC Name + * Resolution spec. Otherwise, falls back to the authority (port portion). + */ + @Nullable + public Integer getServerPort() { + return serverPort; + } + + /** + * @deprecated Use {@link #getServerAddress()} instead. + */ + @Deprecated @Nullable public String getLogicalHost() { - return logicalHost; + return serverAddress; } + /** + * @deprecated Use {@link #getServerPort()} instead. + */ + @Deprecated public int getLogicalPort() { - return logicalPort; + return serverPort != null ? serverPort : -1; } @Nullable @@ -97,4 +155,32 @@ public Long getResponseSize() { public void setResponseSize(Long responseSize) { this.responseSize = responseSize; } + + @Nullable + private static String hostFromAuthority(@Nullable String authority) { + if (authority == null) { + return null; + } + int index = authority.indexOf(':'); + if (index == -1) { + return authority; + } + return authority.substring(0, index); + } + + @Nullable + private static Integer portFromAuthority(@Nullable String authority) { + if (authority == null) { + return null; + } + int index = authority.indexOf(':'); + if (index == -1) { + return null; + } + try { + return Integer.parseInt(authority.substring(index + 1)); + } catch (NumberFormatException e) { + return null; + } + } } 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..d1199f53b5ca 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,50 @@ package io.opentelemetry.instrumentation.grpc.v1_6; import io.grpc.ClientInterceptor; +import io.grpc.ManagedChannelBuilder; 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; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; /** Entrypoint for instrumenting gRPC servers or clients. */ public final class GrpcTelemetry { + private static final Logger logger = Logger.getLogger(GrpcTelemetry.class.getName()); + + // Reflective access to InternalManagedChannelBuilder.interceptWithTarget (available since gRPC + // 1.64.0). Uses the public static accessor instead of the protected method on + // ManagedChannelBuilder to avoid needing setAccessible(true). + @Nullable private static final Method interceptWithTargetMethod; + @Nullable private static final Class interceptorFactoryClass; + + static { + Method method = null; + Class factoryClass = null; + try { + Class internalBuilder = Class.forName("io.grpc.InternalManagedChannelBuilder"); + factoryClass = + Class.forName("io.grpc.InternalManagedChannelBuilder$InternalInterceptorFactory"); + method = + internalBuilder.getMethod( + "interceptWithTarget", ManagedChannelBuilder.class, factoryClass); + } catch (ClassNotFoundException | NoSuchMethodException e) { + // gRPC version < 1.64.0, interceptWithTarget not available + } + interceptWithTargetMethod = method; + interceptorFactoryClass = factoryClass; + + Internal.setClientInterceptorFactory( + (telemetry, target) -> telemetry.newTracingClientInterceptor(target)); + } + /** Returns a new {@link GrpcTelemetry} configured with the given {@link OpenTelemetry}. */ public static GrpcTelemetry create(OpenTelemetry openTelemetry) { return builder(openTelemetry).build(); @@ -44,13 +79,53 @@ public static GrpcTelemetryBuilder builder(OpenTelemetry openTelemetry) { this.emitMessageEvents = emitMessageEvents; } + /** + * Configures the given {@link ManagedChannelBuilder} with OpenTelemetry tracing instrumentation. + * + *

On gRPC 1.64.0+, this method automatically captures the channel's target string for + * populating {@code server.address} and {@code server.port} attributes. On older gRPC versions, + * it falls back to using the channel's authority. + * + *

This is the recommended way to instrument a gRPC channel, instead of calling {@link + * #createClientInterceptor()} and adding the interceptor manually. + * + * @param builder the channel builder to configure + */ + @SuppressWarnings("unchecked") + public void addClientInterceptor(ManagedChannelBuilder builder) { + if (interceptWithTargetMethod != null && interceptorFactoryClass != null) { + try { + Object factory = + Proxy.newProxyInstance( + ManagedChannelBuilder.class.getClassLoader(), + new Class[] {interceptorFactoryClass}, + (proxy, method, args) -> { + if ("newInterceptor".equals(method.getName())) { + String target = (String) args[0]; + return newTracingClientInterceptor(target); + } + return method.invoke(builder, args); + }); + interceptWithTargetMethod.invoke(null, builder, factory); + return; + } catch (Exception e) { + logger.log(Level.FINE, "Failed to use interceptWithTarget, falling back", e); + } + } + + // Fallback for gRPC < 1.64.0: add interceptor without target info + builder.intercept(newTracingClientInterceptor(null)); + } + /** * Returns a new {@link ClientInterceptor} for use with methods like {@link * io.grpc.ManagedChannelBuilder#intercept(ClientInterceptor...)}. + * + * @deprecated Use {@link #addClientInterceptor(ManagedChannelBuilder)} instead. */ + @Deprecated public ClientInterceptor createClientInterceptor() { - return new TracingClientInterceptor( - clientInstrumenter, propagators, captureExperimentalSpanAttributes, emitMessageEvents); + return newTracingClientInterceptor(null); } /** @@ -61,4 +136,13 @@ public ServerInterceptor createServerInterceptor() { return new TracingServerInterceptor( serverInstrumenter, captureExperimentalSpanAttributes, emitMessageEvents); } + + private TracingClientInterceptor newTracingClientInterceptor(@Nullable String target) { + return new TracingClientInterceptor( + clientInstrumenter, + propagators, + captureExperimentalSpanAttributes, + emitMessageEvents, + target); + } } diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingClientInterceptor.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingClientInterceptor.java index 7bce2c0c915b..06dff52b8d74 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingClientInterceptor.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/TracingClientInterceptor.java @@ -22,7 +22,10 @@ import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.grpc.v1_6.internal.GrpcTargetParser; +import io.opentelemetry.instrumentation.grpc.v1_6.internal.ParsedTarget; import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import javax.annotation.Nullable; final class TracingClientInterceptor implements ClientInterceptor { @@ -49,22 +52,25 @@ final class TracingClientInterceptor implements ClientInterceptor { private final ContextPropagators propagators; private final boolean captureExperimentalSpanAttributes; private final boolean emitMessageEvents; + @Nullable private final ParsedTarget parsedTarget; TracingClientInterceptor( Instrumenter instrumenter, ContextPropagators propagators, boolean captureExperimentalSpanAttributes, - boolean emitMessageEvents) { + boolean emitMessageEvents, + @Nullable String target) { this.instrumenter = instrumenter; this.propagators = propagators; this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; this.emitMessageEvents = emitMessageEvents; + this.parsedTarget = GrpcTargetParser.parse(target); } @Override public ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { - GrpcRequest request = new GrpcRequest(method, null, null, next.authority()); + GrpcRequest request = GrpcRequest.createClientRequest(method, next.authority(), parsedTarget); Context parentContext = Context.current(); if (!instrumenter.shouldStart(parentContext, request)) { return next.newCall(method, callOptions); 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 5440d9ba34db..2ffec58f0b81 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 @@ -71,7 +71,7 @@ public ServerCall.Listener interceptCall( authority = GrpcAuthorityStorage.getAuthority(call); } GrpcRequest request = - new GrpcRequest( + GrpcRequest.createServerRequest( call.getMethodDescriptor(), headers, call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR), diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcClientNetworkAttributesGetter.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcClientNetworkAttributesGetter.java index 1a8ce22d8eff..50a150e7e2f1 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcClientNetworkAttributesGetter.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcClientNetworkAttributesGetter.java @@ -23,12 +23,12 @@ public final class GrpcClientNetworkAttributesGetter @Nullable @Override public String getServerAddress(GrpcRequest grpcRequest) { - return grpcRequest.getLogicalHost(); + return grpcRequest.getServerAddress(); } @Override public Integer getServerPort(GrpcRequest grpcRequest) { - return grpcRequest.getLogicalPort(); + return grpcRequest.getServerPort(); } @Override diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java new file mode 100644 index 000000000000..12cd5480336c --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java @@ -0,0 +1,147 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6.internal; + +import javax.annotation.Nullable; + +/** + * Parses gRPC target strings into server address and port per the gRPC Name Resolution spec and semantic conventions. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class GrpcTargetParser { + + private GrpcTargetParser() {} + + @Nullable + public static ParsedTarget parse(@Nullable String target) { + if (target == null || target.isEmpty()) { + return null; + } + + int schemeEnd = target.indexOf("://"); + if (schemeEnd == -1) { + // Check for single-colon scheme like "dns:endpoint" or "unix:/path" + int colonIndex = target.indexOf(':'); + if (colonIndex == -1) { + // No scheme, no port — just a host name + return new ParsedTarget(target, null); + } + + String potentialScheme = target.substring(0, colonIndex); + if (isKnownScheme(potentialScheme)) { + return parseSingleColonScheme(potentialScheme, target.substring(colonIndex + 1)); + } + + // No known scheme — treat as "host:port" + return parseHostPort(target); + } + + String scheme = target.substring(0, schemeEnd); + String rest = target.substring(schemeEnd + 3); // after "://" + + if ("dns".equals(scheme)) { + return parseDnsScheme(rest); + } + + if ("unix".equals(scheme) || "unix-abstract".equals(scheme)) { + // unix://authority/path — the path (after authority) is the address + int slashIndex = rest.indexOf('/'); + if (slashIndex != -1) { + return new ParsedTarget(rest.substring(slashIndex), null); + } + return new ParsedTarget(rest, null); + } + + // Unknown scheme with "://" — use full target string as address, no port + return new ParsedTarget(target, null); + } + + private static ParsedTarget parseSingleColonScheme(String scheme, String rest) { + if ("dns".equals(scheme)) { + return parseHostPort(rest); + } + + if ("unix".equals(scheme) || "unix-abstract".equals(scheme)) { + return new ParsedTarget(rest, null); + } + + // ipv4:, ipv6:, or other — full target as address + return new ParsedTarget(scheme + ":" + rest, null); + } + + private static ParsedTarget parseDnsScheme(String rest) { + int slashIndex = rest.indexOf('/'); + String endpoint; + if (slashIndex != -1) { + endpoint = rest.substring(slashIndex + 1); + } else { + endpoint = rest; + } + return parseHostPort(endpoint); + } + + private static ParsedTarget parseHostPort(String hostPort) { + if (hostPort.isEmpty()) { + return new ParsedTarget(hostPort, null); + } + + // Handle IPv6 in brackets: [::1]:8080 + if (hostPort.startsWith("[")) { + int closeBracket = hostPort.indexOf(']'); + if (closeBracket != -1) { + String host = hostPort.substring(1, closeBracket); + if (closeBracket + 1 < hostPort.length() && hostPort.charAt(closeBracket + 1) == ':') { + Integer port = parsePort(hostPort.substring(closeBracket + 2)); + return new ParsedTarget(host, port); + } + return new ParsedTarget(host, null); + } + } + + int lastColon = hostPort.lastIndexOf(':'); + if (lastColon == -1) { + return new ParsedTarget(hostPort, null); + } + + // Multiple colons — likely bare IPv6, use as-is + int firstColon = hostPort.indexOf(':'); + if (firstColon != lastColon) { + return new ParsedTarget(hostPort, null); + } + + String host = hostPort.substring(0, lastColon); + Integer port = parsePort(hostPort.substring(lastColon + 1)); + if (port != null) { + return new ParsedTarget(host, port); + } + return new ParsedTarget(hostPort, null); + } + + @Nullable + private static Integer parsePort(String portStr) { + try { + int port = Integer.parseInt(portStr); + if (port >= 0 && port <= 65535) { + return port; + } + } catch (NumberFormatException e) { + // ignore + } + return null; + } + + private static boolean isKnownScheme(String scheme) { + return "dns".equals(scheme) + || "unix".equals(scheme) + || "unix-abstract".equals(scheme) + || "ipv4".equals(scheme) + || "ipv6".equals(scheme); + } +} 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..61a30d4ba110 --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/Internal.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6.internal; + +import io.grpc.ClientInterceptor; +import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry; +import java.util.function.BiFunction; +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 { + + private static volatile BiFunction + clientInterceptorFactory; + + public static void setClientInterceptorFactory( + BiFunction factory) { + clientInterceptorFactory = factory; + } + + public static ClientInterceptor createClientInterceptor( + GrpcTelemetry telemetry, @Nullable String target) { + return clientInterceptorFactory.apply(telemetry, target); + } + + private Internal() {} +} diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/ParsedTarget.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/ParsedTarget.java new file mode 100644 index 000000000000..7540aef5ada9 --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/ParsedTarget.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6.internal; + +import javax.annotation.Nullable; + +/** + * Holds the parsed server address and port from a gRPC target string. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +public final class ParsedTarget { + + private final String address; + @Nullable private final Integer port; + + ParsedTarget(String address, @Nullable Integer port) { + this.address = address; + this.port = port; + } + + public String getAddress() { + return address; + } + + @Nullable + public Integer getPort() { + return port; + } +} 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..2f0bca5e6bba 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 @@ -24,8 +24,9 @@ protected ServerBuilder configureServer(ServerBuilder server) { @Override protected ManagedChannelBuilder configureClient(ManagedChannelBuilder client) { - return client.intercept( - GrpcTelemetry.create(testing.getOpenTelemetry()).createClientInterceptor()); + GrpcTelemetry grpcTelemetry = GrpcTelemetry.create(testing.getOpenTelemetry()); + grpcTelemetry.addClientInterceptor(client); + return client; } @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 d24b5c170c02..a521b3a21ac1 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 @@ -53,11 +53,11 @@ protected ServerBuilder configureServer(ServerBuilder server) { @Override protected ManagedChannelBuilder configureClient(ManagedChannelBuilder client) { - return client.intercept( - GrpcTelemetry.builder(testing.getOpenTelemetry()) - .setCapturedClientRequestMetadata(singletonList(CLIENT_REQUEST_METADATA_KEY)) - .build() - .createClientInterceptor()); + GrpcTelemetry.builder(testing.getOpenTelemetry()) + .setCapturedClientRequestMetadata(singletonList(CLIENT_REQUEST_METADATA_KEY)) + .build() + .addClientInterceptor(client); + return client; } @Override @@ -96,16 +96,14 @@ public void sayHello( .build() .start(); - ManagedChannel channel = - createChannel( - ManagedChannelBuilder.forAddress("localhost", server.getPort()) - .intercept( - GrpcTelemetry.builder(testing.getOpenTelemetry()) - .addAttributesExtractor(new CustomAttributesExtractor()) - .addClientAttributeExtractor( - new CustomAttributesExtractorV2("clientSideValue")) - .build() - .createClientInterceptor())); + ManagedChannelBuilder channelBuilder = + ManagedChannelBuilder.forAddress("localhost", server.getPort()); + GrpcTelemetry.builder(testing.getOpenTelemetry()) + .addAttributesExtractor(new CustomAttributesExtractor()) + .addClientAttributeExtractor(new CustomAttributesExtractorV2("clientSideValue")) + .build() + .addClientInterceptor(channelBuilder); + ManagedChannel channel = createChannel(channelBuilder); closer.add(() -> channel.shutdownNow().awaitTermination(10, SECONDS)); closer.add(() -> server.shutdownNow().awaitTermination()); diff --git a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java new file mode 100644 index 000000000000..5e80c948a38d --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.grpc.v1_6.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class GrpcTargetParserTest { + + @ParameterizedTest + @MethodSource("targetProvider") + void parse(String target, String expectedAddress, Integer expectedPort) { + ParsedTarget result = GrpcTargetParser.parse(target); + + assertThat(result).isNotNull(); + assertThat(result.getAddress()).isEqualTo(expectedAddress); + assertThat(result.getPort()).isEqualTo(expectedPort); + } + + static Stream targetProvider() { + return Stream.of( + // dns:/// scheme (triple slash) + Arguments.of("dns:///myhost", "myhost", null), + Arguments.of("dns:///myhost:8080", "myhost", 8080), + + // dns: scheme (single colon) + Arguments.of("dns:myhost", "myhost", null), + Arguments.of("dns:myhost:8080", "myhost", 8080), + + // bare host:port (no scheme) + Arguments.of("myhost", "myhost", null), + Arguments.of("myhost:8080", "myhost", 8080), + Arguments.of("localhost:443", "localhost", 443), + + // unix schemes + Arguments.of("unix:///var/run/grpc.sock", "/var/run/grpc.sock", null), + Arguments.of("unix:/var/run/grpc.sock", "/var/run/grpc.sock", null), + Arguments.of("unix-abstract:name", "name", null), + + // ipv4 scheme + Arguments.of("ipv4:192.168.0.1:8080", "ipv4:192.168.0.1:8080", null), + + // ipv6 scheme + Arguments.of("ipv6:[::1]:8080", "ipv6:[::1]:8080", null), + + // IPv6 in brackets (bare) + Arguments.of("[::1]:8080", "::1", 8080), + Arguments.of("[::1]", "::1", null), + + // bare IPv6 (no brackets) — treated as host with no port + Arguments.of("::1", "::1", null), + + // unknown scheme with :// + Arguments.of("xds:///myservice", "xds:///myservice", null)); + } + + @ParameterizedTest + @NullAndEmptySource + void parseNullOrEmpty(String target) { + assertThat(GrpcTargetParser.parse(target)).isNull(); + } +} From fc251d0d51ac17b99de44f65afcd862fd0734599 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 2 Mar 2026 13:09:04 -0800 Subject: [PATCH 2/9] spotless --- .../instrumentation/grpc/v1_6/GrpcTelemetry.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 d1199f53b5ca..288058068b3c 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 @@ -5,6 +5,8 @@ package io.opentelemetry.instrumentation.grpc.v1_6; +import static java.util.logging.Level.FINE; + import io.grpc.ClientInterceptor; import io.grpc.ManagedChannelBuilder; import io.grpc.ServerInterceptor; @@ -15,7 +17,6 @@ import io.opentelemetry.instrumentation.grpc.v1_6.internal.Internal; import java.lang.reflect.Method; import java.lang.reflect.Proxy; -import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -109,7 +110,7 @@ public void addClientInterceptor(ManagedChannelBuilder builder) { interceptWithTargetMethod.invoke(null, builder, factory); return; } catch (Exception e) { - logger.log(Level.FINE, "Failed to use interceptWithTarget, falling back", e); + logger.log(FINE, "Failed to use interceptWithTarget, falling back", e); } } From 3c7ab54fc0157a127cbf1bc886ca2dfb9820ceda Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 11 May 2026 19:37:40 -0700 Subject: [PATCH 3/9] Use identity semantics for Object methods on InternalInterceptorFactory proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the InvocationHandler forwarded any non-newInterceptor call to the ManagedChannelBuilder instance, which gave Object methods (equals/hashCode/ toString) misleading semantics — e.g. proxy.equals(builder) would return true. Handle equals/hashCode/toString explicitly with identity semantics, and reject any other unexpected method. --- .../grpc/v1_6/GrpcTelemetry.java | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) 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 3c0e73d0dc94..16bd720f4545 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 @@ -95,18 +95,7 @@ public static GrpcTelemetryBuilder builder(OpenTelemetry openTelemetry) { public void addClientInterceptor(ManagedChannelBuilder builder) { if (interceptWithTargetMethod != null && interceptorFactoryClass != null) { try { - Object factory = - Proxy.newProxyInstance( - ManagedChannelBuilder.class.getClassLoader(), - new Class[] {interceptorFactoryClass}, - (proxy, method, args) -> { - if ("newInterceptor".equals(method.getName())) { - String target = (String) args[0]; - return newTracingClientInterceptor(target); - } - return method.invoke(builder, args); - }); - interceptWithTargetMethod.invoke(null, builder, factory); + interceptWithTargetMethod.invoke(null, builder, newInterceptorFactory()); return; } catch (Exception e) { logger.log(FINE, "Failed to use interceptWithTarget, falling back", e); @@ -117,6 +106,30 @@ public void addClientInterceptor(ManagedChannelBuilder builder) { builder.intercept(newTracingClientInterceptor(null)); } + private Object newInterceptorFactory() { + // Proxies InternalManagedChannelBuilder$InternalInterceptorFactory, whose only declared + // method is newInterceptor(String target). Object methods get identity semantics — we have + // no natural delegate to forward them to. + return Proxy.newProxyInstance( + ManagedChannelBuilder.class.getClassLoader(), + new Class[] {interceptorFactoryClass}, + (proxy, method, args) -> { + if ("newInterceptor".equals(method.getName())) { + return newTracingClientInterceptor((String) args[0]); + } + switch (method.getName()) { + case "equals": + return proxy == args[0]; + case "hashCode": + return System.identityHashCode(proxy); + case "toString": + return "GrpcInterceptorFactory"; + default: + throw new UnsupportedOperationException(method.toString()); + } + }); + } + /** * Returns a new {@link ClientInterceptor} for use with methods like {@link * io.grpc.ManagedChannelBuilder#intercept(ClientInterceptor...)}. From 96342415fe235fc6f9ef33c220d184cf704afff5 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 11 May 2026 19:37:49 -0700 Subject: [PATCH 4/9] Fix IPv6 authority parsing for server.address/server.port The GrpcRequest hostFromAuthority/portFromAuthority helpers split on the first ':', which mangles IPv6 authorities like '[::1]:8080' (host becomes '[' and the port parse fails). This is now both the fallback path for clients on gRPC < 1.64 and the primary path for server-side requests. Promote the bracket-aware parsing logic from GrpcTargetParser into a new parseAuthority() helper and route both client fallback and server request construction through it, so authority and target parsing share a single source of truth. --- .../grpc/v1_6/GrpcRequest.java | 43 ++++--------------- .../grpc/v1_6/internal/GrpcTargetParser.java | 12 ++++++ 2 files changed, 21 insertions(+), 34 deletions(-) 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 544e6833052e..92519a654b51 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 @@ -7,6 +7,7 @@ import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.opentelemetry.instrumentation.grpc.v1_6.internal.GrpcTargetParser; import io.opentelemetry.instrumentation.grpc.v1_6.internal.ParsedTarget; import java.net.SocketAddress; import javax.annotation.Nullable; @@ -36,11 +37,12 @@ public static GrpcRequest createClientRequest( MethodDescriptor method, @Nullable String authority, @Nullable ParsedTarget parsedTarget) { - if (parsedTarget != null) { - return new GrpcRequest(method, null, null, parsedTarget.getAddress(), parsedTarget.getPort()); + ParsedTarget effective = + parsedTarget != null ? parsedTarget : GrpcTargetParser.parseAuthority(authority); + if (effective == null) { + return new GrpcRequest(method, null, null, null, null); } - return new GrpcRequest( - method, null, null, hostFromAuthority(authority), portFromAuthority(authority)); + return new GrpcRequest(method, null, null, effective.getAddress(), effective.getPort()); } /** @@ -56,12 +58,13 @@ public static GrpcRequest createServerRequest( @Nullable Metadata metadata, @Nullable SocketAddress peerSocketAddress, @Nullable String authority) { + ParsedTarget parsed = GrpcTargetParser.parseAuthority(authority); return new GrpcRequest( method, metadata, peerSocketAddress, - hostFromAuthority(authority), - portFromAuthority(authority)); + parsed != null ? parsed.getAddress() : null, + parsed != null ? parsed.getPort() : null); } private GrpcRequest( @@ -157,32 +160,4 @@ public Long getResponseSize() { public void setResponseSize(@Nullable Long responseSize) { this.responseSize = responseSize; } - - @Nullable - private static String hostFromAuthority(@Nullable String authority) { - if (authority == null) { - return null; - } - int index = authority.indexOf(':'); - if (index == -1) { - return authority; - } - return authority.substring(0, index); - } - - @Nullable - private static Integer portFromAuthority(@Nullable String authority) { - if (authority == null) { - return null; - } - int index = authority.indexOf(':'); - if (index == -1) { - return null; - } - try { - return Integer.parseInt(authority.substring(index + 1)); - } catch (NumberFormatException e) { - return null; - } - } } diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java index 12cd5480336c..baddb3059a3c 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java @@ -63,6 +63,18 @@ public static ParsedTarget parse(@Nullable String target) { return new ParsedTarget(target, null); } + /** + * Parses an HTTP/2 authority of the form {@code host}, {@code host:port}, or {@code + * [ipv6]:port} into address and port. Returns {@code null} for {@code null}/empty input. + */ + @Nullable + public static ParsedTarget parseAuthority(@Nullable String authority) { + if (authority == null || authority.isEmpty()) { + return null; + } + return parseHostPort(authority); + } + private static ParsedTarget parseSingleColonScheme(String scheme, String rest) { if ("dns".equals(scheme)) { return parseHostPort(rest); From 5264e1e26f3dbdb914c085aafbcbab49690070db Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 11 May 2026 20:04:55 -0700 Subject: [PATCH 5/9] Return null for empty endpoint in GrpcTargetParser Previously parseHostPort("") returned a ParsedTarget with an empty address, which could lead to emitting server.address="" on spans for targets like "dns:" or "dns:///". Return null instead and propagate through parseSingleColonScheme / parseDnsScheme. --- .../grpc/v1_6/internal/GrpcTargetParser.java | 5 ++++- .../grpc/v1_6/internal/GrpcTargetParserTest.java | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java index baddb3059a3c..f008af3a3708 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java @@ -75,6 +75,7 @@ public static ParsedTarget parseAuthority(@Nullable String authority) { return parseHostPort(authority); } + @Nullable private static ParsedTarget parseSingleColonScheme(String scheme, String rest) { if ("dns".equals(scheme)) { return parseHostPort(rest); @@ -88,6 +89,7 @@ private static ParsedTarget parseSingleColonScheme(String scheme, String rest) { return new ParsedTarget(scheme + ":" + rest, null); } + @Nullable private static ParsedTarget parseDnsScheme(String rest) { int slashIndex = rest.indexOf('/'); String endpoint; @@ -99,9 +101,10 @@ private static ParsedTarget parseDnsScheme(String rest) { return parseHostPort(endpoint); } + @Nullable private static ParsedTarget parseHostPort(String hostPort) { if (hostPort.isEmpty()) { - return new ParsedTarget(hostPort, null); + return null; } // Handle IPv6 in brackets: [::1]:8080 diff --git a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java index 5e80c948a38d..6273bbe49626 100644 --- a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java +++ b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; class GrpcTargetParserTest { @@ -67,4 +68,12 @@ static Stream targetProvider() { void parseNullOrEmpty(String target) { assertThat(GrpcTargetParser.parse(target)).isNull(); } + + @ParameterizedTest + @ValueSource(strings = {"dns:", "dns:///"}) + void parseEmptyEndpointReturnsNull(String target) { + // "dns:" -> empty after single-colon scheme + // "dns:///" -> empty endpoint after authority slash + assertThat(GrpcTargetParser.parse(target)).isNull(); + } } From b7f3c9131c85ecd0b352a434f383843ea810d38a Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 11 May 2026 20:05:14 -0700 Subject: [PATCH 6/9] Preserve host when gRPC target port is missing or invalid Previously parseHostPort fell back to ParsedTarget(hostPort, null) when the port failed to parse, causing server.address to contain a trailing colon or invalid port fragment (e.g. "myhost:" or "myhost:abc"). Return ParsedTarget(host, null) instead so server.address holds just the host. --- .../grpc/v1_6/internal/GrpcTargetParser.java | 5 +---- .../grpc/v1_6/internal/GrpcTargetParserTest.java | 8 +++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java index f008af3a3708..2f9af363e3dc 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java @@ -133,10 +133,7 @@ private static ParsedTarget parseHostPort(String hostPort) { String host = hostPort.substring(0, lastColon); Integer port = parsePort(hostPort.substring(lastColon + 1)); - if (port != null) { - return new ParsedTarget(host, port); - } - return new ParsedTarget(hostPort, null); + return new ParsedTarget(host, port); } @Nullable diff --git a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java index 6273bbe49626..cf3467bda8a9 100644 --- a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java +++ b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java @@ -60,7 +60,13 @@ static Stream targetProvider() { Arguments.of("::1", "::1", null), // unknown scheme with :// - Arguments.of("xds:///myservice", "xds:///myservice", null)); + Arguments.of("xds:///myservice", "xds:///myservice", null), + + // host with missing/invalid port — host preserved, port omitted + Arguments.of("myhost:", "myhost", null), + Arguments.of("myhost:abc", "myhost", null), + Arguments.of("dns:myhost:abc", "myhost", null), + Arguments.of("dns:///myhost:", "myhost", null)); } @ParameterizedTest From 5bc9c5844f310b1b26c1ebfee221ea035d3af7de Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 11 May 2026 20:05:29 -0700 Subject: [PATCH 7/9] Make GrpcRequest factories package-private createClientRequest and createServerRequest are only called from TracingClientInterceptor / TracingServerInterceptor in the same package. Avoid widening the public API surface (and avoid exposing the internal ParsedTarget type via a public signature). --- .../opentelemetry/instrumentation/grpc/v1_6/GrpcRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 92519a654b51..09abcfaf5a2f 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 @@ -33,7 +33,7 @@ public final class GrpcRequest { * io.opentelemetry.instrumentation.grpc.v1_6.internal.GrpcTargetParser#parse}), or {@code * null} if unavailable */ - public static GrpcRequest createClientRequest( + static GrpcRequest createClientRequest( MethodDescriptor method, @Nullable String authority, @Nullable ParsedTarget parsedTarget) { @@ -53,7 +53,7 @@ public static GrpcRequest createClientRequest( * @param peerSocketAddress the peer socket address * @param authority the request authority */ - public static GrpcRequest createServerRequest( + static GrpcRequest createServerRequest( MethodDescriptor method, @Nullable Metadata metadata, @Nullable SocketAddress peerSocketAddress, From 85306df2b1a7b3fbcfa6960ce4128fcfee4ec86e Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 11 May 2026 20:09:03 -0700 Subject: [PATCH 8/9] spotless --- .../grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java | 4 +++- .../instrumentation/grpc/v1_6/internal/GrpcTargetParser.java | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java index 9bbb75b839e6..1cced0970691 100644 --- a/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java +++ b/instrumentation/grpc-1.6/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/grpc/v1_6/GrpcClientBuilderBuildInstrumentation.java @@ -50,7 +50,9 @@ public static void addInterceptor( @Advice.FieldValue("target") String target) { if (!Boolean.TRUE.equals(MANAGED_CHANNEL_BUILDER_INSTRUMENTED.get(builder))) { ClientInterceptor interceptor = - emitStableRpcSemconv() ? GrpcSingletons.createClientInterceptor(target) : clientInterceptor(); + emitStableRpcSemconv() + ? GrpcSingletons.createClientInterceptor(target) + : clientInterceptor(); interceptors.add(0, interceptor); MANAGED_CHANNEL_BUILDER_INSTRUMENTED.set(builder, true); } diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java index 2f9af363e3dc..0c690ebeabc0 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java @@ -64,8 +64,8 @@ public static ParsedTarget parse(@Nullable String target) { } /** - * Parses an HTTP/2 authority of the form {@code host}, {@code host:port}, or {@code - * [ipv6]:port} into address and port. Returns {@code null} for {@code null}/empty input. + * Parses an HTTP/2 authority of the form {@code host}, {@code host:port}, or {@code [ipv6]:port} + * into address and port. Returns {@code null} for {@code null}/empty input. */ @Nullable public static ParsedTarget parseAuthority(@Nullable String authority) { From 1be97271398c54fa8e98455cb8f737eb1a10ed49 Mon Sep 17 00:00:00 2001 From: Trask Stalnaker Date: Mon, 11 May 2026 21:01:18 -0700 Subject: [PATCH 9/9] Return null for empty host in GrpcTargetParser --- .../grpc/v1_6/internal/GrpcTargetParser.java | 10 ++++++++++ .../grpc/v1_6/internal/GrpcTargetParserTest.java | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java index 0c690ebeabc0..277987d31be8 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java @@ -27,6 +27,10 @@ public static ParsedTarget parse(@Nullable String target) { int schemeEnd = target.indexOf("://"); if (schemeEnd == -1) { + // Bracketed IPv6 like "[::1]" or "[::1]:8080" + if (target.startsWith("[")) { + return parseHostPort(target); + } // Check for single-colon scheme like "dns:endpoint" or "unix:/path" int colonIndex = target.indexOf(':'); if (colonIndex == -1) { @@ -112,6 +116,9 @@ private static ParsedTarget parseHostPort(String hostPort) { int closeBracket = hostPort.indexOf(']'); if (closeBracket != -1) { String host = hostPort.substring(1, closeBracket); + if (host.isEmpty()) { + return null; + } if (closeBracket + 1 < hostPort.length() && hostPort.charAt(closeBracket + 1) == ':') { Integer port = parsePort(hostPort.substring(closeBracket + 2)); return new ParsedTarget(host, port); @@ -132,6 +139,9 @@ private static ParsedTarget parseHostPort(String hostPort) { } String host = hostPort.substring(0, lastColon); + if (host.isEmpty()) { + return null; + } Integer port = parsePort(hostPort.substring(lastColon + 1)); return new ParsedTarget(host, port); } diff --git a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java index cf3467bda8a9..17ab42ded096 100644 --- a/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java +++ b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java @@ -76,10 +76,12 @@ void parseNullOrEmpty(String target) { } @ParameterizedTest - @ValueSource(strings = {"dns:", "dns:///"}) + @ValueSource(strings = {"dns:", "dns:///", ":8080", ":", "[]:8080", "[]"}) void parseEmptyEndpointReturnsNull(String target) { // "dns:" -> empty after single-colon scheme // "dns:///" -> empty endpoint after authority slash + // ":8080", ":" -> empty host before port + // "[]:8080", "[]" -> empty host inside IPv6 brackets assertThat(GrpcTargetParser.parse(target)).isNull(); } }