diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index 7eb1fcc728..eac7a38ad4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -3,7 +3,7 @@ from collections.abc import Callable -from fnmatch import fnmatch +from fnmatch import fnmatchcase from logging import getLogger from opentelemetry.metrics import Instrument @@ -161,11 +161,17 @@ def _match(self, instrument: _Instrument) -> bool: return False if self._instrument_name is not None: - if not fnmatch(instrument.name, self._instrument_name): + # Instrument names are treated case-insensitively and are stored + # lower-cased by the SDK, so the pattern is lower-cased to match. + # fnmatchcase is used instead of fnmatch so it does not rely + # on the host platform's filename case sensitivity (normcase). + if not fnmatchcase(instrument.name, self._instrument_name.lower()): return False if self._instrument_unit is not None: - if not fnmatch(instrument.unit, self._instrument_unit): + # Units are case-sensitive; fnmatchcase keeps matching consistent + # across platforms. + if not fnmatchcase(instrument.unit, self._instrument_unit): return False if self._meter_name is not None: diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index 03914c99c6..2347b7a6ea 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -25,6 +25,23 @@ def test_instrument_name(self): View(instrument_name="instrument_name")._match(mock_instrument) ) + def test_instrument_name_case_insensitive(self): + # The SDK stores instrument names lower-cased, so a view pattern that + # reuses the instrument's original name must still match regardless of + # case (and regardless of the host platform). + mock_instrument = Mock() + mock_instrument.configure_mock(**{"name": "instrument_name"}) + + self.assertTrue( + View(instrument_name="Instrument_Name")._match(mock_instrument) + ) + self.assertTrue( + View(instrument_name="INSTRUMENT_*")._match(mock_instrument) + ) + self.assertFalse( + View(instrument_name="other_name")._match(mock_instrument) + ) + def test_instrument_unit(self): mock_instrument = Mock() mock_instrument.configure_mock(**{"unit": "instrument_unit"}) @@ -33,6 +50,15 @@ def test_instrument_unit(self): View(instrument_unit="instrument_unit")._match(mock_instrument) ) + def test_instrument_unit_case_sensitive(self): + # Units are case-sensitive and matching must not depend on the host + # platform's filename case sensitivity. + mock_instrument = Mock() + mock_instrument.configure_mock(**{"unit": "By"}) + + self.assertTrue(View(instrument_unit="By")._match(mock_instrument)) + self.assertFalse(View(instrument_unit="by")._match(mock_instrument)) + def test_meter_name(self): self.assertTrue( View(meter_name="meter_name")._match(