Skip to content

Commit 78c19dc

Browse files
authored
Fix open-telemetry#3695: add attributes to get_meter fn and InstrumentationScope (#4015)
1 parent 71416e9 commit 78c19dc

File tree

6 files changed

+104
-7
lines changed

6 files changed

+104
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4040
([#3965](https://github.com/open-telemetry/opentelemetry-python/pull/3965))
4141
- Validate links at span creation
4242
([#3991](https://github.com/open-telemetry/opentelemetry-python/pull/3991))
43+
- Add attributes field in `MeterProvider.get_meter` and `InstrumentationScope`
44+
([#4015](https://github.com/open-telemetry/opentelemetry-python/pull/4015))
4345

4446
## Version 1.25.0/0.46b0 (2024-05-30)
4547

opentelemetry-api/src/opentelemetry/metrics/_internal/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
)
7676
from opentelemetry.util._once import Once
7777
from opentelemetry.util._providers import _load_provider
78+
from opentelemetry.util.types import Attributes
7879

7980
_logger = getLogger(__name__)
8081

@@ -102,6 +103,7 @@ def get_meter(
102103
name: str,
103104
version: Optional[str] = None,
104105
schema_url: Optional[str] = None,
106+
attributes: Optional[Attributes] = None,
105107
) -> "Meter":
106108
"""Returns a `Meter` for use by the given instrumentation library.
107109
@@ -128,6 +130,7 @@ def get_meter(
128130
``importlib.metadata.version(instrumenting_library_name)``.
129131
130132
schema_url: Optional. Specifies the Schema URL of the emitted telemetry.
133+
attributes: Optional. Attributes that are associated with the emitted telemetry.
131134
"""
132135

133136

@@ -139,6 +142,7 @@ def get_meter(
139142
name: str,
140143
version: Optional[str] = None,
141144
schema_url: Optional[str] = None,
145+
attributes: Optional[Attributes] = None,
142146
) -> "Meter":
143147
"""Returns a NoOpMeter."""
144148
return NoOpMeter(name, version=version, schema_url=schema_url)
@@ -155,6 +159,7 @@ def get_meter(
155159
name: str,
156160
version: Optional[str] = None,
157161
schema_url: Optional[str] = None,
162+
attributes: Optional[Attributes] = None,
158163
) -> "Meter":
159164
with self._lock:
160165
if self._real_meter_provider is not None:

opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
from opentelemetry.sdk.resources import Resource
5555
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
5656
from opentelemetry.util._once import Once
57+
from opentelemetry.util.types import Attributes
5758

5859
_logger = getLogger(__name__)
5960

@@ -518,6 +519,7 @@ def get_meter(
518519
name: str,
519520
version: Optional[str] = None,
520521
schema_url: Optional[str] = None,
522+
attributes: Optional[Attributes] = None,
521523
) -> Meter:
522524

523525
if self._disabled:
@@ -534,7 +536,7 @@ def get_meter(
534536
_logger.warning("Meter name cannot be None or empty.")
535537
return NoOpMeter(name, version=version, schema_url=schema_url)
536538

537-
info = InstrumentationScope(name, version, schema_url)
539+
info = InstrumentationScope(name, version, schema_url, attributes)
538540
with self._meter_lock:
539541
if not self._meters.get(info):
540542
# FIXME #2558 pass SDKConfig object to meter so that the meter

opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
from deprecated import deprecated
1818

19+
from opentelemetry.attributes import BoundedAttributes
20+
from opentelemetry.util.types import Attributes
21+
1922

2023
class InstrumentationInfo:
2124
"""Immutable information about an instrumentation library module.
@@ -82,42 +85,56 @@ class InstrumentationScope:
8285
properties.
8386
"""
8487

85-
__slots__ = ("_name", "_version", "_schema_url")
88+
__slots__ = ("_name", "_version", "_schema_url", "_attributes")
8689

8790
def __init__(
8891
self,
8992
name: str,
9093
version: Optional[str] = None,
9194
schema_url: Optional[str] = None,
95+
attributes: Optional[Attributes] = None,
9296
) -> None:
9397
self._name = name
9498
self._version = version
9599
if schema_url is None:
96100
schema_url = ""
97101
self._schema_url = schema_url
102+
self._attributes = BoundedAttributes(attributes=attributes)
98103

99104
def __repr__(self) -> str:
100-
return f"{type(self).__name__}({self._name}, {self._version}, {self._schema_url})"
105+
return f"{type(self).__name__}({self._name}, {self._version}, {self._schema_url}, {self._attributes})"
101106

102107
def __hash__(self) -> int:
103108
return hash((self._name, self._version, self._schema_url))
104109

105110
def __eq__(self, value: object) -> bool:
106111
if not isinstance(value, InstrumentationScope):
107112
return NotImplemented
108-
return (self._name, self._version, self._schema_url) == (
113+
return (
114+
self._name,
115+
self._version,
116+
self._schema_url,
117+
self._attributes,
118+
) == (
109119
value._name,
110120
value._version,
111121
value._schema_url,
122+
value._attributes,
112123
)
113124

114125
def __lt__(self, value: object) -> bool:
115126
if not isinstance(value, InstrumentationScope):
116127
return NotImplemented
117-
return (self._name, self._version, self._schema_url) < (
128+
return (
129+
self._name,
130+
self._version,
131+
self._schema_url,
132+
self._attributes,
133+
) < (
118134
value._name,
119135
value._version,
120136
value._schema_url,
137+
value._attributes,
121138
)
122139

123140
@property
@@ -132,12 +149,19 @@ def version(self) -> Optional[str]:
132149
def name(self) -> str:
133150
return self._name
134151

152+
@property
153+
def attributes(self) -> Attributes:
154+
return self._attributes
155+
135156
def to_json(self, indent=4) -> str:
136157
return dumps(
137158
{
138159
"name": self._name,
139160
"version": self._version,
140161
"schema_url": self._schema_url,
162+
"attributes": (
163+
dict(self._attributes) if bool(self._attributes) else None
164+
),
141165
},
142166
indent=indent,
143167
)

opentelemetry-sdk/tests/metrics/test_metrics.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from typing import Iterable, Sequence
1919
from unittest.mock import MagicMock, Mock, patch
2020

21+
from opentelemetry.attributes import BoundedAttributes
2122
from opentelemetry.metrics import NoOpMeter
2223
from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED
2324
from opentelemetry.sdk.metrics import (
@@ -126,11 +127,36 @@ def test_get_meter(self):
126127
"name",
127128
version="version",
128129
schema_url="schema_url",
130+
attributes={"key": "value"},
129131
)
130132

131133
self.assertEqual(meter._instrumentation_scope.name, "name")
132134
self.assertEqual(meter._instrumentation_scope.version, "version")
133135
self.assertEqual(meter._instrumentation_scope.schema_url, "schema_url")
136+
self.assertEqual(
137+
meter._instrumentation_scope.attributes, {"key": "value"}
138+
)
139+
140+
def test_get_meter_attributes(self):
141+
"""
142+
`MeterProvider.get_meter` arguments are used to create an
143+
`InstrumentationScope` object on the created `Meter`.
144+
"""
145+
146+
meter = MeterProvider().get_meter(
147+
"name",
148+
version="version",
149+
schema_url="schema_url",
150+
attributes={"key": "value", "key2": 5, "key3": "value3"},
151+
)
152+
153+
self.assertEqual(meter._instrumentation_scope.name, "name")
154+
self.assertEqual(meter._instrumentation_scope.version, "version")
155+
self.assertEqual(meter._instrumentation_scope.schema_url, "schema_url")
156+
self.assertEqual(
157+
meter._instrumentation_scope.attributes,
158+
{"key": "value", "key2": 5, "key3": "value3"},
159+
)
134160

135161
def test_get_meter_empty(self):
136162
"""
@@ -180,6 +206,44 @@ def test_get_meter_duplicate(self):
180206
self.assertIs(meter1, meter2)
181207
self.assertIsNot(meter1, meter3)
182208

209+
def test_get_meter_comparison_with_attributes(self):
210+
"""
211+
Subsequent calls to `MeterProvider.get_meter` with the same arguments
212+
should return the same `Meter` instance.
213+
"""
214+
mp = MeterProvider()
215+
meter1 = mp.get_meter(
216+
"name",
217+
version="version",
218+
schema_url="schema_url",
219+
attributes={"key": "value", "key2": 5, "key3": "value3"},
220+
)
221+
meter2 = mp.get_meter(
222+
"name",
223+
version="version",
224+
schema_url="schema_url",
225+
attributes={"key": "value", "key2": 5, "key3": "value3"},
226+
)
227+
meter3 = mp.get_meter(
228+
"name2",
229+
version="version",
230+
schema_url="schema_url",
231+
)
232+
meter4 = mp.get_meter(
233+
"name",
234+
version="version",
235+
schema_url="schema_url",
236+
attributes={"key": "value", "key2": 5, "key3": "value4"},
237+
)
238+
self.assertIs(meter1, meter2)
239+
self.assertIsNot(meter1, meter3)
240+
self.assertTrue(
241+
meter3._instrumentation_scope > meter4._instrumentation_scope
242+
)
243+
self.assertIsInstance(
244+
meter4._instrumentation_scope.attributes, BoundedAttributes
245+
)
246+
183247
def test_shutdown(self):
184248

185249
mock_metric_reader_0 = MagicMock(

opentelemetry-sdk/tests/metrics/test_point.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def setUpClass(cls):
178178
metrics=[cls.metric_0, cls.metric_1, cls.metric_2],
179179
schema_url="schema_url_0",
180180
)
181-
cls.scope_metrics_0_str = f'{{"scope": {{"name": "name_0", "version": "version_0", "schema_url": "schema_url_0"}}, "metrics": [{cls.metric_0_str}, {cls.metric_1_str}, {cls.metric_2_str}], "schema_url": "schema_url_0"}}'
181+
cls.scope_metrics_0_str = f'{{"scope": {{"name": "name_0", "version": "version_0", "schema_url": "schema_url_0", "attributes": null}}, "metrics": [{cls.metric_0_str}, {cls.metric_1_str}, {cls.metric_2_str}], "schema_url": "schema_url_0"}}'
182182

183183
cls.scope_metrics_1 = ScopeMetrics(
184184
scope=InstrumentationScope(
@@ -189,7 +189,7 @@ def setUpClass(cls):
189189
metrics=[cls.metric_0, cls.metric_1, cls.metric_2],
190190
schema_url="schema_url_1",
191191
)
192-
cls.scope_metrics_1_str = f'{{"scope": {{"name": "name_1", "version": "version_1", "schema_url": "schema_url_1"}}, "metrics": [{cls.metric_0_str}, {cls.metric_1_str}, {cls.metric_2_str}], "schema_url": "schema_url_1"}}'
192+
cls.scope_metrics_1_str = f'{{"scope": {{"name": "name_1", "version": "version_1", "schema_url": "schema_url_1", "attributes": null}}, "metrics": [{cls.metric_0_str}, {cls.metric_1_str}, {cls.metric_2_str}], "schema_url": "schema_url_1"}}'
193193

194194
cls.resource_metrics_0 = ResourceMetrics(
195195
resource=Resource(

0 commit comments

Comments
 (0)