1import abc
2import logging
3import re
4import types as python_types
5import typing
6import warnings
7
8from opentelemetry.trace.status import Status, StatusCode
9from opentelemetry.util import types
10
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 to
15# prefix the vendor name. Vendors SHOULD set the tenant ID
16# 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-z
21
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 printable
29# ASCII [RFC0020] characters (i.e., the range 0x20 to 0x7E)
30# except comma (,) and (=).
31# value = 0*255(chr) nblk-chr
32# nblk-chr = %x21-2B / %x2D-3C / %x3E-7E
33# chr = %x20 / nblk-chr
34
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 = 32
42_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 None
51 and isinstance(value, str)
52 and _VALUE_PATTERN.fullmatch(value) is not None
53 )
54
55
56class Span(abc.ABC):
57 """A span represents a single operation within a trace."""
58
59 @abc.abstractmethod
60 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, and
66 implementations are free to ignore or raise on further calls.
67 """
68
69 @abc.abstractmethod
70 def get_span_context(self) -> "SpanContext":
71 """Gets the span's SpanContext.
72
73 Get an immutable, serializable identifier for this span that can be
74 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.abstractmethod
81 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 hence
89 strongly discouraged. It is also preferred to set attributes at span
90 creation, instead of calling this method later since samplers can only
91 consider information already present during span creation.
92 """
93
94 @abc.abstractmethod
95 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 hence
101 strongly discouraged. It is also preferred to set attributes at span
102 creation, instead of calling this method later since samplers can only
103 consider information already present during span creation.
104 """
105
106 @abc.abstractmethod
107 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 and
116 attributes passed as arguments. Implementations should generate a
117 timestamp if the `timestamp` argument is omitted.
118 """
119
120 def add_link( # pylint: disable=no-self-use
121 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 ignore
129 calls with an invalid span context if both attributes and TraceState
130 are empty.
131
132 Note: It is preferred to add links at span creation, instead of calling
133 this method later since samplers can only consider information already
134 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.abstractmethod
142 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 depend
148 on the implementation.
149 """
150
151 @abc.abstractmethod
152 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 like
156 events with the add_event operation and attributes using set_attribute.
157 """
158
159 @abc.abstractmethod
160 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 default
166 Span status.
167 """
168
169 @abc.abstractmethod
170 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 self
185
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, this
201 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-flags
207 """
208
209 DEFAULT = 0x00
210 SAMPLED = 0x01
211
212 @classmethod
213 def get_default(cls) -> "TraceFlags":
214 return cls(cls.DEFAULT)
215
216 @property
217 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-field
233 """
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 return
244 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 return
250
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 continue
256 self._dict[key] = value
257 else:
258 _logger.warning(
259 "Invalid key/value pair (%s, %s) found.", key, value
260 )
261
262 def __contains__(self, item: object) -> bool:
263 return item in self._dict
264
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 should
283 adhere to w3c tracestate identifiers format.
284
285 Args:
286 key: A valid tracestate key to add
287 value: A valid tracestate value to add
288
289 Returns:
290 A new TraceState with the modifications applied.
291
292 If the provided key-value pair is invalid or results in tracestate
293 that violates tracecontext specification, they are discarded and
294 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, value
299 )
300 return self
301 # There can be a maximum of 32 pairs
302 if len(self) >= _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
303 _logger.warning("There can't be more 32 key/value pairs.")
304 return self
305 # Duplicate entries are not allowed
306 if key in self._dict:
307 _logger.warning("The provided key %s already exists.", key)
308 return self
309 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 should
314 adhere to w3c tracestate identifiers format.
315
316 Args:
317 key: A valid tracestate key to update
318 value: A valid tracestate value to update for key
319
320 Returns:
321 A new TraceState with the modifications applied.
322
323 If the provided key-value pair is invalid or results in tracestate
324 that violates tracecontext specification, they are discarded and
325 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, value
330 )
331 return self
332 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 tracestate
342
343 Returns:
344 A new TraceState with the modifications applied.
345
346 If the provided key-value pair is invalid or results in tracestate
347 that violates tracecontext specification, they are discarded and
348 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 self
353 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 tracestate
363 header format.
364 """
365 return ",".join(key + "=" + value for key, value in self._dict.items())
366
367 @classmethod
368 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 from
376 the tracestate header.
377
378 If the format of one headers is illegal, all values will
379 be discarded and an empty tracestate will be returned.
380
381 If the number of keys is beyond the maximum, all values
382 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 continue
391 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 = groups
400 # duplicate keys are not legal in header
401 if key in pairs:
402 return cls()
403 pairs[key] = value
404 return cls(list(pairs.items()))
405
406 @classmethod
407 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 - 1
422_SPAN_ID_MAX_VALUE = 2**64 - 1
423
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 must
431 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_OPTIONS
451 if trace_state is None:
452 trace_state = DEFAULT_TRACE_STATE
453
454 is_valid = (
455 INVALID_TRACE_ID < trace_id <= _TRACE_ID_MAX_VALUE
456 and INVALID_SPAN_ID < span_id <= _SPAN_ID_MAX_VALUE
457 )
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 @property
476 def trace_id(self) -> int:
477 return self[0] # pylint: disable=unsubscriptable-object
478
479 @property
480 def span_id(self) -> int:
481 return self[1] # pylint: disable=unsubscriptable-object
482
483 @property
484 def is_remote(self) -> bool:
485 return self[2] # pylint: disable=unsubscriptable-object
486
487 @property
488 def trace_flags(self) -> "TraceFlags":
489 return self[3] # pylint: disable=unsubscriptable-object
490
491 @property
492 def trace_state(self) -> "TraceState":
493 return self[4] # pylint: disable=unsubscriptable-object
494
495 @property
496 def is_valid(self) -> bool:
497 return self[5] # pylint: disable=unsubscriptable-object
498
499 def __setattr__(self, *args: str) -> None:
500 _logger.debug(
501 "Immutable type, ignoring call to set attribute", stack_info=True
502 )
503
504 def __delattr__(self, *args: str) -> None:
505 _logger.debug(
506 "Immutable type, ignoring call to set attribute", stack_info=True
507 )
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 = context
521
522 def get_span_context(self) -> "SpanContext":
523 return self._context
524
525 def is_recording(self) -> bool:
526 return False
527
528 def end(self, end_time: typing.Optional[int] = None) -> None:
529 pass
530
531 def set_attributes(
532 self, attributes: typing.Mapping[str, types.AttributeValue]
533 ) -> None:
534 pass
535
536 def set_attribute(self, key: str, value: types.AttributeValue) -> None:
537 pass
538
539 def add_event(
540 self,
541 name: str,
542 attributes: types.Attributes = None,
543 timestamp: typing.Optional[int] = None,
544 ) -> None:
545 pass
546
547 def add_link(
548 self,
549 context: "SpanContext",
550 attributes: types.Attributes = None,
551 ) -> None:
552 pass
553
554 def update_name(self, name: str) -> None:
555 pass
556
557 def set_status(
558 self,
559 status: typing.Union[Status, StatusCode],
560 description: typing.Optional[str] = None,
561 ) -> None:
562 pass
563
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 pass
572
573 def __repr__(self) -> str:
574 return f"NonRecordingSpan({self._context!r})"
575
576
577INVALID_SPAN_ID = 0x0000000000000000
578INVALID_TRACE_ID = 0x00000000000000000000000000000000
579INVALID_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 method
591 Args:
592 trace_id: Trace ID int
593
594 Returns:
595 The trace ID (16 bytes) cast to a 32-character hexadecimal string
596 """
597 return format(trace_id, "032x")
598
599
600def format_span_id(span_id: int) -> str:
601 """Convenience span ID formatting method
602 Args:
603 span_id: Span ID int
604
605 Returns:
606 The span ID (8 bytes) cast to a 16-character hexadecimal string
607 """
608 return format(span_id, "016x")