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

213 statements  

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