__init__.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. # Copyright The OpenTelemetry Authors
  2. # SPDX-License-Identifier: Apache-2.0
  3. #
  4. from collections.abc import Iterable, Iterator, Mapping
  5. from logging import getLogger
  6. from re import split
  7. from urllib.parse import quote_plus, unquote_plus
  8. from opentelemetry.baggage import _is_valid_pair, get_all, set_baggage
  9. from opentelemetry.context import get_current
  10. from opentelemetry.context.context import Context
  11. from opentelemetry.propagators import textmap
  12. from opentelemetry.util.re import _DELIMITER_PATTERN
  13. _logger = getLogger(__name__)
  14. def _filter_valid_entries(
  15. entries: Iterable[str],
  16. max_pair_length: int,
  17. ) -> Iterator[str]:
  18. for entry in entries:
  19. if not entry:
  20. continue
  21. if not entry.isascii():
  22. _logger.warning(
  23. "Baggage entry with key `%s` contains non-ASCII characters",
  24. entry.split("=", 1)[0],
  25. )
  26. continue
  27. if len(entry) > max_pair_length:
  28. _logger.warning(
  29. "Baggage entry with key `%s` exceeded the maximum number of bytes per list-member with length %d",
  30. entry.split("=", 1)[0],
  31. len(entry),
  32. )
  33. continue
  34. yield entry
  35. def _apply_baggage_limits(
  36. entries: Iterable[str],
  37. max_pairs: int,
  38. max_pair_length: int,
  39. max_header_length: int,
  40. ) -> Iterator[str]:
  41. """Apply W3C Baggage size limits to a sequence of baggage entries.
  42. Yields entries that fit within the W3C specification limits.
  43. Logs warnings when entries are dropped.
  44. """
  45. length = 0
  46. for index, entry in enumerate(
  47. _filter_valid_entries(entries, max_pair_length)
  48. ):
  49. if index >= max_pairs:
  50. _logger.warning(
  51. "Baggage exceeded the maximum number of list-members"
  52. )
  53. return
  54. length += (1 if index > 0 else 0) + len(entry)
  55. if length > max_header_length:
  56. _logger.warning(
  57. "Baggage exceeded the maximum number of bytes per baggage-string"
  58. )
  59. return
  60. yield entry
  61. class W3CBaggagePropagator(textmap.TextMapPropagator):
  62. """Extracts and injects Baggage which is used to annotate telemetry."""
  63. _MAX_HEADER_LENGTH = 8192
  64. _MAX_PAIR_LENGTH = 4096
  65. _MAX_PAIRS = 180
  66. _BAGGAGE_HEADER_NAME = "baggage"
  67. def extract(
  68. self,
  69. carrier: textmap.CarrierT,
  70. context: Context | None = None,
  71. getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter,
  72. ) -> Context:
  73. """Extract Baggage from the carrier.
  74. See
  75. `opentelemetry.propagators.textmap.TextMapPropagator.extract`
  76. """
  77. if context is None:
  78. context = get_current()
  79. header = _extract_first_element(
  80. getter.get(carrier, self._BAGGAGE_HEADER_NAME)
  81. )
  82. if not header:
  83. return context
  84. if len(header.encode()) > self._MAX_HEADER_LENGTH:
  85. _logger.warning(
  86. "Baggage header `%s` exceeded the maximum number of bytes per baggage-string",
  87. header,
  88. )
  89. return context
  90. baggage_entries = split(_DELIMITER_PATTERN, header)
  91. for entry in _apply_baggage_limits(
  92. baggage_entries,
  93. max_pairs=self._MAX_PAIRS,
  94. max_pair_length=self._MAX_PAIR_LENGTH,
  95. max_header_length=self._MAX_HEADER_LENGTH,
  96. ):
  97. try:
  98. name, value = entry.split("=", 1)
  99. except Exception: # pylint: disable=broad-exception-caught
  100. _logger.warning(
  101. "Baggage list-member `%s` doesn't match the format", entry
  102. )
  103. continue
  104. if not _is_valid_pair(name, value):
  105. _logger.warning("Invalid baggage entry: `%s`", entry)
  106. continue
  107. name = unquote_plus(name).strip()
  108. value = unquote_plus(value).strip()
  109. context = set_baggage(
  110. name,
  111. value,
  112. context=context,
  113. )
  114. return context
  115. def inject(
  116. self,
  117. carrier: textmap.CarrierT,
  118. context: Context | None = None,
  119. setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter,
  120. ) -> None:
  121. """Injects Baggage into the carrier.
  122. See
  123. `opentelemetry.propagators.textmap.TextMapPropagator.inject`
  124. """
  125. baggage_entries = get_all(context=context)
  126. if not baggage_entries:
  127. return
  128. baggage_string = ",".join(
  129. _apply_baggage_limits(
  130. _encode_baggage_pairs(baggage_entries),
  131. max_pairs=self._MAX_PAIRS,
  132. max_pair_length=self._MAX_PAIR_LENGTH,
  133. max_header_length=self._MAX_HEADER_LENGTH,
  134. )
  135. )
  136. if baggage_string:
  137. setter.set(carrier, self._BAGGAGE_HEADER_NAME, baggage_string)
  138. @property
  139. def fields(self) -> set[str]:
  140. """Returns a set with the fields set in `inject`."""
  141. return {self._BAGGAGE_HEADER_NAME}
  142. def _encode_baggage_pairs(
  143. baggage_entries: Mapping[str, object],
  144. ) -> Iterator[str]:
  145. """Yield URL-encoded 'key=value' pairs from baggage entries."""
  146. for key, value in baggage_entries.items():
  147. yield quote_plus(str(key)) + "=" + quote_plus(str(value))
  148. def _extract_first_element(
  149. items: Iterable[textmap.CarrierT] | None,
  150. ) -> textmap.CarrierT | None:
  151. if items is None:
  152. return None
  153. return next(iter(items), None)