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