3636import com .google .api .MetricDescriptor .MetricKind ;
3737import com .google .api .MetricDescriptor .ValueType ;
3838import com .google .api .MonitoredResource ;
39+ import com .google .monitoring .v3 .DroppedLabels ;
3940import com .google .monitoring .v3 .Point ;
41+ import com .google .monitoring .v3 .SpanContext ;
4042import com .google .monitoring .v3 .TimeInterval ;
4143import com .google .monitoring .v3 .TimeSeries ;
4244import com .google .monitoring .v3 .TypedValue ;
45+ import com .google .protobuf .Any ;
46+ import com .google .protobuf .Timestamp ;
4347import com .google .protobuf .util .Timestamps ;
4448import io .opentelemetry .api .common .AttributeKey ;
4549import io .opentelemetry .api .common .Attributes ;
4650import io .opentelemetry .sdk .metrics .data .AggregationTemporality ;
51+ import io .opentelemetry .sdk .metrics .data .DoubleExemplarData ;
4752import io .opentelemetry .sdk .metrics .data .DoublePointData ;
53+ import io .opentelemetry .sdk .metrics .data .ExemplarData ;
4854import io .opentelemetry .sdk .metrics .data .HistogramData ;
4955import io .opentelemetry .sdk .metrics .data .HistogramPointData ;
56+ import io .opentelemetry .sdk .metrics .data .LongExemplarData ;
5057import io .opentelemetry .sdk .metrics .data .LongPointData ;
5158import io .opentelemetry .sdk .metrics .data .MetricData ;
5259import io .opentelemetry .sdk .metrics .data .MetricDataType ;
5764import java .util .List ;
5865import java .util .logging .Level ;
5966import java .util .logging .Logger ;
67+ import java .util .stream .Collectors ;
6068
6169class SpannerCloudMonitoringExporterUtils {
6270
@@ -69,7 +77,8 @@ static String getProjectId(Resource resource) {
6977 return resource .getAttributes ().get (PROJECT_ID_KEY );
7078 }
7179
72- static List <TimeSeries > convertToSpannerTimeSeries (List <MetricData > collection ) {
80+ static List <TimeSeries > convertToSpannerTimeSeries (
81+ List <MetricData > collection , String projectId ) {
7382 List <TimeSeries > allTimeSeries = new ArrayList <>();
7483
7584 for (MetricData metricData : collection ) {
@@ -94,7 +103,8 @@ static List<TimeSeries> convertToSpannerTimeSeries(List<MetricData> collection)
94103 metricData .getData ().getPoints ().stream ()
95104 .map (
96105 pointData ->
97- convertPointToSpannerTimeSeries (metricData , pointData , monitoredResourceBuilder ))
106+ convertPointToSpannerTimeSeries (
107+ metricData , pointData , monitoredResourceBuilder , projectId ))
98108 .forEach (allTimeSeries ::add );
99109 }
100110 return allTimeSeries ;
@@ -103,7 +113,8 @@ static List<TimeSeries> convertToSpannerTimeSeries(List<MetricData> collection)
103113 private static TimeSeries convertPointToSpannerTimeSeries (
104114 MetricData metricData ,
105115 PointData pointData ,
106- MonitoredResource .Builder monitoredResourceBuilder ) {
116+ MonitoredResource .Builder monitoredResourceBuilder ,
117+ String projectId ) {
107118 TimeSeries .Builder builder =
108119 TimeSeries .newBuilder ()
109120 .setMetricKind (convertMetricKind (metricData ))
@@ -135,7 +146,7 @@ private static TimeSeries convertPointToSpannerTimeSeries(
135146 .setEndTime (Timestamps .fromNanos (pointData .getEpochNanos ()))
136147 .build ();
137148
138- builder .addPoints (createPoint (metricData .getType (), pointData , timeInterval ));
149+ builder .addPoints (createPoint (metricData .getType (), pointData , timeInterval , projectId ));
139150
140151 return builder .build ();
141152 }
@@ -191,15 +202,16 @@ private static ValueType convertValueType(MetricDataType metricDataType) {
191202 }
192203
193204 private static Point createPoint (
194- MetricDataType type , PointData pointData , TimeInterval timeInterval ) {
205+ MetricDataType type , PointData pointData , TimeInterval timeInterval , String projectId ) {
195206 Point .Builder builder = Point .newBuilder ().setInterval (timeInterval );
196207 switch (type ) {
197208 case HISTOGRAM :
198209 case EXPONENTIAL_HISTOGRAM :
199210 return builder
200211 .setValue (
201212 TypedValue .newBuilder ()
202- .setDistributionValue (convertHistogramData ((HistogramPointData ) pointData ))
213+ .setDistributionValue (
214+ convertHistogramData ((HistogramPointData ) pointData , projectId ))
203215 .build ())
204216 .build ();
205217 case DOUBLE_GAUGE :
@@ -221,14 +233,73 @@ private static Point createPoint(
221233 }
222234 }
223235
224- private static Distribution convertHistogramData (HistogramPointData pointData ) {
236+ private static Distribution convertHistogramData (HistogramPointData pointData , String projectId ) {
225237 return Distribution .newBuilder ()
226238 .setCount (pointData .getCount ())
227239 .setMean (pointData .getCount () == 0L ? 0.0D : pointData .getSum () / pointData .getCount ())
228240 .setBucketOptions (
229241 BucketOptions .newBuilder ()
230242 .setExplicitBuckets (Explicit .newBuilder ().addAllBounds (pointData .getBoundaries ())))
231243 .addAllBucketCounts (pointData .getCounts ())
244+ .addAllExemplars (
245+ pointData .getExemplars ().stream ()
246+ .map (e -> mapExemplar (e , projectId ))
247+ .collect (Collectors .toList ()))
232248 .build ();
233249 }
250+
251+ private static Distribution .Exemplar mapExemplar (ExemplarData exemplar , String projectId ) {
252+ double value = 0 ;
253+ if (exemplar instanceof DoubleExemplarData ) {
254+ value = ((DoubleExemplarData ) exemplar ).getValue ();
255+ } else if (exemplar instanceof LongExemplarData ) {
256+ value = ((LongExemplarData ) exemplar ).getValue ();
257+ }
258+
259+ Distribution .Exemplar .Builder exemplarBuilder =
260+ Distribution .Exemplar .newBuilder ()
261+ .setValue (value )
262+ .setTimestamp (mapTimestamp (exemplar .getEpochNanos ()));
263+ if (exemplar .getSpanContext ().isValid ()) {
264+ exemplarBuilder .addAttachments (
265+ Any .pack (
266+ SpanContext .newBuilder ()
267+ .setSpanName (
268+ makeSpanName (
269+ projectId ,
270+ exemplar .getSpanContext ().getTraceId (),
271+ exemplar .getSpanContext ().getSpanId ()))
272+ .build ()));
273+ }
274+ if (!exemplar .getFilteredAttributes ().isEmpty ()) {
275+ exemplarBuilder .addAttachments (
276+ Any .pack (mapFilteredAttributes (exemplar .getFilteredAttributes ())));
277+ }
278+ return exemplarBuilder .build ();
279+ }
280+
281+ static final long NANO_PER_SECOND = (long ) 1e9 ;
282+
283+ private static Timestamp mapTimestamp (long epochNanos ) {
284+ return Timestamp .newBuilder ()
285+ .setSeconds (epochNanos / NANO_PER_SECOND )
286+ .setNanos ((int ) (epochNanos % NANO_PER_SECOND ))
287+ .build ();
288+ }
289+
290+ private static String makeSpanName (String projectId , String traceId , String spanId ) {
291+ return String .format ("projects/%s/traces/%s/spans/%s" , projectId , traceId , spanId );
292+ }
293+
294+ private static DroppedLabels mapFilteredAttributes (Attributes attributes ) {
295+ DroppedLabels .Builder labels = DroppedLabels .newBuilder ();
296+ attributes .forEach ((k , v ) -> labels .putLabel (cleanAttributeKey (k .getKey ()), v .toString ()));
297+ return labels .build ();
298+ }
299+
300+ private static String cleanAttributeKey (String key ) {
301+ // . is commonly used in OTel but disallowed in GCM label names,
302+ // https://cloud.google.com/monitoring/api/ref_v3/rest/v3/LabelDescriptor#:~:text=Matches%20the%20following%20regular%20expression%3A
303+ return key .replace ('.' , '_' );
304+ }
234305}
0 commit comments