Skip to content

Commit 7506644

Browse files
author
Andrew Xue
committed
add limits
1 parent 8d5eb3f commit 7506644

File tree

3 files changed

+166
-26
lines changed

3 files changed

+166
-26
lines changed

docs-requirements.txt

+1-4
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,4 @@ sqlalchemy>=1.0
2020
thrift>=0.10.0
2121
wrapt>=1.0.0,<2.0.0
2222
psutil~=5.7.0
23-
google-cloud-core >=1.3.0
24-
google-api-core >=1.17.0
25-
google-cloud-trace >=0.23.0
26-
grpcio >=1.28.1
23+
google-cloud-trace >=0.23.0

ext/opentelemetry-exporter-cloud-trace/src/opentelemetry/exporter/cloud_trace/__init__.py

+60-12
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,17 @@
5353
import opentelemetry.trace as trace_api
5454
from opentelemetry.sdk.trace import Event
5555
from opentelemetry.sdk.trace.export import Span, SpanExporter, SpanExportResult
56+
from opentelemetry.sdk.util import BoundedDict
5657
from opentelemetry.util import types
5758

5859
logger = logging.getLogger(__name__)
5960

61+
MAX_NUM_LINKS = 128
62+
MAX_NUM_EVENTS = 32
63+
MAX_EVENT_ATTRS = 4
64+
MAX_LINK_ATTRS = 32
65+
MAX_SPAN_ATTRS = 32
66+
6067

