|
47 | 47 | from ddtrace.internal.compat import NumericType |
48 | 48 | from ddtrace.internal.compat import ensure_text |
49 | 49 | from ddtrace.internal.compat import is_integer |
| 50 | +from ddtrace.internal.constants import MAX_INT_64BITS as _MAX_INT_64BITS |
50 | 51 | from ddtrace.internal.constants import MAX_UINT_64BITS as _MAX_UINT_64BITS |
| 52 | +from ddtrace.internal.constants import MIN_INT_64BITS as _MIN_INT_64BITS |
51 | 53 | from ddtrace.internal.constants import SPAN_API_DATADOG |
52 | 54 | from ddtrace.internal.logger import get_logger |
53 | 55 | from ddtrace.internal.sampling import SamplingMechanism |
54 | 56 | from ddtrace.internal.sampling import set_sampling_decision_maker |
| 57 | +from ddtrace.internal.utils.deprecations import DDTraceDeprecationWarning |
55 | 58 | from ddtrace.settings._config import config |
| 59 | +from ddtrace.vendor.debtcollector import deprecate |
56 | 60 |
|
57 | 61 |
|
58 | 62 | class SpanEvent: |
@@ -604,42 +608,93 @@ def record_exception( |
604 | 608 | exception: BaseException, |
605 | 609 | attributes: Optional[Dict[str, _AttributeValueType]] = None, |
606 | 610 | timestamp: Optional[int] = None, |
607 | | - escaped=False, |
| 611 | + escaped: bool = False, |
608 | 612 | ) -> None: |
609 | 613 | """ |
610 | | - Records an exception as span event. |
611 | | - If the exception is uncaught, :obj:`escaped` should be set :obj:`True`. It |
612 | | - will tag the span with an error tuple. |
613 | | -
|
614 | | - :param Exception exception: the exception to record |
615 | | - :param dict attributes: optional attributes to add to the span event. It will override |
616 | | - the base attributes if :obj:`attributes` contains existing keys. |
617 | | - :param int timestamp: the timestamp of the span event. Will be set to now() if timestamp is :obj:`None`. |
618 | | - :param bool escaped: sets to :obj:`False` for a handled exception and :obj:`True` for a uncaught exception. |
| 614 | + Records an exception as a span event. Multiple exceptions can be recorded on a span. |
| 615 | +
|
| 616 | + :param exception: The exception to record. |
| 617 | + :param attributes: Optional dictionary of additional attributes to add to the exception event. |
| 618 | + These attributes will override the default exception attributes if they contain the same keys. |
| 619 | + Valid attribute values include (homogeneous array of) strings, booleans, integers, floats. |
| 620 | + :param timestamp: Deprecated. |
| 621 | + :param escaped: Deprecated. |
619 | 622 | """ |
620 | | - if timestamp is None: |
621 | | - timestamp = time_ns() |
622 | | - |
623 | | - exc_type, exc_val, exc_tb = type(exception), exception, exception.__traceback__ |
624 | | - |
625 | 623 | if escaped: |
626 | | - self.set_exc_info(exc_type, exc_val, exc_tb) |
| 624 | + deprecate( |
| 625 | + prefix="The escaped argument is deprecated for record_exception", |
| 626 | + message="""If an exception exits the scope of the span, it will automatically be |
| 627 | + reported in the span tags.""", |
| 628 | + category=DDTraceDeprecationWarning, |
| 629 | + removal_version="4.0.0", |
| 630 | + ) |
| 631 | + if timestamp is not None: |
| 632 | + deprecate( |
| 633 | + prefix="The timestamp argument is deprecated for record_exception", |
| 634 | + message="""The timestamp of the span event should correspond to the time when the |
| 635 | + error is recorded which is set automatically.""", |
| 636 | + category=DDTraceDeprecationWarning, |
| 637 | + removal_version="4.0.0", |
| 638 | + ) |
627 | 639 |
|
628 | | - tb = self._get_traceback(exc_type, exc_val, exc_tb) |
| 640 | + tb = self._get_traceback(type(exception), exception, exception.__traceback__) |
629 | 641 |
|
630 | | - # Set exception attributes in a manner that is consistent with the opentelemetry sdk |
631 | | - # https://github.com/open-telemetry/opentelemetry-python/blob/v1.24.0/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py#L998 |
632 | | - attrs = { |
| 642 | + attrs: Dict[str, _AttributeValueType] = { |
633 | 643 | "exception.type": "%s.%s" % (exception.__class__.__module__, exception.__class__.__name__), |
634 | 644 | "exception.message": str(exception), |
635 | | - "exception.escaped": escaped, |
636 | 645 | "exception.stacktrace": tb, |
637 | 646 | } |
638 | 647 | if attributes: |
| 648 | + attributes = {k: v for k, v in attributes.items() if self._validate_attribute(k, v)} |
| 649 | + |
639 | 650 | # User provided attributes must take precedence over attrs |
640 | 651 | attrs.update(attributes) |
641 | 652 |
|
642 | | - self._add_event(name="exception", attributes=attrs, timestamp=timestamp) |
| 653 | + self._add_event(name="exception", attributes=attrs, timestamp=time_ns()) |
| 654 | + |
| 655 | + def _validate_attribute(self, key: str, value: object) -> bool: |
| 656 | + if isinstance(value, (str, bool, int, float)): |
| 657 | + return self._validate_scalar(key, value) |
| 658 | + |
| 659 | + if not isinstance(value, list): |
| 660 | + log.warning("record_exception: Attribute %s must be a string, number, or boolean: %s.", key, value) |
| 661 | + return False |
| 662 | + |
| 663 | + if len(value) == 0: |
| 664 | + return True |
| 665 | + |
| 666 | + if not isinstance(value[0], (str, bool, int, float)): |
| 667 | + log.warning("record_exception: List values %s must be string, number, or boolean: %s.", key, value) |
| 668 | + return False |
| 669 | + |
| 670 | + first_type = type(value[0]) |
| 671 | + for val in value: |
| 672 | + if not isinstance(val, first_type) or not self._validate_scalar(key, val): |
| 673 | + log.warning("record_exception: Attribute %s array must be homogenous: %s.", key, value) |
| 674 | + return False |
| 675 | + return True |
| 676 | + |
| 677 | + def _validate_scalar(self, key: str, value: Union[bool, str, int, float]) -> bool: |
| 678 | + if isinstance(value, (bool, str)): |
| 679 | + return True |
| 680 | + |
| 681 | + if isinstance(value, int): |
| 682 | + if value < _MIN_INT_64BITS or value > _MAX_INT_64BITS: |
| 683 | + log.warning( |
| 684 | + "record_exception: Attribute %s must be within the range of a signed 64-bit integer: %s.", |
| 685 | + key, |
| 686 | + value, |
| 687 | + ) |
| 688 | + return False |
| 689 | + return True |
| 690 | + |
| 691 | + if isinstance(value, float): |
| 692 | + if not math.isfinite(value): |
| 693 | + log.warning("record_exception: Attribute %s must be a finite number: %s.", key, value) |
| 694 | + return False |
| 695 | + return True |
| 696 | + |
| 697 | + return False |
643 | 698 |
|
644 | 699 | def _pprint(self) -> str: |
645 | 700 | """Return a human readable version of the span.""" |
|
0 commit comments