From bf0ca68c191bba2a03bdffb199b297e43f2a251f Mon Sep 17 00:00:00 2001 From: Soumyajit Ghosh Date: Fri, 18 Sep 2026 21:24:54 +0530 Subject: [PATCH 1/4] fix(sdk): detect character pattern wildcards in View instrument_name (#5664) The OpenTelemetry Metrics Specification specifies that if name is provided, the View MUST match at most one instrument, and if instrument_name contains wildcard characters (*, ?, [seq], [!seq]), name MUST NOT be provided. Previously, View.__init__ only checked for '*' and '?' in instrument_name, allowing character sequence wildcards such as '[' and ']' (e.g. 'http_[0-9]') to bypass validation when name is set. Update View.__init__ to detect character pattern wildcards ('[', ']') in addition to '*' and '?', and add test coverage for character sequence wildcards. Signed-off-by: Soumyajit Ghosh --- .changelog/5664.fixed | 1 + .../sdk/metrics/_internal/view.py | 22 ++++++++++---- opentelemetry-sdk/tests/metrics/test_view.py | 30 ++++++++++++++++--- 3 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 .changelog/5664.fixed diff --git a/.changelog/5664.fixed b/.changelog/5664.fixed new file mode 100644 index 00000000000..40d96838285 --- /dev/null +++ b/.changelog/5664.fixed @@ -0,0 +1 @@ +`opentelemetry-sdk`: reject character set wildcards (`[`, `]`) in `View.instrument_name` when a custom `name` is provided diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index 31ac34c6645..6330a34c6bd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -104,7 +104,9 @@ def __init__( description: str | None = None, attribute_keys: set[str] | None = None, aggregation: Aggregation | None = None, - exemplar_reservoir_factory: Callable[[type[_Aggregation]], ExemplarReservoirBuilder] | None = None, + exemplar_reservoir_factory: ( + Callable[[type[_Aggregation]], ExemplarReservoirBuilder] | None + ) = None, instrument_unit: str | None = None, ): if ( @@ -117,11 +119,19 @@ def __init__( is None ): # pylint: disable=broad-exception-raised - raise Exception(f"Some instrument selection criteria must be provided for View {name}") + raise Exception( + f"Some instrument selection criteria must be provided for View {name}" + ) - if name is not None and instrument_name is not None and ("*" in instrument_name or "?" in instrument_name): + if ( + name is not None + and instrument_name is not None + and any(c in instrument_name for c in "*?[]") + ): # pylint: disable=broad-exception-raised - raise Exception(f"View {name} declared with wildcard characters in instrument_name") + raise Exception( + f"View {name} declared with wildcard characters in instrument_name" + ) # _name, _description, _aggregation, _exemplar_reservoir_factory and # _attribute_keys will be accessed when instantiating a _ViewInstrumentMatch. @@ -136,7 +146,9 @@ def __init__( self._description = description self._attribute_keys = attribute_keys self._aggregation = aggregation or self._default_aggregation - self._exemplar_reservoir_factory = exemplar_reservoir_factory or _default_reservoir_factory + self._exemplar_reservoir_factory = ( + exemplar_reservoir_factory or _default_reservoir_factory + ) # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index 5032e52da11..a9f970e272d 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -50,11 +50,17 @@ def test_instrument_unit_case_sensitive(self): self.assertFalse(View(instrument_unit="by")._match(mock_instrument)) def test_meter_name(self): - self.assertTrue(View(meter_name="meter_name")._match(Mock(**{"instrumentation_scope.name": "meter_name"}))) + self.assertTrue( + View(meter_name="meter_name")._match( + Mock(**{"instrumentation_scope.name": "meter_name"}) + ) + ) def test_meter_version(self): self.assertTrue( - View(meter_version="meter_version")._match(Mock(**{"instrumentation_scope.version": "meter_version"})) + View(meter_version="meter_version")._match( + Mock(**{"instrumentation_scope.version": "meter_version"}) + ) ) def test_meter_schema_url(self): @@ -105,5 +111,21 @@ def test_additive_criteria(self): ) def test_view_name(self): - with self.assertRaises(Exception): - View(name="name", instrument_name="instrument_name*") + for wildcard_name in ( + "instrument_name*", + "*instrument_name", + "instrument?name", + "instrument_[0-9]", + "instrument_[!a-z]", + "instrument_[abc]", + "instrument_[", + "instrument_]", + ): + with self.subTest(wildcard_name=wildcard_name): + with self.assertRaises(Exception): + View(name="name", instrument_name=wildcard_name) + + # Non-wildcard instrument name should succeed + view = View(name="name", instrument_name="instrument_name") + self.assertEqual(view._name, "name") + self.assertEqual(view._instrument_name, "instrument_name") From 17bbd3ef21ef3f13f8b94b7c920502901adef34f Mon Sep 17 00:00:00 2001 From: Soumyajit Ghosh Date: Mon, 21 Sep 2026 23:18:17 +0530 Subject: [PATCH 2/4] fix(changelog): rename fragment to match PR 5673 --- .changelog/{5664.fixed => 5673.fixed} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{5664.fixed => 5673.fixed} (100%) diff --git a/.changelog/5664.fixed b/.changelog/5673.fixed similarity index 100% rename from .changelog/5664.fixed rename to .changelog/5673.fixed From f4e636b6b9be0c1427d1f8b5f7a7de85ef8aae8f Mon Sep 17 00:00:00 2001 From: Soumyajit Ghosh Date: Tue, 22 Sep 2026 01:58:35 +0530 Subject: [PATCH 3/4] style(sdk): apply ruff formatting to view and test_view Signed-off-by: Soumyajit Ghosh --- .../sdk/metrics/_internal/view.py | 22 +++++-------------- opentelemetry-sdk/tests/metrics/test_view.py | 10 ++------- 2 files changed, 7 insertions(+), 25 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index 6330a34c6bd..e47fb0a16cc 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -104,9 +104,7 @@ def __init__( description: str | None = None, attribute_keys: set[str] | None = None, aggregation: Aggregation | None = None, - exemplar_reservoir_factory: ( - Callable[[type[_Aggregation]], ExemplarReservoirBuilder] | None - ) = None, + exemplar_reservoir_factory: (Callable[[type[_Aggregation]], ExemplarReservoirBuilder] | None) = None, instrument_unit: str | None = None, ): if ( @@ -119,19 +117,11 @@ def __init__( is None ): # pylint: disable=broad-exception-raised - raise Exception( - f"Some instrument selection criteria must be provided for View {name}" - ) + raise Exception(f"Some instrument selection criteria must be provided for View {name}") - if ( - name is not None - and instrument_name is not None - and any(c in instrument_name for c in "*?[]") - ): + if name is not None and instrument_name is not None and any(c in instrument_name for c in "*?[]"): # pylint: disable=broad-exception-raised - raise Exception( - f"View {name} declared with wildcard characters in instrument_name" - ) + raise Exception(f"View {name} declared with wildcard characters in instrument_name") # _name, _description, _aggregation, _exemplar_reservoir_factory and # _attribute_keys will be accessed when instantiating a _ViewInstrumentMatch. @@ -146,9 +136,7 @@ def __init__( self._description = description self._attribute_keys = attribute_keys self._aggregation = aggregation or self._default_aggregation - self._exemplar_reservoir_factory = ( - exemplar_reservoir_factory or _default_reservoir_factory - ) + self._exemplar_reservoir_factory = exemplar_reservoir_factory or _default_reservoir_factory # pylint: disable=too-many-return-statements # pylint: disable=too-many-branches diff --git a/opentelemetry-sdk/tests/metrics/test_view.py b/opentelemetry-sdk/tests/metrics/test_view.py index a9f970e272d..9dacab1808d 100644 --- a/opentelemetry-sdk/tests/metrics/test_view.py +++ b/opentelemetry-sdk/tests/metrics/test_view.py @@ -50,17 +50,11 @@ def test_instrument_unit_case_sensitive(self): self.assertFalse(View(instrument_unit="by")._match(mock_instrument)) def test_meter_name(self): - self.assertTrue( - View(meter_name="meter_name")._match( - Mock(**{"instrumentation_scope.name": "meter_name"}) - ) - ) + self.assertTrue(View(meter_name="meter_name")._match(Mock(**{"instrumentation_scope.name": "meter_name"}))) def test_meter_version(self): self.assertTrue( - View(meter_version="meter_version")._match( - Mock(**{"instrumentation_scope.version": "meter_version"}) - ) + View(meter_version="meter_version")._match(Mock(**{"instrumentation_scope.version": "meter_version"})) ) def test_meter_schema_url(self): From 7b37ad2376e6e562e977124a59b5ea7e07377e49 Mon Sep 17 00:00:00 2001 From: Soumyajit Ghosh Date: Tue, 22 Sep 2026 02:03:13 +0530 Subject: [PATCH 4/4] style(sdk): revert unrelated formatting in view constructor Signed-off-by: Soumyajit Ghosh --- .../src/opentelemetry/sdk/metrics/_internal/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py index e47fb0a16cc..99b1f7ffe35 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/view.py @@ -104,7 +104,7 @@ def __init__( description: str | None = None, attribute_keys: set[str] | None = None, aggregation: Aggregation | None = None, - exemplar_reservoir_factory: (Callable[[type[_Aggregation]], ExemplarReservoirBuilder] | None) = None, + exemplar_reservoir_factory: Callable[[type[_Aggregation]], ExemplarReservoirBuilder] | None = None, instrument_unit: str | None = None, ): if (