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

211 statements  

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