Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5622.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: reject views with unsupported aggregations in `_check_view_instrument_compatibility` with a warning, and skip unmapped aggregations in `MetricReaderStorage.collect` to prevent `UnboundLocalError`.
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
from opentelemetry.sdk.metrics._internal.aggregation import (
Aggregation,
AggregationTemporality,
DefaultAggregation,
DropAggregation,
ExplicitBucketHistogramAggregation,
ExponentialBucketHistogramAggregation,
LastValueAggregation,
SumAggregation,
_DropAggregation,
_ExplicitBucketHistogramAggregation,
_ExponentialBucketHistogramAggregation,
Expand Down Expand Up @@ -171,11 +175,12 @@ def collect(self) -> MetricsData | None:
data_points=data_points,
aggregation_temporality=aggregation_temporality,
)
else:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a genuine bug fix, Aggregation is a public API so we can expect to handle this case.

A user with a custom Aggregation would get a warning with every export, so we probably want to reconsider this particular approach.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: warnings are now deduplicated via self._unsupported_aggregation_warned using (instrument, type(view_instrument_match._aggregation)) as the key, ensuring unmapped or custom aggregations only log a warning once per instrument.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest another approach: take look here.

I think it is better to put the check this PR attempts to introduce there as well. If we use the current approach from this PR the warning shows once and only if the process starts after its logging is set up, if not the warning is lost and the user never realizes their data is gone. Here we can just keep the else/continue.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: moved the unsupported aggregation check to _check_view_instrument_compatibility (returning False so the incompatible view is not applied, matching #5461) and kept the clean else: continue fallback in MetricReaderStorage.collect().

continue

metrics.append(
Metric(
# pylint: disable=protected-access
# pylint: disable=possibly-used-before-assignment
name=view_instrument_match._name,
description=view_instrument_match._description,
unit=view_instrument_match._instrument.unit,
Expand Down Expand Up @@ -245,9 +250,25 @@ def _check_view_instrument_compatibility(view: View, instrument: _Instrument) ->
object should be created, `false` otherwise.
"""

result = True

# pylint: disable=protected-access
if isinstance(view._aggregation, Aggregation) and not isinstance(
view._aggregation,
(
DefaultAggregation,
DropAggregation,
ExplicitBucketHistogramAggregation,
ExponentialBucketHistogramAggregation,
LastValueAggregation,
SumAggregation,
),
):
_logger.warning(
"Unsupported aggregation %s for instrument %s",
type(view._aggregation).__name__,
instrument.name,
)
return False

if isinstance(instrument, Asynchronous) and isinstance(
view._aggregation,
(
Expand All @@ -260,6 +281,6 @@ def _check_view_instrument_compatibility(view: View, instrument: _Instrument) ->
view,
instrument,
)
result = False
return False

return result
return True
89 changes: 89 additions & 0 deletions opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
)
from opentelemetry.sdk.metrics.export import AggregationTemporality
from opentelemetry.sdk.metrics.view import (
Aggregation,
DefaultAggregation,
DropAggregation,
ExplicitBucketHistogramAggregation,
Expand Down Expand Up @@ -753,3 +754,91 @@ def test_view_instrument_match_conflict_8(self):
"will cause conflicting metrics",
log.records[0].message,
)

def test_collect_skips_unsupported_aggregation(self):
unsupported_match = Mock(
_aggregation=Mock(),
_name="unsupported_metric",
_description="description",
_instrument=Mock(unit="1"),
)
unsupported_match.collect.return_value = [Mock()]

valid_point = Mock()
valid_match = Mock(
_aggregation=_LastValueAggregation({}, Mock(), instrument_is_synchronous=False),
_name="valid_metric",
_description="description",
_instrument=Mock(unit="1"),
)
valid_match.collect.return_value = [valid_point]

instrument1 = Mock(name="instrument1")
instrument2 = Mock(name="instrument2")
storage = MetricReaderStorage(
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
views=(),
),
MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}),
MagicMock(**{"__getitem__.return_value": DefaultAggregation()}),
)
storage._instrument_view_instrument_matches[instrument1] = [unsupported_match]
storage._instrument_view_instrument_matches[instrument2] = [valid_match]

result = storage.collect()

self.assertIsNotNone(result)
self.assertEqual(len(result.resource_metrics[0].scope_metrics[0].metrics), 1)
self.assertEqual(result.resource_metrics[0].scope_metrics[0].metrics[0].name, "valid_metric")

def test_unsupported_aggregation_view_not_applied(self):
counter = _ObservableCounter(
"test_counter",
Mock(),
[Mock()],
unit="unit",
description="description",
)

class CustomAggregation(Aggregation):
def _create_aggregation(
self,
instrument,
explicit_bucket_boundaries,
exemplar_reservoir_factory,
max_scale,
):
return Mock()

metric_reader_storage = MetricReaderStorage(
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
views=(
View(
instrument_name="test_counter",
aggregation=CustomAggregation(),
),
),
),
MagicMock(**{"__getitem__.return_value": AggregationTemporality.CUMULATIVE}),
MagicMock(**{"__getitem__.return_value": DefaultAggregation()}),
)

with self.assertLogs(
"opentelemetry.sdk.metrics._internal.metric_reader_storage",
level=WARNING,
) as log:
metric_reader_storage.consume_measurement(Measurement(1, time_ns(), counter, Context()))

self.assertEqual(len(log.records), 1)
self.assertIn(
"Unsupported aggregation CustomAggregation for instrument test_counter",
log.records[0].message,
)
self.assertIs(
metric_reader_storage._instrument_view_instrument_matches[counter][0]._view,
_DEFAULT_VIEW,
)
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ def test_exporter_temporality_preference(self):
},
)
pmr = PeriodicExportingMetricReader(exporter)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider self.addCleanup(pmr.shutdown) as in #5641

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to self.addCleanup(pmr.shutdown) as in #5641.

self.addCleanup(pmr.shutdown)
for key, value in pmr._instrument_class_temporality.items():
if key is not _Counter:
self.assertEqual(value, AggregationTemporality.CUMULATIVE)
Expand All @@ -258,6 +259,7 @@ def test_exporter_aggregation_preference(self):
},
)
pmr = PeriodicExportingMetricReader(exporter)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to self.addCleanup(pmr.shutdown) as in #5641.

self.addCleanup(pmr.shutdown)
for key, value in pmr._instrument_class_aggregation.items():
if key is not _Counter:
self.assertTrue(isinstance(value, DefaultAggregation))
Expand Down