Skip to content

Commit 66f95b7

Browse files
authored
census: add per call latency metric (#9906)
* updated call latency measure with AGGREGATION_WITH_MILLIS_HISTOGRAM; added test for call latency view
1 parent 5be17e8 commit 66f95b7

File tree

4 files changed

+123
-8
lines changed

4 files changed

+123
-8
lines changed

census/src/main/java/io/grpc/census/CensusStatsModule.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package io.grpc.census;
1818

1919
import static com.google.common.base.Preconditions.checkNotNull;
20+
import static io.grpc.census.internal.ObservabilityCensusConstants.API_LATENCY_PER_CALL;
2021

2122
import com.google.common.annotations.VisibleForTesting;
2223
import com.google.common.base.Stopwatch;
@@ -415,6 +416,7 @@ static final class CallAttemptsTracerFactory extends
415416
ClientTracer inboundMetricTracer;
416417
private final CensusStatsModule module;
417418
private final Stopwatch stopwatch;
419+
private final Stopwatch callStopwatch;
418420
@GuardedBy("lock")
419421
private boolean callEnded;
420422
private final TagContext parentCtx;
@@ -429,6 +431,7 @@ static final class CallAttemptsTracerFactory extends
429431
private final Object lock = new Object();
430432
// write @GuardedBy("lock") and happens before read
431433
private long retryDelayNanos;
434+
private long callLatencyNanos;
432435
@GuardedBy("lock")
433436
private int activeStreams;
434437
@GuardedBy("lock")
@@ -440,6 +443,7 @@ static final class CallAttemptsTracerFactory extends
440443
this.parentCtx = checkNotNull(parentCtx, "parentCtx");
441444
this.fullMethodName = checkNotNull(fullMethodName, "fullMethodName");
442445
this.stopwatch = module.stopwatchSupplier.get();
446+
this.callStopwatch = module.stopwatchSupplier.get().start();
443447
TagValue methodTag = TagValue.create(fullMethodName);
444448
startCtx = module.tagger.toBuilder(parentCtx)
445449
.putLocal(RpcMeasureConstants.GRPC_CLIENT_METHOD, methodTag)
@@ -501,6 +505,7 @@ void callEnded(Status status) {
501505
if (!module.recordFinishedRpcs) {
502506
return;
503507
}
508+
callStopwatch.stop();
504509
this.status = status;
505510
boolean shouldRecordFinishedCall = false;
506511
synchronized (lock) {
@@ -538,10 +543,12 @@ void recordFinishedCall() {
538543
if (attempts > 0) {
539544
retriesPerCall = attempts - 1;
540545
}
546+
callLatencyNanos = callStopwatch.elapsed(TimeUnit.NANOSECONDS);
541547
MeasureMap measureMap = module.statsRecorder.newMeasureMap()
542548
.put(RETRIES_PER_CALL, retriesPerCall)
543549
.put(TRANSPARENT_RETRIES_PER_CALL, transparentRetriesPerCall.get())
544-
.put(RETRY_DELAY_PER_CALL, retryDelayNanos / NANOS_PER_MILLI);
550+
.put(RETRY_DELAY_PER_CALL, retryDelayNanos / NANOS_PER_MILLI)
551+
.put(API_LATENCY_PER_CALL, callLatencyNanos / NANOS_PER_MILLI);
545552
TagValue methodTag = TagValue.create(fullMethodName);
546553
TagValue statusTag = TagValue.create(status.getCode().toString());
547554
measureMap.record(

census/src/main/java/io/grpc/census/internal/ObservabilityCensusConstants.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,43 @@
2525
import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_SENT_BYTES_PER_RPC;
2626
import static io.opencensus.contrib.grpc.metrics.RpcMeasureConstants.GRPC_SERVER_STATUS;
2727

28+
import com.google.common.annotations.VisibleForTesting;
2829
import io.opencensus.contrib.grpc.metrics.RpcViewConstants;
2930
import io.opencensus.stats.Aggregation;
31+
import io.opencensus.stats.Measure;
32+
import io.opencensus.stats.Measure.MeasureDouble;
3033
import io.opencensus.stats.View;
3134
import java.util.Arrays;
3235

33-
/** Temporary holder class for the observability specific OpenCensus constants.
34-
* The class will be removed once the new views are added in OpenCensus library. */
36+
// TODO(dnvindhya): Remove metric and view definitions from this class once it is moved to
37+
// OpenCensus library.
38+
/**
39+
* Temporary holder class for the observability specific OpenCensus constants. The class will be
40+
* removed once the new views are added in OpenCensus library.
41+
*/
42+
@VisibleForTesting
3543
public final class ObservabilityCensusConstants {
3644

3745
static final Aggregation AGGREGATION_WITH_BYTES_HISTOGRAM =
3846
RpcViewConstants.GRPC_CLIENT_SENT_BYTES_PER_RPC_VIEW.getAggregation();
3947

48+
static final Aggregation AGGREGATION_WITH_MILLIS_HISTOGRAM =
49+
RpcViewConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY_VIEW.getAggregation();
50+
51+
public static final MeasureDouble API_LATENCY_PER_CALL =
52+
Measure.MeasureDouble.create(
53+
"grpc.io/client/api_latency",
54+
"Time taken by gRPC to complete an RPC from application's perspective",
55+
"ms");
56+
57+
public static final View GRPC_CLIENT_API_LATENCY_VIEW =
58+
View.create(
59+
View.Name.create("grpc.io/client/api_latency"),
60+
"Time taken by gRPC to complete an RPC from application's perspective",
61+
API_LATENCY_PER_CALL,
62+
AGGREGATION_WITH_MILLIS_HISTOGRAM,
63+
Arrays.asList(GRPC_CLIENT_METHOD, GRPC_CLIENT_STATUS));
64+
4065
public static final View GRPC_CLIENT_SENT_COMPRESSED_MESSAGE_BYTES_PER_RPC_VIEW =
4166
View.create(
4267
View.Name.create("grpc.io/client/sent_compressed_message_bytes_per_rpc"),

census/src/test/java/io/grpc/census/CensusModulesTest.java

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static io.grpc.census.CensusStatsModule.CallAttemptsTracerFactory.RETRIES_PER_CALL;
2222
import static io.grpc.census.CensusStatsModule.CallAttemptsTracerFactory.RETRY_DELAY_PER_CALL;
2323
import static io.grpc.census.CensusStatsModule.CallAttemptsTracerFactory.TRANSPARENT_RETRIES_PER_CALL;
24+
import static io.grpc.census.internal.ObservabilityCensusConstants.API_LATENCY_PER_CALL;
2425
import static java.util.concurrent.TimeUnit.MILLISECONDS;
2526
import static org.junit.Assert.assertEquals;
2627
import static org.junit.Assert.assertFalse;
@@ -63,6 +64,7 @@
6364
import io.grpc.Status;
6465
import io.grpc.census.CensusTracingModule.CallAttemptsTracerFactory;
6566
import io.grpc.census.internal.DeprecatedCensusConstants;
67+
import io.grpc.census.internal.ObservabilityCensusConstants;
6668
import io.grpc.internal.FakeClock;
6769
import io.grpc.internal.testing.StatsTestUtils;
6870
import io.grpc.internal.testing.StatsTestUtils.FakeStatsRecorder;
@@ -121,6 +123,8 @@
121123
*/
122124
@RunWith(JUnit4.class)
123125
public class CensusModulesTest {
126+
127+
private static final double TOLERANCE = 1e-6;
124128
private static final CallOptions.Key<String> CUSTOM_OPTION =
125129
CallOptions.Key.createWithDefault("option1", "default");
126130
private static final CallOptions CALL_OPTIONS =
@@ -368,7 +372,7 @@ record = statsRecorder.pollRecord();
368372
.setSampleToLocalSpanStore(false)
369373
.build());
370374
verify(spyClientSpan, never()).end();
371-
assertZeroRetryRecorded();
375+
assertPerCallMetrics(0D);
372376
}
373377

374378
@Test
@@ -503,7 +507,7 @@ private void subtestClientBasicStatsDefaultContext(
503507
DeprecatedCensusConstants.RPC_CLIENT_UNCOMPRESSED_RESPONSE_BYTES));
504508
assertEquals(30 + 100 + 16 + 24,
505509
record.getMetricAsLongOrFail(RpcMeasureConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY));
506-
assertZeroRetryRecorded();
510+
assertPerCallMetrics(30D + 100 + 16 + 24);
507511
} else {
508512
assertNull(statsRecorder.pollRecord());
509513
}
@@ -691,6 +695,8 @@ record = statsRecorder.pollRecord();
691695
assertThat(record.getMetric(RETRIES_PER_CALL)).isEqualTo(1);
692696
assertThat(record.getMetric(TRANSPARENT_RETRIES_PER_CALL)).isEqualTo(2);
693697
assertThat(record.getMetric(RETRY_DELAY_PER_CALL)).isEqualTo(1000D + 10 + 10);
698+
assertThat(record.getMetric(API_LATENCY_PER_CALL))
699+
.isEqualTo(30D + 100 + 24 + 1000 + 100 + 10 + 10 + 16 + 24);
694700
}
695701

696702
private void assertRealTimeMetric(
@@ -716,13 +722,14 @@ private void assertRealTimeMetric(
716722
assertEquals(expectedValue, record.getMetricAsLongOrFail(measure));
717723
}
718724

719-
private void assertZeroRetryRecorded() {
725+
private void assertPerCallMetrics(double expectedLatencyValue) {
720726
StatsTestUtils.MetricsRecord record = statsRecorder.pollRecord();
721727
TagValue methodTag = record.tags.get(RpcMeasureConstants.GRPC_CLIENT_METHOD);
722728
assertEquals(method.getFullMethodName(), methodTag.asString());
723729
assertThat(record.getMetric(RETRIES_PER_CALL)).isEqualTo(0);
724730
assertThat(record.getMetric(TRANSPARENT_RETRIES_PER_CALL)).isEqualTo(0);
725731
assertThat(record.getMetric(RETRY_DELAY_PER_CALL)).isEqualTo(0D);
732+
assertThat(record.getMetric(API_LATENCY_PER_CALL)).isEqualTo(expectedLatencyValue);
726733
}
727734

728735
@Test
@@ -849,7 +856,7 @@ record = statsRecorder.pollRecord();
849856
3000,
850857
record.getMetricAsLongOrFail(RpcMeasureConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY));
851858
assertNull(record.getMetric(RpcMeasureConstants.GRPC_CLIENT_SERVER_LATENCY));
852-
assertZeroRetryRecorded();
859+
assertPerCallMetrics(3000D);
853860
}
854861

855862
@Test
@@ -989,7 +996,7 @@ private void subtestStatsHeadersPropagateTags(boolean propagate, boolean recordS
989996
assertNull(clientRecord.getMetric(DeprecatedCensusConstants.RPC_CLIENT_ERROR_COUNT));
990997
TagValue clientPropagatedTag = clientRecord.tags.get(StatsTestUtils.EXTRA_TAG);
991998
assertEquals("extra-tag-value-897", clientPropagatedTag.asString());
992-
assertZeroRetryRecorded();
999+
assertPerCallMetrics(0D);
9931000
}
9941001

9951002
if (!recordStats) {
@@ -1507,6 +1514,81 @@ public Long apply(AggregationData arg) {
15071514
});
15081515
}
15091516

1517+
@Test
1518+
public void callLatencyView() throws InterruptedException {
1519+
StatsComponent localStats = new StatsComponentImpl();
1520+
1521+
localStats
1522+
.getViewManager()
1523+
.registerView(ObservabilityCensusConstants.GRPC_CLIENT_API_LATENCY_VIEW);
1524+
1525+
CensusStatsModule localCensusStats = new CensusStatsModule(
1526+
tagger, tagCtxSerializer, localStats.getStatsRecorder(), fakeClock.getStopwatchSupplier(),
1527+
false, false, true, false /* real-time */, true);
1528+
1529+
CensusStatsModule.CallAttemptsTracerFactory callAttemptsTracerFactory =
1530+
new CensusStatsModule.CallAttemptsTracerFactory(
1531+
localCensusStats, tagger.empty(), method.getFullMethodName());
1532+
1533+
Metadata headers = new Metadata();
1534+
ClientStreamTracer tracer =
1535+
callAttemptsTracerFactory.newClientStreamTracer(STREAM_INFO, headers);
1536+
tracer.streamCreated(Attributes.EMPTY, headers);
1537+
fakeClock.forwardTime(50, MILLISECONDS);
1538+
Status status = Status.OK.withDescription("Success");
1539+
tracer.streamClosed(status);
1540+
callAttemptsTracerFactory.callEnded(status);
1541+
1542+
// Give OpenCensus a chance to update the views asynchronously.
1543+
Thread.sleep(100);
1544+
1545+
assertDistributionData(
1546+
localStats,
1547+
ObservabilityCensusConstants.GRPC_CLIENT_API_LATENCY_VIEW,
1548+
ImmutableList.of(TagValue.create(method.getFullMethodName()), TagValue.create("OK")),
1549+
50.0, 1, 0.0,
1550+
ImmutableList.of(
1551+
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 1L,
1552+
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L));
1553+
}
1554+
1555+
private void assertDistributionData(StatsComponent localStats, View view,
1556+
List<TagValue> dimension, double mean, long count, double sumOfSquaredDeviations,
1557+
List<Long> expectedBucketCounts) {
1558+
AggregationData aggregationData = localStats.getViewManager()
1559+
.getView(view.getName())
1560+
.getAggregationMap()
1561+
.get(dimension);
1562+
1563+
aggregationData.match(
1564+
Functions.</*@Nullable*/ Void>throwAssertionError(),
1565+
Functions.</*@Nullable*/ Void>throwAssertionError(),
1566+
Functions.</*@Nullable*/ Void>throwAssertionError(),
1567+
/* p3= */ new Function<AggregationData.DistributionData, Void>() {
1568+
@Override
1569+
public Void apply(AggregationData.DistributionData arg) {
1570+
assertThat(arg.getMean()).isWithin(TOLERANCE).of(mean);
1571+
assertThat(arg.getCount()).isEqualTo(count);
1572+
assertThat(arg.getSumOfSquaredDeviations())
1573+
.isWithin(TOLERANCE)
1574+
.of(sumOfSquaredDeviations);
1575+
assertThat(arg.getBucketCounts())
1576+
.containsExactlyElementsIn(expectedBucketCounts)
1577+
.inOrder();
1578+
return null;
1579+
}
1580+
},
1581+
Functions.</*@Nullable*/ Void>throwAssertionError(),
1582+
Functions.</*@Nullable*/ Void>throwAssertionError(),
1583+
new Function<AggregationData, Void>() {
1584+
@Override
1585+
public Void apply(AggregationData arg) {
1586+
assertThat(((AggregationData.DistributionData) arg).getCount()).isEqualTo(count);
1587+
return null;
1588+
}
1589+
});
1590+
}
1591+
15101592
static class CallInfo<ReqT, RespT> extends ServerCallInfo<ReqT, RespT> {
15111593
private final MethodDescriptor<ReqT, RespT> methodDescriptor;
15121594
private final Attributes attributes;

gcp-observability/src/main/java/io/grpc/gcp/observability/GcpObservability.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ private static void registerObservabilityViews() {
154154
viewManager.registerView(RpcViewConstants.GRPC_CLIENT_COMPLETED_RPC_VIEW);
155155
viewManager.registerView(RpcViewConstants.GRPC_CLIENT_STARTED_RPC_VIEW);
156156
viewManager.registerView(RpcViewConstants.GRPC_CLIENT_ROUNDTRIP_LATENCY_VIEW);
157+
viewManager.registerView(ObservabilityCensusConstants.GRPC_CLIENT_API_LATENCY_VIEW);
157158
viewManager.registerView(
158159
ObservabilityCensusConstants.GRPC_CLIENT_SENT_COMPRESSED_MESSAGE_BYTES_PER_RPC_VIEW);
159160
viewManager.registerView(

0 commit comments

Comments
 (0)