1313# limitations under the License.
1414
1515import logging
16+ import math
1617import random
1718from dataclasses import replace
1819from time import time_ns
@@ -212,11 +213,7 @@ def _get_metric_descriptor(
212213 elif isinstance (data , Histogram ):
213214 descriptor .metric_kind = MetricDescriptor .MetricKind .CUMULATIVE
214215 elif isinstance (data , ExponentialHistogram ):
215- logger .warning (
216- "Unsupported metric data type %s, ignoring it" ,
217- type (data ).__name__ ,
218- )
219- return None
216+ descriptor .metric_kind = MetricDescriptor .MetricKind .CUMULATIVE
220217 else :
221218 # Exhaustive check
222219 _ : NoReturn = data
@@ -235,6 +232,8 @@ def _get_metric_descriptor(
235232 )
236233 elif isinstance (first_point , HistogramDataPoint ):
237234 descriptor .value_type = MetricDescriptor .ValueType .DISTRIBUTION
235+ elif isinstance (first_point , ExponentialHistogramDataPoint ):
236+ descriptor .value_type = MetricDescriptor .ValueType .DISTRIBUTION
238237 elif first_point is None :
239238 pass
240239 else :
@@ -265,7 +264,9 @@ def _get_metric_descriptor(
265264 @staticmethod
266265 def _to_point (
267266 kind : "MetricDescriptor.MetricKind.V" ,
268- data_point : Union [NumberDataPoint , HistogramDataPoint ],
267+ data_point : Union [
268+ NumberDataPoint , HistogramDataPoint , ExponentialHistogramDataPoint
269+ ],
269270 ) -> Point :
270271 if isinstance (data_point , HistogramDataPoint ):
271272 mean = (
@@ -283,6 +284,55 @@ def _to_point(
283284 ),
284285 )
285286 )
287+ elif isinstance (data_point , ExponentialHistogramDataPoint ):
288+ # Adapted from https://github.com/GoogleCloudPlatform/opentelemetry-operations-go/blob/v1.8.0/exporter/collector/metrics.go#L582
289+
290+ # Calculate underflow bucket (zero count + negative buckets)
291+ underflow = data_point .zero_count
292+ if data_point .negative .bucket_counts :
293+ underflow += sum (data_point .negative .bucket_counts )
294+
295+ # Create bucket counts array: [underflow, positive_buckets..., overflow=0]
296+ bucket_counts = [underflow ]
297+ if data_point .positive .bucket_counts :
298+ bucket_counts .extend (data_point .positive .bucket_counts )
299+ bucket_counts .append (0 ) # overflow bucket is always empty
300+
301+ # Determine bucket options
302+ if not data_point .positive .bucket_counts :
303+ # If no positive buckets, use explicit buckets with bounds=[0]
304+ bucket_options = Distribution .BucketOptions (
305+ explicit_buckets = Distribution .BucketOptions .Explicit (
306+ bounds = [0.0 ],
307+ )
308+ )
309+ else :
310+ # Use exponential bucket options
311+ # growth_factor = 2^(2^(-scale))
312+ growth_factor = math .pow (2 , math .pow (2 , - data_point .scale ))
313+ # scale = growth_factor^(positive_bucket_offset)
314+ scale = math .pow (growth_factor , data_point .positive .offset )
315+ num_finite_buckets = len (bucket_counts ) - 2
316+
317+ bucket_options = Distribution .BucketOptions (
318+ exponential_buckets = Distribution .BucketOptions .Exponential (
319+ num_finite_buckets = num_finite_buckets ,
320+ growth_factor = growth_factor ,
321+ scale = scale ,
322+ )
323+ )
324+
325+ mean = (
326+ data_point .sum / data_point .count if data_point .count else 0.0
327+ )
328+ point_value = TypedValue (
329+ distribution_value = Distribution (
330+ count = data_point .count ,
331+ mean = mean ,
332+ bucket_counts = bucket_counts ,
333+ bucket_options = bucket_options ,
334+ )
335+ )
286336 else :
287337 if isinstance (data_point .value , int ):
288338 point_value = TypedValue (int64_value = data_point .value )
@@ -350,10 +400,6 @@ def export(
350400 continue
351401
352402 for data_point in metric .data .data_points :
353- if isinstance (
354- data_point , ExponentialHistogramDataPoint
355- ):
356- continue
357403 labels = {
358404 _normalize_label_key (key ): str (value )
359405 for key , value in (
0 commit comments