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