Skip to content
Merged
1 change: 1 addition & 0 deletions .changelog/5631.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: raise an error when a named `View` is defined without specifying `instrument_name`.
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class View:

name: This is a metric stream customizing attribute: the name of the
metric stream. If `None`, the name of the instrument will be used.
When `name` is specified, `instrument_name` must also be specified
and cannot contain wildcard characters.

description: This is a metric stream customizing attribute: the
description of the metric stream. If `None`, the description of the instrument will
Expand Down Expand Up @@ -119,9 +121,15 @@ def __init__(
# pylint: disable=broad-exception-raised
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 "*?["):
# pylint: disable=broad-exception-raised
raise Exception(f"View {name} declared with wildcard characters in instrument_name")
if name is not None:
if instrument_name is None:
# pylint: disable=broad-exception-raised
raise Exception(
f"View {name} specifies a name but no instrument_name, which may select multiple instruments"
)
if 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")

# _name, _description, _aggregation, _exemplar_reservoir_factory and
# _attribute_keys will be accessed when instantiating a _ViewInstrumentMatch.
Expand Down
80 changes: 79 additions & 1 deletion opentelemetry-sdk/tests/metrics/test_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from unittest import TestCase
from unittest.mock import Mock

from opentelemetry.sdk.metrics import Counter, MeterProvider
from opentelemetry.sdk.metrics.view import View


Expand Down Expand Up @@ -113,11 +114,88 @@ def test_view_name(self):
"instrument_[!a-z]",
"instrument_[abc]",
"instrument_[",
"[ab]",
):
with self.subTest(wildcard_name=wildcard_name):
with self.assertRaises(Exception):
with self.assertRaisesRegex(
Exception,
r"View name declared with wildcard characters in instrument_name",
):
View(name="name", instrument_name=wildcard_name)

view = View(name="name", instrument_name="instrument_name_1")
self.assertEqual(view._name, "name")
self.assertEqual(view._instrument_name, "instrument_name_1")

def test_view_name_without_instrument_name(self):
cases = [
("custom_name", {"instrument_type": Mock}),
("custom_name", {"meter_name": "some_meter"}),
("custom_name", {"instrument_unit": "ms"}),
("custom_name", {"meter_version": "1.0.0"}),
(
"custom_name",
{"meter_schema_url": "https://opentelemetry.io/schemas/1.4.0"},
),
("name", {"instrument_type": Counter}),
(
"custom_name",
{
"instrument_type": Counter,
"meter_name": "some_meter",
"instrument_unit": "ms",
},
),
]
for name, kwargs in cases:
with self.subTest(name=name, **kwargs):
with self.assertRaisesRegex(
Exception,
rf"View {name} specifies a name but no instrument_name, which may select multiple instruments",
):
View(name=name, **kwargs)

with self.subTest(case="meter_provider"):
with self.assertRaisesRegex(
Exception,
r"View name specifies a name but no instrument_name, which may select multiple instruments",
):
MeterProvider(views=[View(name="name", instrument_type=Counter)])

def test_unnamed_view_without_instrument_name(self):
view = View(instrument_type=Counter)
self.assertIsNone(view._name)
self.assertIs(view._instrument_type, Counter)
self.assertIsNone(view._instrument_name)

meter_provider = MeterProvider(views=[view])
self.assertIsNotNone(meter_provider)

def test_view_name_with_concrete_instrument_name(self):
mock_instrument = Mock()
mock_instrument.configure_mock(name="my_counter")

view = View(name="custom_name", instrument_name="my_counter")
self.assertEqual(view._name, "custom_name")
self.assertEqual(view._instrument_name, "my_counter")
self.assertTrue(view._match(mock_instrument))

other_instrument = Mock()
other_instrument.configure_mock(name="other_counter")
self.assertFalse(view._match(other_instrument))

view_with_type = View(
name="custom_name",
instrument_name="my_counter",
instrument_type=Mock,
)
self.assertTrue(view_with_type._match(mock_instrument))

view_with_counter = View(
name="custom_name",
instrument_name="my_counter",
instrument_type=Counter,
)
self.assertEqual(view_with_counter._name, "custom_name")
self.assertEqual(view_with_counter._instrument_name, "my_counter")
self.assertIs(view_with_counter._instrument_type, Counter)
Loading