|
15 | 15 | from __future__ import annotations |
16 | 16 |
|
17 | 17 | import logging |
18 | | -from collections.abc import Collection |
| 18 | +from collections.abc import Collection, Iterable |
| 19 | +from dataclasses import replace |
| 20 | +from os import environ |
19 | 21 |
|
20 | 22 | from opentelemetry.exporter.otlp.json.common._internal import ( |
21 | 23 | _encode_attributes, |
|
56 | 58 | from opentelemetry.proto_json.resource.v1.resource import ( |
57 | 59 | Resource as JSONResource, |
58 | 60 | ) |
| 61 | +from opentelemetry.sdk._configuration.models import Aggregation |
| 62 | +from opentelemetry.sdk.environment_variables import ( |
| 63 | + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, |
| 64 | + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, |
| 65 | +) |
59 | 66 | from opentelemetry.sdk.metrics import ( |
| 67 | + Counter, |
60 | 68 | Exemplar, |
| 69 | + Histogram, |
| 70 | + ObservableCounter, |
| 71 | + ObservableGauge, |
| 72 | + ObservableUpDownCounter, |
| 73 | + UpDownCounter, |
| 74 | +) |
| 75 | +from opentelemetry.sdk.metrics._internal.aggregation import ( |
| 76 | + AggregationTemporality, |
| 77 | + ExplicitBucketHistogramAggregation, |
| 78 | + ExponentialBucketHistogramAggregation, |
61 | 79 | ) |
62 | 80 | from opentelemetry.sdk.metrics._internal.point import ( |
63 | 81 | ExponentialHistogramDataPoint, |
|
68 | 86 | ScopeMetrics, |
69 | 87 | ) |
70 | 88 | from opentelemetry.sdk.metrics.export import ( |
71 | | - ExponentialHistogram, |
72 | | - Gauge, |
73 | | - Histogram, |
| 89 | + ExponentialHistogram as ExponentialHistogramMetricData, |
| 90 | +) |
| 91 | +from opentelemetry.sdk.metrics.export import ( |
| 92 | + Gauge as GaugeMetricData, |
| 93 | +) |
| 94 | +from opentelemetry.sdk.metrics.export import ( |
| 95 | + Histogram as HistogramMetricData, |
| 96 | +) |
| 97 | +from opentelemetry.sdk.metrics.export import ( |
74 | 98 | MetricsData, |
75 | | - Sum, |
| 99 | +) |
| 100 | +from opentelemetry.sdk.metrics.export import ( |
| 101 | + Sum as SumMetricData, |
76 | 102 | ) |
77 | 103 |
|
78 | 104 | _logger = logging.getLogger(__name__) |
79 | 105 |
|
| 106 | +_METRIC_DATA_FIELDS = ( |
| 107 | + "gauge", |
| 108 | + "sum", |
| 109 | + "histogram", |
| 110 | + "exponential_histogram", |
| 111 | + "summary", |
| 112 | +) |
| 113 | + |
80 | 114 |
|
81 | 115 | def encode_metrics(data: MetricsData) -> JSONExportMetricsServiceRequest: |
82 | 116 | return JSONExportMetricsServiceRequest( |
@@ -114,29 +148,29 @@ def _encode_metric(metric: Metric) -> JSONMetric: |
114 | 148 | description=metric.description, |
115 | 149 | unit=metric.unit, |
116 | 150 | ) |
117 | | - if isinstance(metric.data, Gauge): |
| 151 | + if isinstance(metric.data, GaugeMetricData): |
118 | 152 | json_metric.gauge = JSONGauge( |
119 | 153 | data_points=[ |
120 | 154 | _encode_gauge_data_point(pt) for pt in metric.data.data_points |
121 | 155 | ] |
122 | 156 | ) |
123 | | - elif isinstance(metric.data, Histogram): |
| 157 | + elif isinstance(metric.data, HistogramMetricData): |
124 | 158 | json_metric.histogram = JSONHistogram( |
125 | 159 | data_points=[ |
126 | 160 | _encode_histogram_data_point(pt) |
127 | 161 | for pt in metric.data.data_points |
128 | 162 | ], |
129 | 163 | aggregation_temporality=metric.data.aggregation_temporality, |
130 | 164 | ) |
131 | | - elif isinstance(metric.data, Sum): |
| 165 | + elif isinstance(metric.data, SumMetricData): |
132 | 166 | json_metric.sum = JSONSum( |
133 | 167 | data_points=[ |
134 | 168 | _encode_sum_data_point(pt) for pt in metric.data.data_points |
135 | 169 | ], |
136 | 170 | aggregation_temporality=metric.data.aggregation_temporality, |
137 | 171 | is_monotonic=metric.data.is_monotonic, |
138 | 172 | ) |
139 | | - elif isinstance(metric.data, ExponentialHistogram): |
| 173 | + elif isinstance(metric.data, ExponentialHistogramMetricData): |
140 | 174 | json_metric.exponential_histogram = JSONExponentialHistogram( |
141 | 175 | data_points=[ |
142 | 176 | _encode_exponential_histogram_data_point(pt) |
@@ -269,3 +303,214 @@ def _encode_exemplars( |
269 | 303 | json_exemplars.append(json_exemplar) |
270 | 304 |
|
271 | 305 | return json_exemplars |
| 306 | + |
| 307 | + |
| 308 | +def split_metrics_data( |
| 309 | + metrics_data: JSONExportMetricsServiceRequest, |
| 310 | + max_export_batch_size: int | None, |
| 311 | +) -> Iterable[JSONExportMetricsServiceRequest]: |
| 312 | + """Split an ExportMetricsServiceRequest into batches of at most |
| 313 | + max_export_batch_size data points, preserving resource/scope hierarchy. |
| 314 | + """ |
| 315 | + if max_export_batch_size is None: |
| 316 | + yield metrics_data |
| 317 | + return |
| 318 | + |
| 319 | + batch_size: int = 0 |
| 320 | + resource_metrics_batch: list[JSONResourceMetrics] = [] |
| 321 | + scope_metrics_batch: list[JSONScopeMetrics] = [] |
| 322 | + metrics_batch: list[JSONMetric] = [] |
| 323 | + |
| 324 | + for ( |
| 325 | + resource_metrics, |
| 326 | + scope_metrics, |
| 327 | + metric, |
| 328 | + field_name, |
| 329 | + data_points, |
| 330 | + ) in _iter_metric_data_points(metrics_data): |
| 331 | + if ( |
| 332 | + not resource_metrics_batch |
| 333 | + or resource_metrics_batch[-1].resource |
| 334 | + is not resource_metrics.resource |
| 335 | + ): |
| 336 | + scope_metrics_batch = [] |
| 337 | + resource_metrics_batch.append( |
| 338 | + replace(resource_metrics, scope_metrics=scope_metrics_batch) |
| 339 | + ) |
| 340 | + |
| 341 | + if ( |
| 342 | + not scope_metrics_batch |
| 343 | + or scope_metrics_batch[-1].scope is not scope_metrics.scope |
| 344 | + ): |
| 345 | + metrics_batch = [] |
| 346 | + scope_metrics_batch.append( |
| 347 | + replace(scope_metrics, metrics=metrics_batch) |
| 348 | + ) |
| 349 | + |
| 350 | + data_points_batch: list = [] |
| 351 | + metrics_batch.append( |
| 352 | + _build_metric_with_data_points( |
| 353 | + metric, field_name, data_points_batch |
| 354 | + ) |
| 355 | + ) |
| 356 | + |
| 357 | + for data_point in data_points: |
| 358 | + if batch_size >= max_export_batch_size: |
| 359 | + yield JSONExportMetricsServiceRequest( |
| 360 | + resource_metrics=resource_metrics_batch |
| 361 | + ) |
| 362 | + ( |
| 363 | + resource_metrics_batch, |
| 364 | + scope_metrics_batch, |
| 365 | + metrics_batch, |
| 366 | + data_points_batch, |
| 367 | + ) = _build_empty_metric_batches( |
| 368 | + resource_metrics, scope_metrics, metric, field_name |
| 369 | + ) |
| 370 | + batch_size = 0 |
| 371 | + data_points_batch.append(data_point) |
| 372 | + batch_size += 1 |
| 373 | + |
| 374 | + if batch_size > 0: |
| 375 | + yield JSONExportMetricsServiceRequest( |
| 376 | + resource_metrics=resource_metrics_batch |
| 377 | + ) |
| 378 | + |
| 379 | + |
| 380 | +def _get_metric_data_field_name(metric: JSONMetric) -> str | None: |
| 381 | + return next( |
| 382 | + (f for f in _METRIC_DATA_FIELDS if getattr(metric, f) is not None), |
| 383 | + None, |
| 384 | + ) |
| 385 | + |
| 386 | + |
| 387 | +def _iter_metric_data_points( |
| 388 | + metrics_data: JSONExportMetricsServiceRequest, |
| 389 | +) -> Iterable[ |
| 390 | + tuple[JSONResourceMetrics, JSONScopeMetrics, JSONMetric, str, list] |
| 391 | +]: |
| 392 | + for resource_metrics in metrics_data.resource_metrics: |
| 393 | + for scope_metrics in resource_metrics.scope_metrics: |
| 394 | + for metric in scope_metrics.metrics: |
| 395 | + field_name = _get_metric_data_field_name(metric) |
| 396 | + if field_name is None: |
| 397 | + _logger.warning( |
| 398 | + "Tried to split and export an unsupported metric type. Skipping." |
| 399 | + ) |
| 400 | + continue |
| 401 | + yield ( |
| 402 | + resource_metrics, |
| 403 | + scope_metrics, |
| 404 | + metric, |
| 405 | + field_name, |
| 406 | + getattr(metric, field_name).data_points, |
| 407 | + ) |
| 408 | + |
| 409 | + |
| 410 | +def _build_metric_with_data_points( |
| 411 | + metric: JSONMetric, |
| 412 | + field_name: str, |
| 413 | + data_points: list, |
| 414 | +) -> JSONMetric: |
| 415 | + new_data = replace(getattr(metric, field_name), data_points=data_points) |
| 416 | + return replace(metric, **{field_name: new_data}) |
| 417 | + |
| 418 | + |
| 419 | +def _build_empty_metric_batches( |
| 420 | + resource_metrics: JSONResourceMetrics, |
| 421 | + scope_metrics: JSONScopeMetrics, |
| 422 | + metric: JSONMetric, |
| 423 | + field_name: str, |
| 424 | +) -> tuple[ |
| 425 | + list[JSONResourceMetrics], list[JSONScopeMetrics], list[JSONMetric], list |
| 426 | +]: |
| 427 | + data_points_batch = [] |
| 428 | + metrics_batch = [ |
| 429 | + _build_metric_with_data_points(metric, field_name, data_points_batch) |
| 430 | + ] |
| 431 | + scope_metrics_batch = [replace(scope_metrics, metrics=metrics_batch)] |
| 432 | + resource_metrics_batch = [ |
| 433 | + replace(resource_metrics, scope_metrics=scope_metrics_batch) |
| 434 | + ] |
| 435 | + return ( |
| 436 | + resource_metrics_batch, |
| 437 | + scope_metrics_batch, |
| 438 | + metrics_batch, |
| 439 | + data_points_batch, |
| 440 | + ) |
| 441 | + |
| 442 | + |
| 443 | +def _get_temporality( |
| 444 | + preferred_temporality: dict[type, AggregationTemporality] | None, |
| 445 | +) -> dict[type, AggregationTemporality]: |
| 446 | + preference = ( |
| 447 | + environ.get( |
| 448 | + OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE, "CUMULATIVE" |
| 449 | + ) |
| 450 | + .upper() |
| 451 | + .strip() |
| 452 | + ) |
| 453 | + |
| 454 | + if preference == "DELTA": |
| 455 | + instrument_class_temporality = { |
| 456 | + Counter: AggregationTemporality.DELTA, |
| 457 | + UpDownCounter: AggregationTemporality.CUMULATIVE, |
| 458 | + Histogram: AggregationTemporality.DELTA, |
| 459 | + ObservableCounter: AggregationTemporality.DELTA, |
| 460 | + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, |
| 461 | + ObservableGauge: AggregationTemporality.CUMULATIVE, |
| 462 | + } |
| 463 | + elif preference == "LOWMEMORY": |
| 464 | + instrument_class_temporality = { |
| 465 | + Counter: AggregationTemporality.DELTA, |
| 466 | + UpDownCounter: AggregationTemporality.CUMULATIVE, |
| 467 | + Histogram: AggregationTemporality.DELTA, |
| 468 | + ObservableCounter: AggregationTemporality.CUMULATIVE, |
| 469 | + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, |
| 470 | + ObservableGauge: AggregationTemporality.CUMULATIVE, |
| 471 | + } |
| 472 | + else: |
| 473 | + if preference != "CUMULATIVE": |
| 474 | + _logger.warning( |
| 475 | + "Unrecognized OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE" |
| 476 | + " value found: %s, using CUMULATIVE", |
| 477 | + preference, |
| 478 | + ) |
| 479 | + instrument_class_temporality = { |
| 480 | + Counter: AggregationTemporality.CUMULATIVE, |
| 481 | + UpDownCounter: AggregationTemporality.CUMULATIVE, |
| 482 | + Histogram: AggregationTemporality.CUMULATIVE, |
| 483 | + ObservableCounter: AggregationTemporality.CUMULATIVE, |
| 484 | + ObservableUpDownCounter: AggregationTemporality.CUMULATIVE, |
| 485 | + ObservableGauge: AggregationTemporality.CUMULATIVE, |
| 486 | + } |
| 487 | + |
| 488 | + instrument_class_temporality.update(preferred_temporality or {}) |
| 489 | + return instrument_class_temporality |
| 490 | + |
| 491 | + |
| 492 | +def _get_aggregation( |
| 493 | + preferred_aggregation: dict[type, Aggregation] | None, |
| 494 | +) -> dict[type, Aggregation]: |
| 495 | + default_histogram_aggregation = environ.get( |
| 496 | + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, |
| 497 | + "explicit_bucket_histogram", |
| 498 | + ) |
| 499 | + |
| 500 | + if default_histogram_aggregation == "base2_exponential_bucket_histogram": |
| 501 | + instrument_class_aggregation: dict[type, Aggregation] = { |
| 502 | + Histogram: ExponentialBucketHistogramAggregation(), |
| 503 | + } |
| 504 | + else: |
| 505 | + if default_histogram_aggregation != "explicit_bucket_histogram": |
| 506 | + _logger.warning( |
| 507 | + "Invalid value for %s: %s, using explicit bucket histogram aggregation", |
| 508 | + OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION, |
| 509 | + default_histogram_aggregation, |
| 510 | + ) |
| 511 | + instrument_class_aggregation = { |
| 512 | + Histogram: ExplicitBucketHistogramAggregation(), |
| 513 | + } |
| 514 | + |
| 515 | + instrument_class_aggregation.update(preferred_aggregation or {}) |
| 516 | + return instrument_class_aggregation |
0 commit comments