Skip to content

Commit 01c6954

Browse files
author
alrex
authored
use BoundedAttributes for attributes in link, event, resource, spans (open-telemetry#1915)
1 parent 1e600f3 commit 01c6954

File tree

8 files changed

+217
-141
lines changed

8 files changed

+217
-141
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Attributes for `Link` and `Resource` are immutable as they are for `Event`, which means
1818
any attempt to modify attributes directly will result in a `TypeError` exception.
1919
([#1909](https://github.com/open-telemetry/opentelemetry-python/pull/1909))
20+
- Added `BoundedAttributes` to the API to make it available for `Link` which is defined in the
21+
API. Marked `BoundedDict` in the SDK as deprecated as a result.
22+
([#1915](https://github.com/open-telemetry/opentelemetry-python/pull/1915))
2023

2124
## [1.3.0-0.22b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.3.0-0.22b0) - 2021-06-01
2225

opentelemetry-api/src/opentelemetry/attributes/__init__.py

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
# type: ignore
1515

1616
import logging
17+
import threading
18+
from collections import OrderedDict
19+
from collections.abc import MutableMapping
1720
from types import MappingProxyType
18-
from typing import MutableSequence, Sequence
21+
from typing import MutableSequence, Optional, Sequence
1922

2023
from opentelemetry.util import types
2124

@@ -104,7 +107,72 @@ def _filter_attributes(attributes: types.Attributes) -> None:
104107
attributes.pop(attr_key)
105108

106109

107-
def _create_immutable_attributes(
108-
attributes: types.Attributes,
109-
) -> types.Attributes:
110-
return MappingProxyType(attributes.copy() if attributes else {})
110+
_DEFAULT_LIMIT = 128
111+
112+
113+
class BoundedAttributes(MutableMapping):
114+
"""An ordered dict with a fixed max capacity.
115+
116+
Oldest elements are dropped when the dict is full and a new element is
117+
added.
118+
"""
119+
120+
def __init__(
121+
self,
122+
maxlen: Optional[int] = _DEFAULT_LIMIT,
123+
attributes: types.Attributes = None,
124+
immutable: bool = True,
125+
):
126+
if maxlen is not None:
127+
if not isinstance(maxlen, int) or maxlen < 0:
128+
raise ValueError(
129+
"maxlen must be valid int greater or equal to 0"
130+
)
131+
self.maxlen = maxlen
132+
self.dropped = 0
133+
self._dict = OrderedDict() # type: OrderedDict
134+
self._lock = threading.Lock() # type: threading.Lock
135+
if attributes:
136+
_filter_attributes(attributes)
137+
for key, value in attributes.items():
138+
self[key] = value
139+
self._immutable = immutable
140+
141+
def __repr__(self):
142+
return "{}({}, maxlen={})".format(
143+
type(self).__name__, dict(self._dict), self.maxlen
144+
)
145+
146+
def __getitem__(self, key):
147+
return self._dict[key]
148+
149+
def __setitem__(self, key, value):
150+
if getattr(self, "_immutable", False):
151+
raise TypeError
152+
with self._lock:
153+
if self.maxlen is not None and self.maxlen == 0:
154+
self.dropped += 1
155+
return
156+
157+
if key in self._dict:
158+
del self._dict[key]
159+
elif self.maxlen is not None and len(self._dict) == self.maxlen:
160+
del self._dict[next(iter(self._dict.keys()))]
161+
self.dropped += 1
162+
self._dict[key] = value
163+
164+
def __delitem__(self, key):
165+
if getattr(self, "_immutable", False):
166+
raise TypeError
167+
with self._lock:
168+
del self._dict[key]
169+
170+
def __iter__(self):
171+
with self._lock:
172+
return iter(self._dict.copy())
173+
174+
def __len__(self):
175+
return len(self._dict)
176+
177+
def copy(self):
178+
return self._dict.copy()

opentelemetry-api/src/opentelemetry/trace/__init__.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,7 @@
8282
from typing import Iterator, Optional, Sequence, cast
8383

8484
from opentelemetry import context as context_api
85-
from opentelemetry.attributes import ( # type: ignore
86-
_create_immutable_attributes,
87-
)
85+
from opentelemetry.attributes import BoundedAttributes # type: ignore
8886
from opentelemetry.context.context import Context
8987
from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER
9088
from opentelemetry.trace.propagation import (
@@ -142,8 +140,8 @@ def __init__(
142140
attributes: types.Attributes = None,
143141
) -> None:
144142
super().__init__(context)
145-
self._attributes = _create_immutable_attributes(
146-
attributes
143+
self._attributes = BoundedAttributes(
144+
attributes=attributes
147145
) # type: types.Attributes
148146

149147
@property

opentelemetry-api/tests/attributes/test_attributes.py

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414

1515
# type: ignore
1616

17+
import collections
1718
import unittest
1819

1920
from opentelemetry.attributes import (
20-
_create_immutable_attributes,
21+
BoundedAttributes,
2122
_filter_attributes,
2223
_is_valid_attribute_value,
2324
)
@@ -79,9 +80,95 @@ def test_filter_attributes(self):
7980
},
8081
)
8182

82-
def test_create_immutable_attributes(self):
83-
attrs = {"key": "value", "pi": 3.14}
84-
immutable = _create_immutable_attributes(attrs)
85-
# TypeError: 'mappingproxy' object does not support item assignment
83+
84+
class TestBoundedAttributes(unittest.TestCase):
85+
base = collections.OrderedDict(
86+
[
87+
("name", "Firulais"),
88+
("age", 7),
89+
("weight", 13),
90+
("vaccinated", True),
91+
]
92+
)
93+
94+
def test_negative_maxlen(self):
95+
with self.assertRaises(ValueError):
96+
BoundedAttributes(-1)
97+
98+
def test_from_map(self):
99+
dic_len = len(self.base)
100+
base_copy = collections.OrderedDict(self.base)
101+
bdict = BoundedAttributes(dic_len, base_copy)
102+
103+
self.assertEqual(len(bdict), dic_len)
104+
105+
# modify base_copy and test that bdict is not changed
106+
base_copy["name"] = "Bruno"
107+
base_copy["age"] = 3
108+
109+
for key in self.base:
110+
self.assertEqual(bdict[key], self.base[key])
111+
112+
# test that iter yields the correct number of elements
113+
self.assertEqual(len(tuple(bdict)), dic_len)
114+
115+
# map too big
116+
half_len = dic_len // 2
117+
bdict = BoundedAttributes(half_len, self.base)
118+
self.assertEqual(len(tuple(bdict)), half_len)
119+
self.assertEqual(bdict.dropped, dic_len - half_len)
120+
121+
def test_bounded_dict(self):
122+
# create empty dict
123+
dic_len = len(self.base)
124+
bdict = BoundedAttributes(dic_len, immutable=False)
125+
self.assertEqual(len(bdict), 0)
126+
127+
# fill dict
128+
for key in self.base:
129+
bdict[key] = self.base[key]
130+
131+
self.assertEqual(len(bdict), dic_len)
132+
self.assertEqual(bdict.dropped, 0)
133+
134+
for key in self.base:
135+
self.assertEqual(bdict[key], self.base[key])
136+
137+
# test __iter__ in BoundedAttributes
138+
for key in bdict:
139+
self.assertEqual(bdict[key], self.base[key])
140+
141+
# updating an existing element should not drop
142+
bdict["name"] = "Bruno"
143+
self.assertEqual(bdict.dropped, 0)
144+
145+
# try to append more elements
146+
for key in self.base:
147+
bdict["new-" + key] = self.base[key]
148+
149+
self.assertEqual(len(bdict), dic_len)
150+
self.assertEqual(bdict.dropped, dic_len)
151+
152+
# test that elements in the dict are the new ones
153+
for key in self.base:
154+
self.assertEqual(bdict["new-" + key], self.base[key])
155+
156+
# delete an element
157+
del bdict["new-name"]
158+
self.assertEqual(len(bdict), dic_len - 1)
159+
160+
with self.assertRaises(KeyError):
161+
_ = bdict["new-name"]
162+
163+
def test_no_limit_code(self):
164+
bdict = BoundedAttributes(maxlen=None, immutable=False)
165+
for num in range(100):
166+
bdict[num] = num
167+
168+
for num in range(100):
169+
self.assertEqual(bdict[num], num)
170+
171+
def test_immutable(self):
172+
bdict = BoundedAttributes()
86173
with self.assertRaises(TypeError):
87-
immutable["pi"] = 1.34
174+
bdict["should-not-work"] = "dict immutable"

opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,7 @@
6464

6565
import pkg_resources
6666

67-
from opentelemetry.attributes import (
68-
_create_immutable_attributes,
69-
_filter_attributes,
70-
)
67+
from opentelemetry.attributes import BoundedAttributes
7168
from opentelemetry.sdk.environment_variables import (
7269
OTEL_RESOURCE_ATTRIBUTES,
7370
OTEL_SERVICE_NAME,
@@ -147,8 +144,7 @@ class Resource:
147144
def __init__(
148145
self, attributes: Attributes, schema_url: typing.Optional[str] = None
149146
):
150-
_filter_attributes(attributes)
151-
self._attributes = _create_immutable_attributes(attributes)
147+
self._attributes = BoundedAttributes(attributes=attributes)
152148
if schema_url is None:
153149
schema_url = ""
154150
self._schema_url = schema_url

0 commit comments

Comments
 (0)