add
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
# Copyright The OpenTelemetry Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from collections.abc import Mapping
|
||||
from logging import getLogger
|
||||
from re import compile
|
||||
from types import MappingProxyType
|
||||
|
||||
from opentelemetry.context import create_key, get_value, set_value
|
||||
from opentelemetry.context.context import Context
|
||||
from opentelemetry.util.re import (
|
||||
_BAGGAGE_PROPERTY_FORMAT,
|
||||
_KEY_FORMAT,
|
||||
_VALUE_FORMAT,
|
||||
)
|
||||
|
||||
_BAGGAGE_KEY = create_key("baggage")
|
||||
_logger = getLogger(__name__)
|
||||
|
||||
_KEY_PATTERN = compile(_KEY_FORMAT)
|
||||
_VALUE_PATTERN = compile(_VALUE_FORMAT)
|
||||
_PROPERT_PATTERN = compile(_BAGGAGE_PROPERTY_FORMAT)
|
||||
|
||||
|
||||
def get_all(
|
||||
context: Context | None = None,
|
||||
) -> Mapping[str, object]:
|
||||
"""Returns the name/value pairs in the Baggage
|
||||
|
||||
Args:
|
||||
context: The Context to use. If not set, uses current Context
|
||||
|
||||
Returns:
|
||||
The name/value pairs in the Baggage
|
||||
"""
|
||||
return MappingProxyType(_get_baggage_value(context=context))
|
||||
|
||||
|
||||
def get_baggage(name: str, context: Context | None = None) -> object | None:
|
||||
"""Provides access to the value for a name/value pair in the
|
||||
Baggage
|
||||
|
||||
Args:
|
||||
name: The name of the value to retrieve
|
||||
context: The Context to use. If not set, uses current Context
|
||||
|
||||
Returns:
|
||||
The value associated with the given name, or null if the given name is
|
||||
not present.
|
||||
"""
|
||||
return _get_baggage_value(context=context).get(name)
|
||||
|
||||
|
||||
def set_baggage(name: str, value: object, context: Context | None = None) -> Context:
|
||||
"""Sets a value in the Baggage
|
||||
|
||||
Args:
|
||||
name: The name of the value to set
|
||||
value: The value to set
|
||||
context: The Context to use. If not set, uses current Context
|
||||
|
||||
Returns:
|
||||
A Context with the value updated
|
||||
"""
|
||||
baggage = _get_baggage_value(context=context).copy()
|
||||
baggage[name] = value
|
||||
return set_value(_BAGGAGE_KEY, baggage, context=context)
|
||||
|
||||
|
||||
def remove_baggage(name: str, context: Context | None = None) -> Context:
|
||||
"""Removes a value from the Baggage
|
||||
|
||||
Args:
|
||||
name: The name of the value to remove
|
||||
context: The Context to use. If not set, uses current Context
|
||||
|
||||
Returns:
|
||||
A Context with the name/value removed
|
||||
"""
|
||||
baggage = _get_baggage_value(context=context).copy()
|
||||
baggage.pop(name, None)
|
||||
|
||||
return set_value(_BAGGAGE_KEY, baggage, context=context)
|
||||
|
||||
|
||||
def clear(context: Context | None = None) -> Context:
|
||||
"""Removes all values from the Baggage
|
||||
|
||||
Args:
|
||||
context: The Context to use. If not set, uses current Context
|
||||
|
||||
Returns:
|
||||
A Context with all baggage entries removed
|
||||
"""
|
||||
return set_value(_BAGGAGE_KEY, {}, context=context)
|
||||
|
||||
|
||||
def _get_baggage_value(context: Context | None = None) -> dict[str, object]:
|
||||
baggage = get_value(_BAGGAGE_KEY, context=context)
|
||||
if isinstance(baggage, dict):
|
||||
return baggage
|
||||
return {}
|
||||
|
||||
|
||||
def _is_valid_key(name: str) -> bool:
|
||||
return _KEY_PATTERN.fullmatch(str(name)) is not None
|
||||
|
||||
|
||||
def _is_valid_value(value: object) -> bool:
|
||||
parts = str(value).split(";")
|
||||
# W3C Baggage allows optional whitespace between the value and ";"
|
||||
is_valid_value = _VALUE_PATTERN.fullmatch(parts[0].rstrip(" \t")) is not None
|
||||
if len(parts) > 1: # one or more properties metadata
|
||||
for property in parts[1:]:
|
||||
if _PROPERT_PATTERN.fullmatch(property) is None:
|
||||
is_valid_value = False
|
||||
break
|
||||
return is_valid_value
|
||||
|
||||
|
||||
def _is_valid_pair(key: str, value: str) -> bool:
|
||||
return _is_valid_key(key) and _is_valid_value(value)
|
||||
BIN
Binary file not shown.
+179
@@ -0,0 +1,179 @@
|
||||
# Copyright The OpenTelemetry Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
from collections.abc import Iterable, Iterator, Mapping
|
||||
from logging import getLogger
|
||||
from re import split
|
||||
from urllib.parse import quote_plus, unquote_plus
|
||||
|
||||
from opentelemetry.baggage import _is_valid_pair, get_all, set_baggage
|
||||
from opentelemetry.context import get_current
|
||||
from opentelemetry.context.context import Context
|
||||
from opentelemetry.propagators import textmap
|
||||
from opentelemetry.util.re import _DELIMITER_PATTERN
|
||||
|
||||
_logger = getLogger(__name__)
|
||||
|
||||
|
||||
def _filter_valid_entries(
|
||||
entries: Iterable[str],
|
||||
max_pair_length: int,
|
||||
) -> Iterator[str]:
|
||||
for entry in entries:
|
||||
if not entry:
|
||||
continue
|
||||
if not entry.isascii():
|
||||
_logger.warning(
|
||||
"Baggage entry with key `%s` contains non-ASCII characters",
|
||||
entry.split("=", 1)[0],
|
||||
)
|
||||
continue
|
||||
if len(entry) > max_pair_length:
|
||||
_logger.warning(
|
||||
"Baggage entry with key `%s` exceeded the maximum number of bytes per list-member with length %d",
|
||||
entry.split("=", 1)[0],
|
||||
len(entry),
|
||||
)
|
||||
continue
|
||||
yield entry
|
||||
|
||||
|
||||
def _apply_baggage_limits(
|
||||
entries: Iterable[str],
|
||||
max_pairs: int,
|
||||
max_pair_length: int,
|
||||
max_header_length: int,
|
||||
) -> Iterator[str]:
|
||||
"""Apply W3C Baggage size limits to a sequence of baggage entries.
|
||||
|
||||
Yields entries that fit within the W3C specification limits.
|
||||
Logs warnings when entries are dropped.
|
||||
"""
|
||||
length = 0
|
||||
for index, entry in enumerate(_filter_valid_entries(entries, max_pair_length)):
|
||||
if index >= max_pairs:
|
||||
_logger.warning("Baggage exceeded the maximum number of list-members")
|
||||
return
|
||||
|
||||
length += (1 if index > 0 else 0) + len(entry)
|
||||
if length > max_header_length:
|
||||
_logger.warning("Baggage exceeded the maximum number of bytes per baggage-string")
|
||||
return
|
||||
yield entry
|
||||
|
||||
|
||||
class W3CBaggagePropagator(textmap.TextMapPropagator):
|
||||
"""Extracts and injects Baggage which is used to annotate telemetry."""
|
||||
|
||||
_MAX_HEADER_LENGTH = 8192
|
||||
_MAX_PAIR_LENGTH = 4096
|
||||
_MAX_PAIRS = 180
|
||||
_BAGGAGE_HEADER_NAME = "baggage"
|
||||
|
||||
def extract(
|
||||
self,
|
||||
carrier: textmap.CarrierT,
|
||||
context: Context | None = None,
|
||||
getter: textmap.Getter[textmap.CarrierT] = textmap.default_getter,
|
||||
) -> Context:
|
||||
"""Extract Baggage from the carrier.
|
||||
|
||||
See
|
||||
`opentelemetry.propagators.textmap.TextMapPropagator.extract`
|
||||
"""
|
||||
|
||||
if context is None:
|
||||
context = get_current()
|
||||
|
||||
header = _extract_first_element(getter.get(carrier, self._BAGGAGE_HEADER_NAME))
|
||||
|
||||
if not header:
|
||||
return context
|
||||
|
||||
if len(header.encode()) > self._MAX_HEADER_LENGTH:
|
||||
_logger.warning(
|
||||
"Baggage header `%s` exceeded the maximum number of bytes per baggage-string",
|
||||
header,
|
||||
)
|
||||
return context
|
||||
|
||||
baggage_entries = split(_DELIMITER_PATTERN, header)
|
||||
|
||||
for entry in _apply_baggage_limits(
|
||||
baggage_entries,
|
||||
max_pairs=self._MAX_PAIRS,
|
||||
max_pair_length=self._MAX_PAIR_LENGTH,
|
||||
max_header_length=self._MAX_HEADER_LENGTH,
|
||||
):
|
||||
try:
|
||||
name, value = entry.split("=", 1)
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
_logger.warning("Baggage list-member `%s` doesn't match the format", entry)
|
||||
continue
|
||||
|
||||
# W3C Baggage allows optional whitespace around "="
|
||||
name = name.strip(" \t")
|
||||
value = value.strip(" \t")
|
||||
|
||||
if not _is_valid_pair(name, value):
|
||||
_logger.warning("Invalid baggage entry: `%s`", entry)
|
||||
continue
|
||||
|
||||
name = unquote_plus(name).strip()
|
||||
value = unquote_plus(value).strip()
|
||||
|
||||
context = set_baggage(
|
||||
name,
|
||||
value,
|
||||
context=context,
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
def inject(
|
||||
self,
|
||||
carrier: textmap.CarrierT,
|
||||
context: Context | None = None,
|
||||
setter: textmap.Setter[textmap.CarrierT] = textmap.default_setter,
|
||||
) -> None:
|
||||
"""Injects Baggage into the carrier.
|
||||
|
||||
See
|
||||
`opentelemetry.propagators.textmap.TextMapPropagator.inject`
|
||||
"""
|
||||
baggage_entries = get_all(context=context)
|
||||
if not baggage_entries:
|
||||
return
|
||||
|
||||
baggage_string = ",".join(
|
||||
_apply_baggage_limits(
|
||||
_encode_baggage_pairs(baggage_entries),
|
||||
max_pairs=self._MAX_PAIRS,
|
||||
max_pair_length=self._MAX_PAIR_LENGTH,
|
||||
max_header_length=self._MAX_HEADER_LENGTH,
|
||||
)
|
||||
)
|
||||
|
||||
if baggage_string:
|
||||
setter.set(carrier, self._BAGGAGE_HEADER_NAME, baggage_string)
|
||||
|
||||
@property
|
||||
def fields(self) -> set[str]:
|
||||
"""Returns a set with the fields set in `inject`."""
|
||||
return {self._BAGGAGE_HEADER_NAME}
|
||||
|
||||
|
||||
def _encode_baggage_pairs(
|
||||
baggage_entries: Mapping[str, object],
|
||||
) -> Iterator[str]:
|
||||
"""Yield URL-encoded 'key=value' pairs from baggage entries."""
|
||||
for key, value in baggage_entries.items():
|
||||
yield quote_plus(str(key)) + "=" + quote_plus(str(value))
|
||||
|
||||
|
||||
def _extract_first_element(
|
||||
items: Iterable[textmap.CarrierT] | None,
|
||||
) -> textmap.CarrierT | None:
|
||||
if items is None:
|
||||
return None
|
||||
return next(iter(items), None)
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user