6168
class CloudTraceSpanExporter(SpanExporter):
6269
"""Cloud Trace span exporter for OpenTelemetry.
@@ -129,7 +136,11 @@ def _translate_to_cloud_trace(
129136
start_time = _get_time_from_ns(span.start_time)
130137
end_time = _get_time_from_ns(span.end_time)
131138

132-
attributes = _extract_attributes(span.attributes)
139+
if len(span.attributes) > MAX_SPAN_ATTRS:
140+
logger.warning(
141+
"Span has more then %s attributes, some will be truncated",
142+
MAX_SPAN_ATTRS,
143+
)
133144

134145
cloud_trace_spans.append(
135146
{
@@ -141,7 +152,9 @@ def _translate_to_cloud_trace(
141152
"start_time": start_time,
142153
"end_time": end_time,
143154
"parent_span_id": parent_id,
144-
"attributes": attributes,
155+
"attributes": _extract_attributes(
156+
span.attributes, MAX_SPAN_ATTRS
157+
),
145158
"links": _extract_links(span.links),
146159
"status": _extract_status(span.status),
147160
"time_events": _extract_events(span.events),
@@ -185,7 +198,9 @@ def _get_truncatable_str_object(str_to_convert: str, max_length: int):
185198

186199
def _truncate_str(str_to_check: str, limit: int) -> Tuple[str, int]:
187200
"""Check the length of a string. If exceeds limit, then truncate it."""
188-
return str_to_check[:limit], max(0, len(str_to_check) - limit)
201+
encoded = str_to_check.encode("utf-8")
202+
truncated_str = encoded[:limit].decode("utf-8", errors="ignore")
203+
return truncated_str, len(encoded) - len(truncated_str.encode("utf-8"))
189204

190205

191206
def _extract_status(status: trace_api.Status) -> Status:
@@ -205,30 +220,56 @@ def _extract_links(links: Sequence[trace_api.Link]) -> ProtoSpan.Links:
205220
if not links:
206221
return None
207222
extracted_links = []
223+
dropped_links = 0
224+
if len(links) > MAX_NUM_LINKS:
225+
logger.warning(
226+
"Exporting more then %s links, some will be truncated",
227+
MAX_NUM_LINKS,
228+
)
229+
dropped_links = len(links) - MAX_NUM_LINKS
230+
links = links[:MAX_NUM_LINKS]
208231
for link in links:
232+
if len(link.attributes) > MAX_LINK_ATTRS:
233+
logger.warning(
234+
"Link has more then %s attributes, some will be truncated",
235+
MAX_LINK_ATTRS,
236+
)
209237
trace_id = _get_hexadecimal_trace_id(link.context.trace_id)
210238
span_id = _get_hexadecimal_span_id(link.context.span_id)
211239
extracted_links.append(
212240
{
213241
"trace_id": trace_id,
214242
"span_id": span_id,
215243
"type": "TYPE_UNSPECIFIED",
216-
"attributes": _extract_attributes(link.attributes),
244+
"attributes": _extract_attributes(
245+
link.attributes, MAX_LINK_ATTRS
246+
),
217247
}
218248
)
219-
return ProtoSpan.Links(link=extracted_links, dropped_links_count=0)
249+
return ProtoSpan.Links(
250+
link=extracted_links, dropped_links_count=dropped_links
251+
)
220252

221253

222254
def _extract_events(events: Sequence[Event]) -> ProtoSpan.TimeEvents:
223255
"""Convert span.events to dict."""
224256
if not events:
225257
return None
226258
logs = []
259+
dropped_annontations = 0
260+
if len(events) > MAX_NUM_EVENTS:
261+
logger.warning(
262+
"Exporting more then %s annotations, some will be truncated",
263+
MAX_NUM_EVENTS,
264+
)
265+
dropped_annontations = len(events) - MAX_NUM_EVENTS
266+
events = events[:MAX_NUM_EVENTS]
227267
for event in events:
228-
if len(event.attributes) > 4:
268+
if len(event.attributes) > MAX_EVENT_ATTRS:
229269
logger.warning(
230-
"Event %s has more then 4 attributes, some will be truncated",
270+
"Event %s has more then %s attributes, some will be truncated",
231271
event.name,
272+
MAX_EVENT_ATTRS,
232273
)
233274
logs.append(
234275
{
@@ -237,28 +278,35 @@ def _extract_events(events: Sequence[Event]) -> ProtoSpan.TimeEvents:
237278
"description": _get_truncatable_str_object(
238279
event.name, 256
239280
),
240-
"attributes": _extract_attributes(event.attributes),
281+
"attributes": _extract_attributes(
282+
event.attributes, MAX_EVENT_ATTRS
283+
),
241284
},
242285
}
243286
)
244287
return ProtoSpan.TimeEvents(
245288
time_event=logs,
246-
dropped_annotations_count=0,
289+
dropped_annotations_count=dropped_annontations,
247290
dropped_message_events_count=0,
248291
)
249292

250293

251-
def _extract_attributes(attrs: types.Attributes) -> ProtoSpan.Attributes:
294+
def _extract_attributes(
295+
attrs: types.Attributes, num_attrs_limit: int
296+
) -> ProtoSpan.Attributes:
252297
"""Convert span.attributes to dict."""
253-
attributes_dict = {}
298+
attributes_dict = BoundedDict(num_attrs_limit)
254299

255300
for key, value in attrs.items():
256301
key = _truncate_str(key, 128)[0]
257302
value = _format_attribute_value(value)
258303

259304
if value is not None:
260305
attributes_dict[key] = value
261-
return ProtoSpan.Attributes(attribute_map=attributes_dict)
306+
return ProtoSpan.Attributes(
307+
attribute_map=attributes_dict,
308+
dropped_attributes_count=len(attrs) - len(attributes_dict),
309+
)
262310

263311

264312
def _format_attribute_value(value: types.AttributeValue) -> AttributeValue:

ext/opentelemetry-exporter-cloud-trace/tests/test_cloud_trace_exporter.py

+105-10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
from google.rpc.status_pb2 import Status
2222

2323
from opentelemetry.exporter.cloud_trace import (
24+
MAX_EVENT_ATTRS,
25+
MAX_LINK_ATTRS,
26+
MAX_NUM_EVENTS,
27+
MAX_NUM_LINKS,
2428
CloudTraceSpanExporter,
2529
_extract_attributes,
2630
_extract_events,
@@ -47,7 +51,6 @@ def setUp(self):
4751
"bool_key": False,
4852
"double_key": 1.421,
4953
"int_key": 123,
50-
"int_key2": 1234,
5154
}
5255
self.extracted_attributes_variety_pack = ProtoSpan.Attributes(
5356
attribute_map={
@@ -63,7 +66,6 @@ def setUp(self):
6366
)
6467
),
6568
"int_key": AttributeValue(int_value=123),
66-
"int_key2": AttributeValue(int_value=1234),
6769
}
6870
)
6971

@@ -141,18 +143,24 @@ def test_extract_status(self):
141143

142144
def test_extract_attributes(self):
143145
self.assertEqual(
144-
_extract_attributes({}), ProtoSpan.Attributes(attribute_map={})
146+
_extract_attributes({}, 4), ProtoSpan.Attributes(attribute_map={})
145147
)
146148
self.assertEqual(
147-
_extract_attributes(self.attributes_variety_pack),
149+
_extract_attributes(self.attributes_variety_pack, 4),
148150
self.extracted_attributes_variety_pack,
149151
)
150152
# Test ignoring attributes with illegal value type
151153
self.assertEqual(
152-
_extract_attributes({"illegal_attribute_value": dict()}),
153-
ProtoSpan.Attributes(attribute_map={}),
154+
_extract_attributes({"illegal_attribute_value": dict()}, 4),
155+
ProtoSpan.Attributes(attribute_map={}, dropped_attributes_count=1),
154156
)
155157

158+
too_many_attrs = {}
159+
for attr_key in range(5):
160+
too_many_attrs[str(attr_key)] = 0
161+
proto_attrs = _extract_attributes(too_many_attrs, 4)
162+
self.assertEqual(proto_attrs.dropped_attributes_count, 1)
163+
156164
def test_extract_events(self):
157165
self.assertIsNone(_extract_events([]))
158166
time_in_ns1 = 1589919268850900051
@@ -189,7 +197,7 @@ def test_extract_events(self):
189197
value="event2", truncated_byte_count=0
190198
),
191199
"attributes": ProtoSpan.Attributes(
192-
attribute_map={}
200+
attribute_map={}, dropped_attributes_count=1
193201
),
194202
},
195203
},
@@ -249,20 +257,27 @@ def test_extract_links(self):
249257
"attributes": {
250258
"attribute_map": {
251259
"int_attr_value": AttributeValue(int_value=123)
252-
}
260+
},
261+
"dropped_attributes_count": 1,
253262
},
254263
},
255264
]
256265
),
257266
)
258267

259-
def test_truncate_string(self):
268+
# pylint:disable=too-many-locals
269+
def test_truncate(self):
270+
"""Cloud Trace API imposes limits on the length of many things,
271+
e.g. strings, number of events, number of attributes. We truncate
272+
these things before sending it to the API as an optimization.
273+
"""
260274
str_300 = "a" * 300
261275
str_256 = "a" * 256
262276
str_128 = "a" * 128
263277
self.assertEqual(_truncate_str("aaaa", 1), ("a", 3))
264278
self.assertEqual(_truncate_str("aaaa", 5), ("aaaa", 0))
265279
self.assertEqual(_truncate_str("aaaa", 4), ("aaaa", 0))
280+
self.assertEqual(_truncate_str("中文翻译", 4), ("中", 9))
266281

267282
self.assertEqual(
268283
_format_attribute_value(str_300),
@@ -272,8 +287,9 @@ def test_truncate_string(self):
272287
)
273288
),
274289
)
290+
275291
self.assertEqual(
276-
_extract_attributes({str_300: str_300}),
292+
_extract_attributes({str_300: str_300}, 4),
277293
ProtoSpan.Attributes(
278294
attribute_map={
279295
str_128: AttributeValue(
@@ -304,3 +320,82 @@ def test_truncate_string(self):
304320
]
305321
),
306322
)
323+
324+
trace_id = "6e0c63257de34c92bf9efcd03927272e"
325+
span_id = "95bb5edabd45950f"
326+
link = Link(
327+
context=SpanContext(
328+
trace_id=int(trace_id, 16),
329+
span_id=int(span_id, 16),
330+
is_remote=False,
331+
),
332+
attributes={},
333+
)
334+
too_many_links = [link] * (MAX_NUM_LINKS + 1)
335+
self.assertEqual(
336+
_extract_links(too_many_links),
337+
ProtoSpan.Links(
338+
link=[
339+
{
340+
"trace_id": trace_id,
341+
"span_id": span_id,
342+
"type": "TYPE_UNSPECIFIED",
343+
"attributes": {},
344+
}
345+
]
346+
* MAX_NUM_LINKS,
347+
dropped_links_count=len(too_many_links) - MAX_NUM_LINKS,
348+
),
349+
)
350+
351+
link_attrs = {}
352+
for attr_key in range(MAX_LINK_ATTRS + 1):
353+
link_attrs[str(attr_key)] = 0
354+
attr_link = Link(
355+
context=SpanContext(
356+
trace_id=int(trace_id, 16),
357+
span_id=int(span_id, 16),
358+
is_remote=False,
359+
),
360+
attributes=link_attrs,
361+
)
362+
363+
proto_link = _extract_links([attr_link])
364+
self.assertEqual(
365+
len(proto_link.link[0].attributes.attribute_map), MAX_LINK_ATTRS
366+
)
367+
368+
too_many_events = [event1] * (MAX_NUM_EVENTS + 1)
369+
self.assertEqual(
370+
_extract_events(too_many_events),
371+
ProtoSpan.TimeEvents(
372+
time_event=[
373+
{
374+
"time": time_in_ms_and_ns1,
375+
"annotation": {
376+
"description": TruncatableString(
377+
value=str_256, truncated_byte_count=300 - 256
378+
),
379+
"attributes": {},
380+
},
381+
},
382+
]
383+
* MAX_NUM_EVENTS,
384+
dropped_annotations_count=len(too_many_events)
385+
- MAX_NUM_EVENTS,
386+
),
387+
)
388+
389+
time_in_ns1 = 1589919268850900051
390+
event_attrs = {}
391+
for attr_key in range(MAX_EVENT_ATTRS + 1):
392+
event_attrs[str(attr_key)] = 0
393+
proto_events = _extract_events(
394+
[Event(name="a", attributes=event_attrs, timestamp=time_in_ns1)]
395+
)
396+
self.assertEqual(
397+
len(
398+
proto_events.time_event[0].annotation.attributes.attribute_map
399+
),
400+
MAX_EVENT_ATTRS,
401+
)

0 commit comments

Comments
 (0)