1# Copyright The OpenTelemetry Authors
2# SPDX-License-Identifier: Apache-2.0
3
4import abc
5import logging
6import re
7import types as python_types
8import typing
9import warnings
10
11from opentelemetry.trace.status import Status, StatusCode
12from opentelemetry.util import types
13
14# The key MUST begin with a lowercase letter or a digit,
15# and can only contain lowercase letters (a-z), digits (0-9),
16# underscores (_), dashes (-), asterisks (*), and forward slashes (/).
17# For multi-tenant vendor scenarios, an at sign (@) can be used to
18# prefix the vendor name. Vendors SHOULD set the tenant ID
19# at the beginning of the key.
20
21# key = ( lcalpha ) 0*255( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )
22# key = ( lcalpha / DIGIT ) 0*240( lcalpha / DIGIT / "_" / "-"/ "*" / "/" ) "@" lcalpha 0*13( lcalpha / DIGIT / "_" / "-"/ "*" / "/" )
23# lcalpha = %x61-7A ; a-z
24
25_KEY_FORMAT = (
26 r"[a-z][_0-9a-z\-\*\/]{0,255}|"
27 r"[a-z0-9][_0-9a-z\-\*\/]{0,240}@[a-z][_0-9a-z\-\*\/]{0,13}"
28)
29_KEY_PATTERN = re.compile(_KEY_FORMAT)
30
31# The value is an opaque string containing up to 256 printable
32# ASCII [RFC0020] characters (i.e., the range 0x20 to 0x7E)
33# except comma (,) and (=).
34# value = 0*255(chr) nblk-chr
35# nblk-chr = %x21-2B / %x2D-3C / %x3E-7E
36# chr = %x20 / nblk-chr
37
38_VALUE_FORMAT = (
39 r"[\x20-\x2b\x2d-\x3c\x3e-\x7e]{0,255}[\x21-\x2b\x2d-\x3c\x3e-\x7e]"
40)
41_VALUE_PATTERN = re.compile(_VALUE_FORMAT)
42
43
44_TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS = 32
45_delimiter_pattern = re.compile(r"[ \t]*,[ \t]*")
46_member_pattern = re.compile(f"({_KEY_FORMAT})(=)({_VALUE_FORMAT})[ \t]*")
47_logger = logging.getLogger(__name__)
48
49
50def _is_valid_pair(key: str, value: str) -> bool:
51 return (
52 isinstance(key, str)
53 and _KEY_PATTERN.fullmatch(key) is not None
54 and isinstance(value, str)
55 and _VALUE_PATTERN.fullmatch(value) is not None
56 )
57
58
59class Span(abc.ABC):
60 """A span represents a single operation within a trace."""
61
62 @abc.abstractmethod
63 def end(self, end_time: int | None = None) -> None:
64 """Sets the current time as the span's end time.
65
66 The span's end time is the wall time at which the operation finished.
67
68 Only the first call to `end` should modify the span, and
69 implementations are free to ignore or raise on further calls.
70 """
71
72 @abc.abstractmethod
73 def get_span_context(self) -> "SpanContext":
74 """Gets the span's SpanContext.
75
76 Get an immutable, serializable identifier for this span that can be
77 used to create new child spans.
78
79 Returns:
80 A :class:`opentelemetry.trace.SpanContext` with a copy of this span's immutable state.
81 """
82
83 @abc.abstractmethod
84 def set_attributes(
85 self, attributes: typing.Mapping[str, types.AttributeValue]
86 ) -> None:
87 """Sets Attributes.
88
89 Sets Attributes with the key and value passed as arguments dict.
90
91 Note: The behavior of `None` value attributes is undefined, and hence
92 strongly discouraged. It is also preferred to set attributes at span
93 creation, instead of calling this method later since samplers can only
94 consider information already present during span creation.
95 """
96
97 @abc.abstractmethod
98 def set_attribute(self, key: str, value: types.AttributeValue) -> None:
99 """Sets an Attribute.
100
101 Sets a single Attribute with the key and value passed as arguments.
102
103 Note: The behavior of `None` value attributes is undefined, and hence
104 strongly discouraged. It is also preferred to set attributes at span
105 creation, instead of calling this method later since samplers can only
106 consider information already present during span creation.
107 """
108
109 @abc.abstractmethod
110 def add_event(
111 self,
112 name: str,
113 attributes: types.Attributes = None,
114 timestamp: int | None = None,
115 ) -> None:
116 """Adds an `Event`.
117
118 Adds a single `Event` with the name and, optionally, a timestamp and
119 attributes passed as arguments. Implementations should generate a
120 timestamp if the `timestamp` argument is omitted.
121 """
122
123 def add_link( # pylint: disable=no-self-use
124 self,
125 context: "SpanContext",
126 attributes: types.Attributes = None,
127 ) -> None:
128 """Adds a `Link`.
129
130 Adds a single `Link` with the `SpanContext` of the span to link to and,
131 optionally, attributes passed as arguments. Implementations may ignore
132 calls with an invalid span context if both attributes and TraceState
133 are empty.
134
135 Note: It is preferred to add links at span creation, instead of calling
136 this method later since samplers can only consider information already
137 present during span creation.
138 """
139 warnings.warn(
140 "Span.add_link() not implemented and will be a no-op. "
141 "Use opentelemetry-sdk >= 1.23 to add links after span creation"
142 )
143
144 @abc.abstractmethod
145 def update_name(self, name: str) -> None:
146 """Updates the `Span` name.
147
148 This will override the name provided via :func:`opentelemetry.trace.Tracer.start_span`.
149
150 Upon this update, any sampling behavior based on Span name will depend
151 on the implementation.
152 """
153
154 @abc.abstractmethod
155 def is_recording(self) -> bool:
156 """Returns whether this span will be recorded.
157
158 Returns true if this Span is active and recording information like
159 events with the add_event operation and attributes using set_attribute.
160 """
161
162 @abc.abstractmethod
163 def set_status(
164 self,
165 status: Status | StatusCode,
166 description: str | None = None,
167 ) -> None:
168 """Sets the Status of the Span. If used, this will override the default
169 Span status.
170 """
171
172 @abc.abstractmethod
173 def record_exception(
174 self,
175 exception: BaseException,
176 attributes: types.Attributes = None,
177 timestamp: int | None = None,
178 escaped: bool = False,
179 ) -> None:
180 """Records an exception as a span event."""
181
182 def __enter__(self) -> "Span":
183 """Invoked when `Span` is used as a context manager.
184
185 Returns the `Span` itself.
186 """
187 return self
188
189 def __exit__(
190 self,
191 exc_type: type[BaseException] | None,
192 exc_val: BaseException | None,
193 exc_tb: python_types.TracebackType | None,
194 ) -> None:
195 """Ends context manager and calls `end` on the `Span`."""
196
197 self.end()
198
199
200class TraceFlags(int):
201 """A bitmask that represents options specific to the trace.
202
203 Supported flags:
204 - "sampled" (``0x01``): Indicates the trace may have been sampled upstream.
205 - "random-trace-id" (``0x02``): Indicates the trace ID was generated
206 randomly, with at least the 7 rightmost bytes (56 bits) selected with
207 uniform distribution.
208
209 See the `W3C Trace Context - Traceparent`_ spec for details.
210
211 .. _W3C Trace Context - Traceparent:
212 https://www.w3.org/TR/trace-context-2/#trace-flags
213 """
214
215 DEFAULT = 0x00
216 SAMPLED = 0x01
217 RANDOM_TRACE_ID = 0x02
218
219 @classmethod
220 def get_default(cls) -> "TraceFlags":
221 return cls(cls.DEFAULT)
222
223 @property
224 def sampled(self) -> bool:
225 return bool(self & TraceFlags.SAMPLED)
226
227 @property
228 def random_trace_id(self) -> bool:
229 return bool(self & TraceFlags.RANDOM_TRACE_ID)
230
231
232DEFAULT_TRACE_OPTIONS = TraceFlags.get_default()
233
234
235class TraceState(typing.Mapping[str, str]):
236 """A list of key-value pairs representing vendor-specific trace info.
237
238 Keys and values are strings of up to 256 printable US-ASCII characters.
239 Implementations should conform to the `W3C Trace Context - Tracestate`_
240 spec, which describes additional restrictions on valid field values.
241
242 .. _W3C Trace Context - Tracestate:
243 https://www.w3.org/TR/trace-context/#tracestate-field
244 """
245
246 def __init__(
247 self,
248 entries: typing.Sequence[tuple[str, str]] | None = None,
249 ) -> None:
250 self._dict = {} # type: dict[str, str]
251 if entries is None:
252 return
253 if len(entries) > _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
254 _logger.warning(
255 "There can't be more than %s key/value pairs.",
256 _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS,
257 )
258 return
259
260 for key, value in entries:
261 if _is_valid_pair(key, value):
262 if key in self._dict:
263 _logger.warning("Duplicate key: %s found.", key)
264 continue
265 self._dict[key] = value
266 else:
267 _logger.warning(
268 "Invalid key/value pair (%s, %s) found.", key, value
269 )
270
271 def __contains__(self, item: object) -> bool:
272 return item in self._dict
273
274 def __getitem__(self, key: str) -> str:
275 return self._dict[key]
276
277 def __iter__(self) -> typing.Iterator[str]:
278 return iter(self._dict)
279
280 def __len__(self) -> int:
281 return len(self._dict)
282
283 def __repr__(self) -> str:
284 pairs = [
285 f"{{key={key}, value={value}}}"
286 for key, value in self._dict.items()
287 ]
288 return str(pairs)
289
290 def add(self, key: str, value: str) -> "TraceState":
291 """Adds a key-value pair to tracestate. The provided pair should
292 adhere to w3c tracestate identifiers format.
293
294 Args:
295 key: A valid tracestate key to add
296 value: A valid tracestate value to add
297
298 Returns:
299 A new TraceState with the modifications applied.
300
301 If the provided key-value pair is invalid or results in tracestate
302 that violates tracecontext specification, they are discarded and
303 same tracestate will be returned.
304 """
305 if not _is_valid_pair(key, value):
306 _logger.warning(
307 "Invalid key/value pair (%s, %s) found.", key, value
308 )
309 return self
310 # There can be a maximum of 32 pairs
311 if len(self) >= _TRACECONTEXT_MAXIMUM_TRACESTATE_KEYS:
312 _logger.warning("There can't be more 32 key/value pairs.")
313 return self
314 # Duplicate entries are not allowed
315 if key in self._dict:
316 _logger.warning("The provided key %s already exists.", key)
317 return self
318 new_state = [(key, value)] + list(self._dict.items())
319 return TraceState(new_state)
320
321 def update(self, key: str, value: str) -> "TraceState":
322 """Updates a key-value pair in tracestate. The provided pair should
323 adhere to w3c tracestate identifiers format.
324
325 Args:
326 key: A valid tracestate key to update
327 value: A valid tracestate value to update for key
328
329 Returns:
330 A new TraceState with the modifications applied.
331
332 If the provided key-value pair is invalid or results in tracestate
333 that violates tracecontext specification, they are discarded and
334 same tracestate will be returned.
335 """
336 if not _is_valid_pair(key, value):
337 _logger.warning(
338 "Invalid key/value pair (%s, %s) found.", key, value
339 )
340 return self
341 prev_state = self._dict.copy()
342 prev_state.pop(key, None)
343 new_state = [(key, value), *prev_state.items()]
344 return TraceState(new_state)
345
346 def delete(self, key: str) -> "TraceState":
347 """Deletes a key-value from tracestate.
348
349 Args:
350 key: A valid tracestate key to remove key-value pair from tracestate
351
352 Returns:
353 A new TraceState with the modifications applied.
354
355 If the provided key-value pair is invalid or results in tracestate
356 that violates tracecontext specification, they are discarded and
357 same tracestate will be returned.
358 """
359 if key not in self._dict:
360 _logger.warning("The provided key %s doesn't exist.", key)
361 return self
362 prev_state = self._dict.copy()
363 prev_state.pop(key)
364 new_state = list(prev_state.items())
365 return TraceState(new_state)
366
367 def to_header(self) -> str:
368 """Creates a w3c tracestate header from a TraceState.
369
370 Returns:
371 A string that adheres to the w3c tracestate
372 header format.
373 """
374 return ",".join(key + "=" + value for key, value in self._dict.items())
375
376 @classmethod
377 def from_header(cls, header_list: list[str]) -> "TraceState":
378 """Parses one or more w3c tracestate header into a TraceState.
379
380 Args:
381 header_list: one or more w3c tracestate headers.
382
383 Returns:
384 A valid TraceState that contains values extracted from
385 the tracestate header.
386
387 If the format of one headers is illegal, all values will
388 be discarded and an empty tracestate will be returned.
389
390 If the number of keys is beyond the maximum, all values
391 will be discarded and an empty tracestate will be returned.
392 """
393 pairs = {} # type: dict[str, str]
394 for header in header_list:
395 members: list[str] = re.split(_delimiter_pattern, header)
396 for member in members:
397 # empty members are valid, but no need to process further.
398 if not member:
399 continue
400 match = _member_pattern.fullmatch(member)
401 if not match:
402 _logger.warning(
403 "Member doesn't match the w3c identifiers format %s",
404 member,
405 )
406 return cls()
407 groups: tuple[str, ...] = match.groups()
408 key, _eq, value = groups
409 # duplicate keys are not legal in header
410 if key in pairs:
411 return cls()
412 pairs[key] = value
413 return cls(list(pairs.items()))
414
415 @classmethod
416 def get_default(cls) -> "TraceState":
417 return cls()
418
419 def keys(self) -> typing.KeysView[str]:
420 return self._dict.keys()
421
422 def items(self) -> typing.ItemsView[str, str]:
423 return self._dict.items()
424
425 def values(self) -> typing.ValuesView[str]:
426 return self._dict.values()
427
428
429DEFAULT_TRACE_STATE = TraceState.get_default()
430_TRACE_ID_MAX_VALUE = 2**128 - 1
431_SPAN_ID_MAX_VALUE = 2**64 - 1
432
433
434class SpanContext(tuple[int, int, bool, "TraceFlags", "TraceState", bool]):
435 """The state of a Span to propagate between processes.
436
437 This class includes the immutable attributes of a :class:`.Span` that must
438 be propagated to a span's children and across process boundaries.
439
440 Args:
441 trace_id: The ID of the trace that this span belongs to.
442 span_id: This span's ID.
443 is_remote: True if propagated from a remote parent.
444 trace_flags: Trace options to propagate.
445 trace_state: Tracing-system-specific info to propagate.
446 """
447
448 def __new__(
449 cls,
450 trace_id: int,
451 span_id: int,
452 is_remote: bool,
453 trace_flags: typing.Optional["TraceFlags"] = DEFAULT_TRACE_OPTIONS,
454 trace_state: typing.Optional["TraceState"] = DEFAULT_TRACE_STATE,
455 ) -> "SpanContext":
456 if trace_flags is None:
457 trace_flags = DEFAULT_TRACE_OPTIONS
458 if trace_state is None:
459 trace_state = DEFAULT_TRACE_STATE
460
461 is_valid = (
462 INVALID_TRACE_ID < trace_id <= _TRACE_ID_MAX_VALUE
463 and INVALID_SPAN_ID < span_id <= _SPAN_ID_MAX_VALUE
464 )
465
466 return tuple.__new__(
467 cls,
468 (trace_id, span_id, is_remote, trace_flags, trace_state, is_valid),
469 )
470
471 def __getnewargs__(
472 self,
473 ) -> tuple[int, int, bool, "TraceFlags", "TraceState"]:
474 return (
475 self.trace_id,
476 self.span_id,
477 self.is_remote,
478 self.trace_flags,
479 self.trace_state,
480 )
481
482 @property
483 def trace_id(self) -> int:
484 return self[0] # pylint: disable=unsubscriptable-object
485
486 @property
487 def span_id(self) -> int:
488 return self[1] # pylint: disable=unsubscriptable-object
489
490 @property
491 def is_remote(self) -> bool:
492 return self[2] # pylint: disable=unsubscriptable-object
493
494 @property
495 def trace_flags(self) -> "TraceFlags":
496 return self[3] # pylint: disable=unsubscriptable-object
497
498 @property
499 def trace_state(self) -> "TraceState":
500 return self[4] # pylint: disable=unsubscriptable-object
501
502 @property
503 def is_valid(self) -> bool:
504 return self[5] # pylint: disable=unsubscriptable-object
505
506 def __setattr__(self, *args: str) -> None:
507 _logger.debug(
508 "Immutable type, ignoring call to set attribute", stack_info=True
509 )
510
511 def __delattr__(self, *args: str) -> None:
512 _logger.debug(
513 "Immutable type, ignoring call to set attribute", stack_info=True
514 )
515
516 def __repr__(self) -> str:
517 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})"
518
519
520class NonRecordingSpan(Span):
521 """The Span that is used when no Span implementation is available.
522
523 All operations are no-op except context propagation.
524 """
525
526 def __init__(self, context: "SpanContext") -> None:
527 self._context = context
528
529 def get_span_context(self) -> "SpanContext":
530 return self._context
531
532 def is_recording(self) -> bool:
533 return False
534
535 def end(self, end_time: int | None = None) -> None:
536 pass
537
538 def set_attributes(
539 self, attributes: typing.Mapping[str, types.AttributeValue]
540 ) -> None:
541 pass
542
543 def set_attribute(self, key: str, value: types.AttributeValue) -> None:
544 pass
545
546 def add_event(
547 self,
548 name: str,
549 attributes: types.Attributes = None,
550 timestamp: int | None = None,
551 ) -> None:
552 pass
553
554 def add_link(
555 self,
556 context: "SpanContext",
557 attributes: types.Attributes = None,
558 ) -> None:
559 pass
560
561 def update_name(self, name: str) -> None:
562 pass
563
564 def set_status(
565 self,
566 status: Status | StatusCode,
567 description: str | None = None,
568 ) -> None:
569 pass
570
571 def record_exception(
572 self,
573 exception: BaseException,
574 attributes: types.Attributes = None,
575 timestamp: int | None = None,
576 escaped: bool = False,
577 ) -> None:
578 pass
579
580 def __repr__(self) -> str:
581 return f"NonRecordingSpan({self._context!r})"
582
583
584INVALID_SPAN_ID = 0x0000000000000000
585INVALID_TRACE_ID = 0x00000000000000000000000000000000
586INVALID_SPAN_CONTEXT = SpanContext(
587 trace_id=INVALID_TRACE_ID,
588 span_id=INVALID_SPAN_ID,
589 is_remote=False,
590 trace_flags=DEFAULT_TRACE_OPTIONS,
591 trace_state=DEFAULT_TRACE_STATE,
592)
593INVALID_SPAN = NonRecordingSpan(INVALID_SPAN_CONTEXT)
594
595
596def format_trace_id(trace_id: int) -> str:
597 """Convenience trace ID formatting method
598 Args:
599 trace_id: Trace ID int
600
601 Returns:
602 The trace ID (16 bytes) cast to a 32-character hexadecimal string
603 """
604 return format(trace_id, "032x")
605
606
607def format_span_id(span_id: int) -> str:
608 """Convenience span ID formatting method
609 Args:
610 span_id: Span ID int
611
612 Returns:
613 The span ID (8 bytes) cast to a 16-character hexadecimal string
614 """
615 return format(span_id, "016x")