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

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