-
Notifications
You must be signed in to change notification settings - Fork 820
Fix TraceState to adhere to specs #1502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4344ce2
ee01f21
a1ea2f9
640809c
a4660b9
39a4e4e
bbba511
c110522
e280fd4
a9eb66f
758bcb5
0e57e9e
af0c284
1687914
f255c5e
bcd433a
476e788
9e9a563
9a23c74
98a8e3d
883e80e
ae755d8
e2102d5
8cedb93
c89b7b6
c125fee
f5f7870
8ebe62c
df39956
e9deabe
344b6d6
dea21fd
2fb93c9
f85aa23
6234ae1
9a6ca2a
80a981e
26aa976
e72294a
233e15f
af7de36
2ff5aae
246f4a7
3f88937
4013599
09cd2c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,18 @@ | ||
| import abc | ||
| import logging | ||
| import re | ||
| import types as python_types | ||
| import typing | ||
| from collections import OrderedDict | ||
|
|
||
| from opentelemetry.trace.status import Status | ||
| from opentelemetry.util import types | ||
| from opentelemetry.util.tracestate import ( | ||
| _DELIMITER_PATTERN, | ||
| _MEMBER_PATTERN, | ||
| _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS, | ||
| _is_valid_pair, | ||
|
Comment on lines
+11
to
+14
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These imports shouldn't be named with underscore prefixes right?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think these are for internal use and should have leading underscore. Ideally users should only rely on |
||
| ) | ||
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -135,7 +143,7 @@ def sampled(self) -> bool: | |
| DEFAULT_TRACE_OPTIONS = TraceFlags.get_default() | ||
|
|
||
|
|
||
| class TraceState(typing.Dict[str, str]): | ||
| class TraceState(typing.Mapping[str, str]): | ||
| """A list of key-value pairs representing vendor-specific trace info. | ||
|
|
||
| Keys and values are strings of up to 256 printable US-ASCII characters. | ||
|
|
@@ -146,10 +154,186 @@ class TraceState(typing.Dict[str, str]): | |
| https://www.w3.org/TR/trace-context/#tracestate-field | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| entries: typing.Optional[ | ||
| typing.Sequence[typing.Tuple[str, str]] | ||
| ] = None, | ||
| ) -> None: | ||
| self._dict = OrderedDict() # type: OrderedDict[str, str] | ||
| if entries is None: | ||
| return | ||
| if len(entries) > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS: | ||
| _logger.warning( | ||
| "There can't be more than %s key/value pairs.", | ||
| _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS, | ||
| ) | ||
| return | ||
|
|
||
| for key, value in entries: | ||
| if _is_valid_pair(key, value): | ||
| if key in self._dict: | ||
| _logger.warning("Duplicate key: %s found.", key) | ||
| continue | ||
| self._dict[key] = value | ||
srikanthccv marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| else: | ||
| _logger.warning( | ||
| "Invalid key/value pair (%s, %s) found.", key, value | ||
| ) | ||
|
|
||
| def __getitem__(self, key: str) -> typing.Optional[str]: # type: ignore | ||
| return self._dict.get(key) | ||
|
|
||
| def __iter__(self) -> typing.Iterator[str]: | ||
| return iter(self._dict) | ||
|
|
||
| def __len__(self) -> int: | ||
| return len(self._dict) | ||
|
|
||
| def __repr__(self) -> str: | ||
| pairs = [ | ||
| "{key=%s, value=%s}" % (key, value) | ||
| for key, value in self._dict.items() | ||
| ] | ||
| return str(pairs) | ||
|
|
||
| def add(self, key: str, value: str) -> "TraceState": | ||
| """Adds a key-value pair to tracestate. The provided pair should | ||
| adhere to w3c tracestate identifiers format. | ||
|
|
||
| Args: | ||
| key: A valid tracestate key to add | ||
| value: A valid tracestate value to add | ||
|
|
||
| Returns: | ||
| A new TraceState with the modifications applied. | ||
|
|
||
| If the provided key-value pair is invalid or results in tracestate | ||
| that violates tracecontext specification, they are discarded and | ||
| same tracestate will be returned. | ||
| """ | ||
| if not _is_valid_pair(key, value): | ||
| _logger.warning( | ||
| "Invalid key/value pair (%s, %s) found.", key, value | ||
| ) | ||
| return self | ||
| # There can be a maximum of 32 pairs | ||
| if len(self) >= _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS: | ||
| _logger.warning("There can't be more 32 key/value pairs.") | ||
| return self | ||
| # Duplicate entries are not allowed | ||
| if key in self._dict: | ||
| _logger.warning("The provided key %s already exists.", key) | ||
| return self | ||
| new_state = [(key, value)] + list(self._dict.items()) | ||
| return TraceState(new_state) | ||
|
|
||
| def update(self, key: str, value: str) -> "TraceState": | ||
| """Updates a key-value pair in tracestate. The provided pair should | ||
| adhere to w3c tracestate identifiers format. | ||
|
|
||
| Args: | ||
| key: A valid tracestate key to update | ||
| value: A valid tracestate value to update for key | ||
|
|
||
| Returns: | ||
| A new TraceState with the modifications applied. | ||
|
|
||
| If the provided key-value pair is invalid or results in tracestate | ||
| that violates tracecontext specification, they are discarded and | ||
| same tracestate will be returned. | ||
| """ | ||
| if not _is_valid_pair(key, value): | ||
| _logger.warning( | ||
| "Invalid key/value pair (%s, %s) found.", key, value | ||
| ) | ||
| return self | ||
| prev_state = self._dict.copy() | ||
| prev_state[key] = value | ||
| prev_state.move_to_end(key, last=False) | ||
| new_state = list(prev_state.items()) | ||
| return TraceState(new_state) | ||
|
|
||
| def delete(self, key: str) -> "TraceState": | ||
| """Deletes a key-value from tracestate. | ||
|
|
||
| Args: | ||
| key: A valid tracestate key to remove key-value pair from tracestate | ||
|
|
||
| Returns: | ||
| A new TraceState with the modifications applied. | ||
|
|
||
| If the provided key-value pair is invalid or results in tracestate | ||
| that violates tracecontext specification, they are discarded and | ||
| same tracestate will be returned. | ||
| """ | ||
| if key not in self._dict: | ||
| _logger.warning("The provided key %s doesn't exist.", key) | ||
| return self | ||
| prev_state = self._dict.copy() | ||
| prev_state.pop(key) | ||
| new_state = list(prev_state.items()) | ||
| return TraceState(new_state) | ||
|
|
||
| def to_header(self) -> str: | ||
| """Creates a w3c tracestate header from a TraceState. | ||
|
|
||
| Returns: | ||
| A string that adheres to the w3c tracestate | ||
| header format. | ||
| """ | ||
| return ",".join(key + "=" + value for key, value in self._dict.items()) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Im not super familiar with the tracestate spec; do we need some escaping in the values here?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From W3C Trace Context spec
If I understand it correctly we don't need to do any escaping as this header will be ASCII only which is totally fine with HTTP Headers. |
||
|
|
||
| @classmethod | ||
| def from_header(cls, header_list: typing.List[str]) -> "TraceState": | ||
| """Parses one or more w3c tracestate header into a TraceState. | ||
|
|
||
| Args: | ||
| header_list: one or more w3c tracestate headers. | ||
|
|
||
| Returns: | ||
| A valid TraceState that contains values extracted from | ||
| the tracestate header. | ||
|
|
||
| If the format of one headers is illegal, all values will | ||
| be discarded and an empty tracestate will be returned. | ||
|
|
||
| If the number of keys is beyond the maximum, all values | ||
| will be discarded and an empty tracestate will be returned. | ||
| """ | ||
| pairs = OrderedDict() | ||
| for header in header_list: | ||
| for member in re.split(_DELIMITER_PATTERN, header): | ||
| # empty members are valid, but no need to process further. | ||
| if not member: | ||
| continue | ||
| match = _MEMBER_PATTERN.fullmatch(member) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have util like
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All the mutation operations on trace state should validate the key/value. The member would be in format of |
||
| if not match: | ||
| _logger.warning( | ||
| "Member doesn't match the w3c identifiers format %s", | ||
| member, | ||
| ) | ||
| return cls() | ||
| key, _eq, value = match.groups() | ||
| # duplicate keys are not legal in header | ||
| if key in pairs: | ||
| return cls() | ||
| pairs[key] = value | ||
| return cls(list(pairs.items())) | ||
|
|
||
| @classmethod | ||
| def get_default(cls) -> "TraceState": | ||
| return cls() | ||
|
|
||
| def keys(self) -> typing.KeysView[str]: | ||
| return self._dict.keys() | ||
|
|
||
| def items(self) -> typing.ItemsView[str, str]: | ||
| return self._dict.items() | ||
|
|
||
| def values(self) -> typing.ValuesView[str]: | ||
| return self._dict.values() | ||
|
|
||
|
|
||
| DEFAULT_TRACE_STATE = TraceState.get_default() | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.