From 64d11a00f903cdffa876ac9375c0995574af44fc Mon Sep 17 00:00:00 2001 From: winklemad Date: Fri, 17 Jul 2026 21:43:31 +0530 Subject: [PATCH] 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(