Skip to content

Commit 5c9135a

Browse files
authored
feat: Add an OpenCensus stats implementation. (#665)
1 parent f2d334f commit 5c9135a

File tree

5 files changed

+346
-0
lines changed

5 files changed

+346
-0
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ dependencies {
5454
compileOnly 'com.google.appengine:appengine-api-1.0-sdk:1.9.76'
5555
api 'com.squareup.okhttp3:okhttp:3.14.4'
5656
api 'com.google.code.gson:gson:2.8.5'
57+
api 'io.opencensus:opencensus-api:0.25.0'
5758
implementation 'org.slf4j:slf4j-api:1.7.26'
5859
testImplementation 'junit:junit:4.12'
5960
testImplementation 'org.mockito:mockito-core:3.0.0'
@@ -62,6 +63,7 @@ dependencies {
6263
testImplementation 'org.slf4j:slf4j-simple:1.7.26'
6364
testImplementation 'org.apache.commons:commons-lang3:3.9'
6465
testImplementation 'org.json:json:20180813'
66+
testImplementation 'io.opencensus:opencensus-impl:0.25.0'
6567
}
6668

6769
task updateVersion(type: Copy) {
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.google.maps.metrics;
2+
3+
import io.opencensus.stats.Aggregation;
4+
import io.opencensus.stats.Aggregation.Count;
5+
import io.opencensus.stats.Aggregation.Distribution;
6+
import io.opencensus.stats.BucketBoundaries;
7+
import io.opencensus.stats.Measure.MeasureLong;
8+
import io.opencensus.stats.Stats;
9+
import io.opencensus.stats.View;
10+
import io.opencensus.stats.ViewManager;
11+
import io.opencensus.tags.TagKey;
12+
import java.util.Arrays;
13+
import java.util.Collections;
14+
import java.util.List;
15+
16+
/*
17+
* OpenCensus metrics which are measured for every request.
18+
*/
19+
public final class OpenCensusMetrics {
20+
private OpenCensusMetrics() {}
21+
22+
public static final class Tags {
23+
private Tags() {}
24+
25+
public static final TagKey REQUEST_NAME = TagKey.create("request_name");
26+
public static final TagKey HTTP_CODE = TagKey.create("http_code");
27+
public static final TagKey API_STATUS = TagKey.create("api_status");
28+
}
29+
30+
public static final class Measures {
31+
private Measures() {}
32+
33+
public static final MeasureLong LATENCY =
34+
MeasureLong.create(
35+
"maps.googleapis.com/measure/client/latency",
36+
"Total time between library method called and results returned",
37+
"ms");
38+
39+
public static final MeasureLong NETWORK_LATENCY =
40+
MeasureLong.create(
41+
"maps.googleapis.com/measure/client/network_latency",
42+
"Network time inside the library",
43+
"ms");
44+
45+
public static final MeasureLong RETRY_COUNT =
46+
MeasureLong.create(
47+
"maps.googleapis.com/measure/client/retry_count",
48+
"How many times any request was retried",
49+
"1");
50+
}
51+
52+
private static final class Aggregations {
53+
private Aggregations() {}
54+
55+
private static final Aggregation COUNT = Count.create();
56+
57+
private static final Aggregation DISTRIBUTION_INTEGERS_10 =
58+
Distribution.create(
59+
BucketBoundaries.create(
60+
Arrays.asList(0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0)));
61+
62+
// every bucket is ~25% bigger = 20 * 2^(N/3)
63+
private static final Aggregation DISTRIBUTION_LATENCY =
64+
Distribution.create(
65+
BucketBoundaries.create(
66+
Arrays.asList(
67+
0.0, 20.0, 25.2, 31.7, 40.0, 50.4, 63.5, 80.0, 100.8, 127.0, 160.0, 201.6,
68+
254.0, 320.0, 403.2, 508.0, 640.0, 806.3, 1015.9, 1280.0, 1612.7, 2031.9,
69+
2560.0, 3225.4, 4063.7)));
70+
}
71+
72+
public static final class Views {
73+
private Views() {}
74+
75+
private static final List<TagKey> fields =
76+
tags(Tags.REQUEST_NAME, Tags.HTTP_CODE, Tags.API_STATUS);
77+
78+
public static final View REQUEST_COUNT =
79+
View.create(
80+
View.Name.create("maps.googleapis.com/client/request_count"),
81+
"Request counts",
82+
Measures.LATENCY,
83+
Aggregations.COUNT,
84+
fields);
85+
86+
public static final View REQUEST_LATENCY =
87+
View.create(
88+
View.Name.create("maps.googleapis.com/client/request_latency"),
89+
"Latency in msecs",
90+
Measures.LATENCY,
91+
Aggregations.DISTRIBUTION_LATENCY,
92+
fields);
93+
94+
public static final View NETWORK_LATENCY =
95+
View.create(
96+
View.Name.create("maps.googleapis.com/client/network_latency"),
97+
"Network latency in msecs (internal)",
98+
Measures.NETWORK_LATENCY,
99+
Aggregations.DISTRIBUTION_LATENCY,
100+
fields);
101+
102+
public static final View RETRY_COUNT =
103+
View.create(
104+
View.Name.create("maps.googleapis.com/client/retry_count"),
105+
"Retries per request",
106+
Measures.RETRY_COUNT,
107+
Aggregations.DISTRIBUTION_INTEGERS_10,
108+
fields);
109+
}
110+
111+
public static void registerAllViews() {
112+
registerAllViews(Stats.getViewManager());
113+
}
114+
115+
public static void registerAllViews(ViewManager viewManager) {
116+
View[] views_to_register =
117+
new View[] {
118+
Views.REQUEST_COUNT, Views.REQUEST_LATENCY, Views.NETWORK_LATENCY, Views.RETRY_COUNT
119+
};
120+
for (View view : views_to_register) {
121+
viewManager.registerView(view);
122+
}
123+
}
124+
125+
private static List<TagKey> tags(TagKey... items) {
126+
return Collections.unmodifiableList(Arrays.asList(items));
127+
}
128+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.google.maps.metrics;
2+
3+
import io.opencensus.stats.StatsRecorder;
4+
import io.opencensus.tags.TagContext;
5+
import io.opencensus.tags.TagValue;
6+
import io.opencensus.tags.Tagger;
7+
8+
/** An OpenCensus logger that generates success and latency metrics. */
9+
final class OpenCensusRequestMetrics implements RequestMetrics {
10+
private final String requestName;
11+
private final Tagger tagger;
12+
private final StatsRecorder statsRecorder;
13+
14+
private long requestStart;
15+
private long networkStart;
16+
private long networkTime;
17+
private boolean finished;
18+
19+
OpenCensusRequestMetrics(String requestName, Tagger tagger, StatsRecorder statsRecorder) {
20+
this.requestName = requestName;
21+
this.tagger = tagger;
22+
this.statsRecorder = statsRecorder;
23+
this.requestStart = milliTime();
24+
this.networkStart = milliTime();
25+
this.networkTime = 0;
26+
this.finished = false;
27+
}
28+
29+
@Override
30+
public void startNetwork() {
31+
this.networkStart = milliTime();
32+
}
33+
34+
@Override
35+
public void endNetwork() {
36+
this.networkTime += milliTime() - this.networkStart;
37+
}
38+
39+
@Override
40+
public void endRequest(Exception exception, int httpStatusCode, long retryCount) {
41+
// multiple endRequest are ignored
42+
if (this.finished) {
43+
return;
44+
}
45+
this.finished = true;
46+
long requestTime = milliTime() - this.requestStart;
47+
48+
TagContext tagContext =
49+
tagger
50+
.currentBuilder()
51+
.putLocal(OpenCensusMetrics.Tags.REQUEST_NAME, TagValue.create(requestName))
52+
.putLocal(
53+
OpenCensusMetrics.Tags.HTTP_CODE, TagValue.create(Integer.toString(httpStatusCode)))
54+
.putLocal(OpenCensusMetrics.Tags.API_STATUS, TagValue.create(exceptionName(exception)))
55+
.build();
56+
statsRecorder
57+
.newMeasureMap()
58+
.put(OpenCensusMetrics.Measures.LATENCY, requestTime)
59+
.put(OpenCensusMetrics.Measures.NETWORK_LATENCY, this.networkTime)
60+
.put(OpenCensusMetrics.Measures.RETRY_COUNT, retryCount)
61+
.record(tagContext);
62+
}
63+
64+
private String exceptionName(Exception exception) {
65+
if (exception == null) {
66+
return "";
67+
} else {
68+
return exception.getClass().getName();
69+
}
70+
}
71+
72+
private long milliTime() {
73+
return System.currentTimeMillis();
74+
}
75+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.google.maps.metrics;
2+
3+
import io.opencensus.stats.Stats;
4+
import io.opencensus.stats.StatsRecorder;
5+
import io.opencensus.tags.Tagger;
6+
import io.opencensus.tags.Tags;
7+
8+
/** An OpenCensus logger that generates success and latency metrics. */
9+
public final class OpenCensusRequestMetricsReporter implements RequestMetricsReporter {
10+
private static final Tagger tagger = Tags.getTagger();
11+
private static final StatsRecorder statsRecorder = Stats.getStatsRecorder();
12+
13+
public OpenCensusRequestMetricsReporter() {}
14+
15+
@Override
16+
public RequestMetrics newRequest(String requestName) {
17+
return new OpenCensusRequestMetrics(requestName, tagger, statsRecorder);
18+
}
19+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package com.google.maps;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertNotNull;
5+
import static org.junit.Assert.assertTrue;
6+
7+
import com.google.maps.internal.ApiConfig;
8+
import com.google.maps.metrics.OpenCensusMetrics;
9+
import com.google.maps.metrics.OpenCensusRequestMetricsReporter;
10+
import com.google.maps.model.GeocodingResult;
11+
import io.opencensus.stats.AggregationData;
12+
import io.opencensus.stats.Stats;
13+
import io.opencensus.stats.View;
14+
import io.opencensus.stats.ViewData;
15+
import io.opencensus.tags.TagValue;
16+
import java.io.IOException;
17+
import java.util.Arrays;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.concurrent.TimeUnit;
21+
import okhttp3.mockwebserver.MockResponse;
22+
import okhttp3.mockwebserver.MockWebServer;
23+
import org.junit.After;
24+
import org.junit.Before;
25+
import org.junit.Test;
26+
import org.junit.experimental.categories.Category;
27+
28+
@Category(MediumTests.class)
29+
public class OpenCensusTest {
30+
31+
private MockWebServer server;
32+
private GeoApiContext context;
33+
34+
@Before
35+
public void Setup() {
36+
server = new MockWebServer();
37+
context =
38+
new GeoApiContext.Builder()
39+
.apiKey("AIza...")
40+
.requestMetricsReporter(new OpenCensusRequestMetricsReporter())
41+
.baseUrlOverride("http://127.0.0.1:" + server.getPort())
42+
.build();
43+
OpenCensusMetrics.registerAllViews();
44+
}
45+
46+
@After
47+
@SuppressWarnings("CatchAndPrintStackTrace")
48+
public void Teardown() {
49+
try {
50+
server.shutdown();
51+
} catch (IOException e) {
52+
e.printStackTrace();
53+
}
54+
}
55+
56+
private MockResponse mockResponse(int code, String status, int delayMs) {
57+
MockResponse response = new MockResponse();
58+
response.setResponseCode(code);
59+
if (status != null) {
60+
response.setBody("{\"results\" : [{}], \"status\" : \"" + status + "\" }");
61+
}
62+
response.setBodyDelay(delayMs, TimeUnit.MILLISECONDS);
63+
return response;
64+
}
65+
66+
private void sleep(int milliseconds) {
67+
try {
68+
TimeUnit.MILLISECONDS.sleep(milliseconds);
69+
} catch (Exception e) {
70+
}
71+
}
72+
73+
private Map.Entry<List<TagValue>, AggregationData> getMetric(String name) {
74+
sleep(10);
75+
ViewData viewData = Stats.getViewManager().getView(View.Name.create(name));
76+
Map<List<TagValue>, AggregationData> values = viewData.getAggregationMap();
77+
assertEquals(1, values.size());
78+
for (Map.Entry<List<TagValue>, AggregationData> entry : values.entrySet()) {
79+
return entry;
80+
}
81+
return null;
82+
}
83+
84+
@Test
85+
public void testSuccess() throws Exception {
86+
server.enqueue(mockResponse(500, "OK", 100)); // retry 1
87+
server.enqueue(mockResponse(500, "OK", 100)); // retry 2
88+
server.enqueue(mockResponse(200, "OK", 300)); // succeed
89+
90+
GeocodingResult[] result =
91+
context.get(new ApiConfig("/path"), GeocodingApi.Response.class, "k", "v").await();
92+
assertEquals(1, result.length);
93+
94+
List<TagValue> tags =
95+
Arrays.asList(TagValue.create(""), TagValue.create("200"), TagValue.create("/path"));
96+
97+
Map.Entry<List<TagValue>, AggregationData> latencyMetric =
98+
getMetric("maps.googleapis.com/client/request_latency");
99+
assertNotNull(latencyMetric);
100+
assertEquals(tags, latencyMetric.getKey());
101+
AggregationData.DistributionData latencyDist =
102+
(AggregationData.DistributionData) latencyMetric.getValue();
103+
assertEquals(1, latencyDist.getCount());
104+
assertTrue(latencyDist.getMean() > 500);
105+
106+
Map.Entry<List<TagValue>, AggregationData> retryMetric =
107+
getMetric("maps.googleapis.com/client/retry_count");
108+
assertNotNull(retryMetric);
109+
assertEquals(tags, retryMetric.getKey());
110+
AggregationData.DistributionData retryDist =
111+
(AggregationData.DistributionData) retryMetric.getValue();
112+
assertEquals(1, retryDist.getCount());
113+
assertEquals(2.0, retryDist.getMean(), 0.1);
114+
115+
Map.Entry<List<TagValue>, AggregationData> countMetric =
116+
getMetric("maps.googleapis.com/client/request_count");
117+
assertNotNull(countMetric);
118+
assertEquals(tags, countMetric.getKey());
119+
AggregationData.CountData count = (AggregationData.CountData) countMetric.getValue();
120+
assertEquals(1, count.getCount());
121+
}
122+
}

0 commit comments

Comments
 (0)