Skip to content

Commit 5456988

Browse files
ocelotlsrikanthccv
andauthored
Check instrument names and units (#2648)
* Check instrument names and units Fixes #2647 * Update opentelemetry-api/src/opentelemetry/_metrics/instrument.py Co-authored-by: Srikanth Chekuri <[email protected]> * Add missing type * Add missing type * Fix error message * Update opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py Co-authored-by: Srikanth Chekuri <[email protected]> Co-authored-by: Srikanth Chekuri <[email protected]>
1 parent 2972ebc commit 5456988

File tree

5 files changed

+80
-14
lines changed

5 files changed

+80
-14
lines changed

opentelemetry-api/src/opentelemetry/_metrics/_internal/instrument.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717

1818
from abc import ABC, abstractmethod
1919
from logging import getLogger
20+
from re import ASCII
21+
from re import compile as re_compile
2022
from typing import (
2123
Callable,
2224
Generator,
2325
Generic,
2426
Iterable,
2527
Optional,
2628
Sequence,
29+
Tuple,
2730
TypeVar,
2831
Union,
2932
)
@@ -42,6 +45,9 @@
4245

4346
_logger = getLogger(__name__)
4447

48+
_name_regex = re_compile(r"[a-zA-Z][-.\w]{0,62}", ASCII)
49+
_unit_regex = re_compile(r"\w{0,63}", ASCII)
50+
4551

4652
class Instrument(ABC):
4753
"""Abstract class that serves as base for all instruments."""
@@ -55,9 +61,21 @@ def __init__(
5561
) -> None:
5662
pass
5763

58-
# FIXME check that the instrument name is valid
59-
# FIXME check that the unit is 63 characters or shorter
60-
# FIXME check that the unit contains only ASCII characters
64+
@staticmethod
65+
def _check_name_and_unit(name: str, unit: str) -> Tuple[bool, bool]:
66+
"""
67+
Checks the following instrument name and unit for compliance with the
68+
spec.
69+
70+
Returns a tuple of boolean value, the first one will be `True` if the
71+
name is valid, `False` otherwise. The second value will be `True` if
72+
the unit is valid, `False` otherwise.
73+
"""
74+
75+
return (
76+
_name_regex.fullmatch(name) is not None,
77+
_unit_regex.fullmatch(unit) is not None,
78+
)
6179

6280

6381
class _ProxyInstrument(ABC, Generic[InstrumentT]):
@@ -124,7 +142,6 @@ def add(
124142
amount: Union[int, float],
125143
attributes: Optional[Attributes] = None,
126144
) -> None:
127-
# FIXME check that the amount is non negative
128145
pass
129146

130147

opentelemetry-api/tests/metrics/test_instruments.py

+22
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,25 @@ def test_observable_up_down_counter_callback(self):
562562
].default,
563563
Signature.empty,
564564
)
565+
566+
def test_name_regex(self):
567+
568+
instrument = ChildInstrument("name")
569+
570+
self.assertTrue(instrument._check_name_and_unit("a" * 63, "unit")[0])
571+
self.assertTrue(instrument._check_name_and_unit("a.", "unit")[0])
572+
self.assertTrue(instrument._check_name_and_unit("a-", "unit")[0])
573+
self.assertTrue(instrument._check_name_and_unit("a_", "unit")[0])
574+
self.assertFalse(instrument._check_name_and_unit("a" * 64, "unit")[0])
575+
self.assertFalse(instrument._check_name_and_unit("Ñ", "unit")[0])
576+
self.assertFalse(instrument._check_name_and_unit("_a", "unit")[0])
577+
self.assertFalse(instrument._check_name_and_unit("1a", "unit")[0])
578+
self.assertFalse(instrument._check_name_and_unit("", "unit")[0])
579+
580+
def test_unit_regex(self):
581+
582+
instrument = ChildInstrument("name")
583+
584+
self.assertTrue(instrument._check_name_and_unit("name", "a" * 63)[1])
585+
self.assertFalse(instrument._check_name_and_unit("name", "a" * 64)[1])
586+
self.assertFalse(instrument._check_name_and_unit("name", "Ñ")[1])

opentelemetry-sdk/src/opentelemetry/sdk/_metrics/instrument.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
_logger = getLogger(__name__)
3939

4040

