From 64d11a00f903cdffa876ac9375c0995574af44fc Mon Sep 17 00:00:00 2001 From: winklemad Date: Fri, 17 Jul 2026 21:43:31 +0530 Subject: [PATCH 1/3] Fix View instrument-name matching to be case-insensitive and deterministic Instrument names are valid mixed-case (the API regex allows [a-zA-Z]...), but the SDK lower-cases them on construction (_Synchronous/_Asynchronous set self.name = name.lower()). View._match compared the user's instrument_name pattern against that lower-cased name with fnmatch, without lower-casing the pattern, so a View built with the instrument's real name (e.g. View(instrument_name="MyLatency")) silently failed to match and its aggregation/drop/rename/attribute filter was ignored. fnmatch also applies os.path.normcase, which lower-cases on Windows but is identity on POSIX, so the same View matched on Windows but not on Linux/macOS. Lower-case the name pattern (matching the SDK's own lower-casing) and use fnmatchcase for both name and unit so matching no longer depends on the host platform's filename case sensitivity. Units stay case-sensitive, per UCUM. Assisted-by: Claude (Anthropic) --- .../sdk/metrics/_internal/view.py | 12 ++++++--- opentelemetry-sdk/tests/metrics/test_view.py | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index 7eb1fcc728e..eac7a38ad47 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 03914c99c6f..2347b7a6ea1 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( From 602c686b5a7028c1b528c1242fcd0ec93b089e77 Mon Sep 17 00:00:00 2001 From: winklemad Date: Sat, 8 Aug 2026 07:28:22 +0530 Subject: [PATCH 2/3] Add changelog fragment for #5430 Assisted-by: Claude (Anthropic) --- .changelog/5430.fixed | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changelog/5430.fixed diff --git a/.changelog/5430.fixed b/.changelog/5430.fixed new file mode 100644 index 00000000000..6ba712ab5d1 --- /dev/null +++ b/.changelog/5430.fixed @@ -0,0 +1 @@ +`opentelemetry-sdk`: fix `View` instrument-name matching so a view configured with an instrument's real (mixed-case) name is applied; matching is now case-insensitive and platform-independent instead of relying on `fnmatch`'s OS-dependent case handling From a47c8d78d334163ca1618d784699743bc4d2e393 Mon Sep 17 00:00:00 2001 From: Aaron Abbott Date: Wed, 12 Aug 2026 21:11:06 +0000 Subject: [PATCH 3/3] precommit --- opentelemetry-sdk/tests/metrics/test_view.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index bff469f7f72..6c65299843e 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -30,15 +30,9 @@ def test_instrument_name_case_insensitive(self): 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) - ) + 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()