Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4ab3978

Browse files
authored
add check for event attribute values (open-telemetry#678)
This PR addresses issue open-telemetry#352 Validates attribute values and events upon span creation and setting attributes on spans. Validation logic involves checking if value is one of (int, float, str, bool, list), and if list, each element must be valid and must be all the same primitive, valid type list values will not contain nested lists If value is a list, grants immutable property by converting value to a tuple.
1 parent 11810d8 commit 4ab3978

File tree

4 files changed

+125
-71
lines changed

4 files changed

+125
-71
lines changed

ext/opentelemetry-ext-opentracing-shim/tests/test_shim.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -463,9 +463,6 @@ def test_span_on_error(self):
463463

464464
# Verify exception details have been added to span.
465465
self.assertEqual(scope.span.unwrap().attributes["error"], True)
466-
self.assertEqual(
467-
scope.span.unwrap().events[0].attributes["error.kind"], Exception
468-
)
469466

470467
def test_inject_http_headers(self):
471468
"""Test `inject()` method for Format.HTTP_HEADERS."""

opentelemetry-sdk/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Validate span attribute types in SDK (#678)
6+
57
## 0.7b1
68

79
Released 2020-05-12

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

Lines changed: 64 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import atexit
1818
import json
1919
import logging
20-
import os
2120
import random
2221
import threading
2322
from collections import OrderedDict
@@ -41,6 +40,7 @@
4140
MAX_NUM_ATTRIBUTES = 32
4241
MAX_NUM_EVENTS = 128
4342
MAX_NUM_LINKS = 32
43+
VALID_ATTR_VALUE_TYPES = (bool, str, int, float)
4444

4545

4646
class SpanProcessor:
@@ -189,6 +189,48 @@ def attributes(self) -> types.Attributes:
189189
return self._event_formatter()
190190

191191

192+
def _is_valid_attribute_value(value: types.AttributeValue) -> bool:
193+
"""Checks if attribute value is valid.
194+
195+
An attribute value is valid if it is one of the valid types. If the value
196+
is a sequence, it is only valid if all items in the sequence are of valid
197+
type, not a sequence, and are of the same type.
198+
"""
199+
200+
if isinstance(value, Sequence):
201+
if len(value) == 0:
202+
return True
203+
204+
first_element_type = type(value[0])
205+
206+
if first_element_type not in VALID_ATTR_VALUE_TYPES:
207+
logger.warning(
208+
"Invalid type %s in attribute value sequence. Expected one of "
209+
"%s or a sequence of those types",
210+
first_element_type.__name__,
211+
[valid_type.__name__ for valid_type in VALID_ATTR_VALUE_TYPES],
212+
)
213+
return False
214+
215+
for element in list(value)[1:]:
216+
if not isinstance(element, first_element_type):
217+
logger.warning(
218+
"Mixed types %s and %s in attribute value sequence",
219+
first_element_type.__name__,
220+
type(element).__name__,
221+
)
222+
return False
223+
elif not isinstance(value, VALID_ATTR_VALUE_TYPES):
224+
logger.warning(
225+
"Invalid type %s for attribute value. Expected one of %s or a "
226+
"sequence of those types",
227+
type(value).__name__,
228+
[valid_type.__name__ for valid_type in VALID_ATTR_VALUE_TYPES],
229+
)
230+
return False
231+
return True
232+
233+
192234
class Span(trace_api.Span):
193235
"""See `opentelemetry.trace.Span`.
194236
@@ -245,7 +287,8 @@ def __init__(
245287
self.status = None
246288
self._lock = threading.Lock()
247289

248-
if attributes is None:
290+
self._filter_attribute_values(attributes)
291+
if not attributes:
249292
self.attributes = Span._empty_attributes
250293
else:
251294
self.attributes = BoundedDict.from_map(
@@ -255,7 +298,10 @@ def __init__(
255298
if events is None:
256299
self.events = Span._empty_events
257300
else:
258-
self.events = BoundedList.from_seq(MAX_NUM_EVENTS, events)
301+
self.events = BoundedList(MAX_NUM_EVENTS)
302+
for event in events:
303+
self._filter_attribute_values(event.attributes)
304+
self.events.append(event)
259305

260306
if links is None:
261307
self.links = Span._empty_links
@@ -372,37 +418,24 @@ def set_attribute(self, key: str, value: types.AttributeValue) -> None:
372418
logger.warning("invalid key (empty or null)")
373419
return
374420

375-
if isinstance(value, Sequence):
376-
error_message = self._check_attribute_value_sequence(value)
377-
if error_message is not None:
378-
logger.warning("%s in attribute value sequence", error_message)
379-
return
421+
if _is_valid_attribute_value(value):
380422
# Freeze mutable sequences defensively
381423
if isinstance(value, MutableSequence):
382424
value = tuple(value)
383-
elif not isinstance(value, (bool, str, int, float)):
384-
logger.warning("invalid type for attribute value")
385-
return
386-
387-
self.attributes[key] = value
425+
with self._lock:
426+
self.attributes[key] = value
388427

389428
@staticmethod
390-
def _check_attribute_value_sequence(sequence: Sequence) -> Optional[str]:
391-
"""
392-
Checks if sequence items are valid and are of the same type
393-
"""
394-
if len(sequence) == 0:
395-
return None
396-
397-
first_element_type = type(sequence[0])
398-
399-
if first_element_type not in (bool, str, int, float):
400-
return "invalid type"
401-
402-
for element in sequence:
403-
if not isinstance(element, first_element_type):
404-
return "different type"
405-
return None
429+
def _filter_attribute_values(attributes: types.Attributes):
430+
if attributes:
431+
for attr_key, attr_value in list(attributes.items()):
432+
if _is_valid_attribute_value(attr_value):
433+
if isinstance(attr_value, MutableSequence):
434+
attributes[attr_key] = tuple(attr_value)
435+
else:
436+
attributes[attr_key] = attr_value
437+
else:
438+
attributes.pop(attr_key)
406439

407440
def _add_event(self, event: EventBase) -> None:
408441
with self._lock:
@@ -423,7 +456,8 @@ def add_event(
423456
attributes: types.Attributes = None,
424457
timestamp: Optional[int] = None,
425458
) -> None:
426-
if attributes is None:
459+
self._filter_attribute_values(attributes)
460+
if not attributes:
427461
attributes = Span._empty_attributes
428462
self._add_event(
429463
Event(
@@ -514,7 +548,6 @@ def __exit__(
514548
and self._set_status_on_exception
515549
and exc_val is not None
516550
):
517-
518551
self.set_status(
519552
Status(
520553
canonical_code=StatusCanonicalCode.UNKNOWN,

opentelemetry-sdk/tests/trace/test_trace.py

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -487,38 +487,26 @@ def test_invalid_attribute_values(self):
487487

488488
self.assertEqual(len(root.attributes), 0)
489489

490-
def test_check_sequence_helper(self):
490+
def test_check_attribute_helper(self):
491491
# pylint: disable=protected-access
492-
self.assertEqual(
493-
trace.Span._check_attribute_value_sequence([1, 2, 3.4, "ss", 4]),
494-
"different type",
495-
)
496-
self.assertEqual(
497-
trace.Span._check_attribute_value_sequence([dict(), 1, 2, 3.4, 4]),
498-
"invalid type",
499-
)
500-
self.assertEqual(
501-
trace.Span._check_attribute_value_sequence(
502-
["sw", "lf", 3.4, "ss"]
503-
),
504-
"different type",
505-
)
506-
self.assertEqual(
507-
trace.Span._check_attribute_value_sequence([1, 2, 3.4, 5]),
508-
"different type",
492+
self.assertFalse(trace._is_valid_attribute_value([1, 2, 3.4, "ss", 4]))
493+
self.assertFalse(
494+
trace._is_valid_attribute_value([dict(), 1, 2, 3.4, 4])
509495
)
510-
self.assertIsNone(
511-
trace.Span._check_attribute_value_sequence([1, 2, 3, 5])
512-
)
513-
self.assertIsNone(
514-
trace.Span._check_attribute_value_sequence([1.2, 2.3, 3.4, 4.5])
515-
)
516-
self.assertIsNone(
517-
trace.Span._check_attribute_value_sequence([True, False])
518-
)
519-
self.assertIsNone(
520-
trace.Span._check_attribute_value_sequence(["ss", "dw", "fw"])
496+
self.assertFalse(
497+
trace._is_valid_attribute_value(["sw", "lf", 3.4, "ss"])
521498
)
499+
self.assertFalse(trace._is_valid_attribute_value([1, 2, 3.4, 5]))
500+
self.assertTrue(trace._is_valid_attribute_value([1, 2, 3, 5]))
501+
self.assertTrue(trace._is_valid_attribute_value([1.2, 2.3, 3.4, 4.5]))
502+
self.assertTrue(trace._is_valid_attribute_value([True, False]))
503+
self.assertTrue(trace._is_valid_attribute_value(["ss", "dw", "fw"]))
504+
self.assertTrue(trace._is_valid_attribute_value([]))
505+
self.assertFalse(trace._is_valid_attribute_value(dict()))
506+
self.assertTrue(trace._is_valid_attribute_value(True))
507+
self.assertTrue(trace._is_valid_attribute_value("hi"))
508+
self.assertTrue(trace._is_valid_attribute_value(3.4))
509+
self.assertTrue(trace._is_valid_attribute_value(15))
522510

523511
def test_sampling_attributes(self):
524512
decision_attributes = {
@@ -561,33 +549,67 @@ def test_events(self):
561549

562550
# event name and attributes
563551
now = time_ns()
564-
root.add_event("event1", {"name": "pluto"})
552+
root.add_event(
553+
"event1", {"name": "pluto", "some_bools": [True, False]}
554+
)
565555

566556
# event name, attributes and timestamp
567557
now = time_ns()
568-
root.add_event("event2", {"name": "birthday"}, now)
558+
root.add_event("event2", {"name": ["birthday"]}, now)
559+
560+
mutable_list = ["original_contents"]
561+
root.add_event("event3", {"name": mutable_list})
569562

570563
def event_formatter():
571564
return {"name": "hello"}
572565

573566
# lazy event
574-
root.add_lazy_event("event3", event_formatter, now)
567+
root.add_lazy_event("event4", event_formatter, now)
575568

576-
self.assertEqual(len(root.events), 4)
569+
self.assertEqual(len(root.events), 5)
577570

578571
self.assertEqual(root.events[0].name, "event0")
579572
self.assertEqual(root.events[0].attributes, {})
580573

581574
self.assertEqual(root.events[1].name, "event1")
582-
self.assertEqual(root.events[1].attributes, {"name": "pluto"})
575+
self.assertEqual(
576+
root.events[1].attributes,
577+
{"name": "pluto", "some_bools": (True, False)},
578+
)
583579

584580
self.assertEqual(root.events[2].name, "event2")
585-
self.assertEqual(root.events[2].attributes, {"name": "birthday"})
581+
self.assertEqual(
582+
root.events[2].attributes, {"name": ("birthday",)}
583+
)
586584
self.assertEqual(root.events[2].timestamp, now)
587585

588586
self.assertEqual(root.events[3].name, "event3")
589-
self.assertEqual(root.events[3].attributes, {"name": "hello"})
590-
self.assertEqual(root.events[3].timestamp, now)
587+
self.assertEqual(
588+
root.events[3].attributes, {"name": ("original_contents",)}
589+
)
590+
mutable_list = ["new_contents"]
591+
self.assertEqual(
592+
root.events[3].attributes, {"name": ("original_contents",)}
593+
)
594+
595+
self.assertEqual(root.events[4].name, "event4")
596+
self.assertEqual(root.events[4].attributes, {"name": "hello"})
597+
self.assertEqual(root.events[4].timestamp, now)
598+
599+
def test_invalid_event_attributes(self):
600+
self.assertIsNone(self.tracer.get_current_span())
601+
602+
with self.tracer.start_as_current_span("root") as root:
603+
root.add_event("event0", {"attr1": True, "attr2": ["hi", False]})
604+
root.add_event("event0", {"attr1": dict()})
605+
root.add_event("event0", {"attr1": [[True]]})
606+
root.add_event("event0", {"attr1": [dict()], "attr2": [1, 2]})
607+
608+
self.assertEqual(len(root.events), 4)
609+
self.assertEqual(root.events[0].attributes, {"attr1": True})
610+
self.assertEqual(root.events[1].attributes, {})
611+
self.assertEqual(root.events[2].attributes, {})
612+
self.assertEqual(root.events[3].attributes, {"attr2": (1, 2)})
591613

592614
def test_links(self):
593615
other_context1 = trace_api.SpanContext(

0 commit comments

Comments
 (0)