Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/opentelemetry/trace/span.py: 56%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

206 statements  

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")