Skip to content
This repository was archived by the owner on Dec 23, 2023. It is now read-only.

Commit 2bcac95

Browse files
authored
Exporter/Stats/Stackdriver: Support Exemplar. (#1771)
* Exporter/Stats/Stackdriver: Support Exemplar. * Fix nullness checker. * Support SpanContext attachment. * Fix nullness checker. * Use refactored Exemplar.
1 parent 70f9701 commit 2bcac95

File tree

6 files changed

+191
-22
lines changed

6 files changed

+191
-22
lines changed

buildscripts/import-control.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,9 @@ General guidelines on imports:
221221
<subpackage name="stackdriver">
222222
<allow pkg="com.google"/>
223223
<allow pkg="io.opencensus.exporter.stats.stackdriver"/>
224+
<allow pkg="io.opencensus.metrics.data"/>
224225
<allow pkg="io.opencensus.trace"/>
226+
<allow pkg="io.opencensus.contrib.exemplar.util"/>
225227
<allow pkg="io.opencensus.contrib.resource.util"/>
226228
</subpackage>
227229
</subpackage>

exporters/stats/stackdriver/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ dependencies {
99
compileOnly libraries.auto_value
1010

1111
compile project(':opencensus-api'),
12+
project(':opencensus-contrib-exemplar-util'),
1213
project(':opencensus-contrib-resource-util'),
1314
project(':opencensus-exporter-metrics-util'),
1415
libraries.google_auth,

exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/CreateTimeSeriesExporter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ public void export(Collection<Metric> metrics) {
6262
List<TimeSeries> timeSeriesList = new ArrayList<>(metrics.size());
6363
for (Metric metric : metrics) {
6464
timeSeriesList.addAll(
65-
StackdriverExportUtils.createTimeSeriesList(metric, monitoredResource, domain));
65+
StackdriverExportUtils.createTimeSeriesList(
66+
metric, monitoredResource, domain, projectName.getProject()));
6667
}
6768

6869
Span span = tracer.getCurrentSpan();

exporters/stats/stackdriver/src/main/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtils.java

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.api.Distribution;
2020
import com.google.api.Distribution.BucketOptions;
2121
import com.google.api.Distribution.BucketOptions.Explicit;
22+
import com.google.api.Distribution.Exemplar;
2223
import com.google.api.LabelDescriptor;
2324
import com.google.api.LabelDescriptor.ValueType;
2425
import com.google.api.Metric;
@@ -31,18 +32,24 @@
3132
import com.google.common.collect.Lists;
3233
import com.google.common.collect.Maps;
3334
import com.google.monitoring.v3.Point;
35+
import com.google.monitoring.v3.SpanContext;
3436
import com.google.monitoring.v3.TimeInterval;
3537
import com.google.monitoring.v3.TimeSeries;
3638
import com.google.monitoring.v3.TypedValue;
39+
import com.google.protobuf.Any;
40+
import com.google.protobuf.ByteString;
3741
import com.google.protobuf.Timestamp;
3842
import io.opencensus.common.Function;
3943
import io.opencensus.common.Functions;
44+
import io.opencensus.contrib.exemplar.util.AttachmentValueSpanContext;
45+
import io.opencensus.contrib.exemplar.util.ExemplarUtils;
4046
import io.opencensus.contrib.resource.util.AwsEc2InstanceResource;
4147
import io.opencensus.contrib.resource.util.GcpGceInstanceResource;
4248
import io.opencensus.contrib.resource.util.K8sContainerResource;
4349
import io.opencensus.contrib.resource.util.ResourceUtils;
4450
import io.opencensus.metrics.LabelKey;
4551
import io.opencensus.metrics.LabelValue;
52+
import io.opencensus.metrics.data.AttachmentValue;
4653
import io.opencensus.metrics.export.Distribution.Bucket;
4754
import io.opencensus.metrics.export.Distribution.BucketOptions.ExplicitOptions;
4855
import io.opencensus.metrics.export.MetricDescriptor.Type;
@@ -106,6 +113,22 @@ final class StackdriverExportUtils {
106113
@VisibleForTesting static final String SUMMARY_SUFFIX_COUNT = "_summary_count";
107114
@VisibleForTesting static final String SUMMARY_SUFFIX_SUM = "_summary_sum";
108115

116+
// Cached project ID only for Exemplar attachments. Without this we'll have to pass the project ID
117+
// every time when we convert a Distribution value.
118+
@javax.annotation.Nullable private static volatile String cachedProjectIdForExemplar = null;
119+
120+
@VisibleForTesting
121+
static final String EXEMPLAR_ATTACHMENT_TYPE_STRING =
122+
"type.googleapis.com/google.protobuf.StringValue";
123+
124+
@VisibleForTesting
125+
static final String EXEMPLAR_ATTACHMENT_TYPE_SPAN_CONTEXT =
126+
"type.googleapis.com/google.monitoring.v3.SpanContext";
127+
128+
// TODO: add support for dropped label attachment.
129+
// private static final String EXEMPLAR_ATTACHMENT_TYPE_DROPPED_LABELS =
130+
// "type.googleapis.com/google.monitoring.v3.DroppedLabels";
131+
109132
// Constant functions for TypedValue.
110133
private static final Function<Double, TypedValue> typedValueDoubleFunction =
111134
new Function<Double, TypedValue>() {
@@ -258,10 +281,15 @@ static MetricDescriptor.ValueType createValueType(Type type) {
258281
static List<TimeSeries> createTimeSeriesList(
259282
io.opencensus.metrics.export.Metric metric,
260283
MonitoredResource monitoredResource,
261-
String domain) {
284+
String domain,
285+
String projectId) {
262286
List<TimeSeries> timeSeriesList = Lists.newArrayList();
263287
io.opencensus.metrics.export.MetricDescriptor metricDescriptor = metric.getMetricDescriptor();
264288

289+
if (!projectId.equals(cachedProjectIdForExemplar)) {
290+
cachedProjectIdForExemplar = projectId;
291+
}
292+
265293
// Shared fields for all TimeSeries generated from the same Metric
266294
TimeSeries.Builder shared = TimeSeries.newBuilder();
267295
shared.setMetricKind(createMetricKind(metricDescriptor.getType()));
@@ -337,13 +365,15 @@ static TypedValue createTypedValue(Value value) {
337365
// Convert a OpenCensus Distribution to a StackDriver Distribution
338366
@VisibleForTesting
339367
static Distribution createDistribution(io.opencensus.metrics.export.Distribution distribution) {
340-
return Distribution.newBuilder()
341-
.setBucketOptions(createBucketOptions(distribution.getBucketOptions()))
342-
.addAllBucketCounts(createBucketCounts(distribution.getBuckets()))
343-
.setCount(distribution.getCount())
344-
.setMean(distribution.getCount() == 0 ? 0 : distribution.getSum() / distribution.getCount())
345-
.setSumOfSquaredDeviation(distribution.getSumOfSquaredDeviations())
346-
.build();
368+
Distribution.Builder builder =
369+
Distribution.newBuilder()
370+
.setBucketOptions(createBucketOptions(distribution.getBucketOptions()))
371+
.setCount(distribution.getCount())
372+
.setMean(
373+
distribution.getCount() == 0 ? 0 : distribution.getSum() / distribution.getCount())
374+
.setSumOfSquaredDeviation(distribution.getSumOfSquaredDeviations());
375+
setBucketCountsAndExemplars(distribution.getBuckets(), builder);
376+
return builder.build();
347377
}
348378

349379
// Convert a OpenCensus BucketOptions to a StackDriver BucketOptions
@@ -360,16 +390,73 @@ static BucketOptions createBucketOptions(
360390
bucketOptionsExplicitFunction, Functions.<BucketOptions>throwIllegalArgumentException());
361391
}
362392

363-
// Convert a OpenCensus Buckets to a list of counts
364-
private static List<Long> createBucketCounts(List<Bucket> buckets) {
365-
List<Long> bucketCounts = new ArrayList<>();
393+
// Convert OpenCensus Buckets to a list of bucket counts and a list of proto Exemplars, then set
394+
// them to the builder.
395+
private static void setBucketCountsAndExemplars(
396+
List<Bucket> buckets, Distribution.Builder builder) {
366397
// The first bucket (underflow bucket) should always be 0 count because the Metrics first bucket
367398
// is [0, first_bound) but StackDriver distribution consists of an underflow bucket (number 0).
368-
bucketCounts.add(0L);
399+
builder.addBucketCounts(0L);
369400
for (Bucket bucket : buckets) {
370-
bucketCounts.add(bucket.getCount());
401+
builder.addBucketCounts(bucket.getCount());
402+
@javax.annotation.Nullable
403+
io.opencensus.metrics.data.Exemplar exemplar = bucket.getExemplar();
404+
if (exemplar != null) {
405+
builder.addExemplars(toProtoExemplar(exemplar));
406+
}
371407
}
372-
return bucketCounts;
408+
}
409+
410+
private static Exemplar toProtoExemplar(io.opencensus.metrics.data.Exemplar exemplar) {
411+
Exemplar.Builder builder =
412+
Exemplar.newBuilder()
413+
.setValue(exemplar.getValue())
414+
.setTimestamp(convertTimestamp(exemplar.getTimestamp()));
415+
@javax.annotation.Nullable io.opencensus.trace.SpanContext spanContext = null;
416+
for (Map.Entry<String, AttachmentValue> attachment : exemplar.getAttachments().entrySet()) {
417+
String key = attachment.getKey();
418+
AttachmentValue value = attachment.getValue();
419+
if (ExemplarUtils.ATTACHMENT_KEY_SPAN_CONTEXT.equals(key)) {
420+
spanContext = ((AttachmentValueSpanContext) value).getSpanContext();
421+
} else { // Everything else will be treated as plain strings for now.
422+
builder.addAttachments(toProtoStringAttachment(value));
423+
}
424+
}
425+
if (spanContext != null && cachedProjectIdForExemplar != null) {
426+
SpanContext protoSpanContext = toProtoSpanContext(spanContext, cachedProjectIdForExemplar);
427+
builder.addAttachments(toProtoSpanContextAttachment(protoSpanContext));
428+
}
429+
return builder.build();
430+
}
431+
432+
private static Any toProtoStringAttachment(AttachmentValue attachmentValue) {
433+
return Any.newBuilder()
434+
.setTypeUrl(EXEMPLAR_ATTACHMENT_TYPE_STRING)
435+
.setValue(ByteString.copyFromUtf8(attachmentValue.getValue()))
436+
.build();
437+
}
438+
439+
private static Any toProtoSpanContextAttachment(SpanContext protoSpanContext) {
440+
return Any.newBuilder()
441+
.setTypeUrl(EXEMPLAR_ATTACHMENT_TYPE_SPAN_CONTEXT)
442+
.setValue(protoSpanContext.toByteString())
443+
.build();
444+
}
445+
446+
private static SpanContext toProtoSpanContext(
447+
io.opencensus.trace.SpanContext spanContext, String projectId) {
448+
String spanName =
449+
String.format(
450+
"projects/%s/traces/%s/spans/%s",
451+
projectId,
452+
spanContext.getTraceId().toLowerBase16(),
453+
spanContext.getSpanId().toLowerBase16());
454+
return SpanContext.newBuilder().setSpanName(spanName).build();
455+
}
456+
457+
@VisibleForTesting
458+
static void setCachedProjectIdForExemplar(@javax.annotation.Nullable String projectId) {
459+
cachedProjectIdForExemplar = projectId;
373460
}
374461

375462
// Convert a OpenCensus Timestamp to a StackDriver Timestamp

exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/CreateTimeSeriesExporterTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public void export() {
106106

107107
List<TimeSeries> timeSeries =
108108
StackdriverExportUtils.createTimeSeriesList(
109-
METRIC, DEFAULT_RESOURCE, StackdriverExportUtils.CUSTOM_OPENCENSUS_DOMAIN);
109+
METRIC, DEFAULT_RESOURCE, StackdriverExportUtils.CUSTOM_OPENCENSUS_DOMAIN, PROJECT_ID);
110110

111111
verify(mockCreateTimeSeriesCallable, times(1))
112112
.call(

exporters/stats/stackdriver/src/test/java/io/opencensus/exporter/stats/stackdriver/StackdriverExportUtilsTest.java

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static io.opencensus.exporter.stats.stackdriver.StackdriverExportUtils.SUMMARY_SUFFIX_COUNT;
2626
import static io.opencensus.exporter.stats.stackdriver.StackdriverExportUtils.SUMMARY_SUFFIX_SUM;
2727

28+
import com.google.api.Distribution;
2829
import com.google.api.Distribution.BucketOptions;
2930
import com.google.api.Distribution.BucketOptions.Explicit;
3031
import com.google.api.LabelDescriptor;
@@ -33,15 +34,24 @@
3334
import com.google.api.MetricDescriptor;
3435
import com.google.api.MetricDescriptor.MetricKind;
3536
import com.google.api.MonitoredResource;
37+
import com.google.common.collect.ImmutableMap;
38+
import com.google.monitoring.v3.SpanContext;
3639
import com.google.monitoring.v3.TimeInterval;
3740
import com.google.monitoring.v3.TimeSeries;
3841
import com.google.monitoring.v3.TypedValue;
42+
import com.google.protobuf.Any;
43+
import com.google.protobuf.ByteString;
3944
import io.opencensus.common.Timestamp;
45+
import io.opencensus.contrib.exemplar.util.AttachmentValueSpanContext;
46+
import io.opencensus.contrib.exemplar.util.ExemplarUtils;
4047
import io.opencensus.contrib.resource.util.AwsEc2InstanceResource;
4148
import io.opencensus.contrib.resource.util.GcpGceInstanceResource;
4249
import io.opencensus.contrib.resource.util.K8sContainerResource;
4350
import io.opencensus.metrics.LabelKey;
4451
import io.opencensus.metrics.LabelValue;
52+
import io.opencensus.metrics.data.AttachmentValue;
53+
import io.opencensus.metrics.data.AttachmentValue.AttachmentValueString;
54+
import io.opencensus.metrics.data.Exemplar;
4555
import io.opencensus.metrics.export.Distribution.Bucket;
4656
import io.opencensus.metrics.export.MetricDescriptor.Type;
4757
import io.opencensus.metrics.export.Point;
@@ -115,13 +125,32 @@ public class StackdriverExportUtilsTest {
115125
private static final String DEFAULT_TASK_VALUE =
116126
"java-" + ManagementFactory.getRuntimeMXBean().getName();
117127

128+
private static final io.opencensus.trace.SpanContext SPAN_CONTEXT_INVALID =
129+
io.opencensus.trace.SpanContext.INVALID;
130+
private static final Exemplar EXEMPLAR_1 =
131+
Exemplar.create(
132+
1.2,
133+
TIMESTAMP_2,
134+
Collections.<String, AttachmentValue>singletonMap(
135+
"key", AttachmentValueString.create("value")));
136+
private static final Exemplar EXEMPLAR_2 =
137+
Exemplar.create(
138+
5.6,
139+
TIMESTAMP_3,
140+
ImmutableMap.<String, AttachmentValue>of(
141+
ExemplarUtils.ATTACHMENT_KEY_SPAN_CONTEXT,
142+
AttachmentValueSpanContext.create(SPAN_CONTEXT_INVALID)));
118143
private static final io.opencensus.metrics.export.Distribution DISTRIBUTION =
119144
io.opencensus.metrics.export.Distribution.create(
120145
3,
121146
2,
122147
14,
123148
BUCKET_OPTIONS,
124-
Arrays.asList(Bucket.create(3), Bucket.create(1), Bucket.create(2), Bucket.create(4)));
149+
Arrays.asList(
150+
Bucket.create(3),
151+
Bucket.create(1, EXEMPLAR_1),
152+
Bucket.create(2),
153+
Bucket.create(4, EXEMPLAR_2)));
125154
private static final Summary SUMMARY =
126155
Summary.create(
127156
10L,
@@ -308,14 +337,33 @@ public void createBucketOptions_Null() {
308337

309338
@Test
310339
public void createDistribution() {
340+
StackdriverExportUtils.setCachedProjectIdForExemplar(null);
311341
assertThat(StackdriverExportUtils.createDistribution(DISTRIBUTION))
312342
.isEqualTo(
313-
com.google.api.Distribution.newBuilder()
343+
Distribution.newBuilder()
314344
.setCount(3)
315345
.setMean(0.6666666666666666)
316346
.setBucketOptions(StackdriverExportUtils.createBucketOptions(BUCKET_OPTIONS))
317347
.addAllBucketCounts(Arrays.asList(0L, 3L, 1L, 2L, 4L))
318348
.setSumOfSquaredDeviation(14)
349+
.addAllExemplars(
350+
Arrays.<Distribution.Exemplar>asList(
351+
Distribution.Exemplar.newBuilder()
352+
.setValue(1.2)
353+
.setTimestamp(StackdriverExportUtils.convertTimestamp(TIMESTAMP_2))
354+
.addAttachments(
355+
Any.newBuilder()
356+
.setTypeUrl(
357+
StackdriverExportUtils.EXEMPLAR_ATTACHMENT_TYPE_STRING)
358+
.setValue(ByteString.copyFromUtf8("value"))
359+
.build())
360+
.build(),
361+
Distribution.Exemplar.newBuilder()
362+
.setValue(5.6)
363+
.setTimestamp(StackdriverExportUtils.convertTimestamp(TIMESTAMP_3))
364+
// Cached project ID is set to null, so no SpanContext attachment will
365+
// be created.
366+
.build()))
319367
.build());
320368
}
321369

@@ -428,7 +476,7 @@ public void createMetricDescriptor_cumulative() {
428476
public void createTimeSeriesList_Cumulative() {
429477
List<TimeSeries> timeSeriesList =
430478
StackdriverExportUtils.createTimeSeriesList(
431-
METRIC, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN);
479+
METRIC, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN, PROJECT_ID);
432480
assertThat(timeSeriesList).hasSize(1);
433481
TimeSeries expectedTimeSeries =
434482
TimeSeries.newBuilder()
@@ -447,11 +495,13 @@ public void createTimeSeriesList_Cumulative() {
447495
public void createTimeSeriesList_Distribution() {
448496
List<TimeSeries> timeSeriesList =
449497
StackdriverExportUtils.createTimeSeriesList(
450-
DISTRIBUTION_METRIC, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN);
498+
DISTRIBUTION_METRIC, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN, PROJECT_ID);
451499

452500
assertThat(timeSeriesList.size()).isEqualTo(1);
453501
TimeSeries timeSeries = timeSeriesList.get(0);
454502
assertThat(timeSeries.getPointsCount()).isEqualTo(1);
503+
String expectedSpanName =
504+
"projects/id/traces/00000000000000000000000000000000/spans/0000000000000000";
455505
assertThat(timeSeries.getPoints(0).getValue().getDistributionValue())
456506
.isEqualTo(
457507
com.google.api.Distribution.newBuilder()
@@ -466,6 +516,33 @@ public void createTimeSeriesList_Distribution() {
466516
.build())
467517
.addAllBucketCounts(Arrays.asList(0L, 3L, 1L, 2L, 4L))
468518
.setSumOfSquaredDeviation(14)
519+
.addAllExemplars(
520+
Arrays.<Distribution.Exemplar>asList(
521+
Distribution.Exemplar.newBuilder()
522+
.setValue(1.2)
523+
.setTimestamp(StackdriverExportUtils.convertTimestamp(TIMESTAMP_2))
524+
.addAttachments(
525+
Any.newBuilder()
526+
.setTypeUrl(
527+
StackdriverExportUtils.EXEMPLAR_ATTACHMENT_TYPE_STRING)
528+
.setValue(ByteString.copyFromUtf8("value"))
529+
.build())
530+
.build(),
531+
Distribution.Exemplar.newBuilder()
532+
.setValue(5.6)
533+
.setTimestamp(StackdriverExportUtils.convertTimestamp(TIMESTAMP_3))
534+
.addAttachments(
535+
Any.newBuilder()
536+
.setTypeUrl(
537+
StackdriverExportUtils
538+
.EXEMPLAR_ATTACHMENT_TYPE_SPAN_CONTEXT)
539+
.setValue(
540+
SpanContext.newBuilder()
541+
.setSpanName(expectedSpanName)
542+
.build()
543+
.toByteString())
544+
.build())
545+
.build()))
469546
.build());
470547
}
471548

@@ -477,7 +554,7 @@ public void createTimeSeriesList_Gauge() {
477554

478555
List<TimeSeries> timeSeriesList =
479556
StackdriverExportUtils.createTimeSeriesList(
480-
metric, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN);
557+
metric, DEFAULT_RESOURCE, CUSTOM_OPENCENSUS_DOMAIN, PROJECT_ID);
481558
assertThat(timeSeriesList).hasSize(2);
482559
TimeSeries expected1 =
483560
TimeSeries.newBuilder()
@@ -507,7 +584,8 @@ public void createTimeSeriesList_withCustomMonitoredResource() {
507584
MonitoredResource resource =
508585
MonitoredResource.newBuilder().setType("global").putLabels("key", "value").build();
509586
List<TimeSeries> timeSeriesList =
510-
StackdriverExportUtils.createTimeSeriesList(METRIC, resource, CUSTOM_OPENCENSUS_DOMAIN);
587+
StackdriverExportUtils.createTimeSeriesList(
588+
METRIC, resource, CUSTOM_OPENCENSUS_DOMAIN, PROJECT_ID);
511589
assertThat(timeSeriesList)
512590
.containsExactly(
513591
TimeSeries.newBuilder()

0 commit comments

Comments
 (0)