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
4 changes: 2 additions & 2 deletions instrumentation-api-incubator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ tasks {
val testStableSemconv by registering(Test::class) {
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath
jvmArgs("-Dotel.semconv-stability.opt-in=database,code,service.peer")
jvmArgs("-Dotel.semconv-stability.opt-in=database,code,service.peer,rpc")
inputs.dir(jflexOutputDir)
}

val testBothSemconv by registering(Test::class) {
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath
jvmArgs("-Dotel.semconv-stability.opt-in=database/dup,code/dup,service.peer/dup")
jvmArgs("-Dotel.semconv-stability.opt-in=database/dup,code/dup,service.peer/dup,rpc/dup")
inputs.dir(jflexOutputDir)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,18 @@
* library/framework. It will be used by the {@link RpcClientAttributesExtractor} or {@link
* RpcServerAttributesExtractor} to obtain the various RPC attributes in a type-generic way.
*/
public interface RpcAttributesGetter<REQUEST> {
public interface RpcAttributesGetter<REQUEST, RESPONSE> {

@Nullable
String getSystem(REQUEST request);

@Nullable
String getService(REQUEST request);

/**
* @deprecated Use {@link #getRpcMethod(REQUEST)} for stable semconv.
*/
@Deprecated
@Nullable
String getMethod(REQUEST request);

Expand All @@ -34,4 +38,62 @@ default Long getRequestSize(REQUEST request) {
default Long getResponseSize(REQUEST request) {
return null;
}

/**
* Returns the fully-qualified RPC method name for stable semconv.
*
* @param request the request object
* @return the fully-qualified RPC method name (e.g., "my.Service/Method"), or null if service or
* method is unavailable
*/
@Nullable
default String getRpcMethod(REQUEST request) {
return null;
}

/**
* Returns a description of a class of error the operation ended with.
*
* <p>This method should return {@code null} if there was no error.
*
* <p>If this method is not implemented, or if it returns {@code null}, the exception class name
* will be used as error type.
*
* <p>The cardinality of the error type should be low. The instrumentations implementing this
* method are recommended to document the custom values they support.
*
* <p>Examples: {@code OK}, {@code CANCELLED}, {@code UNKNOWN}, {@code -32602}
*/
@Nullable
default String getErrorType(
REQUEST request, @Nullable RESPONSE response, @Nullable Throwable error) {
return null;
}

/**
* Returns whether the RPC method is recognized as a predefined method by the RPC framework or
* library.
*
* <p>Some RPC frameworks or libraries provide a fixed set of recognized methods for client stubs
* and server implementations. Instrumentations for such frameworks MUST return {@code true} only
* when the method is recognized by the framework or library.
*
* <p>When the method is not recognized (for example, when the server receives a request for a
* method that is not predefined on the server), or when instrumentation is not able to reliably
* detect if the method is predefined, this method MUST return {@code false}.
*
* <p>When this method returns {@code false}, the {@code rpc.method} attribute will be set to
* {@code "_OTHER"} and the {@code rpc.method_original} attribute will be set to the original
* method name.
*
* <p>Note: If the RPC instrumentation could end up converting valid RPC methods to {@code
* "_OTHER"}, then it SHOULD provide a way to configure the list of recognized RPC methods.
*
* @param request the request object
* @return {@code true} if the method is recognized as predefined by the framework, {@code false}
* otherwise
*/
default boolean isPredefined(REQUEST request) {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ public final class RpcClientAttributesExtractor<REQUEST, RESPONSE>

/** Creates the RPC client attributes extractor. */
public static <REQUEST, RESPONSE> AttributesExtractor<REQUEST, RESPONSE> create(
RpcAttributesGetter<REQUEST> getter) {
RpcAttributesGetter<REQUEST, RESPONSE> getter) {
return new RpcClientAttributesExtractor<>(getter);
}

private RpcClientAttributesExtractor(RpcAttributesGetter<REQUEST> getter) {
private RpcClientAttributesExtractor(RpcAttributesGetter<REQUEST, RESPONSE> getter) {
super(getter);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

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

import static io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcCommonAttributesExtractor.OLD_RPC_METHOD_CONTEXT_KEY;
import static io.opentelemetry.instrumentation.api.incubator.semconv.rpc.RpcCommonAttributesExtractor.RPC_METHOD;
import static java.util.logging.Level.FINE;

import com.google.auto.value.AutoValue;
Expand All @@ -19,8 +21,10 @@
import io.opentelemetry.instrumentation.api.instrumenter.OperationListener;
import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics;
import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil;
import io.opentelemetry.instrumentation.api.internal.SemconvStability;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import javax.annotation.Nullable;

/**
* {@link OperationListener} which keeps track of <a
Expand All @@ -30,42 +34,62 @@
public final class RpcClientMetrics implements OperationListener {

private static final double NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1);
private static final double NANOS_PER_S = TimeUnit.SECONDS.toNanos(1);

private static final ContextKey<RpcClientMetrics.State> RPC_CLIENT_REQUEST_METRICS_STATE =
ContextKey.named("rpc-client-request-metrics-state");

private static final Logger logger = Logger.getLogger(RpcClientMetrics.class.getName());

private final DoubleHistogram clientDurationHistogram;
private final LongHistogram clientRequestSize;
private final LongHistogram clientResponseSize;
@Nullable private final DoubleHistogram oldClientDurationHistogram;
@Nullable private final DoubleHistogram stableClientDurationHistogram;
private final LongHistogram oldClientRequestSize;
private final LongHistogram oldClientResponseSize;

private RpcClientMetrics(Meter meter) {
DoubleHistogramBuilder durationBuilder =
meter
.histogramBuilder("rpc.client.duration")
.setDescription("The duration of an outbound RPC invocation.")
.setUnit("ms");
RpcMetricsAdvice.applyClientDurationAdvice(durationBuilder);
clientDurationHistogram = durationBuilder.build();
// Old metric (milliseconds)
if (SemconvStability.emitOldRpcSemconv()) {
DoubleHistogramBuilder oldDurationBuilder =
meter
.histogramBuilder("rpc.client.duration")
.setDescription("The duration of an outbound RPC invocation.")
.setUnit("ms");
RpcMetricsAdvice.applyClientDurationAdvice(oldDurationBuilder, false);
oldClientDurationHistogram = oldDurationBuilder.build();
} else {
oldClientDurationHistogram = null;
Comment on lines +59 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by default fields are initialized to null, is there anything to gain from explicitly assigning the null here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this allows the field to be final

}

// Stable metric (seconds)
if (SemconvStability.emitStableRpcSemconv()) {
DoubleHistogramBuilder stableDurationBuilder =
meter
.histogramBuilder("rpc.client.call.duration")
.setDescription("Measures the duration of outbound remote procedure calls (RPC).")
.setUnit("s");
RpcMetricsAdvice.applyClientDurationAdvice(stableDurationBuilder, true);
stableClientDurationHistogram = stableDurationBuilder.build();
} else {
stableClientDurationHistogram = null;
}

LongHistogramBuilder requestSizeBuilder =
meter
.histogramBuilder("rpc.client.request.size")
.setUnit("By")
.setDescription("Measures the size of RPC request messages (uncompressed).")
.ofLongs();
RpcMetricsAdvice.applyClientRequestSizeAdvice(requestSizeBuilder);
clientRequestSize = requestSizeBuilder.build();
RpcMetricsAdvice.applyOldClientRequestSizeAdvice(requestSizeBuilder);
oldClientRequestSize = requestSizeBuilder.build();

LongHistogramBuilder responseSizeBuilder =
meter
.histogramBuilder("rpc.client.response.size")
.setUnit("By")
.setDescription("Measures the size of RPC response messages (uncompressed).")
.ofLongs();
RpcMetricsAdvice.applyClientRequestSizeAdvice(responseSizeBuilder);
clientResponseSize = responseSizeBuilder.build();
RpcMetricsAdvice.applyOldClientRequestSizeAdvice(responseSizeBuilder);
oldClientResponseSize = responseSizeBuilder.build();
}

/**
Expand All @@ -81,7 +105,8 @@ public static OperationMetrics get() {
public Context onStart(Context context, Attributes startAttributes, long startNanos) {
return context.with(
RPC_CLIENT_REQUEST_METRICS_STATE,
new AutoValue_RpcClientMetrics_State(startAttributes, startNanos));
new AutoValue_RpcClientMetrics_State(
startAttributes, startNanos, context.get(OLD_RPC_METHOD_CONTEXT_KEY)));
}

@Override
Expand All @@ -95,18 +120,41 @@ public void onEnd(Context context, Attributes endAttributes, long endNanos) {
return;
}
Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build();
clientDurationHistogram.record(
(endNanos - state.startTimeNanos()) / NANOS_PER_MS, attributes, context);
double durationNanos = endNanos - state.startTimeNanos();

// Record to old histogram (milliseconds)
if (oldClientDurationHistogram != null) {
Attributes oldAttributes = getOldAttributes(attributes, state);
oldClientDurationHistogram.record(durationNanos / NANOS_PER_MS, oldAttributes, context);
}

Long rpcClientRequestBodySize = attributes.get(RpcSizeAttributesExtractor.RPC_REQUEST_SIZE);
if (rpcClientRequestBodySize != null) {
clientRequestSize.record(rpcClientRequestBodySize, attributes, context);
// Record to stable histogram (seconds)
if (stableClientDurationHistogram != null) {
stableClientDurationHistogram.record(durationNanos / NANOS_PER_S, attributes, context);
}

Long rpcClientResponseBodySize = attributes.get(RpcSizeAttributesExtractor.RPC_RESPONSE_SIZE);
if (rpcClientResponseBodySize != null) {
clientResponseSize.record(rpcClientResponseBodySize, attributes, context);
if (SemconvStability.emitOldRpcSemconv()) {
Attributes oldAttributes = getOldAttributes(attributes, state);

Long rpcClientRequestBodySize = attributes.get(RpcSizeAttributesExtractor.RPC_REQUEST_SIZE);
if (rpcClientRequestBodySize != null) {
oldClientRequestSize.record(rpcClientRequestBodySize, oldAttributes, context);
}

Long rpcClientResponseBodySize = attributes.get(RpcSizeAttributesExtractor.RPC_RESPONSE_SIZE);
if (rpcClientResponseBodySize != null) {
oldClientResponseSize.record(rpcClientResponseBodySize, oldAttributes, context);
}
}
}

private static Attributes getOldAttributes(Attributes attributes, State state) {
String oldRpcMethod = state.oldRpcMethod();
if (oldRpcMethod != null) {
// dup mode: replace stable rpc.method with old value
return attributes.toBuilder().put(RPC_METHOD, oldRpcMethod).build();
}
return attributes;
}

@AutoValue
Expand All @@ -115,5 +163,8 @@ abstract static class State {
abstract Attributes startAttributes();

abstract long startTimeNanos();

@Nullable
abstract String oldRpcMethod();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,84 @@
package io.opentelemetry.instrumentation.api.incubator.semconv.rpc;

import static io.opentelemetry.instrumentation.api.internal.AttributesExtractorUtil.internalSet;
import static io.opentelemetry.semconv.ErrorAttributes.ERROR_TYPE;

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.ContextKey;
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
import io.opentelemetry.instrumentation.api.instrumenter.ContextCustomizer;
import io.opentelemetry.instrumentation.api.internal.SemconvStability;
import javax.annotation.Nullable;

abstract class RpcCommonAttributesExtractor<REQUEST, RESPONSE>
public abstract class RpcCommonAttributesExtractor<REQUEST, RESPONSE>
implements AttributesExtractor<REQUEST, RESPONSE> {

// copied from RpcIncubatingAttributes
static final AttributeKey<String> RPC_METHOD = AttributeKey.stringKey("rpc.method");

static final ContextKey<String> OLD_RPC_METHOD_CONTEXT_KEY =
ContextKey.named("otel-rpc-old-method");

@SuppressWarnings("deprecation") // for getMethod()
public static <REQUEST> ContextCustomizer<REQUEST> oldMethodContextCustomizer(
RpcAttributesGetter<REQUEST, ?> getter) {
return (context, request, startAttributes) -> {
if (SemconvStability.emitOldRpcSemconv() && SemconvStability.emitStableRpcSemconv()) {
String oldMethod = getter.getMethod(request);
if (oldMethod != null) {
return context.with(OLD_RPC_METHOD_CONTEXT_KEY, oldMethod);
}
}
return context;
};
}

// Stable semconv keys
static final AttributeKey<String> RPC_SYSTEM_NAME = AttributeKey.stringKey("rpc.system.name");

// removed in stable semconv (merged into rpc.method)
static final AttributeKey<String> RPC_SERVICE = AttributeKey.stringKey("rpc.service");

// use RPC_SYSTEM_NAME for stable semconv
static final AttributeKey<String> RPC_SYSTEM = AttributeKey.stringKey("rpc.system");

private final RpcAttributesGetter<REQUEST> getter;
static final AttributeKey<String> RPC_METHOD_ORIGINAL =
AttributeKey.stringKey("rpc.method_original");

private final RpcAttributesGetter<REQUEST, RESPONSE> getter;

RpcCommonAttributesExtractor(RpcAttributesGetter<REQUEST> getter) {
RpcCommonAttributesExtractor(RpcAttributesGetter<REQUEST, RESPONSE> getter) {
this.getter = getter;
}

@SuppressWarnings("deprecation") // for getMethod()
@Override
public final void onStart(AttributesBuilder attributes, Context parentContext, REQUEST request) {
internalSet(attributes, RPC_SYSTEM, getter.getSystem(request));
internalSet(attributes, RPC_SERVICE, getter.getService(request));
internalSet(attributes, RPC_METHOD, getter.getMethod(request));
String system = getter.getSystem(request);

if (SemconvStability.emitStableRpcSemconv()) {
internalSet(
attributes,
RPC_SYSTEM_NAME,
system == null ? null : SemconvStability.stableRpcSystemName(system));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it would be better to handle null in SemconvStability.stableRpcSystemName

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the param would need to be nullable - which I'd like to avoid

String method = getter.getRpcMethod(request);
if (getter.isPredefined(request)) {
internalSet(attributes, RPC_METHOD, method);
} else {
internalSet(attributes, RPC_METHOD_ORIGINAL, method);
internalSet(attributes, RPC_METHOD, "_OTHER");
}
}

if (SemconvStability.emitOldRpcSemconv()) {
internalSet(attributes, RPC_SYSTEM, system);
internalSet(attributes, RPC_SERVICE, getter.getService(request));
if (!SemconvStability.emitStableRpcSemconv()) {
// only set old rpc.method on spans when there's no clash with stable rpc.method
internalSet(attributes, RPC_METHOD, getter.getMethod(request));
}
}
}

@Override
Expand All @@ -41,6 +93,13 @@ public final void onEnd(
REQUEST request,
@Nullable RESPONSE response,
@Nullable Throwable error) {
// No response attributes
if (SemconvStability.emitStableRpcSemconv()) {
String errorType = getter.getErrorType(request, response, error);
// fall back to exception class name & _OTHER
if (errorType == null && error != null) {
errorType = error.getClass().getName();
}
internalSet(attributes, ERROR_TYPE, errorType);
}
}
}
Loading