1919import com .google .api .Distribution ;
2020import com .google .api .Distribution .BucketOptions ;
2121import com .google .api .Distribution .BucketOptions .Explicit ;
22+ import com .google .api .Distribution .Exemplar ;
2223import com .google .api .LabelDescriptor ;
2324import com .google .api .LabelDescriptor .ValueType ;
2425import com .google .api .Metric ;
3132import com .google .common .collect .Lists ;
3233import com .google .common .collect .Maps ;
3334import com .google .monitoring .v3 .Point ;
35+ import com .google .monitoring .v3 .SpanContext ;
3436import com .google .monitoring .v3 .TimeInterval ;
3537import com .google .monitoring .v3 .TimeSeries ;
3638import com .google .monitoring .v3 .TypedValue ;
39+ import com .google .protobuf .Any ;
40+ import com .google .protobuf .ByteString ;
3741import com .google .protobuf .Timestamp ;
3842import io .opencensus .common .Function ;
3943import io .opencensus .common .Functions ;
44+ import io .opencensus .contrib .exemplar .util .AttachmentValueSpanContext ;
45+ import io .opencensus .contrib .exemplar .util .ExemplarUtils ;
4046import io .opencensus .contrib .resource .util .AwsEc2InstanceResource ;
4147import io .opencensus .contrib .resource .util .GcpGceInstanceResource ;
4248import io .opencensus .contrib .resource .util .K8sContainerResource ;
4349import io .opencensus .contrib .resource .util .ResourceUtils ;
4450import io .opencensus .metrics .LabelKey ;
4551import io .opencensus .metrics .LabelValue ;
52+ import io .opencensus .metrics .data .AttachmentValue ;
4653import io .opencensus .metrics .export .Distribution .Bucket ;
4754import io .opencensus .metrics .export .Distribution .BucketOptions .ExplicitOptions ;
4855import 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
0 commit comments