codekingpro/portable-devtools
114k
1import abc2import logging3import re4import types as python_types5import typing6import warnings7 8from opentelemetry.trace.status import Status, StatusCode9from opentelemetry.util import types10 11# The key MUST begin with a lowercase letter or a digit,12# and can only contain lowercase letters (a-z), digits (0-9),13# underscores (_), dashes (-), asterisks (*), and forward slashes (/).14# For multi-tenant vendor scenarios, an at sign (@) can be used to15# prefix the vendor name. Vendors SHOULD set the tenant ID16# at the beginning of the key.17 18# key = ( lcalpha ) 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )19# key = ( lcalpha / DIGIT ) 0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) "@" lcalpha 0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )20# lcalpha = %x61-7A ; a-z21 22_KEY_FORMAT = (23 r"[a-z][_0-9a-z\-\*\/]{0,255}|"24 r"[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}"25)26_KEY_PATTERN = re.compile(_KEY_FORMAT)27 28# The value is an opaque string containing up to 256 printable29# ASCII [RFC0020] characters (i.e., the range 0x20 to 0x7E)30# except comma (,) and (=).31# value = 0*255(chr) nblk-chr32# nblk-chr = %x21-2B / %x2D-3C / %x3E-7E33# chr = %x20 / nblk-chr34 35_VALUE_FORMAT = (36 r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]"37)38_VALUE_PATTERN = re.compile(_VALUE_FORMAT)39 40 41_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 3242_delimiter_pattern = re.compile(r"[ \t]*,[ \t]*")43_member_pattern = re.compile(f"({_KEY_FORMAT})(=)({_VALUE_FORMAT})[ \t]*")44_logger = logging.getLogger(__name__)45 46 47def _is_valid_pair(key: str, value: str) -> bool:48 return (49 isinstance(key, str)50 and _KEY_PATTERN.fullmatch(key) is not None51 and isinstance(value, str)52 and _VALUE_PATTERN.fullmatch(value) is not None53 )54 55 56class Span(abc.ABC):57 """A span represents a single operation within a trace."""58 59 @abc.abstractmethod60 def end(self, end_time: typing.Optional[int] = None) -> None:61 """Sets the current time as the span's end time.62 63 The span's end time is the wall time at which the operation finished.64 65 Only the first call to `end` should modify the span, and66 implementations are free to ignore or raise on further calls.67 """68 69 @abc.abstractmethod70 def get_span_context(self) -> "SpanContext":71 """Gets the span's SpanContext.72 73 Get an immutable, serializable identifier for this span that can be74 used to create new child spans.75 76 Returns:77 A :class:`opentelemetry.trace.SpanContext` with a copy of this span's immutable state.78 """79 80 @abc.abstractmethod81 def set_attributes(82 self, attributes: typing.Mapping[str, types.AttributeValue]83 ) -> None:84 """Sets Attributes.85 86 Sets Attributes with the key and value passed as arguments dict.87 88 Note: The behavior of `None` value attributes is undefined, and hence89 strongly discouraged. It is also preferred to set attributes at span90 creation, instead of calling this method later since samplers can only91 consider information already present during span creation.92 """93 94 @abc.abstractmethod95 def set_attribute(self, key: str, value: types.AttributeValue) -> None:96 """Sets an Attribute.97 98 Sets a single Attribute with the key and value passed as arguments.99 100 Note: The behavior of `None` value attributes is undefined, and hence101 strongly discouraged. It is also preferred to set attributes at span102 creation, instead of calling this method later since samplers can only103 consider information already present during span creation.104 """105 106 @abc.abstractmethod107 def add_event(108 self,109 name: str,110 attributes: types.Attributes = None,111 timestamp: typing.Optional[int] = None,112 ) -> None:113 """Adds an `Event`.114 115 Adds a single `Event` with the name and, optionally, a timestamp and116 attributes passed as arguments. Implementations should generate a117 timestamp if the `timestamp` argument is omitted.118 """119 120 def add_link( # pylint: disable=no-self-use121 self,122 context: "SpanContext",123 attributes: types.Attributes = None,124 ) -> None:125 """Adds a `Link`.126 127 Adds a single `Link` with the `SpanContext` of the span to link to and,128 optionally, attributes passed as arguments. Implementations may ignore129 calls with an invalid span context if both attributes and TraceState130 are empty.131 132 Note: It is preferred to add links at span creation, instead of calling133 this method later since samplers can only consider information already134 present during span creation.135 """136 warnings.warn(137 "Span.add_link() not implemented and will be a no-op. "138 "Use opentelemetry-sdk >= 1.23 to add links after span creation"139 )140 141 @abc.abstractmethod142 def update_name(self, name: str) -> None:143 """Updates the `Span` name.144 145 This will override the name provided via :func:`opentelemetry.trace.Tracer.start_span`.146 147 Upon this update, any sampling behavior based on Span name will depend148 on the implementation.149 """150 151 @abc.abstractmethod152 def is_recording(self) -> bool:153 """Returns whether this span will be recorded.154 155 Returns true if this Span is active and recording information like156 events with the add_event operation and attributes using set_attribute.157 """158 159 @abc.abstractmethod160 def set_status(161 self,162 status: typing.Union[Status, StatusCode],163 description: typing.Optional[str] = None,164 ) -> None:165 """Sets the Status of the Span. If used, this will override the default166 Span status.167 """168 169 @abc.abstractmethod170 def record_exception(171 self,172 exception: BaseException,173 attributes: types.Attributes = None,174 timestamp: typing.Optional[int] = None,175 escaped: bool = False,176 ) -> None:177 """Records an exception as a span event."""178 179 def __enter__(self) -> "Span":180 """Invoked when `Span` is used as a context manager.181 182 Returns the `Span` itself.183 """184 return self185 186 def __exit__(187 self,188 exc_type: typing.Optional[typing.Type[BaseException]],189 exc_val: typing.Optional[BaseException],190 exc_tb: typing.Optional[python_types.TracebackType],191 ) -> None:192 """Ends context manager and calls `end` on the `Span`."""193 194 self.end()195 196 197class TraceFlags(int):198 """A bitmask that represents options specific to the trace.199 200 The only supported option is the "sampled" flag (``0x01``). If set, this201 flag indicates that the trace may have been sampled upstream.202 203 See the `W3C Trace Context - Traceparent`_ spec for details.204 205 .. _W3C Trace Context - Traceparent:206 https://www.w3.org/TR/trace-context/#trace-flags207 """208 209 DEFAULT = 0x00210 SAMPLED = 0x01211 212 @classmethod213 def get_default(cls) -> "TraceFlags":214 return cls(cls.DEFAULT)215 216 @property217 def sampled(self) -> bool:218 return bool(self & TraceFlags.SAMPLED)219 220 221DEFAULT_TRACE_OPTIONS = TraceFlags.get_default()222 223 224class TraceState(typing.Mapping[str, str]):225 """A list of key-value pairs representing vendor-specific trace info.226 227 Keys and values are strings of up to 256 printable US-ASCII characters.228 Implementations should conform to the `W3C Trace Context - Tracestate`_229 spec, which describes additional restrictions on valid field values.230 231 .. _W3C Trace Context - Tracestate:232 https://www.w3.org/TR/trace-context/#tracestate-field233 """234 235 def __init__(236 self,237 entries: typing.Optional[238 typing.Sequence[typing.Tuple[str, str]]239 ] = None,240 ) -> None:241 self._dict = {} # type: dict[str, str]242 if entries is None:243 return244 if len(entries) > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:245 _logger.warning(246 "There can't be more than %s key/value pairs.",247 _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS,248 )249 return250 251 for key, value in entries:252 if _is_valid_pair(key, value):253 if key in self._dict:254 _logger.warning("Duplicate key: %s found.", key)255 continue256 self._dict[key] = value257 else:258 _logger.warning(259 "Invalid key/value pair (%s, %s) found.", key, value260 )261 262 def __contains__(self, item: object) -> bool:263 return item in self._dict264 265 def __getitem__(self, key: str) -> str:266 return self._dict[key]267 268 def __iter__(self) -> typing.Iterator[str]:269 return iter(self._dict)270 271 def __len__(self) -> int:272 return len(self._dict)273 274 def __repr__(self) -> str:275 pairs = [276 f"{{key={key}, value={value}}}"277 for key, value in self._dict.items()278 ]279 return str(pairs)280 281 def add(self, key: str, value: str) -> "TraceState":282 """Adds a key-value pair to tracestate. The provided pair should283 adhere to w3c tracestate identifiers format.284 285 Args:286 key: A valid tracestate key to add287 value: A valid tracestate value to add288 289 Returns:290 A new TraceState with the modifications applied.291 292 If the provided key-value pair is invalid or results in tracestate293 that violates tracecontext specification, they are discarded and294 same tracestate will be returned.295 """296 if not _is_valid_pair(key, value):297 _logger.warning(298 "Invalid key/value pair (%s, %s) found.", key, value299 )300 return self301 # There can be a maximum of 32 pairs302 if len(self) >= _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:303 _logger.warning("There can't be more 32 key/value pairs.")304 return self305 # Duplicate entries are not allowed306 if key in self._dict:307 _logger.warning("The provided key %s already exists.", key)308 return self309 new_state = [(key, value)] + list(self._dict.items())310 return TraceState(new_state)311 312 def update(self, key: str, value: str) -> "TraceState":313 """Updates a key-value pair in tracestate. The provided pair should314 adhere to w3c tracestate identifiers format.315 316 Args:317 key: A valid tracestate key to update318 value: A valid tracestate value to update for key319 320 Returns:321 A new TraceState with the modifications applied.322 323 If the provided key-value pair is invalid or results in tracestate324 that violates tracecontext specification, they are discarded and325 same tracestate will be returned.326 """327 if not _is_valid_pair(key, value):328 _logger.warning(329 "Invalid key/value pair (%s, %s) found.", key, value330 )331 return self332 prev_state = self._dict.copy()333 prev_state.pop(key, None)334 new_state = [(key, value), *prev_state.items()]335 return TraceState(new_state)336 337 def delete(self, key: str) -> "TraceState":338 """Deletes a key-value from tracestate.339 340 Args:341 key: A valid tracestate key to remove key-value pair from tracestate342 343 Returns:344 A new TraceState with the modifications applied.345 346 If the provided key-value pair is invalid or results in tracestate347 that violates tracecontext specification, they are discarded and348 same tracestate will be returned.349 """350 if key not in self._dict:351 _logger.warning("The provided key %s doesn't exist.", key)352 return self353 prev_state = self._dict.copy()354 prev_state.pop(key)355 new_state = list(prev_state.items())356 return TraceState(new_state)357 358 def to_header(self) -> str:359 """Creates a w3c tracestate header from a TraceState.360 361 Returns:362 A string that adheres to the w3c tracestate363 header format.364 """365 return ",".join(key + "=" + value for key, value in self._dict.items())366 367 @classmethod368 def from_header(cls, header_list: typing.List[str]) -> "TraceState":369 """Parses one or more w3c tracestate header into a TraceState.370 371 Args:372 header_list: one or more w3c tracestate headers.373 374 Returns:375 A valid TraceState that contains values extracted from376 the tracestate header.377 378 If the format of one headers is illegal, all values will379 be discarded and an empty tracestate will be returned.380 381 If the number of keys is beyond the maximum, all values382 will be discarded and an empty tracestate will be returned.383 """384 pairs = {} # type: dict[str, str]385 for header in header_list:386 members: typing.List[str] = re.split(_delimiter_pattern, header)387 for member in members:388 # empty members are valid, but no need to process further.389 if not member:390 continue391 match = _member_pattern.fullmatch(member)392 if not match:393 _logger.warning(394 "Member doesn't match the w3c identifiers format %s",395 member,396 )397 return cls()398 groups: typing.Tuple[str, ...] = match.groups()399 key, _eq, value = groups400 # duplicate keys are not legal in header401 if key in pairs:402 return cls()403 pairs[key] = value404 return cls(list(pairs.items()))405 406 @classmethod407 def get_default(cls) -> "TraceState":408 return cls()409 410 def keys(self) -> typing.KeysView[str]:411 return self._dict.keys()412 413 def items(self) -> typing.ItemsView[str, str]:414 return self._dict.items()415 416 def values(self) -> typing.ValuesView[str]:417 return self._dict.values()418 419 420DEFAULT_TRACE_STATE = TraceState.get_default()421_TRACE_ID_MAX_VALUE = 2**128 - 1422_SPAN_ID_MAX_VALUE = 2**64 - 1423 424 425class SpanContext(426 typing.Tuple[int, int, bool, "TraceFlags", "TraceState", bool]427):428 """The state of a Span to propagate between processes.429 430 This class includes the immutable attributes of a :class:`.Span` that must431 be propagated to a span's children and across process boundaries.432 433 Args:434 trace_id: The ID of the trace that this span belongs to.435 span_id: This span's ID.436 is_remote: True if propagated from a remote parent.437 trace_flags: Trace options to propagate.438 trace_state: Tracing-system-specific info to propagate.439 """440 441 def __new__(442 cls,443 trace_id: int,444 span_id: int,445 is_remote: bool,446 trace_flags: typing.Optional["TraceFlags"] = DEFAULT_TRACE_OPTIONS,447 trace_state: typing.Optional["TraceState"] = DEFAULT_TRACE_STATE,448 ) -> "SpanContext":449 if trace_flags is None:450 trace_flags = DEFAULT_TRACE_OPTIONS451 if trace_state is None:452 trace_state = DEFAULT_TRACE_STATE453 454 is_valid = (455 INVALID_TRACE_ID < trace_id <= _TRACE_ID_MAX_VALUE456 and INVALID_SPAN_ID < span_id <= _SPAN_ID_MAX_VALUE457 )458 459 return tuple.__new__(460 cls,461 (trace_id, span_id, is_remote, trace_flags, trace_state, is_valid),462 )463 464 def __getnewargs__(465 self,466 ) -> typing.Tuple[int, int, bool, "TraceFlags", "TraceState"]:467 return (468 self.trace_id,469 self.span_id,470 self.is_remote,471 self.trace_flags,472 self.trace_state,473 )474 475 @property476 def trace_id(self) -> int:477 return self[0] # pylint: disable=unsubscriptable-object478 479 @property480 def span_id(self) -> int:481 return self[1] # pylint: disable=unsubscriptable-object482 483 @property484 def is_remote(self) -> bool:485 return self[2] # pylint: disable=unsubscriptable-object486 487 @property488 def trace_flags(self) -> "TraceFlags":489 return self[3] # pylint: disable=unsubscriptable-object490 491 @property492 def trace_state(self) -> "TraceState":493 return self[4] # pylint: disable=unsubscriptable-object494 495 @property496 def is_valid(self) -> bool:497 return self[5] # pylint: disable=unsubscriptable-object498 499 def __setattr__(self, *args: str) -> None:500 _logger.debug(501 "Immutable type, ignoring call to set attribute", stack_info=True502 )503 504 def __delattr__(self, *args: str) -> None:505 _logger.debug(506 "Immutable type, ignoring call to set attribute", stack_info=True507 )508 509 def __repr__(self) -> str:510 return f"{type(self).__name__}(trace_id=0x{format_trace_id(self.trace_id)}, span_id=0x{format_span_id(self.span_id)}, trace_flags=0x{self.trace_flags:02x}, trace_state={self.trace_state!r}, is_remote={self.is_remote})"511 512 513class NonRecordingSpan(Span):514 """The Span that is used when no Span implementation is available.515 516 All operations are no-op except context propagation.517 """518 519 def __init__(self, context: "SpanContext") -> None:520 self._context = context521 522 def get_span_context(self) -> "SpanContext":523 return self._context524 525 def is_recording(self) -> bool:526 return False527 528 def end(self, end_time: typing.Optional[int] = None) -> None:529 pass530 531 def set_attributes(532 self, attributes: typing.Mapping[str, types.AttributeValue]533 ) -> None:534 pass535 536 def set_attribute(self, key: str, value: types.AttributeValue) -> None:537 pass538 539 def add_event(540 self,541 name: str,542 attributes: types.Attributes = None,543 timestamp: typing.Optional[int] = None,544 ) -> None:545 pass546 547 def add_link(548 self,549 context: "SpanContext",550 attributes: types.Attributes = None,551 ) -> None:552 pass553 554 def update_name(self, name: str) -> None:555 pass556 557 def set_status(558 self,559 status: typing.Union[Status, StatusCode],560 description: typing.Optional[str] = None,561 ) -> None:562 pass563 564 def record_exception(565 self,566 exception: BaseException,567 attributes: types.Attributes = None,568 timestamp: typing.Optional[int] = None,569 escaped: bool = False,570 ) -> None:571 pass572 573 def __repr__(self) -> str:574 return f"NonRecordingSpan({self._context!r})"575 576 577INVALID_SPAN_ID = 0x0000000000000000578INVALID_TRACE_ID = 0x00000000000000000000000000000000579INVALID_SPAN_CONTEXT = SpanContext(580 trace_id=INVALID_TRACE_ID,581 span_id=INVALID_SPAN_ID,582 is_remote=False,583 trace_flags=DEFAULT_TRACE_OPTIONS,584 trace_state=DEFAULT_TRACE_STATE,585)586INVALID_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT)587 588 589def format_trace_id(trace_id: int) -> str:590 """Convenience trace ID formatting method591 Args:592 trace_id: Trace ID int593 594 Returns:595 The trace ID (16 bytes) cast to a 32-character hexadecimal string596 """597 return format(trace_id, "032x")598 599 600def format_span_id(span_id: int) -> str:601 """Convenience span ID formatting method602 Args:603 span_id: Span ID int604 605 Returns:606 The span ID (8 bytes) cast to a 16-character hexadecimal string607 """608 return format(span_id, "016x")609 