41+
_ERROR_MESSAGE = (
42+
"Expected ASCII string of maximum length 63 characters but got {}"
43+
)
44+
45+
4146
class _Synchronous:
4247
def __init__(
4348
self,
@@ -47,7 +52,15 @@ def __init__(
4752
unit: str = "",
4853
description: str = "",
4954
):
50-
self.name = name
55+
# pylint: disable=no-member
56+
is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit)
57+
58+
if not is_name_valid:
59+
raise Exception(_ERROR_MESSAGE.format(name))
60+
61+
if not is_unit_valid:
62+
raise Exception(_ERROR_MESSAGE.format(unit))
63+
self.name = name.lower()
5164
self.unit = unit
5265
self.description = description
5366
self.instrumentation_scope = instrumentation_scope
@@ -65,7 +78,15 @@ def __init__(
6578
unit: str = "",
6679
description: str = "",
6780
):
68-
self.name = name
81+
# pylint: disable=no-member
82+
is_name_valid, is_unit_valid = self._check_name_and_unit(name, unit)
83+
84+
if not is_name_valid:
85+
raise Exception(_ERROR_MESSAGE.format(name))
86+
87+
if not is_unit_valid:
88+
raise Exception(_ERROR_MESSAGE.format(unit))
89+
self.name = name.lower()
6990
self.unit = unit
7091
self.description = description
7192
self.instrumentation_scope = instrumentation_scope

opentelemetry-sdk/tests/metrics/test_aggregation.py

+6-8
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ def setUpClass(cls):
875875
def test_counter(self):
876876

877877
aggregation = self.default_aggregation._create_aggregation(
878-
Counter(Mock(), Mock(), Mock())
878+
Counter("name", Mock(), Mock())
879879
)
880880
self.assertIsInstance(aggregation, _SumAggregation)
881881
self.assertTrue(aggregation._instrument_is_monotonic)
@@ -886,7 +886,7 @@ def test_counter(self):
886886
def test_up_down_counter(self):
887887

888888
aggregation = self.default_aggregation._create_aggregation(
889-
UpDownCounter(Mock(), Mock(), Mock())
889+
UpDownCounter("name", Mock(), Mock())
890890
)
891891
self.assertIsInstance(aggregation, _SumAggregation)
892892
self.assertFalse(aggregation._instrument_is_monotonic)
@@ -897,7 +897,7 @@ def test_up_down_counter(self):
897897
def test_observable_counter(self):
898898

899899
aggregation = self.default_aggregation._create_aggregation(
900-
ObservableCounter(Mock(), Mock(), Mock(), callbacks=[Mock()])
900+
ObservableCounter("name", Mock(), Mock(), callbacks=[Mock()])
901901
)
902902
self.assertIsInstance(aggregation, _SumAggregation)
903903
self.assertTrue(aggregation._instrument_is_monotonic)
@@ -909,7 +909,7 @@ def test_observable_counter(self):
909909
def test_observable_up_down_counter(self):
910910

911911
aggregation = self.default_aggregation._create_aggregation(
912-
ObservableUpDownCounter(Mock(), Mock(), Mock(), callbacks=[Mock()])
912+
ObservableUpDownCounter("name", Mock(), Mock(), callbacks=[Mock()])
913913
)
914914
self.assertIsInstance(aggregation, _SumAggregation)
915915
self.assertFalse(aggregation._instrument_is_monotonic)
@@ -922,9 +922,7 @@ def test_histogram(self):
922922

923923
aggregation = self.default_aggregation._create_aggregation(
924924
Histogram(
925-
Mock(),
926-
Mock(),
927-
Mock(),
925+
"name",
928926
Mock(),
929927
Mock(),
930928
)
@@ -935,7 +933,7 @@ def test_observable_gauge(self):
935933

936934
aggregation = self.default_aggregation._create_aggregation(
937935
ObservableGauge(
938-
Mock(),
936+
"name",
939937
Mock(),
940938
Mock(),
941939
callbacks=[Mock()],

opentelemetry-sdk/tests/metrics/test_instrument.py

+8
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828

2929

3030
class TestCounter(TestCase):
31+
def testname(self):
32+
self.assertEqual(Counter("name", Mock(), Mock()).name, "name")
33+
self.assertEqual(Counter("Name", Mock(), Mock()).name, "name")
34+
3135
def test_add(self):
3236
mc = Mock()
3337
counter = Counter("name", Mock(), mc)
@@ -91,6 +95,10 @@ def generator_callback_1():
9195

9296

9397
class TestObservableGauge(TestCase):
98+
def testname(self):
99+
self.assertEqual(ObservableGauge("name", Mock(), Mock()).name, "name")
100+
self.assertEqual(ObservableGauge("Name", Mock(), Mock()).name, "name")
101+
94102
def test_callable_callback_0(self):
95103
observable_gauge = ObservableGauge(
96104
"name", Mock(), Mock(), [callable_callback_0]

0 commit comments

Comments
 (0)