opentelemetry: Add grpc.target label to per-call metrics

As defined by gRFC A66, the target is on all client-side per-call
metrics (both call and attempt).
This commit is contained in:
Eric Anderson 2024-05-05 11:10:44 -07:00
parent ca35577327
commit 8516cfef9c
3 changed files with 45 additions and 19 deletions

View File

@ -19,6 +19,7 @@ package io.grpc.opentelemetry;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.METHOD_KEY;
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.STATUS_KEY;
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.TARGET_KEY;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
@ -97,8 +98,8 @@ final class OpenTelemetryMetricsModule {
/**
* Returns the client interceptor that facilitates OpenTelemetry metrics reporting.
*/
ClientInterceptor getClientInterceptor() {
return new MetricsClientInterceptor();
ClientInterceptor getClientInterceptor(String target) {
return new MetricsClientInterceptor(target);
}
static String recordMethodName(String fullMethodName, boolean isGeneratedMethod) {
@ -135,6 +136,7 @@ final class OpenTelemetryMetricsModule {
final CallAttemptsTracerFactory attemptsState;
final OpenTelemetryMetricsModule module;
final StreamInfo info;
final String target;
final String fullMethodName;
volatile long outboundWireSize;
volatile long inboundWireSize;
@ -142,10 +144,11 @@ final class OpenTelemetryMetricsModule {
Code statusCode;
ClientTracer(CallAttemptsTracerFactory attemptsState, OpenTelemetryMetricsModule module,
StreamInfo info, String fullMethodName) {
StreamInfo info, String target, String fullMethodName) {
this.attemptsState = attemptsState;
this.module = module;
this.info = info;
this.target = target;
this.fullMethodName = fullMethodName;
this.stopwatch = module.stopwatchSupplier.get().start();
}
@ -189,9 +192,9 @@ final class OpenTelemetryMetricsModule {
}
void recordFinishedAttempt() {
// TODO(dnvindhya) : add target as an attribute
io.opentelemetry.api.common.Attributes attribute =
io.opentelemetry.api.common.Attributes.of(METHOD_KEY, fullMethodName,
TARGET_KEY, target,
STATUS_KEY, statusCode.toString());
if (module.resource.clientAttemptDurationCounter() != null ) {
@ -212,6 +215,7 @@ final class OpenTelemetryMetricsModule {
@VisibleForTesting
static final class CallAttemptsTracerFactory extends ClientStreamTracer.Factory {
private final OpenTelemetryMetricsModule module;
private final String target;
private final Stopwatch attemptStopwatch;
private final Stopwatch callStopWatch;
@GuardedBy("lock")
@ -226,15 +230,17 @@ final class OpenTelemetryMetricsModule {
@GuardedBy("lock")
private boolean finishedCallToBeRecorded;
CallAttemptsTracerFactory(OpenTelemetryMetricsModule module, String fullMethodName) {
CallAttemptsTracerFactory(
OpenTelemetryMetricsModule module, String target, String fullMethodName) {
this.module = checkNotNull(module, "module");
this.target = checkNotNull(target, "target");
this.fullMethodName = checkNotNull(fullMethodName, "fullMethodName");
this.attemptStopwatch = module.stopwatchSupplier.get();
this.callStopWatch = module.stopwatchSupplier.get().start();
// TODO(dnvindhya) : add target as an attribute
io.opentelemetry.api.common.Attributes attribute =
io.opentelemetry.api.common.Attributes.of(METHOD_KEY, fullMethodName);
io.opentelemetry.api.common.Attributes attribute = io.opentelemetry.api.common.Attributes.of(
METHOD_KEY, fullMethodName,
TARGET_KEY, target);
// Record here in case mewClientStreamTracer() would never be called.
if (module.resource.clientAttemptCountCounter() != null) {
@ -257,9 +263,9 @@ final class OpenTelemetryMetricsModule {
// CallAttemptsTracerFactory constructor. attemptsPerCall will be non-zero after the first
// attempt, as first attempt cannot be a transparent retry.
if (attemptsPerCall.get() > 0) {
// TODO(dnvindhya): Add target as an attribute
io.opentelemetry.api.common.Attributes attribute =
io.opentelemetry.api.common.Attributes.of(METHOD_KEY, fullMethodName);
io.opentelemetry.api.common.Attributes.of(METHOD_KEY, fullMethodName,
TARGET_KEY, target);
if (module.resource.clientAttemptCountCounter() != null) {
module.resource.clientAttemptCountCounter().add(1, attribute);
}
@ -267,7 +273,7 @@ final class OpenTelemetryMetricsModule {
if (!info.isTransparentRetry()) {
attemptsPerCall.incrementAndGet();
}
return new ClientTracer(this, module, info, fullMethodName);
return new ClientTracer(this, module, info, target, fullMethodName);
}
// Called whenever each attempt is ended.
@ -309,15 +315,15 @@ final class OpenTelemetryMetricsModule {
void recordFinishedCall() {
if (attemptsPerCall.get() == 0) {
ClientTracer tracer = new ClientTracer(this, module, null, fullMethodName);
ClientTracer tracer = new ClientTracer(this, module, null, target, fullMethodName);
tracer.attemptNanos = attemptStopwatch.elapsed(TimeUnit.NANOSECONDS);
tracer.statusCode = status.getCode();
tracer.recordFinishedAttempt();
}
callLatencyNanos = callStopWatch.elapsed(TimeUnit.NANOSECONDS);
// TODO(dnvindhya): record target as an attribute
io.opentelemetry.api.common.Attributes attribute =
io.opentelemetry.api.common.Attributes.of(METHOD_KEY, fullMethodName,
TARGET_KEY, target,
STATUS_KEY, status.getCode().toString());
if (module.resource.clientCallDurationCounter() != null) {
@ -459,6 +465,12 @@ final class OpenTelemetryMetricsModule {
@VisibleForTesting
final class MetricsClientInterceptor implements ClientInterceptor {
private final String target;
MetricsClientInterceptor(String target) {
this.target = checkNotNull(target, "target");
}
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
@ -466,7 +478,7 @@ final class OpenTelemetryMetricsModule {
// which is true for all generated methods. Otherwise, programatically
// created methods result in high cardinality metrics.
final CallAttemptsTracerFactory tracerFactory = new CallAttemptsTracerFactory(
OpenTelemetryMetricsModule.this, recordMethodName(method.getFullMethodName(),
OpenTelemetryMetricsModule.this, target, recordMethodName(method.getFullMethodName(),
method.isSampledToLocalTracing()));
ClientCall<ReqT, RespT> call =
next.newCall(method, callOptions.withStreamTracerFactory(tracerFactory));

View File

@ -27,6 +27,7 @@ import com.google.common.collect.ImmutableMap;
import io.grpc.ExperimentalApi;
import io.grpc.InternalConfigurator;
import io.grpc.InternalConfiguratorRegistry;
import io.grpc.InternalManagedChannelBuilder;
import io.grpc.ManagedChannelBuilder;
import io.grpc.MetricSink;
import io.grpc.ServerBuilder;
@ -146,7 +147,8 @@ public final class OpenTelemetryModule {
*/
public void configureChannelBuilder(ManagedChannelBuilder<?> builder) {
builder.addMetricSink(sink);
builder.intercept(openTelemetryMetricsModule.getClientInterceptor());
InternalManagedChannelBuilder.interceptWithTarget(
builder, openTelemetryMetricsModule::getClientInterceptor);
}
/**

View File

@ -19,6 +19,7 @@ package io.grpc.opentelemetry;
import static io.grpc.ClientStreamTracer.NAME_RESOLUTION_DELAYED;
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.METHOD_KEY;
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.STATUS_KEY;
import static io.grpc.opentelemetry.internal.OpenTelemetryConstants.TARGET_KEY;
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertEquals;
@ -185,7 +186,7 @@ public class OpenTelemetryMetricsModuleTest {
Channel interceptedChannel =
ClientInterceptors.intercept(
grpcServerRule.getChannel(), callOptionsCatureInterceptor,
module.getClientInterceptor());
module.getClientInterceptor("target:///"));
ClientCall<String, String> call;
call = interceptedChannel.newCall(method, CALL_OPTIONS);
@ -211,16 +212,18 @@ public class OpenTelemetryMetricsModuleTest {
@Test
public void clientBasicMetrics() {
String target = "target:///";
OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter,
enabledMetricsMap, disableDefaultMetrics);
OpenTelemetryMetricsModule module =
new OpenTelemetryMetricsModule(fakeClock.getStopwatchSupplier(), resource);
OpenTelemetryMetricsModule.CallAttemptsTracerFactory callAttemptsTracerFactory =
new CallAttemptsTracerFactory(module, method.getFullMethodName());
new CallAttemptsTracerFactory(module, target, method.getFullMethodName());
Metadata headers = new Metadata();
ClientStreamTracer tracer =
callAttemptsTracerFactory.newClientStreamTracer(STREAM_INFO, headers);
io.opentelemetry.api.common.Attributes attributes = io.opentelemetry.api.common.Attributes.of(
TARGET_KEY, target,
METHOD_KEY, method.getFullMethodName());
assertThat(openTelemetryTesting.getMetrics())
@ -262,6 +265,7 @@ public class OpenTelemetryMetricsModuleTest {
io.opentelemetry.api.common.Attributes clientAttributes
= io.opentelemetry.api.common.Attributes.of(
TARGET_KEY, target,
METHOD_KEY, method.getFullMethodName(),
STATUS_KEY, Status.Code.OK.toString());
@ -346,17 +350,19 @@ public class OpenTelemetryMetricsModuleTest {
// This test is only unit-testing the metrics recording logic. The retry behavior is faked.
@Test
public void recordAttemptMetrics() {
String target = "dns:///example.com";
OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter,
enabledMetricsMap, disableDefaultMetrics);
OpenTelemetryMetricsModule module =
new OpenTelemetryMetricsModule(fakeClock.getStopwatchSupplier(), resource);
OpenTelemetryMetricsModule.CallAttemptsTracerFactory callAttemptsTracerFactory =
new OpenTelemetryMetricsModule.CallAttemptsTracerFactory(module,
new OpenTelemetryMetricsModule.CallAttemptsTracerFactory(module, target,
method.getFullMethodName());
ClientStreamTracer tracer =
callAttemptsTracerFactory.newClientStreamTracer(STREAM_INFO, new Metadata());
io.opentelemetry.api.common.Attributes attributes = io.opentelemetry.api.common.Attributes.of(
TARGET_KEY, target,
METHOD_KEY, method.getFullMethodName());
assertThat(openTelemetryTesting.getMetrics())
@ -387,6 +393,7 @@ public class OpenTelemetryMetricsModuleTest {
io.opentelemetry.api.common.Attributes clientAttributes
= io.opentelemetry.api.common.Attributes.of(
TARGET_KEY, target,
METHOD_KEY, method.getFullMethodName(),
STATUS_KEY, Code.UNAVAILABLE.toString());
@ -467,6 +474,7 @@ public class OpenTelemetryMetricsModuleTest {
io.opentelemetry.api.common.Attributes clientAttributes1
= io.opentelemetry.api.common.Attributes.of(
TARGET_KEY, target,
METHOD_KEY, method.getFullMethodName(),
STATUS_KEY, Code.NOT_FOUND.toString());
@ -653,6 +661,7 @@ public class OpenTelemetryMetricsModuleTest {
io.opentelemetry.api.common.Attributes clientAttributes2
= io.opentelemetry.api.common.Attributes.of(
TARGET_KEY, target,
METHOD_KEY, method.getFullMethodName(),
STATUS_KEY, Code.OK.toString());
@ -767,12 +776,13 @@ public class OpenTelemetryMetricsModuleTest {
@Test
public void clientStreamNeverCreatedStillRecordMetrics() {
String target = "dns:///foo.example.com";
OpenTelemetryMetricsResource resource = OpenTelemetryModule.createMetricInstruments(testMeter,
enabledMetricsMap, disableDefaultMetrics);
OpenTelemetryMetricsModule module =
new OpenTelemetryMetricsModule(fakeClock.getStopwatchSupplier(), resource);
OpenTelemetryMetricsModule.CallAttemptsTracerFactory callAttemptsTracerFactory =
new OpenTelemetryMetricsModule.CallAttemptsTracerFactory(module,
new OpenTelemetryMetricsModule.CallAttemptsTracerFactory(module, target,
method.getFullMethodName());
fakeClock.forwardTime(3000, MILLISECONDS);
Status status = Status.DEADLINE_EXCEEDED.withDescription("5 seconds");
@ -780,10 +790,12 @@ public class OpenTelemetryMetricsModuleTest {
io.opentelemetry.api.common.Attributes attemptStartedAttributes
= io.opentelemetry.api.common.Attributes.of(
TARGET_KEY, target,
METHOD_KEY, method.getFullMethodName());
io.opentelemetry.api.common.Attributes clientAttributes
= io.opentelemetry.api.common.Attributes.of(
TARGET_KEY, target,
METHOD_KEY, method.getFullMethodName(),
STATUS_KEY,
Code.DEADLINE_EXCEEDED.toString());