Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

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

- `GrpcRequest.getMethod()` now returns `@Nullable` to support unregistered-service requests where
no `MethodDescriptor` is available
([#16214](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/16214))

## Version 2.25.0 (2026-02-13)

### ⚠️ Breaking changes to non-stable APIs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ default String getRpcMethod(REQUEST request) {
return null;
}

/**
* Returns the original method name when the method reported via {@link #getRpcMethod(REQUEST)} is
* set to {@code _OTHER} because the method is not recognized by the RPC framework.
*/
@Nullable
default String getRpcMethodOriginal(REQUEST request) {
return null;
}

/**
* Returns a description of a class of error the operation ended with.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ abstract class RpcCommonAttributesExtractor<REQUEST, RESPONSE>
implements AttributesExtractor<REQUEST, RESPONSE> {

static final AttributeKey<String> RPC_METHOD = AttributeKey.stringKey("rpc.method");
static final AttributeKey<String> RPC_METHOD_ORIGINAL =
AttributeKey.stringKey("rpc.method_original");

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

if (emitOldRpcSemconv()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.linecorp.armeria.server.grpc.GrpcServiceBuilder;
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 net.bytebuddy.asm.Advice;
Expand All @@ -36,7 +37,8 @@ public static class BuildAdvice {

@Advice.OnMethodEnter
public static void onEnter(@Advice.This GrpcServiceBuilder builder) {
builder.intercept(GrpcTelemetry.create(GlobalOpenTelemetry.get()).createServerInterceptor());
GrpcTelemetry telemetry = GrpcTelemetry.create(GlobalOpenTelemetry.get());
builder.intercept(Internal.createServerInterceptor(telemetry));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static CallDepth onEnter(@Advice.This ServerBuilder<?> serverBuilder) {
return callDepth;
}
if (!Boolean.TRUE.equals(SERVER_BUILDER_INSTRUMENTED.get(serverBuilder))) {
serverBuilder.intercept(GrpcSingletons.SERVER_INTERCEPTOR);
GrpcSingletons.configureServerBuilder(serverBuilder);
SERVER_BUILDER_INSTRUMENTED.set(serverBuilder, true);
}
return callDepth;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import io.grpc.Context;
import io.grpc.ManagedChannelBuilder;
import io.grpc.ServerBuilder;
import io.grpc.ServerInterceptor;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
import io.opentelemetry.instrumentation.api.incubator.config.internal.DeclarativeConfigUtil;
Expand All @@ -33,7 +32,7 @@ public final class GrpcSingletons {

public static final ClientInterceptor CLIENT_INTERCEPTOR;

public static final ServerInterceptor SERVER_INTERCEPTOR;
private static final GrpcTelemetry GRPC_TELEMETRY;

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

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

GrpcTelemetry telemetry =
GRPC_TELEMETRY =
GrpcTelemetry.builder(GlobalOpenTelemetry.get())
.setEmitMessageEvents(emitMessageEvents)
.setCaptureExperimentalSpanAttributes(experimentalSpanAttributes)
.setCapturedClientRequestMetadata(clientRequestMetadata)
.setCapturedServerRequestMetadata(serverRequestMetadata)
.build();

CLIENT_INTERCEPTOR = telemetry.createClientInterceptor();
SERVER_INTERCEPTOR = telemetry.createServerInterceptor();
CLIENT_INTERCEPTOR = GRPC_TELEMETRY.createClientInterceptor();
}

public static Context.Storage getStorage() {
Expand All @@ -77,5 +75,9 @@ public static Context.Storage setStorage(Context.Storage storage) {
return getStorage();
}

public static void configureServerBuilder(ServerBuilder<?> serverBuilder) {
GRPC_TELEMETRY.configureServerBuilder(serverBuilder);
}

private GrpcSingletons() {}
}
6 changes: 3 additions & 3 deletions instrumentation/grpc-1.6/library/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ void configureClientInterceptor(OpenTelemetry openTelemetry, NettyChannelBuilder
nettyChannelBuilder.intercept(grpcTelemetry.createClientInterceptor());
}

// For server-side, attatch the interceptor to your service.
ServerServiceDefinition configureServerInterceptor(OpenTelemetry openTelemetry, ServerServiceDefinition serviceDefinition) {
// For server-side, configure the server builder.
void configureServer(OpenTelemetry openTelemetry, ServerBuilder<?> serverBuilder) {
GrpcTelemetry grpcTelemetry = GrpcTelemetry.create(openTelemetry);
return ServerInterceptors.intercept(serviceDefinition, grpcTelemetry.createServerInterceptor());
grpcTelemetry.configureServerBuilder(serverBuilder);
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@

public final class GrpcRequest {

private final MethodDescriptor<?, ?> method;
@Nullable private volatile MethodDescriptor<?, ?> method;
private final String fullMethodName;
@Nullable private final String originalFullMethodName;

@Nullable private volatile Metadata metadata;

Expand All @@ -29,11 +31,19 @@ public final class GrpcRequest {
@Nullable SocketAddress peerSocketAddress,
@Nullable String authority) {
this.method = method;
this.fullMethodName = method.getFullMethodName();
this.originalFullMethodName = null;
this.metadata = metadata;
this.peerSocketAddress = peerSocketAddress;
setLogicalAddress(authority);
}

GrpcRequest(String fullMethodName, @Nullable String originalFullMethodName, Metadata metadata) {
this.fullMethodName = fullMethodName;
this.originalFullMethodName = originalFullMethodName;
this.metadata = metadata;
}

private void setLogicalAddress(@Nullable String authority) {
if (authority == null) {
return;
Expand All @@ -51,10 +61,20 @@ private void setLogicalAddress(@Nullable String authority) {
}
}

@Nullable
public MethodDescriptor<?, ?> getMethod() {
return method;
}

String getFullMethodName() {
return fullMethodName;
}

@Nullable
String getOriginalFullMethodName() {
return originalFullMethodName;
}

@Nullable
public Metadata getMetadata() {
return metadata;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public String getSystem(GrpcRequest request) {
@Override
@Nullable
public String getService(GrpcRequest request) {
String fullMethodName = request.getMethod().getFullMethodName();
String fullMethodName = request.getFullMethodName();
int slashIndex = fullMethodName.lastIndexOf('/');
if (slashIndex == -1) {
return null;
Expand All @@ -38,7 +38,7 @@ public String getService(GrpcRequest request) {
@Override
@Nullable
public String getMethod(GrpcRequest request) {
String fullMethodName = request.getMethod().getFullMethodName();
String fullMethodName = request.getFullMethodName();
int slashIndex = fullMethodName.lastIndexOf('/');
if (slashIndex == -1) {
return null;
Expand All @@ -47,8 +47,9 @@ public String getMethod(GrpcRequest request) {
}

@Override
@Nullable
public String getRpcMethod(GrpcRequest request) {
return request.getMethod().getFullMethodName();
return request.getFullMethodName();
}

@Override
Expand All @@ -63,6 +64,12 @@ public Long getResponseSize(GrpcRequest request) {
return request.getResponseSize();
}

@Override
@Nullable
public String getRpcMethodOriginal(GrpcRequest request) {
return request.getOriginalFullMethodName();
}

List<String> metadataValue(GrpcRequest request, String key) {
if (request.getMetadata() == null) {
return emptyList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
final class GrpcSpanNameExtractor implements SpanNameExtractor<GrpcRequest> {
@Override
public String extract(GrpcRequest request) {
return request.getMethod().getFullMethodName();
return request.getFullMethodName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@
package io.opentelemetry.instrumentation.grpc.v1_6;

import io.grpc.ClientInterceptor;
import io.grpc.ServerBuilder;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.grpc.v1_6.internal.Internal;

/** Entrypoint for instrumenting gRPC servers or clients. */
public final class GrpcTelemetry {

static {
Internal.setServerInterceptorFactory(GrpcTelemetry::buildServerInterceptor);
}

/** Returns a new {@link GrpcTelemetry} configured with the given {@link OpenTelemetry}. */
public static GrpcTelemetry create(OpenTelemetry openTelemetry) {
return builder(openTelemetry).build();
Expand Down Expand Up @@ -53,11 +59,30 @@ public ClientInterceptor createClientInterceptor() {
clientInstrumenter, propagators, captureExperimentalSpanAttributes, emitMessageEvents);
}

/**
* Configures a {@link ServerBuilder} with both the server interceptor and the stream tracer
* factory. The interceptor handles registered service methods, while the stream tracer factory
* creates spans for requests to unregistered services that are not seen by server interceptors.
*/
public void configureServerBuilder(ServerBuilder<?> serverBuilder) {
serverBuilder.intercept(buildServerInterceptor());
serverBuilder.addStreamTracerFactory(
new TracingServerStreamTracerFactory(serverInstrumenter, propagators));
}

/**
* Returns a new {@link ServerInterceptor} for use with methods like {@link
* io.grpc.ServerBuilder#intercept(ServerInterceptor)}.
*
* @deprecated Use {@link #configureServerBuilder(ServerBuilder)} instead, which also registers
* the stream tracer factory needed to capture requests to unregistered services.
*/
@Deprecated
public ServerInterceptor createServerInterceptor() {
return buildServerInterceptor();
}

ServerInterceptor buildServerInterceptor() {
return new TracingServerInterceptor(
serverInstrumenter, captureExperimentalSpanAttributes, emitMessageEvents);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ public <REQUEST, RESPONSE> ServerCall.Listener<REQUEST> interceptCall(
// field.
authority = GrpcAuthorityStorage.getAuthority(call);
}

// If a ServerStreamTracer is active, mark it as handled so it won't create a span for this
// request in streamClosed().
TracingServerStreamTracer streamTracer = TracingServerStreamTracer.STREAM_TRACER_KEY.get();
if (streamTracer != null) {
streamTracer.markInterceptorHandled();
}

GrpcRequest request =
new GrpcRequest(
call.getMethodDescriptor(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.grpc.v1_6;

import io.grpc.Grpc;
import io.grpc.Metadata;
import io.grpc.ServerCall;
import io.grpc.ServerStreamTracer;
import io.grpc.Status;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
import io.opentelemetry.instrumentation.api.internal.InstrumenterUtil;
import java.net.SocketAddress;
import java.time.Instant;
import javax.annotation.Nullable;

/**
* A {@link ServerStreamTracer} that detects whether a gRPC server request was handled by the {@link
* TracingServerInterceptor}. If the interceptor does not fire (unregistered method), {@link
* #streamClosed(Status)} creates a span for the unhandled request.
*/
final class TracingServerStreamTracer extends ServerStreamTracer {

static final io.grpc.Context.Key<TracingServerStreamTracer> STREAM_TRACER_KEY =
io.grpc.Context.key("otel-grpc-stream-tracer");

private static final String UNKNOWN_METHOD_SPAN_NAME = "_OTHER";

private final Instrumenter<GrpcRequest, Status> instrumenter;
private final ContextPropagators propagators;
private final String fullMethodName;
private final Metadata headers;
private final Context parentContext;
private final Instant startTime;

private volatile boolean interceptorHandled;
@Nullable private volatile SocketAddress peerAddress;

TracingServerStreamTracer(
Instrumenter<GrpcRequest, Status> instrumenter,
ContextPropagators propagators,
String fullMethodName,
Metadata headers,
Context parentContext) {
this.instrumenter = instrumenter;
this.propagators = propagators;
this.fullMethodName = fullMethodName;
this.headers = headers;
this.parentContext = parentContext;
this.startTime = Instant.now();
}

void markInterceptorHandled() {
interceptorHandled = true;
}

@Override
public io.grpc.Context filterContext(io.grpc.Context context) {
return context.withValue(STREAM_TRACER_KEY, this);
}

@Override
public void serverCallStarted(ServerCall<?, ?> call) {
if (peerAddress == null) {
SocketAddress addr = call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR);
if (addr != null) {
peerAddress = addr;
}
}
}

@Override
public void streamClosed(Status status) {
if (interceptorHandled) {
return;
}
// Interceptor did not fire — this is an unregistered method
GrpcRequest request = new GrpcRequest(UNKNOWN_METHOD_SPAN_NAME, fullMethodName, headers);
if (peerAddress != null) {
request.setPeerSocketAddress(peerAddress);
}
// Extract trace context from incoming headers (e.g. W3C traceparent)
Context extracted =
propagators
.getTextMapPropagator()
.extract(parentContext, request, GrpcRequestGetter.INSTANCE);
if (instrumenter.shouldStart(extracted, request)) {
InstrumenterUtil.startAndEnd(
instrumenter, extracted, request, status, status.getCause(), startTime, Instant.now());
}
}
}
Loading
Loading