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 dd0e59d5c1b7..36378d3909f5 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,14 +5,18 @@ package io.opentelemetry.javaagent.instrumentation.armeria.grpc.v1_14; +import static io.opentelemetry.instrumentation.api.internal.SemconvStability.emitStableRpcSemconv; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; 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; @@ -34,8 +38,14 @@ public void transform(TypeTransformer transformer) { public static class BuildAdvice { @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) - 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 17d3df8775b8..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 @@ -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 @@ -44,9 +46,14 @@ public static class AddInterceptorAdvice { @Advice.OnMethodEnter(suppress = Throwable.class, inline = false) 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, clientInterceptor()); + ClientInterceptor interceptor = + emitStableRpcSemconv() + ? GrpcSingletons.createClientInterceptor(target) + : clientInterceptor(); + interceptors.add(0, interceptor); 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 68ae16349620..be05aed21dce 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,6 +18,7 @@ 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; @@ -32,6 +33,8 @@ public class GrpcSingletons { public static final VirtualField, Boolean> SERVER_BUILDER_INSTRUMENTED = VirtualField.find(ServerBuilder.class, Boolean.class); + private static final GrpcTelemetry telemetry; + private static final ClientInterceptor clientInterceptor; private static final ServerInterceptor serverInterceptor; @@ -57,7 +60,7 @@ public class GrpcSingletons { .get("server") .getScalarList("request", String.class, emptyList()); - GrpcTelemetry telemetry = + GrpcTelemetry configuredTelemetry = GrpcTelemetry.builder(GlobalOpenTelemetry.get()) .setEmitMessageEvents(emitMessageEvents) .setCaptureExperimentalSpanAttributes(experimentalSpanAttributes) @@ -65,8 +68,9 @@ public class GrpcSingletons { .setCapturedServerRequestMetadata(serverRequestMetadata) .build(); - clientInterceptor = telemetry.createClientInterceptor(); - serverInterceptor = telemetry.createServerInterceptor(); + telemetry = configuredTelemetry; + clientInterceptor = Internal.createClientInterceptor(configuredTelemetry, null); + serverInterceptor = configuredTelemetry.createServerInterceptor(); } public static ClientInterceptor clientInterceptor() { @@ -82,6 +86,10 @@ public static Context.Storage storage() { return storageReference.get(); } + public static ClientInterceptor createClientInterceptor(@Nullable String target) { + return Internal.createClientInterceptor(telemetry, target); + } + public static Context.Storage setStorage(Context.Storage storage) { storageReference.compareAndSet(null, new ContextStorageBridge(storage)); return storage(); diff --git a/instrumentation/grpc-1.6/library/README.md b/instrumentation/grpc-1.6/library/README.md index ecba1980aa12..1809e09e102d 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/GrpcRequest.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcRequest.java index 88f52ce454ff..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 @@ -7,6 +7,8 @@ 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; @@ -15,40 +17,67 @@ 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; @Nullable private volatile Long requestSize; @Nullable private volatile 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 + */ + static GrpcRequest createClientRequest( + MethodDescriptor method, + @Nullable String authority, + @Nullable ParsedTarget parsedTarget) { + 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, effective.getAddress(), effective.getPort()); + } + + /** + * 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 + */ + static GrpcRequest createServerRequest( MethodDescriptor method, @Nullable Metadata metadata, @Nullable SocketAddress peerSocketAddress, @Nullable String authority) { + ParsedTarget parsed = GrpcTargetParser.parseAuthority(authority); + return new GrpcRequest( + method, + metadata, + peerSocketAddress, + parsed != null ? parsed.getAddress() : null, + parsed != null ? parsed.getPort() : null); + } + + 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 +93,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 diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcServerNetworkAttributesGetter.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcServerNetworkAttributesGetter.java index c1d0657cffb6..b302d23f582b 100644 --- a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcServerNetworkAttributesGetter.java +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcServerNetworkAttributesGetter.java @@ -18,14 +18,13 @@ final class GrpcServerNetworkAttributesGetter @Nullable @Override public String getServerAddress(GrpcRequest grpcRequest) { - return grpcRequest.getLogicalHost(); + return grpcRequest.getServerAddress(); } @Override @Nullable public Integer getServerPort(GrpcRequest grpcRequest) { - int port = grpcRequest.getLogicalPort(); - return port == -1 ? null : port; + return grpcRequest.getServerPort(); } @Nullable diff --git a/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetry.java b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/GrpcTelemetry.java index b2e1b1728da7..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 @@ -5,12 +5,20 @@ 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; 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.Logger; +import javax.annotation.Nullable; /** Entrypoint for instrumenting gRPC servers or clients. */ public final class GrpcTelemetry { @@ -20,6 +28,34 @@ public final class GrpcTelemetry { private final boolean captureExperimentalSpanAttributes; private final boolean emitMessageEvents; + 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(); @@ -43,13 +79,66 @@ 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 { + interceptWithTargetMethod.invoke(null, builder, newInterceptorFactory()); + return; + } catch (Exception e) { + logger.log(FINE, "Failed to use interceptWithTarget, falling back", e); + } + } + + // Fallback for gRPC < 1.64.0: add interceptor without target info + 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...)}. + * + * @deprecated Use {@link #addClientInterceptor(ManagedChannelBuilder)} instead. */ + @Deprecated public ClientInterceptor createClientInterceptor() { - return new TracingClientInterceptor( - clientInstrumenter, propagators, captureExperimentalSpanAttributes, emitMessageEvents); + return newTracingClientInterceptor(null); } /** @@ -60,4 +149,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 e83cc5a1e34d..21b786d09ed7 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 4f78cb825093..35e08c7f3cb3 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 e2a36c0d6b06..1a8df14ba016 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,14 +23,13 @@ public final class GrpcClientNetworkAttributesGetter @Nullable @Override public String getServerAddress(GrpcRequest grpcRequest) { - return grpcRequest.getLogicalHost(); + return grpcRequest.getServerAddress(); } @Override @Nullable public Integer getServerPort(GrpcRequest grpcRequest) { - int port = grpcRequest.getLogicalPort(); - return port == -1 ? null : port; + 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..277987d31be8 --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/main/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParser.java @@ -0,0 +1,169 @@ +/* + * 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) { + // 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) { + // 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); + } + + /** + * 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); + } + + @Nullable + 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); + } + + @Nullable + 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); + } + + @Nullable + private static ParsedTarget parseHostPort(String hostPort) { + if (hostPort.isEmpty()) { + return null; + } + + // Handle IPv6 in brackets: [::1]:8080 + if (hostPort.startsWith("[")) { + 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); + } + 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); + if (host.isEmpty()) { + return null; + } + Integer port = parsePort(hostPort.substring(lastColon + 1)); + return new ParsedTarget(host, port); + } + + @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 29079d02641e..4c2bc608c09d 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 @@ -49,11 +49,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 @@ -92,16 +92,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..17ab42ded096 --- /dev/null +++ b/instrumentation/grpc-1.6/library/src/test/java/io/opentelemetry/instrumentation/grpc/v1_6/internal/GrpcTargetParserTest.java @@ -0,0 +1,87 @@ +/* + * 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; +import org.junit.jupiter.params.provider.ValueSource; + +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), + + // 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 + @NullAndEmptySource + void parseNullOrEmpty(String target) { + assertThat(GrpcTargetParser.parse(target)).isNull(); + } + + @ParameterizedTest + @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(); + } +}